adapters/schema/
object.rs1use super::SchemaValidator;
6use crate::error::ValidationError;
7use crate::value::Value;
8use std::collections::BTreeMap;
9
10#[derive(Default)]
12pub struct ObjectSchema {
13 field_order: Vec<String>,
14 field_map: BTreeMap<String, Box<dyn SchemaValidator>>,
15 strict: bool,
16 required: bool,
17 optional: bool,
18}
19
20impl ObjectSchema {
21 pub fn new() -> Self {
23 Self::default()
24 }
25
26 pub fn field(mut self, name: &str, schema: impl SchemaValidator + 'static) -> Self {
28 self.field_order.push(name.to_string());
29 self.field_map.insert(name.to_string(), Box::new(schema));
30 self
31 }
32
33 pub fn strict(mut self) -> Self {
35 self.strict = true;
36 self
37 }
38
39 pub fn required(mut self) -> Self {
41 self.required = true;
42 self
43 }
44
45 pub fn optional(mut self) -> Self {
47 self.optional = true;
48 self
49 }
50}
51
52impl SchemaValidator for ObjectSchema {
53 fn validate(&self, value: &Value, field: &str) -> Result<(), ValidationError> {
54 if self.optional && value.is_null() {
55 return Ok(());
56 }
57 if self.required && value.is_null() {
58 return Err(ValidationError::new(field, "field is required", "required"));
59 }
60 let obj = match value.as_object() {
61 Some(o) => o,
62 None => {
63 return Err(ValidationError::new(
64 field,
65 format!("expected object, got {}", value.type_name()),
66 "type_mismatch",
67 ));
68 }
69 };
70
71 if self.strict {
72 for key in obj.keys() {
73 if !self.field_map.contains_key(key) {
74 return Err(ValidationError::new(
75 field,
76 format!("unknown field '{key}' (strict mode)"),
77 "unknown_field",
78 ));
79 }
80 }
81 }
82
83 for name in &self.field_order {
84 let schema = &self.field_map[name];
85 let child_field = if field == "root" || field.is_empty() {
86 name.clone()
87 } else {
88 format!("{field}.{name}")
89 };
90
91 let val = match obj.get(name) {
92 Some(v) => v.clone(),
93 None => {
94 if let Some(def) = schema.default_value() {
95 def
96 } else if schema.is_required() {
97 return Err(ValidationError::new(
98 &child_field,
99 format!("required field '{name}' is missing"),
100 "required",
101 ));
102 } else {
103 Value::Null
104 }
105 }
106 };
107
108 schema.validate(&val, &child_field)?;
109 }
110 Ok(())
111 }
112
113 fn is_required(&self) -> bool {
114 self.required
115 }
116 fn default_value(&self) -> Option<Value> {
117 None
118 }
119 fn schema_type(&self) -> &'static str {
120 "object"
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::schema::{IntegerSchema, StringSchema};
128
129 fn simple_user_schema() -> ObjectSchema {
130 ObjectSchema::new()
131 .field("username", StringSchema::new().required().min_length(3))
132 .field("age", IntegerSchema::new().required().min(18))
133 }
134
135 fn make_obj(fields: &[(&str, Value)]) -> Value {
136 let mut m = BTreeMap::new();
137 for (k, v) in fields {
138 m.insert(k.to_string(), v.clone());
139 }
140 Value::Object(m)
141 }
142
143 #[test]
144 fn test_object_valid() {
145 let s = simple_user_schema();
146 let v = make_obj(&[
147 ("username", Value::String("alice".into())),
148 ("age", Value::Int(25)),
149 ]);
150 assert!(s.validate(&v, "root").is_ok());
151 }
152
153 #[test]
154 fn test_object_missing_required_field() {
155 let s = simple_user_schema();
156 let v = make_obj(&[("username", Value::String("alice".into()))]);
157 assert!(s.validate(&v, "root").is_err());
158 }
159
160 #[test]
161 fn test_object_field_validation_fails() {
162 let s = simple_user_schema();
163 let v = make_obj(&[
164 ("username", Value::String("al".into())),
165 ("age", Value::Int(25)),
166 ]);
167 let err = s.validate(&v, "root").unwrap_err();
168 assert!(err.field.contains("username"), "field: {}", err.field);
169 }
170
171 #[test]
172 fn test_object_strict_rejects_unknown() {
173 let s = simple_user_schema().strict();
174 let v = make_obj(&[
175 ("username", Value::String("alice".into())),
176 ("age", Value::Int(25)),
177 ("extra", Value::Bool(true)),
178 ]);
179 assert!(s.validate(&v, "root").is_err());
180 }
181
182 #[test]
183 fn test_object_not_an_object_fails() {
184 let s = simple_user_schema();
185 assert!(
186 s.validate(&Value::String("not an object".into()), "root")
187 .is_err()
188 );
189 }
190
191 #[test]
192 fn test_object_optional_null_passes() {
193 let s = ObjectSchema::new().optional();
194 assert!(s.validate(&Value::Null, "addr").is_ok());
195 }
196
197 #[test]
198 fn test_nested_error_path() {
199 let inner = ObjectSchema::new().field("city", StringSchema::new().required().min_length(3));
200 let outer = ObjectSchema::new().field("address", inner);
201 let bad_city = make_obj(&[("city", Value::String("ab".into()))]);
202 let v = make_obj(&[("address", bad_city)]);
203 let err = outer.validate(&v, "root").unwrap_err();
204 assert!(err.field.contains("address"), "path: {}", err.field);
205 }
206}