zod_rs/schema/
tuple.rs

1use crate::schema::Schema;
2use serde_json::Value;
3use std::{fmt::Debug, sync::Arc};
4use zod_rs_util::{ValidateResult, ValidationError, ValidationResult, ValidationType};
5
6#[derive(Debug, Clone)]
7pub struct TupleSchema {
8    elements: Vec<Arc<dyn TupleElementValidator>>,
9}
10
11impl TupleSchema {
12    pub fn new() -> Self {
13        Self {
14            elements: Vec::new(),
15        }
16    }
17
18    pub fn element<S, T>(mut self, schema: S) -> Self
19    where
20        S: Schema<T> + Send + Sync + 'static,
21        T: serde::Serialize + Send + Sync + Debug + 'static,
22    {
23        self.elements
24            .push(Arc::new(TupleElementValidatorImpl::new(schema)));
25        self
26    }
27}
28
29impl Default for TupleSchema {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35trait TupleElementValidator: Send + Sync + Debug {
36    fn validate_element(&self, value: &Value) -> ValidateResult<Value>;
37}
38
39#[derive(Debug)]
40struct TupleElementValidatorImpl<S, T> {
41    schema: S,
42    _phantom: std::marker::PhantomData<T>,
43}
44
45impl<S, T> TupleElementValidatorImpl<S, T> {
46    fn new(schema: S) -> Self {
47        Self {
48            schema,
49            _phantom: std::marker::PhantomData,
50        }
51    }
52}
53
54impl<S, T> TupleElementValidator for TupleElementValidatorImpl<S, T>
55where
56    S: Schema<T> + Send + Sync + Debug,
57    T: serde::Serialize + Send + Sync + Debug,
58{
59    fn validate_element(&self, value: &Value) -> ValidateResult<Value> {
60        let validated = self.schema.validate(value)?;
61        serde_json::to_value(validated).map_err(|e| {
62            ValidationError::custom(format!("Failed to serialize validated value: {}", e)).into()
63        })
64    }
65}
66
67impl Schema<Value> for TupleSchema {
68    fn validate(&self, value: &Value) -> ValidateResult<Value> {
69        let arr = value.as_array().ok_or_else(|| {
70            ValidationResult::from(ValidationError::invalid_type(
71                ValidationType::Array,
72                ValidationType::from(value),
73            ))
74        })?;
75
76        if arr.len() != self.elements.len() {
77            return Err(ValidationError::custom(format!(
78                "Expected tuple of {} elements, got {}",
79                self.elements.len(),
80                arr.len()
81            ))
82            .into());
83        }
84
85        let mut result = Vec::with_capacity(arr.len());
86        let mut validation_result = ValidationResult::new();
87
88        for (i, (element, schema)) in arr.iter().zip(&self.elements).enumerate() {
89            match schema.validate_element(element) {
90                Ok(validated) => result.push(validated),
91                Err(mut errors) => {
92                    errors.prefix_path(i.to_string());
93                    validation_result.merge(errors);
94                }
95            }
96        }
97
98        if validation_result.is_empty() {
99            Ok(Value::Array(result))
100        } else {
101            Err(validation_result)
102        }
103    }
104}
105
106pub fn tuple() -> TupleSchema {
107    TupleSchema::new()
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::schema::{boolean, number, string};
114    use serde_json::json;
115
116    #[test]
117    fn test_empty_tuple() {
118        let schema = tuple();
119        assert!(schema.validate(&json!([])).is_ok());
120        assert!(schema.validate(&json!([1])).is_err());
121    }
122
123    #[test]
124    fn test_single_element_tuple() {
125        let schema = tuple().element(string());
126        assert!(schema.validate(&json!(["hello"])).is_ok());
127        assert!(schema.validate(&json!([123])).is_err());
128        assert!(schema.validate(&json!([])).is_err());
129        assert!(schema.validate(&json!(["a", "b"])).is_err());
130    }
131
132    #[test]
133    fn test_two_element_tuple() {
134        let schema = tuple().element(string()).element(number());
135        assert!(schema.validate(&json!(["hello", 42])).is_ok());
136        assert!(schema.validate(&json!([42, "hello"])).is_err());
137        assert!(schema.validate(&json!(["hello"])).is_err());
138        assert!(schema.validate(&json!(["hello", 42, true])).is_err());
139    }
140
141    #[test]
142    fn test_three_element_tuple() {
143        let schema = tuple()
144            .element(string())
145            .element(number())
146            .element(boolean());
147
148        assert!(schema.validate(&json!(["test", 123, true])).is_ok());
149        assert!(schema.validate(&json!(["test", 123, false])).is_ok());
150        assert!(schema.validate(&json!(["test", 123])).is_err());
151    }
152
153    #[test]
154    fn test_tuple_with_constraints() {
155        let schema = tuple()
156            .element(string().min(3))
157            .element(number().positive());
158
159        assert!(schema.validate(&json!(["abc", 1])).is_ok());
160        assert!(schema.validate(&json!(["ab", 1])).is_err()); // String too short
161        assert!(schema.validate(&json!(["abc", -1])).is_err()); // Number not positive
162    }
163
164    #[test]
165    fn test_tuple_returns_array_value() {
166        let schema = tuple().element(string()).element(number());
167        let result = schema.validate(&json!(["hello", 42]));
168        assert!(result.is_ok());
169        assert_eq!(result.unwrap(), json!(["hello", 42.0]));
170    }
171
172    #[test]
173    fn test_tuple_rejects_non_array() {
174        let schema = tuple().element(string());
175        assert!(schema.validate(&json!("hello")).is_err());
176        assert!(schema.validate(&json!(123)).is_err());
177        assert!(schema.validate(&json!(null)).is_err());
178        assert!(schema.validate(&json!({})).is_err());
179    }
180
181    #[test]
182    fn test_tuple_error_includes_index() {
183        let schema = tuple().element(string()).element(number());
184        let result = schema.validate(&json!(["hello", "not a number"]));
185        assert!(result.is_err());
186        let err = result.unwrap_err();
187        // Error should include path information about which element failed
188        assert!(!err.issues.is_empty());
189    }
190
191    #[test]
192    fn test_homogeneous_tuple() {
193        let schema = tuple()
194            .element(number())
195            .element(number())
196            .element(number());
197
198        assert!(schema.validate(&json!([1, 2, 3])).is_ok());
199        assert!(schema.validate(&json!([1, 2])).is_err());
200        assert!(schema.validate(&json!([1, 2, "3"])).is_err());
201    }
202}