Skip to main content

vld/collections/
record.rs

1use serde_json::Value;
2use std::collections::HashMap;
3
4use crate::error::{value_type_name, IssueCode, PathSegment, VldError};
5use crate::schema::VldSchema;
6
7/// Schema for validating JSON objects as key-value records.
8/// Created via [`vld::record()`](crate::record).
9///
10/// All keys are strings (JSON constraint). Values are validated against the inner schema.
11///
12/// # Example
13/// ```
14/// use vld::prelude::*;
15///
16/// let schema = vld::record(vld::number().int().positive());
17/// let result = schema.parse(r#"{"a": 1, "b": 2}"#).unwrap();
18/// assert_eq!(result.get("a"), Some(&1));
19/// ```
20pub struct ZRecord<V: VldSchema> {
21    value_schema: V,
22    min_keys: Option<usize>,
23    max_keys: Option<usize>,
24}
25
26impl<V: VldSchema> ZRecord<V> {
27    pub fn new(value_schema: V) -> Self {
28        Self {
29            value_schema,
30            min_keys: None,
31            max_keys: None,
32        }
33    }
34
35    /// Minimum number of keys.
36    pub fn min_keys(mut self, n: usize) -> Self {
37        self.min_keys = Some(n);
38        self
39    }
40
41    /// Maximum number of keys.
42    pub fn max_keys(mut self, n: usize) -> Self {
43        self.max_keys = Some(n);
44        self
45    }
46
47    #[allow(dead_code)]
48    pub(crate) fn value_schema_ref(&self) -> &V {
49        &self.value_schema
50    }
51
52    /// Generate a JSON Schema (called by [`JsonSchema`](crate::json_schema::JsonSchema) trait impl).
53    ///
54    /// Requires the `openapi` feature.
55    #[cfg(feature = "openapi")]
56    pub fn to_json_schema_inner(&self) -> serde_json::Value
57    where
58        V: crate::json_schema::JsonSchema,
59    {
60        serde_json::json!({
61            "type": "object",
62            "additionalProperties": self.value_schema.json_schema(),
63        })
64    }
65}
66
67impl<V: VldSchema> VldSchema for ZRecord<V> {
68    type Output = HashMap<String, V::Output>;
69
70    fn parse_value(&self, value: &Value) -> Result<HashMap<String, V::Output>, VldError> {
71        let obj = value.as_object().ok_or_else(|| {
72            VldError::single(
73                IssueCode::InvalidType {
74                    expected: "object".to_string(),
75                    received: value_type_name(value),
76                },
77                format!("Expected object, received {}", value_type_name(value)),
78            )
79        })?;
80
81        let mut errors = VldError::new();
82
83        if let Some(min) = self.min_keys {
84            if obj.len() < min {
85                errors.push(
86                    IssueCode::TooSmall {
87                        minimum: min as f64,
88                        inclusive: true,
89                    },
90                    format!("Record must have at least {} keys", min),
91                );
92            }
93        }
94
95        if let Some(max) = self.max_keys {
96            if obj.len() > max {
97                errors.push(
98                    IssueCode::TooBig {
99                        maximum: max as f64,
100                        inclusive: true,
101                    },
102                    format!("Record must have at most {} keys", max),
103                );
104            }
105        }
106
107        let mut result = HashMap::new();
108
109        for (key, val) in obj {
110            match self.value_schema.parse_value(val) {
111                Ok(v) => {
112                    result.insert(key.clone(), v);
113                }
114                Err(e) => {
115                    errors = errors.merge(e.with_prefix(PathSegment::Field(key.clone())));
116                }
117            }
118        }
119
120        if errors.is_empty() {
121            Ok(result)
122        } else {
123            Err(errors)
124        }
125    }
126}