Skip to main content

vld/collections/
set.rs

1use serde_json::Value;
2use std::collections::HashSet;
3
4use crate::error::{IssueCode, PathSegment, SizeOrigin, VldError};
5use crate::schema::VldSchema;
6
7/// Schema for validating a JSON array into a `HashSet` (unique elements).
8///
9/// Created via [`vld::set()`](crate::set).
10///
11/// Duplicates (after validation) are silently merged, so the generated JSON
12/// Schema does **not** declare `uniqueItems` (duplicate input is valid).
13///
14/// # Example
15/// ```ignore
16/// let schema = vld::set(vld::string().min(1));
17/// // Input: ["a", "b", "a"]
18/// // Output: HashSet { "a", "b" }
19/// ```
20pub struct ZSet<T: VldSchema> {
21    element: T,
22    min_size: Option<usize>,
23    max_size: Option<usize>,
24}
25
26impl<T: VldSchema> ZSet<T> {
27    /// Create the schema (the same as its `vld::` constructor function).
28    pub fn new(element: T) -> Self {
29        Self {
30            element,
31            min_size: None,
32            max_size: None,
33        }
34    }
35
36    /// Minimum number of unique elements.
37    pub fn min_size(mut self, n: usize) -> Self {
38        self.min_size = Some(n);
39        self
40    }
41
42    /// Maximum number of unique elements.
43    pub fn max_size(mut self, n: usize) -> Self {
44        self.max_size = Some(n);
45        self
46    }
47}
48
49impl<T> VldSchema for ZSet<T>
50where
51    T: VldSchema,
52    T::Output: Eq + std::hash::Hash,
53{
54    type Output = HashSet<T::Output>;
55
56    fn parse_value(&self, value: &Value) -> Result<Self::Output, VldError> {
57        let arr = value
58            .as_array()
59            .ok_or_else(|| crate::primitives::type_error(&None, "array", value))?;
60
61        let mut result = HashSet::new();
62        let mut errors = VldError::new();
63        // An inner `Err` may carry no issues: track failure, not issue count.
64        let mut failed = false;
65
66        for (i, item) in arr.iter().enumerate() {
67            match self.element.parse_value(item) {
68                Ok(v) => {
69                    result.insert(v);
70                }
71                Err(e) => {
72                    failed = true;
73                    errors = errors.merge(e.with_prefix(PathSegment::Index(i)));
74                }
75            }
76        }
77
78        if let Some(min) = self.min_size {
79            if result.len() < min {
80                errors.push_code(IssueCode::TooSmall {
81                    origin: SizeOrigin::Set,
82                    minimum: min.into(),
83                    inclusive: true,
84                });
85            }
86        }
87
88        if let Some(max) = self.max_size {
89            if result.len() > max {
90                errors.push_code(IssueCode::TooBig {
91                    origin: SizeOrigin::Set,
92                    maximum: max.into(),
93                    inclusive: true,
94                });
95            }
96        }
97
98        if errors.is_empty() && !failed {
99            Ok(result)
100        } else {
101            Err(errors)
102        }
103    }
104}
105
106/// `min_size` becomes `minItems` (n unique items need at least n items);
107/// `max_size` is *not* emitted as `maxItems`, because duplicates in the
108/// input are allowed and do not count towards the limit. The bound is
109/// exposed as `x-maxUniqueItems` instead.
110#[cfg(feature = "openapi")]
111impl<T> crate::openapi::JsonSchema for ZSet<T>
112where
113    T: VldSchema + crate::openapi::JsonSchema,
114    T::Output: Eq + std::hash::Hash,
115{
116    fn json_schema(&self) -> serde_json::Value {
117        let mut schema = serde_json::json!({
118            "type": "array",
119            "items": self.element.json_schema(),
120        });
121        if let Some(min) = self.min_size {
122            // `minItems` counts duplicates too; generators need the distinct count.
123            schema["minItems"] = serde_json::json!(min);
124            schema["x-minUniqueItems"] = serde_json::json!(min);
125        }
126        if let Some(max) = self.max_size {
127            schema["x-maxUniqueItems"] = serde_json::json!(max);
128        }
129        schema
130    }
131}
132
133#[cfg(feature = "openapi")]
134impl<T> crate::openapi::CollectNestedSchemas for ZSet<T>
135where
136    T: VldSchema + crate::openapi::CollectNestedSchemas,
137    T::Output: Eq + std::hash::Hash,
138{
139    fn collect_nested_schemas(&self, out: &mut Vec<crate::schema::NestedSchemaEntry>) {
140        self.element.collect_nested_schemas(out);
141    }
142}