1use crate::error::{NomlError, Result};
69use crate::value::Value;
70use std::collections::HashMap;
71
72#[derive(Debug, Clone, PartialEq)]
74pub struct Schema {
75 pub fields: HashMap<String, FieldSchema>,
77 pub allow_additional: bool,
79}
80
81#[derive(Debug, Clone, PartialEq)]
83pub struct FieldSchema {
84 pub field_type: FieldType,
86 pub required: bool,
88 pub description: Option<String>,
90 pub default: Option<Value>,
92}
93
94#[derive(Debug, Clone, PartialEq)]
96pub enum FieldType {
97 String,
99 Integer,
101 Float,
103 Bool,
105 Binary,
107 DateTime,
109 Array(Box<FieldType>),
111 Table(Schema),
113 Any,
115 Union(Vec<FieldType>),
117}
118
119impl Schema {
120 pub fn new() -> Self {
122 Self {
123 fields: HashMap::new(),
124 allow_additional: true,
125 }
126 }
127
128 pub fn required_field(mut self, name: &str, field_type: FieldType) -> Self {
130 self.fields.insert(
131 name.to_string(),
132 FieldSchema {
133 field_type,
134 required: true,
135 description: None,
136 default: None,
137 },
138 );
139 self
140 }
141
142 pub fn optional_field(mut self, name: &str, field_type: FieldType) -> Self {
144 self.fields.insert(
145 name.to_string(),
146 FieldSchema {
147 field_type,
148 required: false,
149 description: None,
150 default: None,
151 },
152 );
153 self
154 }
155
156 pub fn field_with_default(mut self, name: &str, field_type: FieldType, default: Value) -> Self {
158 self.fields.insert(
159 name.to_string(),
160 FieldSchema {
161 field_type,
162 required: false,
163 description: None,
164 default: Some(default),
165 },
166 );
167 self
168 }
169
170 pub fn allow_additional(mut self, allow: bool) -> Self {
172 self.allow_additional = allow;
173 self
174 }
175
176 pub fn validate(&self, value: &Value) -> Result<()> {
178 match value {
179 Value::Table(table) => {
180 for (field_name, field_schema) in &self.fields {
182 if field_schema.required && !table.contains_key(field_name) {
183 return Err(NomlError::validation(format!(
184 "Required field '{field_name}' is missing"
185 )));
186 }
187 }
188
189 for (key, val) in table {
191 if let Some(field_schema) = self.fields.get(key) {
192 self.validate_field_type(val, &field_schema.field_type, key)?;
193 } else if !self.allow_additional {
194 return Err(NomlError::validation(format!(
195 "Additional field '{key}' is not allowed"
196 )));
197 }
198 }
199
200 Ok(())
201 }
202 _ => Err(NomlError::validation(
203 "Schema validation requires a table/object at the root".to_string(),
204 )),
205 }
206 }
207
208 fn validate_field_type(
210 &self,
211 value: &Value,
212 expected_type: &FieldType,
213 field_path: &str,
214 ) -> Result<()> {
215 match (value, expected_type) {
216 (Value::String(_), FieldType::String) => Ok(()),
217 (Value::Integer(_), FieldType::Integer) => Ok(()),
218 (Value::Float(_), FieldType::Float) => Ok(()),
219 (Value::Bool(_), FieldType::Bool) => Ok(()),
220 (Value::Binary(_), FieldType::Binary) => Ok(()),
221 #[cfg(feature = "chrono")]
222 (Value::DateTime(_), FieldType::DateTime) => Ok(()),
223 (_, FieldType::Any) => Ok(()),
224
225 (Value::Array(arr), FieldType::Array(element_type)) => {
226 for (i, item) in arr.iter().enumerate() {
227 let item_path = format!("{field_path}[{i}]");
228 self.validate_field_type(item, element_type, &item_path)?;
229 }
230 Ok(())
231 }
232
233 (Value::Table(_), FieldType::Table(nested_schema)) => nested_schema.validate(value),
234
235 (val, FieldType::Union(types)) => {
236 for field_type in types {
237 if self
238 .validate_field_type(val, field_type, field_path)
239 .is_ok()
240 {
241 return Ok(());
242 }
243 }
244 Err(NomlError::validation(format!(
245 "Field '{field_path}' does not match any of the expected types"
246 )))
247 }
248
249 _ => Err(NomlError::validation(format!(
250 "Field '{field_path}' has incorrect type. Expected {expected_type:?}, got {:?}",
251 self.value_type_name(value)
252 ))),
253 }
254 }
255
256 fn value_type_name(&self, value: &Value) -> &'static str {
258 match value {
259 Value::String(_) => "String",
260 Value::Integer(_) => "Integer",
261 Value::Float(_) => "Float",
262 Value::Bool(_) => "Bool",
263 Value::Array(_) => "Array",
264 Value::Table(_) => "Table",
265 Value::Null => "Null",
266 Value::Size(_) => "Size",
267 Value::Duration(_) => "Duration",
268 Value::Binary(_) => "Binary",
269 #[cfg(feature = "chrono")]
270 Value::DateTime(_) => "DateTime",
271 }
272 }
273}
274
275impl Default for Schema {
276 fn default() -> Self {
277 Self::new()
278 }
279}
280
281pub struct SchemaBuilder {
283 schema: Schema,
284}
285
286impl SchemaBuilder {
287 pub fn new() -> Self {
289 Self {
290 schema: Schema::new(),
291 }
292 }
293
294 pub fn require_string(mut self, name: &str) -> Self {
296 self.schema = self.schema.required_field(name, FieldType::String);
297 self
298 }
299
300 pub fn require_integer(mut self, name: &str) -> Self {
302 self.schema = self.schema.required_field(name, FieldType::Integer);
303 self
304 }
305
306 pub fn optional_bool(mut self, name: &str) -> Self {
308 self.schema = self.schema.optional_field(name, FieldType::Bool);
309 self
310 }
311
312 pub fn build(self) -> Schema {
314 self.schema
315 }
316}
317
318impl Default for SchemaBuilder {
319 fn default() -> Self {
320 Self::new()
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::value::Value;
328 use std::collections::BTreeMap;
329
330 #[test]
331 fn test_basic_schema_validation() {
332 let schema = Schema::new()
333 .required_field("name", FieldType::String)
334 .required_field("port", FieldType::Integer)
335 .optional_field("debug", FieldType::Bool);
336
337 let mut config = BTreeMap::new();
339 config.insert("name".to_string(), Value::String("test".to_string()));
340 config.insert("port".to_string(), Value::Integer(8080));
341 config.insert("debug".to_string(), Value::Bool(true));
342
343 let valid_value = Value::Table(config);
344 assert!(schema.validate(&valid_value).is_ok());
345
346 let mut invalid_config = BTreeMap::new();
348 invalid_config.insert("name".to_string(), Value::String("test".to_string()));
349 let invalid_value = Value::Table(invalid_config);
352 assert!(schema.validate(&invalid_value).is_err());
353 }
354
355 #[test]
356 fn test_schema_builder() {
357 let schema = SchemaBuilder::new()
358 .require_string("app_name")
359 .require_integer("version")
360 .optional_bool("debug")
361 .build();
362
363 let mut config = BTreeMap::new();
364 config.insert("app_name".to_string(), Value::String("MyApp".to_string()));
365 config.insert("version".to_string(), Value::Integer(1));
366
367 let value = Value::Table(config);
368 assert!(schema.validate(&value).is_ok());
369 }
370
371 #[test]
372 fn test_array_validation() {
373 let schema =
374 Schema::new().required_field("tags", FieldType::Array(Box::new(FieldType::String)));
375
376 let mut config = BTreeMap::new();
377 config.insert(
378 "tags".to_string(),
379 Value::Array(vec![
380 Value::String("web".to_string()),
381 Value::String("api".to_string()),
382 ]),
383 );
384
385 let value = Value::Table(config);
386 assert!(schema.validate(&value).is_ok());
387
388 let mut invalid_config = BTreeMap::new();
390 invalid_config.insert(
391 "tags".to_string(),
392 Value::Array(vec![
393 Value::String("web".to_string()),
394 Value::Integer(123), ]),
396 );
397
398 let invalid_value = Value::Table(invalid_config);
399 assert!(schema.validate(&invalid_value).is_err());
400 }
401}