configs/
validated_types.rs

1//! Wrapper types that encode validation constraints in the type system.
2//!
3//! These types make invalid config states unrepresentable at parse time:
4//! - `PositiveI64` / `PositiveI32` / `PositiveF64` — value must be > 0
5//! - `NonZeroU64` — u64 that must be != 0
6//! - `WeightMultiplier` — f64 that must be >= 1.0
7//! - `NonEmptyVec<T>` — Vec that must contain at least one element
8
9use std::fmt;
10use std::ops::Deref;
11
12use schemars::JsonSchema;
13use schemars::r#gen::SchemaGenerator;
14use schemars::schema::{InstanceType, Schema, SchemaObject};
15use serde::{Deserialize, Deserializer, Serialize};
16
17// ---------------------------------------------------------------------------
18// PositiveI64 (i64 > 0)
19// ---------------------------------------------------------------------------
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
22#[serde(transparent)]
23pub struct PositiveI64(i64);
24
25impl PositiveI64 {
26    /// Creates a new `PositiveI64`. Panics if `value <= 0`.
27    pub fn new(value: i64) -> Self {
28        assert!(value > 0, "PositiveI64: expected > 0, got {value}");
29        Self(value)
30    }
31
32    pub fn get(self) -> i64 {
33        self.0
34    }
35}
36
37impl<'de> Deserialize<'de> for PositiveI64 {
38    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
39        let v = i64::deserialize(deserializer)?;
40        if v <= 0 {
41            return Err(serde::de::Error::custom(format!(
42                "expected positive i64 (> 0), got {v}"
43            )));
44        }
45        Ok(Self(v))
46    }
47}
48
49impl JsonSchema for PositiveI64 {
50    fn schema_name() -> String {
51        "PositiveI64".to_owned()
52    }
53
54    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
55        SchemaObject {
56            instance_type: Some(InstanceType::Integer.into()),
57            number: Some(Box::new(schemars::schema::NumberValidation {
58                minimum: Some(1.0),
59                ..Default::default()
60            })),
61            ..Default::default()
62        }
63        .into()
64    }
65}
66
67impl fmt::Display for PositiveI64 {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        self.0.fmt(f)
70    }
71}
72
73// ---------------------------------------------------------------------------
74// PositiveI32 (i32 > 0)
75// ---------------------------------------------------------------------------
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
78#[serde(transparent)]
79pub struct PositiveI32(i32);
80
81impl PositiveI32 {
82    /// Creates a new `PositiveI32`. Panics if `value <= 0`.
83    pub fn new(value: i32) -> Self {
84        assert!(value > 0, "PositiveI32: expected > 0, got {value}");
85        Self(value)
86    }
87
88    pub fn get(self) -> i32 {
89        self.0
90    }
91}
92
93impl<'de> Deserialize<'de> for PositiveI32 {
94    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
95        let v = i32::deserialize(deserializer)?;
96        if v <= 0 {
97            return Err(serde::de::Error::custom(format!(
98                "expected positive i32 (> 0), got {v}"
99            )));
100        }
101        Ok(Self(v))
102    }
103}
104
105impl JsonSchema for PositiveI32 {
106    fn schema_name() -> String {
107        "PositiveI32".to_owned()
108    }
109
110    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
111        SchemaObject {
112            instance_type: Some(InstanceType::Integer.into()),
113            number: Some(Box::new(schemars::schema::NumberValidation {
114                minimum: Some(1.0),
115                ..Default::default()
116            })),
117            ..Default::default()
118        }
119        .into()
120    }
121}
122
123impl fmt::Display for PositiveI32 {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        self.0.fmt(f)
126    }
127}
128
129// ---------------------------------------------------------------------------
130// PositiveF64 (f64 > 0.0)
131// ---------------------------------------------------------------------------
132
133#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize)]
134#[serde(transparent)]
135pub struct PositiveF64(f64);
136
137impl PositiveF64 {
138    /// Creates a new `PositiveF64`. Panics if `value <= 0.0`.
139    pub fn new(value: f64) -> Self {
140        assert!(value > 0.0, "PositiveF64: expected > 0.0, got {value}");
141        Self(value)
142    }
143
144    pub fn get(self) -> f64 {
145        self.0
146    }
147}
148
149impl<'de> Deserialize<'de> for PositiveF64 {
150    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
151        let v = f64::deserialize(deserializer)?;
152        if v <= 0.0 {
153            return Err(serde::de::Error::custom(format!(
154                "expected positive f64 (> 0.0), got {v}"
155            )));
156        }
157        Ok(Self(v))
158    }
159}
160
161impl JsonSchema for PositiveF64 {
162    fn schema_name() -> String {
163        "PositiveF64".to_owned()
164    }
165
166    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
167        SchemaObject {
168            instance_type: Some(InstanceType::Number.into()),
169            number: Some(Box::new(schemars::schema::NumberValidation {
170                exclusive_minimum: Some(0.0),
171                ..Default::default()
172            })),
173            ..Default::default()
174        }
175        .into()
176    }
177}
178
179impl fmt::Display for PositiveF64 {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        self.0.fmt(f)
182    }
183}
184
185// ---------------------------------------------------------------------------
186// NonZeroU64 (u64 != 0)
187// ---------------------------------------------------------------------------
188
189#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
190#[serde(transparent)]
191pub struct NonZeroU64(u64);
192
193impl NonZeroU64 {
194    /// Creates a new `NonZeroU64`. Panics if `value == 0`.
195    pub fn new(value: u64) -> Self {
196        assert!(value != 0, "NonZeroU64: expected != 0, got 0");
197        Self(value)
198    }
199
200    pub fn get(self) -> u64 {
201        self.0
202    }
203}
204
205impl<'de> Deserialize<'de> for NonZeroU64 {
206    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
207        let v = u64::deserialize(deserializer)?;
208        if v == 0 {
209            return Err(serde::de::Error::custom("expected non-zero u64, got 0"));
210        }
211        Ok(Self(v))
212    }
213}
214
215impl JsonSchema for NonZeroU64 {
216    fn schema_name() -> String {
217        "NonZeroU64".to_owned()
218    }
219
220    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
221        SchemaObject {
222            instance_type: Some(InstanceType::Integer.into()),
223            number: Some(Box::new(schemars::schema::NumberValidation {
224                minimum: Some(1.0),
225                ..Default::default()
226            })),
227            ..Default::default()
228        }
229        .into()
230    }
231}
232
233impl fmt::Display for NonZeroU64 {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        self.0.fmt(f)
236    }
237}
238
239// ---------------------------------------------------------------------------
240// WeightMultiplier (f64 >= 1.0)
241// ---------------------------------------------------------------------------
242
243#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize)]
244#[serde(transparent)]
245pub struct WeightMultiplier(f64);
246
247impl WeightMultiplier {
248    /// Creates a new `WeightMultiplier`. Panics if `value < 1.0`.
249    pub fn new(value: f64) -> Self {
250        assert!(
251            value >= 1.0,
252            "WeightMultiplier: expected >= 1.0, got {value}"
253        );
254        Self(value)
255    }
256
257    pub fn get(self) -> f64 {
258        self.0
259    }
260}
261
262impl<'de> Deserialize<'de> for WeightMultiplier {
263    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
264        let v = f64::deserialize(deserializer)?;
265        if v < 1.0 {
266            return Err(serde::de::Error::custom(format!(
267                "expected weight multiplier >= 1.0, got {v}"
268            )));
269        }
270        Ok(Self(v))
271    }
272}
273
274impl JsonSchema for WeightMultiplier {
275    fn schema_name() -> String {
276        "WeightMultiplier".to_owned()
277    }
278
279    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
280        SchemaObject {
281            instance_type: Some(InstanceType::Number.into()),
282            number: Some(Box::new(schemars::schema::NumberValidation {
283                minimum: Some(1.0),
284                ..Default::default()
285            })),
286            ..Default::default()
287        }
288        .into()
289    }
290}
291
292impl fmt::Display for WeightMultiplier {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        self.0.fmt(f)
295    }
296}
297
298// ---------------------------------------------------------------------------
299// NonEmptyVec<T> (Vec<T> with len >= 1)
300// ---------------------------------------------------------------------------
301
302#[derive(Clone, Debug, Serialize)]
303#[serde(transparent)]
304pub struct NonEmptyVec<T>(Vec<T>);
305
306impl<T> NonEmptyVec<T> {
307    /// Creates a new `NonEmptyVec`. Panics if `vec` is empty.
308    pub fn new(vec: Vec<T>) -> Self {
309        assert!(!vec.is_empty(), "NonEmptyVec: expected non-empty vec");
310        Self(vec)
311    }
312
313    pub fn first(&self) -> &T {
314        // SAFETY: guaranteed non-empty by construction
315        &self.0[0]
316    }
317
318    pub fn last(&self) -> &T {
319        // SAFETY: guaranteed non-empty by construction
320        self.0.last().unwrap()
321    }
322
323    pub fn into_inner(self) -> Vec<T> {
324        self.0
325    }
326}
327
328impl<T> Deref for NonEmptyVec<T> {
329    type Target = [T];
330
331    fn deref(&self) -> &[T] {
332        &self.0
333    }
334}
335
336impl<T> std::ops::DerefMut for NonEmptyVec<T> {
337    fn deref_mut(&mut self) -> &mut [T] {
338        &mut self.0
339    }
340}
341
342impl<'a, T> IntoIterator for &'a NonEmptyVec<T> {
343    type Item = &'a T;
344    type IntoIter = std::slice::Iter<'a, T>;
345
346    fn into_iter(self) -> Self::IntoIter {
347        self.0.iter()
348    }
349}
350
351impl<'a, T> IntoIterator for &'a mut NonEmptyVec<T> {
352    type Item = &'a mut T;
353    type IntoIter = std::slice::IterMut<'a, T>;
354
355    fn into_iter(self) -> Self::IntoIter {
356        self.0.iter_mut()
357    }
358}
359
360impl<T> IntoIterator for NonEmptyVec<T> {
361    type Item = T;
362    type IntoIter = std::vec::IntoIter<T>;
363
364    fn into_iter(self) -> Self::IntoIter {
365        self.0.into_iter()
366    }
367}
368
369impl<'de, T: Deserialize<'de>> Deserialize<'de> for NonEmptyVec<T> {
370    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
371        let v = Vec::<T>::deserialize(deserializer)?;
372        if v.is_empty() {
373            return Err(serde::de::Error::custom(
374                "expected non-empty array, got empty array",
375            ));
376        }
377        Ok(Self(v))
378    }
379}
380
381impl<T: JsonSchema> JsonSchema for NonEmptyVec<T> {
382    fn schema_name() -> String {
383        format!("NonEmptyVec_{}", T::schema_name())
384    }
385
386    fn json_schema(generator: &mut SchemaGenerator) -> Schema {
387        let inner = generator.subschema_for::<Vec<T>>();
388        // Add minItems: 1 constraint
389        if let Schema::Object(mut obj) = inner {
390            let arr = obj.array.get_or_insert_with(Default::default);
391            arr.min_items = Some(1);
392            Schema::Object(obj)
393        } else {
394            inner
395        }
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn positive_i64_rejects_zero() {
405        let result: Result<PositiveI64, _> = serde_json::from_str("0");
406        assert!(result.is_err());
407    }
408
409    #[test]
410    fn positive_i64_rejects_negative() {
411        let result: Result<PositiveI64, _> = serde_json::from_str("-5");
412        assert!(result.is_err());
413    }
414
415    #[test]
416    fn positive_i64_accepts_positive() {
417        let v: PositiveI64 = serde_json::from_str("42").unwrap();
418        assert_eq!(v.get(), 42);
419    }
420
421    #[test]
422    fn positive_f64_rejects_zero() {
423        let result: Result<PositiveF64, _> = serde_json::from_str("0.0");
424        assert!(result.is_err());
425    }
426
427    #[test]
428    fn positive_f64_accepts_positive() {
429        let v: PositiveF64 = serde_json::from_str("2.72").unwrap();
430        assert!((v.get() - 2.72).abs() < f64::EPSILON);
431    }
432
433    #[test]
434    fn non_zero_u64_rejects_zero() {
435        let result: Result<NonZeroU64, _> = serde_json::from_str("0");
436        assert!(result.is_err());
437    }
438
439    #[test]
440    fn non_zero_u64_accepts_positive() {
441        let v: NonZeroU64 = serde_json::from_str("100").unwrap();
442        assert_eq!(v.get(), 100);
443    }
444
445    #[test]
446    fn weight_multiplier_rejects_below_one() {
447        let result: Result<WeightMultiplier, _> = serde_json::from_str("0.5");
448        assert!(result.is_err());
449    }
450
451    #[test]
452    fn weight_multiplier_accepts_one() {
453        let v: WeightMultiplier = serde_json::from_str("1.0").unwrap();
454        assert!((v.get() - 1.0).abs() < f64::EPSILON);
455    }
456
457    #[test]
458    fn non_empty_vec_rejects_empty() {
459        let result: Result<NonEmptyVec<i32>, _> = serde_json::from_str("[]");
460        assert!(result.is_err());
461    }
462
463    #[test]
464    fn non_empty_vec_accepts_non_empty() {
465        let v: NonEmptyVec<i32> = serde_json::from_str("[1, 2, 3]").unwrap();
466        assert_eq!(v.len(), 3);
467        assert_eq!(v[0], 1);
468    }
469
470    #[test]
471    fn non_empty_vec_deref_to_slice() {
472        let v: NonEmptyVec<i32> = serde_json::from_str("[10, 20]").unwrap();
473        let sum: i32 = v.iter().sum();
474        assert_eq!(sum, 30);
475    }
476
477    #[test]
478    fn positive_i64_roundtrip() {
479        let v: PositiveI64 = serde_json::from_str("7").unwrap();
480        let json = serde_json::to_string(&v).unwrap();
481        assert_eq!(json, "7");
482    }
483
484    #[test]
485    fn non_empty_vec_roundtrip() {
486        let v: NonEmptyVec<i32> = serde_json::from_str("[1, 2]").unwrap();
487        let json = serde_json::to_string(&v).unwrap();
488        assert_eq!(json, "[1,2]");
489    }
490}