siderite_validation/types/
float.rs1use std::marker::PhantomData;
5
6use serde::de::Error as _;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use serde_json::{Number, Value};
9
10use crate::context::ValidationContext;
11use crate::dump::Dump;
12use crate::error::FieldError;
13use crate::rules;
14use crate::schema::{Schema, SchemaObject, SchemaRegistry};
15use crate::validate::Validate;
16
17pub trait FloatBounds {
19 const MIN: Option<f64>;
21 const MAX: Option<f64>;
23 const EXCLUSIVE_MIN: bool = false;
25 const EXCLUSIVE_MAX: bool = false;
27}
28
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub struct UnitInterval;
32
33impl FloatBounds for UnitInterval {
34 const MIN: Option<f64> = Some(0.0);
35 const MAX: Option<f64> = Some(1.0);
36}
37
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
40pub struct NonNegative;
41
42impl FloatBounds for NonNegative {
43 const MIN: Option<f64> = Some(0.0);
44 const MAX: Option<f64> = None;
45}
46
47#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub struct Positive;
50
51impl FloatBounds for Positive {
52 const MIN: Option<f64> = Some(0.0);
53 const MAX: Option<f64> = None;
54 const EXCLUSIVE_MIN: bool = true;
55}
56
57#[derive(Debug, PartialEq, PartialOrd)]
65pub struct BoundedFloat<B>(f64, PhantomData<fn() -> B>);
66
67impl<B> Clone for BoundedFloat<B> {
68 fn clone(&self) -> Self {
69 *self
70 }
71}
72
73impl<B> Copy for BoundedFloat<B> {}
74
75impl<B: FloatBounds> BoundedFloat<B> {
76 pub fn new(value: f64) -> Result<Self, FieldError> {
78 if !value.is_finite() {
79 return Err(FieldError::new("finite_number", "expected a finite number"));
80 }
81 if let Some(min) = B::MIN {
82 if B::EXCLUSIVE_MIN {
83 rules::gt(&value, &min)?;
84 } else {
85 rules::ge(&value, &min)?;
86 }
87 }
88 if let Some(max) = B::MAX {
89 if B::EXCLUSIVE_MAX {
90 rules::lt(&value, &max)?;
91 } else {
92 rules::le(&value, &max)?;
93 }
94 }
95 Ok(Self(value, PhantomData))
96 }
97}
98
99impl<B> BoundedFloat<B> {
100 pub fn into_inner(self) -> f64 {
102 self.0
103 }
104}
105
106impl<B> AsRef<f64> for BoundedFloat<B> {
107 fn as_ref(&self) -> &f64 {
108 &self.0
109 }
110}
111
112impl<B> std::ops::Deref for BoundedFloat<B> {
113 type Target = f64;
114
115 fn deref(&self) -> &Self::Target {
116 &self.0
117 }
118}
119
120impl<B> std::fmt::Display for BoundedFloat<B> {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 self.0.fmt(f)
123 }
124}
125
126impl<B> From<BoundedFloat<B>> for f64 {
127 fn from(value: BoundedFloat<B>) -> Self {
128 value.0
129 }
130}
131
132impl<B: FloatBounds> TryFrom<f64> for BoundedFloat<B> {
133 type Error = FieldError;
134
135 fn try_from(value: f64) -> Result<Self, Self::Error> {
136 Self::new(value)
137 }
138}
139
140impl<B> Serialize for BoundedFloat<B> {
141 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
142 where
143 S: Serializer,
144 {
145 self.0.serialize(serializer)
146 }
147}
148
149impl<'de, B: FloatBounds> Deserialize<'de> for BoundedFloat<B> {
150 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
151 where
152 D: Deserializer<'de>,
153 {
154 let value = f64::deserialize(deserializer)?;
155 Self::new(value).map_err(D::Error::custom)
156 }
157}
158
159impl<B: FloatBounds> Validate for BoundedFloat<B> {
160 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
161 let before = ctx.error_count();
162 f64::prepare(input, ctx);
163 if ctx.error_count() != before {
164 return;
165 }
166 let Some(value) = input.as_f64() else {
167 return;
168 };
169 ctx.check(Self::new(value).map(|_| ()));
170 }
171}
172
173fn f64_schema_value(value: f64) -> Value {
174 Number::from_f64(value).map_or(Value::Null, Value::Number)
175}
176
177impl<B: FloatBounds> Schema for BoundedFloat<B> {
178 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
179 let mut schema = SchemaObject::of_type("number").with("format", "double");
180 if let Some(min) = B::MIN {
181 let key = if B::EXCLUSIVE_MIN {
182 "exclusiveMinimum"
183 } else {
184 "minimum"
185 };
186 schema = schema.with(key, f64_schema_value(min));
187 }
188 if let Some(max) = B::MAX {
189 let key = if B::EXCLUSIVE_MAX {
190 "exclusiveMaximum"
191 } else {
192 "maximum"
193 };
194 schema = schema.with(key, f64_schema_value(max));
195 }
196 schema
197 }
198}
199
200impl<B: FloatBounds> Dump for BoundedFloat<B> {}
201
202#[cfg(test)]
203mod tests {
204 #![allow(clippy::unwrap_used)]
205
206 use super::*;
207 use crate::context::ValidationContext;
208 use crate::dump::{Dump, DumpOptions};
209 use crate::schema::schema_for;
210 use crate::types::prepare_codes;
211 use serde_json::json;
212
213 type Unit = BoundedFloat<UnitInterval>;
214 type Pos = BoundedFloat<Positive>;
215 type NonNeg = BoundedFloat<NonNegative>;
216
217 #[test]
218 fn bounded_float_construction() {
219 assert_eq!(Unit::new(0.0).unwrap().into_inner(), 0.0);
220 assert_eq!(Unit::new(1.0).unwrap().into_inner(), 1.0);
221 assert_eq!(Unit::new(-0.1).unwrap_err().code, "greater_than_equal");
222 assert_eq!(Unit::new(1.1).unwrap_err().code, "less_than_equal");
223 assert_eq!(Pos::new(0.0).unwrap_err().code, "greater_than");
224 assert!(Pos::new(0.1).is_ok());
225 assert!(NonNeg::new(0.0).is_ok());
226 assert_eq!(Unit::new(f64::NAN).unwrap_err().code, "finite_number");
227 assert_eq!(Unit::new(f64::INFINITY).unwrap_err().code, "finite_number");
228 }
229
230 #[test]
231 fn bounded_float_serde_round_trip_and_reject() {
232 let value = Unit::new(0.5).unwrap();
233 let encoded = serde_json::to_string(&value).unwrap();
234 assert_eq!(encoded, "0.5");
235 let round: Unit = serde_json::from_str(&encoded).unwrap();
236 assert_eq!(*round, 0.5);
237 assert!(serde_json::from_str::<Unit>("1.5").is_err());
238 assert!(serde_json::from_str::<Pos>("0").is_err());
239 }
240
241 #[test]
242 fn bounded_float_prepare_lax_and_strict() {
243 assert_eq!(
244 prepare_codes::<Unit>(json!("0.25"), ValidationContext::new()),
245 (json!(0.25), vec![])
246 );
247 assert_eq!(
248 prepare_codes::<Unit>(json!("0.25"), ValidationContext::new().with_strict(true)).1,
249 ["float_type"]
250 );
251 assert_eq!(
252 prepare_codes::<Unit>(json!(1.5), ValidationContext::new()).1,
253 ["less_than_equal"]
254 );
255 assert_eq!(
256 prepare_codes::<Pos>(json!(0.0), ValidationContext::new()).1,
257 ["greater_than"]
258 );
259 assert_eq!(
260 prepare_codes::<Unit>(json!("nan"), ValidationContext::new()).1,
261 ["float_parsing"]
262 );
263 }
264
265 #[test]
266 fn bounded_float_schema_dump_and_parse_value() {
267 let unit = schema_for::<Unit>().0.into_value();
268 assert_eq!(unit["type"], json!("number"));
269 assert_eq!(unit["format"], json!("double"));
270 assert_eq!(unit["minimum"], json!(0.0));
271 assert_eq!(unit["maximum"], json!(1.0));
272
273 let pos = schema_for::<Pos>().0.into_value();
274 assert_eq!(pos["exclusiveMinimum"], json!(0.0));
275 assert!(pos.get("minimum").is_none());
276
277 let value = Unit::new(0.5).unwrap();
278 assert_eq!(value.dump(&DumpOptions::new()).unwrap(), json!(0.5));
279 let parsed = crate::parse_value::<Unit>(json!("0.5"), ValidationContext::new()).unwrap();
280 assert_eq!(*parsed, 0.5);
281 let err = crate::parse_value::<Unit>(json!(2.0), ValidationContext::new()).unwrap_err();
282 assert_eq!(err.errors[0].code, "less_than_equal");
283 }
284}