Skip to main content

siderite_validation/types/
vec.rs

1//! Length-constrained vector newtype.
2
3use serde::de::Error as _;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use serde_json::Value;
6
7use crate::context::ValidationContext;
8use crate::dump::{Dump, DumpError, DumpOptions};
9use crate::error::FieldError;
10use crate::schema::{Schema, SchemaObject, SchemaRegistry};
11use crate::validate::Validate;
12
13/// A `Vec<T>` whose length is in `MIN..=MAX`.
14///
15/// Pydantic `conlist` is a factory; this is a const-generic newtype. Length
16/// errors use `too_short` / `too_long` (the same codes Pydantic uses for
17/// collections, not `string_too_short` / `string_too_long`).
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct ConstrainedVec<T, const MIN: usize, const MAX: usize>(Vec<T>);
20
21fn check_len<const MIN: usize, const MAX: usize>(len: usize) -> Result<(), FieldError> {
22    const { assert!(MIN <= MAX, "ConstrainedVec MIN must be <= MAX") };
23    if len < MIN {
24        Err(FieldError::new(
25            "too_short",
26            format!("ensure this value has at least {MIN} items"),
27        ))
28    } else if len > MAX {
29        Err(FieldError::new(
30            "too_long",
31            format!("ensure this value has at most {MAX} items"),
32        ))
33    } else {
34        Ok(())
35    }
36}
37
38impl<T, const MIN: usize, const MAX: usize> ConstrainedVec<T, MIN, MAX> {
39    /// Validate `value`'s length and wrap it.
40    pub fn new(value: Vec<T>) -> Result<Self, FieldError> {
41        check_len::<MIN, MAX>(value.len())?;
42        Ok(Self(value))
43    }
44}
45
46impl<T, const MIN: usize, const MAX: usize> ConstrainedVec<T, MIN, MAX> {
47    /// Unwrap the inner vector.
48    pub fn into_inner(self) -> Vec<T> {
49        self.0
50    }
51}
52
53impl<T, const MIN: usize, const MAX: usize> AsRef<[T]> for ConstrainedVec<T, MIN, MAX> {
54    fn as_ref(&self) -> &[T] {
55        &self.0
56    }
57}
58
59impl<T, const MIN: usize, const MAX: usize> AsRef<Vec<T>> for ConstrainedVec<T, MIN, MAX> {
60    fn as_ref(&self) -> &Vec<T> {
61        &self.0
62    }
63}
64
65impl<T, const MIN: usize, const MAX: usize> std::ops::Deref for ConstrainedVec<T, MIN, MAX> {
66    type Target = [T];
67
68    fn deref(&self) -> &Self::Target {
69        &self.0
70    }
71}
72
73impl<T, const MIN: usize, const MAX: usize> TryFrom<Vec<T>> for ConstrainedVec<T, MIN, MAX> {
74    type Error = FieldError;
75
76    fn try_from(value: Vec<T>) -> Result<Self, Self::Error> {
77        Self::new(value)
78    }
79}
80
81impl<T, const MIN: usize, const MAX: usize> From<ConstrainedVec<T, MIN, MAX>> for Vec<T> {
82    fn from(value: ConstrainedVec<T, MIN, MAX>) -> Self {
83        value.0
84    }
85}
86
87impl<T: Serialize, const MIN: usize, const MAX: usize> Serialize for ConstrainedVec<T, MIN, MAX> {
88    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
89    where
90        S: Serializer,
91    {
92        self.0.serialize(serializer)
93    }
94}
95
96impl<'de, T: Deserialize<'de>, const MIN: usize, const MAX: usize> Deserialize<'de>
97    for ConstrainedVec<T, MIN, MAX>
98{
99    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
100    where
101        D: Deserializer<'de>,
102    {
103        let value = Vec::<T>::deserialize(deserializer)?;
104        Self::new(value).map_err(D::Error::custom)
105    }
106}
107
108impl<T: Validate, const MIN: usize, const MAX: usize> Validate for ConstrainedVec<T, MIN, MAX> {
109    fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
110        Vec::<T>::prepare(input, ctx);
111        if let Some(items) = input.as_array() {
112            ctx.check(check_len::<MIN, MAX>(items.len()));
113        }
114    }
115
116    fn validate(&self, ctx: &mut ValidationContext) {
117        self.0.validate(ctx);
118    }
119}
120
121impl<T: Schema + 'static, const MIN: usize, const MAX: usize> Schema
122    for ConstrainedVec<T, MIN, MAX>
123{
124    fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
125        SchemaObject::of_type("array")
126            .with("items", registry.subschema::<T>())
127            .with("minItems", MIN as u64)
128            .with("maxItems", MAX as u64)
129    }
130}
131
132impl<T: Dump, const MIN: usize, const MAX: usize> Dump for ConstrainedVec<T, MIN, MAX> {
133    fn dump(&self, opts: &DumpOptions) -> Result<Value, DumpError> {
134        self.0.dump(opts)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    #![allow(clippy::unwrap_used)]
141
142    use super::*;
143    use crate::context::ValidationContext;
144    use crate::dump::{Dump, DumpOptions};
145    use crate::schema::schema_for;
146    use crate::types::{SecretString, prepare_codes};
147    use serde_json::json;
148
149    type Pair = ConstrainedVec<i32, 2, 3>;
150
151    #[test]
152    fn constrained_vec_construction() {
153        assert_eq!(Pair::new(vec![1, 2]).unwrap().into_inner(), vec![1, 2]);
154        assert!(Pair::new(vec![1, 2, 3]).is_ok());
155        assert_eq!(Pair::new(vec![1]).unwrap_err().code, "too_short");
156        assert_eq!(Pair::new(vec![1, 2, 3, 4]).unwrap_err().code, "too_long");
157        assert_eq!(Pair::new(vec![1, 2]).unwrap().len(), 2);
158    }
159
160    #[test]
161    fn constrained_vec_serde_round_trip_and_reject() {
162        let value = Pair::new(vec![1, 2]).unwrap();
163        let encoded = serde_json::to_string(&value).unwrap();
164        assert_eq!(encoded, "[1,2]");
165        let round: Pair = serde_json::from_str(&encoded).unwrap();
166        assert_eq!(&*round, &[1, 2]);
167        assert!(serde_json::from_str::<Pair>("[1]").is_err());
168        assert!(serde_json::from_str::<Pair>("[1,2,3,4]").is_err());
169    }
170
171    #[test]
172    fn constrained_vec_prepare_lax_and_strict() {
173        let (out, codes) = prepare_codes::<Pair>(json!([1, 2]), ValidationContext::new());
174        assert!(codes.is_empty());
175        assert_eq!(out, json!([1, 2]));
176        assert_eq!(
177            prepare_codes::<Pair>(json!([1]), ValidationContext::new()).1,
178            ["too_short"]
179        );
180        assert_eq!(
181            prepare_codes::<Pair>(json!(1), ValidationContext::new()).1,
182            ["list_type"]
183        );
184        assert_eq!(
185            prepare_codes::<Pair>(json!(1), ValidationContext::new().with_strict(true)).1,
186            ["list_type"]
187        );
188        // Text input wraps a scalar into a one-item list, then length fails.
189        let (out, codes) =
190            prepare_codes::<Pair>(json!("3"), crate::context::ValidationContext::for_text());
191        assert_eq!(out, json!([3]));
192        assert_eq!(codes, ["too_short"]);
193    }
194
195    #[test]
196    fn constrained_vec_schema_dump_and_parse_value() {
197        assert_eq!(
198            schema_for::<Pair>().0.into_value(),
199            json!({"type": "array", "items": {"type": "integer", "format": "int32"}, "minItems": 2, "maxItems": 3})
200        );
201        let value = Pair::new(vec![1, 2, 3]).unwrap();
202        assert_eq!(value.dump(&DumpOptions::new()).unwrap(), json!([1, 2, 3]));
203
204        type Secrets = ConstrainedVec<SecretString, 1, 2>;
205        let secrets = Secrets::new(vec![SecretString::new("hunter2").unwrap()]).unwrap();
206        assert_eq!(
207            secrets.dump(&DumpOptions::new()).unwrap(),
208            json!(["**********"])
209        );
210
211        let parsed = crate::parse_value::<Pair>(json!([9, 8]), ValidationContext::new()).unwrap();
212        assert_eq!(&*parsed, &[9, 8]);
213        let err = crate::parse_value::<Pair>(json!([]), ValidationContext::new()).unwrap_err();
214        assert_eq!(err.errors[0].code, "too_short");
215    }
216
217    #[test]
218    fn constrained_vec_prepare_checks_elements_and_length() {
219        type Names = ConstrainedVec<String, 1, 2>;
220        let (out, codes) = prepare_codes::<Names>(json!([1, "ok", "x"]), ValidationContext::new());
221        assert_eq!(out, json!([1, "ok", "x"]));
222        assert_eq!(codes, ["string_type", "too_long"]);
223    }
224}