Skip to main content

siderite_validation/types/
int.rs

1//! Signed integer newtypes that complement [`super::PositiveInt`].
2
3use serde::{Deserialize, Serialize};
4
5use crate::context::ValidationContext;
6use crate::dump::Dump;
7use crate::error::FieldError;
8use crate::rules;
9use crate::schema::{Schema, SchemaObject, SchemaRegistry};
10use crate::validate::Validate;
11use serde_json::Value;
12
13/// Non-negative `i64` (`value >= 0`).
14#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
15#[serde(try_from = "i64", into = "i64")]
16pub struct NonNegativeInt(i64);
17
18impl NonNegativeInt {
19    /// Validate `value >= 0` and wrap it.
20    pub fn new(value: i64) -> Result<Self, FieldError> {
21        rules::ge(&value, &0)?;
22        Ok(Self(value))
23    }
24}
25
26crate::types::impl_wrapper!(NonNegativeInt => i64);
27
28impl TryFrom<i64> for NonNegativeInt {
29    type Error = FieldError;
30
31    fn try_from(value: i64) -> Result<Self, Self::Error> {
32        Self::new(value)
33    }
34}
35
36impl Validate for NonNegativeInt {
37    fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
38        let before = ctx.error_count();
39        i64::prepare(input, ctx);
40        if ctx.error_count() == before {
41            ctx.check(rules::ge(&input.as_i64().unwrap_or_default(), &0));
42        }
43    }
44}
45
46impl Schema for NonNegativeInt {
47    fn schema(_: &mut SchemaRegistry) -> SchemaObject {
48        SchemaObject::of_type("integer").with("minimum", 0_i64)
49    }
50}
51
52impl Dump for NonNegativeInt {}
53
54/// Strictly negative `i64` (`value < 0`).
55#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
56#[serde(try_from = "i64", into = "i64")]
57pub struct NegativeInt(i64);
58
59impl NegativeInt {
60    /// Validate `value < 0` and wrap it.
61    pub fn new(value: i64) -> Result<Self, FieldError> {
62        rules::lt(&value, &0)?;
63        Ok(Self(value))
64    }
65}
66
67crate::types::impl_wrapper!(NegativeInt => i64);
68
69impl TryFrom<i64> for NegativeInt {
70    type Error = FieldError;
71
72    fn try_from(value: i64) -> Result<Self, Self::Error> {
73        Self::new(value)
74    }
75}
76
77impl Validate for NegativeInt {
78    fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
79        let before = ctx.error_count();
80        i64::prepare(input, ctx);
81        if ctx.error_count() == before {
82            ctx.check(rules::lt(&input.as_i64().unwrap_or_default(), &0));
83        }
84    }
85}
86
87impl Schema for NegativeInt {
88    fn schema(_: &mut SchemaRegistry) -> SchemaObject {
89        SchemaObject::of_type("integer").with("exclusiveMaximum", 0_i64)
90    }
91}
92
93impl Dump for NegativeInt {}
94
95#[cfg(test)]
96mod tests {
97    #![allow(clippy::unwrap_used)]
98
99    use super::*;
100    use crate::context::ValidationContext;
101    use crate::dump::{Dump, DumpOptions};
102    use crate::schema::schema_for;
103    use crate::types::prepare_codes;
104    use serde_json::json;
105
106    #[test]
107    fn non_negative_int_construction() {
108        assert_eq!(NonNegativeInt::new(0).unwrap().into_inner(), 0);
109        assert_eq!(NonNegativeInt::new(3).unwrap().into_inner(), 3);
110        assert_eq!(
111            NonNegativeInt::new(-1).unwrap_err().code,
112            "greater_than_equal"
113        );
114    }
115
116    #[test]
117    fn negative_int_construction() {
118        assert_eq!(NegativeInt::new(-1).unwrap().into_inner(), -1);
119        assert_eq!(NegativeInt::new(0).unwrap_err().code, "less_than");
120        assert_eq!(NegativeInt::new(2).unwrap_err().code, "less_than");
121    }
122
123    #[test]
124    fn int_serde_round_trip_and_reject() {
125        let value: NonNegativeInt = serde_json::from_str("0").unwrap();
126        assert_eq!(serde_json::to_string(&value).unwrap(), "0");
127        assert!(serde_json::from_str::<NonNegativeInt>("-1").is_err());
128        let value: NegativeInt = serde_json::from_str("-4").unwrap();
129        assert_eq!(serde_json::to_string(&value).unwrap(), "-4");
130        assert!(serde_json::from_str::<NegativeInt>("0").is_err());
131    }
132
133    #[test]
134    fn int_prepare_lax_and_strict() {
135        assert_eq!(
136            prepare_codes::<NonNegativeInt>(json!("7"), ValidationContext::new()),
137            (json!(7), vec![])
138        );
139        assert_eq!(
140            prepare_codes::<NonNegativeInt>(json!("7"), ValidationContext::new().with_strict(true))
141                .1,
142            ["int_type"]
143        );
144        assert_eq!(
145            prepare_codes::<NonNegativeInt>(json!(-1), ValidationContext::new()).1,
146            ["greater_than_equal"]
147        );
148        assert_eq!(
149            prepare_codes::<NegativeInt>(json!("-2"), ValidationContext::new()),
150            (json!(-2), vec![])
151        );
152        assert_eq!(
153            prepare_codes::<NegativeInt>(json!(0), ValidationContext::new()).1,
154            ["less_than"]
155        );
156    }
157
158    #[test]
159    fn int_schema_dump_and_parse_value() {
160        assert_eq!(
161            schema_for::<NonNegativeInt>().0.into_value(),
162            json!({"type": "integer", "minimum": 0})
163        );
164        assert_eq!(
165            schema_for::<NegativeInt>().0.into_value(),
166            json!({"type": "integer", "exclusiveMaximum": 0})
167        );
168        let n = NonNegativeInt::new(4).unwrap();
169        assert_eq!(n.dump(&DumpOptions::new()).unwrap(), json!(4));
170        let parsed =
171            crate::parse_value::<NegativeInt>(json!("-9"), ValidationContext::new()).unwrap();
172        assert_eq!(parsed.into_inner(), -9);
173        let err =
174            crate::parse_value::<NonNegativeInt>(json!(-3), ValidationContext::new()).unwrap_err();
175        assert_eq!(err.errors[0].code, "greater_than_equal");
176    }
177}