Skip to main content

tanzim_validate/
schema.rs

1//! Build validators from a self-describing schema document.
2//!
3//! A schema is an ordinary [`Value`] tree (parse it with serde via [`SchemaValue`], or hand
4//! one over directly from `tanzim-parse`). Every node is a map with a `"type"` tag plus the
5//! options for that validator; the [`Registry`] dispatches on the tag to a constructor.
6//! Custom validator types can be added with [`Registry::register`].
7
8use std::collections::HashMap;
9
10use serde::de::{self, Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
11
12#[cfg(feature = "boolean")]
13use crate::Bool;
14#[cfg(feature = "dynamic_map")]
15use crate::DynamicMap;
16#[cfg(feature = "either")]
17use crate::Either;
18#[cfg(feature = "enumeration")]
19use crate::Enum;
20#[cfg(feature = "float")]
21use crate::Float;
22#[cfg(feature = "integer")]
23use crate::Integer;
24#[cfg(feature = "list")]
25use crate::List;
26#[cfg(feature = "non_empty")]
27use crate::NonEmpty;
28#[cfg(feature = "number")]
29use crate::Number;
30#[cfg(feature = "percentage")]
31use crate::Percentage;
32use crate::Segment;
33#[cfg(feature = "static_map")]
34use crate::StaticMap;
35#[cfg(feature = "string")]
36use crate::Str;
37use crate::Validator;
38#[cfg(feature = "net")]
39use crate::{Domain, Email, Host, IpAddr, Port, SocketAddr};
40#[cfg(feature = "path")]
41use crate::{Path, PathKind};
42use tanzim_value::{LocatedValue, Location, Map, Value};
43
44/// Location used for values produced by the serde deserializer, which carry no source span.
45fn schema_location() -> Location {
46    Location::at("schema", "", None, None, None)
47}
48
49/// A [`Value`] that can be produced by any serde deserializer (e.g. `serde_json`).
50///
51/// This is the bridge between the serde world and tanzim's own [`Value`] type. Deserialize a
52/// schema into a `SchemaValue`, then feed it to [`build_value`] or a [`Registry`].
53#[derive(Debug, Clone, PartialEq)]
54pub struct SchemaValue(pub Value);
55
56impl SchemaValue {
57    pub fn value(&self) -> &Value {
58        &self.0
59    }
60
61    pub fn into_value(self) -> Value {
62        self.0
63    }
64}
65
66struct SchemaValueVisitor;
67
68impl<'de> Visitor<'de> for SchemaValueVisitor {
69    type Value = SchemaValue;
70
71    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.write_str("a configuration value (no null)")
73    }
74
75    fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
76        Ok(SchemaValue(Value::Bool(value)))
77    }
78
79    fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
80        match isize::try_from(value) {
81            Ok(number) => Ok(SchemaValue(Value::Int(number))),
82            Err(_) => Err(de::Error::custom("integer out of range")),
83        }
84    }
85
86    fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
87        match isize::try_from(value) {
88            Ok(number) => Ok(SchemaValue(Value::Int(number))),
89            Err(_) => Err(de::Error::custom("integer out of range")),
90        }
91    }
92
93    fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> {
94        Ok(SchemaValue(Value::Float(value)))
95    }
96
97    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
98        Ok(SchemaValue(Value::String(value.to_string())))
99    }
100
101    fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
102        Ok(SchemaValue(Value::String(value)))
103    }
104
105    fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
106        Ok(SchemaValue(Value::Null))
107    }
108
109    fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
110        Ok(SchemaValue(Value::Null))
111    }
112
113    fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
114        let mut items = Vec::new();
115        while let Some(element) = seq.next_element::<SchemaValue>()? {
116            items.push(LocatedValue::new(element.0, schema_location()));
117        }
118        Ok(SchemaValue(Value::List(items)))
119    }
120
121    fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
122        let mut map = Map::new();
123        while let Some((key, element)) = access.next_entry::<String, SchemaValue>()? {
124            map.insert(key, LocatedValue::new(element.0, schema_location()));
125        }
126        Ok(SchemaValue(Value::Map(map)))
127    }
128}
129
130impl<'de> Deserialize<'de> for SchemaValue {
131    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
132        deserializer.deserialize_any(SchemaValueVisitor)
133    }
134}
135
136/// What went wrong while building a validator from a schema document.
137#[derive(Debug, Clone, PartialEq)]
138pub enum SchemaErrorKind {
139    /// A validator node was not a map.
140    NotMap,
141    /// The `"type"` tag named a validator the registry does not know.
142    UnknownType { tag: String },
143    /// A required field was absent.
144    MissingField { field: String },
145    /// A field had the wrong value type.
146    WrongType {
147        field: String,
148        expected: &'static str,
149    },
150    /// A field had a structurally valid but semantically invalid value.
151    InvalidValue { field: String, message: String },
152}
153
154impl std::fmt::Display for SchemaErrorKind {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self {
157            Self::NotMap => write!(f, "validator schema must be a map"),
158            Self::UnknownType { tag } => write!(f, "unknown validator type `{tag}`"),
159            Self::MissingField { field } => write!(f, "missing field `{field}`"),
160            Self::WrongType { field, expected } => {
161                write!(f, "field `{field}` must be {expected}")
162            }
163            Self::InvalidValue { field, message } => write!(f, "field `{field}`: {message}"),
164        }
165    }
166}
167
168/// A schema-construction failure, with a breadcrumb path and (when known) source location.
169#[derive(Debug, Clone, PartialEq)]
170pub struct SchemaError {
171    pub kind: SchemaErrorKind,
172    pub path: Vec<Segment>,
173    /// Boxed to keep the error small (`clippy::result_large_err`).
174    pub location: Option<Box<Location>>,
175}
176
177impl std::fmt::Display for SchemaError {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        for (position, segment) in self.path.iter().enumerate() {
180            match segment {
181                Segment::Key(key) => {
182                    if position > 0 {
183                        write!(f, ".")?;
184                    }
185                    write!(f, "{key}")?;
186                }
187                Segment::Index(index) => write!(f, "[{index}]")?,
188            }
189        }
190        if !self.path.is_empty() {
191            write!(f, ": ")?;
192        }
193        write!(f, "{}", self.kind)?;
194        if let Some(location) = &self.location {
195            write!(f, " at {location}")?;
196        }
197        Ok(())
198    }
199}
200
201impl std::error::Error for SchemaError {}
202
203/// A validator node: the map of options plus what's needed to read them and recurse.
204///
205/// Passed to each [`Registry`] constructor. Custom constructors use its readers
206/// (`opt_int`, `flag`, `child`, …) to pull options and build nested validators.
207pub struct Node<'a> {
208    registry: &'a Registry,
209    map: &'a Map,
210    location: &'a Location,
211    path: Vec<Segment>,
212}
213
214impl Node<'_> {
215    /// Build an error anchored at this node.
216    pub fn error(&self, kind: SchemaErrorKind) -> SchemaError {
217        SchemaError {
218            kind,
219            path: self.path.clone(),
220            location: Some(Box::new(self.location.clone())),
221        }
222    }
223
224    fn missing(&self, field: &str) -> SchemaError {
225        self.error(SchemaErrorKind::MissingField {
226            field: field.to_string(),
227        })
228    }
229
230    fn wrong(&self, field: &str, expected: &'static str) -> SchemaError {
231        self.error(SchemaErrorKind::WrongType {
232            field: field.to_string(),
233            expected,
234        })
235    }
236
237    /// Read a required string field.
238    pub fn req_str(&self, field: &str) -> Result<&str, SchemaError> {
239        match self.opt_str(field)? {
240            Some(text) => Ok(text),
241            None => Err(self.missing(field)),
242        }
243    }
244
245    /// Read an optional string field.
246    pub fn opt_str(&self, field: &str) -> Result<Option<&str>, SchemaError> {
247        match self.map.get(field) {
248            None => Ok(None),
249            Some(entry) => match entry.value() {
250                Value::String(text) => Ok(Some(text)),
251                _ => Err(self.wrong(field, "a string")),
252            },
253        }
254    }
255
256    /// Read an optional integer field.
257    pub fn opt_int(&self, field: &str) -> Result<Option<isize>, SchemaError> {
258        match self.map.get(field) {
259            None => Ok(None),
260            Some(entry) => match entry.value() {
261                Value::Int(number) => Ok(Some(*number)),
262                _ => Err(self.wrong(field, "an integer")),
263            },
264        }
265    }
266
267    /// Read an optional non-negative integer field as a `usize`.
268    pub fn opt_usize(&self, field: &str) -> Result<Option<usize>, SchemaError> {
269        match self.opt_int(field)? {
270            None => Ok(None),
271            Some(number) => match usize::try_from(number) {
272                Ok(value) => Ok(Some(value)),
273                Err(_) => Err(self.error(SchemaErrorKind::InvalidValue {
274                    field: field.to_string(),
275                    message: "must be non-negative".to_string(),
276                })),
277            },
278        }
279    }
280
281    /// Read an optional number field (integer or float) as an `f64`.
282    pub fn opt_f64(&self, field: &str) -> Result<Option<f64>, SchemaError> {
283        match self.map.get(field) {
284            None => Ok(None),
285            Some(entry) => match entry.value() {
286                Value::Float(number) => Ok(Some(*number)),
287                Value::Int(number) => Ok(Some(*number as f64)),
288                _ => Err(self.wrong(field, "a number")),
289            },
290        }
291    }
292
293    /// Read an optional boolean field.
294    pub fn opt_bool(&self, field: &str) -> Result<Option<bool>, SchemaError> {
295        match self.map.get(field) {
296            None => Ok(None),
297            Some(entry) => match entry.value() {
298                Value::Bool(value) => Ok(Some(*value)),
299                _ => Err(self.wrong(field, "a boolean")),
300            },
301        }
302    }
303
304    /// Read a boolean field, defaulting to `false` when absent.
305    pub fn flag(&self, field: &str) -> Result<bool, SchemaError> {
306        match self.opt_bool(field)? {
307            Some(value) => Ok(value),
308            None => Ok(false),
309        }
310    }
311
312    /// Read a list field as raw values (used by `enum`). Absent → empty.
313    pub fn values(&self, field: &str) -> Result<Vec<Value>, SchemaError> {
314        match self.map.get(field) {
315            None => Ok(Vec::new()),
316            Some(entry) => match entry.value() {
317                Value::List(items) => {
318                    let mut out = Vec::new();
319                    for item in items {
320                        out.push(item.value().clone());
321                    }
322                    Ok(out)
323                }
324                _ => Err(self.wrong(field, "a list")),
325            },
326        }
327    }
328
329    /// Read a list-of-strings field (used by `path.extensions`, `url.schemes`). Absent → empty.
330    pub fn str_list(&self, field: &str) -> Result<Vec<String>, SchemaError> {
331        match self.map.get(field) {
332            None => Ok(Vec::new()),
333            Some(entry) => match entry.value() {
334                Value::List(items) => {
335                    let mut out = Vec::new();
336                    for item in items {
337                        match item.value() {
338                            Value::String(text) => out.push(text.clone()),
339                            _ => return Err(self.wrong(field, "a list of strings")),
340                        }
341                    }
342                    Ok(out)
343                }
344                _ => Err(self.wrong(field, "a list of strings")),
345            },
346        }
347    }
348
349    /// Build a required nested validator from a sub-schema field.
350    pub fn child(&self, field: &str) -> Result<Box<dyn Validator>, SchemaError> {
351        match self.map.get(field) {
352            Some(entry) => self.build_sub(entry, field),
353            None => Err(self.missing(field)),
354        }
355    }
356
357    /// Build an optional nested validator from a sub-schema field.
358    pub fn opt_child(&self, field: &str) -> Result<Option<Box<dyn Validator>>, SchemaError> {
359        match self.map.get(field) {
360            Some(entry) => Ok(Some(self.build_sub(entry, field)?)),
361            None => Ok(None),
362        }
363    }
364
365    fn build_sub(
366        &self,
367        entry: &LocatedValue,
368        field: &str,
369    ) -> Result<Box<dyn Validator>, SchemaError> {
370        let mut path = self.path.clone();
371        path.push(Segment::Key(field.to_string()));
372        let node = self.registry.node(entry, path)?;
373        self.registry.build_node(&node)
374    }
375}
376
377/// Constructs one validator kind from its [`Node`].
378pub type Constructor = Box<dyn Fn(&Node) -> Result<Box<dyn Validator>, SchemaError>>;
379
380/// Maps `"type"` tags to validator constructors.
381pub struct Registry {
382    constructors: HashMap<String, Constructor>,
383}
384
385impl Default for Registry {
386    fn default() -> Self {
387        Self::with_builtins()
388    }
389}
390
391impl Registry {
392    /// An empty registry with no constructors.
393    pub fn empty() -> Self {
394        Self {
395            constructors: HashMap::new(),
396        }
397    }
398
399    /// Register (or replace) the constructor for `tag`.
400    pub fn register(
401        &mut self,
402        tag: impl Into<String>,
403        constructor: impl Fn(&Node) -> Result<Box<dyn Validator>, SchemaError> + 'static,
404    ) {
405        self.constructors.insert(tag.into(), Box::new(constructor));
406    }
407
408    /// Build a validator from a located schema node, seeding source locations into errors.
409    pub fn build(&self, value: &LocatedValue) -> Result<Box<dyn Validator>, SchemaError> {
410        let node = self.node(value, Vec::new())?;
411        self.build_node(&node)
412    }
413
414    /// Build a validator from a bare [`Value`] (errors carry no source location).
415    pub fn build_value(&self, value: &Value) -> Result<Box<dyn Validator>, SchemaError> {
416        let located = LocatedValue::new(value.clone(), schema_location());
417        self.build(&located)
418    }
419
420    fn node<'a>(
421        &'a self,
422        value: &'a LocatedValue,
423        path: Vec<Segment>,
424    ) -> Result<Node<'a>, SchemaError> {
425        match value.value() {
426            Value::Map(map) => Ok(Node {
427                registry: self,
428                map,
429                location: value.location(),
430                path,
431            }),
432            _ => Err(SchemaError {
433                kind: SchemaErrorKind::NotMap,
434                path,
435                location: Some(Box::new(value.location().clone())),
436            }),
437        }
438    }
439
440    fn build_node(&self, node: &Node) -> Result<Box<dyn Validator>, SchemaError> {
441        let tag = node.req_str("type")?;
442        match self.constructors.get(tag) {
443            Some(constructor) => constructor(node),
444            None => Err(node.error(SchemaErrorKind::UnknownType {
445                tag: tag.to_string(),
446            })),
447        }
448    }
449
450    /// A registry pre-loaded with every built-in validator type.
451    pub fn with_builtins() -> Self {
452        // `mut` is unused when no validator features are enabled (schema-only build).
453        #[allow(unused_mut)]
454        let mut registry = Self::empty();
455
456        #[cfg(feature = "boolean")]
457        registry.register("bool", |_node| Ok(Box::new(Bool::new())));
458        #[cfg(feature = "non_empty")]
459        registry.register("non_empty", |_node| Ok(Box::new(NonEmpty::new())));
460        #[cfg(feature = "percentage")]
461        registry.register("percentage", |_node| Ok(Box::new(Percentage::new())));
462
463        #[cfg(feature = "integer")]
464        registry.register("integer", |node| {
465            let mut validator = Integer::new();
466            if let Some(min) = node.opt_int("min")? {
467                validator = validator.min(min);
468            }
469            if let Some(max) = node.opt_int("max")? {
470                validator = validator.max(max);
471            }
472            if node.flag("positive")? {
473                validator = validator.positive();
474            }
475            if node.flag("non_negative")? {
476                validator = validator.non_negative();
477            }
478            if node.flag("negative")? {
479                validator = validator.negative();
480            }
481            if node.flag("non_positive")? {
482                validator = validator.non_positive();
483            }
484            Ok(Box::new(validator))
485        });
486
487        #[cfg(feature = "float")]
488        registry.register("float", |node| {
489            let mut validator = Float::new();
490            if let Some(min) = node.opt_f64("min")? {
491                validator = validator.min(min);
492            }
493            if let Some(max) = node.opt_f64("max")? {
494                validator = validator.max(max);
495            }
496            if node.flag("positive")? {
497                validator = validator.positive();
498            }
499            if node.flag("non_negative")? {
500                validator = validator.non_negative();
501            }
502            if node.flag("negative")? {
503                validator = validator.negative();
504            }
505            if node.flag("non_positive")? {
506                validator = validator.non_positive();
507            }
508            Ok(Box::new(validator))
509        });
510
511        #[cfg(feature = "number")]
512        registry.register("number", |node| {
513            let mut validator = Number::new();
514            if let Some(min) = node.opt_f64("min")? {
515                validator = validator.min(min);
516            }
517            if let Some(max) = node.opt_f64("max")? {
518                validator = validator.max(max);
519            }
520            if node.flag("positive")? {
521                validator = validator.positive();
522            }
523            if node.flag("non_negative")? {
524                validator = validator.non_negative();
525            }
526            if node.flag("negative")? {
527                validator = validator.negative();
528            }
529            if node.flag("non_positive")? {
530                validator = validator.non_positive();
531            }
532            Ok(Box::new(validator))
533        });
534
535        #[cfg(feature = "string")]
536        registry.register("string", |node| {
537            let mut validator = Str::new();
538            if let Some(min) = node.opt_usize("min_chars")? {
539                validator = validator.min_chars(min);
540            }
541            if let Some(max) = node.opt_usize("max_chars")? {
542                validator = validator.max_chars(max);
543            }
544            #[cfg(feature = "regex")]
545            if let Some(pattern) = node.opt_str("regex")? {
546                validator = match validator.regex(pattern) {
547                    Ok(validator) => validator,
548                    Err(message) => {
549                        return Err(node.error(SchemaErrorKind::InvalidValue {
550                            field: "regex".to_string(),
551                            message,
552                        }));
553                    }
554                };
555            }
556            Ok(Box::new(validator))
557        });
558
559        #[cfg(feature = "list")]
560        registry.register("list", |node| {
561            let mut validator = List::new();
562            if let Some(min) = node.opt_usize("min_len")? {
563                validator = validator.min_len(min);
564            }
565            if let Some(max) = node.opt_usize("max_len")? {
566                validator = validator.max_len(max);
567            }
568            if node.flag("unique")? {
569                validator = validator.unique();
570            }
571            if let Some(items) = node.opt_child("items")? {
572                validator = validator.items(items);
573            }
574            Ok(Box::new(validator))
575        });
576
577        #[cfg(feature = "dynamic_map")]
578        registry.register("dynamic_map", |node| {
579            let mut validator = DynamicMap::new();
580            if let Some(min) = node.opt_usize("min_len")? {
581                validator = validator.min_len(min);
582            }
583            if let Some(max) = node.opt_usize("max_len")? {
584                validator = validator.max_len(max);
585            }
586            if let Some(values) = node.opt_child("values")? {
587                validator = validator.values(values);
588            }
589            Ok(Box::new(validator))
590        });
591
592        #[cfg(feature = "static_map")]
593        registry.register("static_map", |node| {
594            let mut validator = StaticMap::new();
595            if node.flag("allow_unknown")? {
596                validator = validator.allow_unknown();
597            }
598            if let Some(entry) = node.map.get("fields") {
599                let fields = match entry.value() {
600                    Value::Map(map) => map,
601                    _ => return Err(node.wrong("fields", "a map")),
602                };
603                for (key, field_entry) in fields.entries() {
604                    let mut path = node.path.clone();
605                    path.push(Segment::Key("fields".to_string()));
606                    path.push(Segment::Key(key.clone()));
607                    let field_node = node.registry.node(field_entry, path)?;
608                    let required = field_node.flag("required")?;
609                    let field_validator = field_node.opt_child("validator")?;
610                    validator = match (required, field_validator) {
611                        (true, Some(inner)) => validator.required(key.clone(), inner),
612                        (true, None) => validator.required_any(key.clone()),
613                        (false, Some(inner)) => validator.optional(key.clone(), inner),
614                        (false, None) => validator.optional_any(key.clone()),
615                    };
616                }
617            }
618            Ok(Box::new(validator))
619        });
620
621        #[cfg(feature = "enumeration")]
622        registry.register("enum", |node| {
623            let mut validator = Enum::new(node.values("values")?);
624            if node.flag("case_insensitive")? {
625                validator = validator.case_insensitive();
626            }
627            Ok(Box::new(validator))
628        });
629
630        #[cfg(feature = "either")]
631        registry.register("either", |node| {
632            let first = node.child("first")?;
633            let second = node.child("second")?;
634            Ok(Box::new(Either::new(first, second)))
635        });
636
637        #[cfg(feature = "net")]
638        {
639            registry.register("host", |_node| Ok(Box::new(Host::new())));
640            registry.register("email", |_node| Ok(Box::new(Email::new())));
641            registry.register("socket_addr", |_node| Ok(Box::new(SocketAddr::new())));
642
643            registry.register("domain", |node| {
644                let mut validator = Domain::new();
645                if node.flag("require_dot")? {
646                    validator = validator.require_dot();
647                }
648                Ok(Box::new(validator))
649            });
650
651            registry.register("port", |node| {
652                let mut validator = Port::new();
653                if node.flag("allow_zero")? {
654                    validator = validator.allow_zero();
655                }
656                if let Some(privileged) = node.opt_bool("privileged_ok")? {
657                    validator = validator.privileged_ok(privileged);
658                }
659                Ok(Box::new(validator))
660            });
661
662            registry.register("ip_addr", |node| {
663                let mut validator = IpAddr::new();
664                if node.flag("v4_only")? {
665                    validator = validator.v4_only();
666                }
667                if node.flag("v6_only")? {
668                    validator = validator.v6_only();
669                }
670                Ok(Box::new(validator))
671            });
672        }
673
674        #[cfg(feature = "path")]
675        registry.register("path", |node| {
676            let mut validator = Path::new();
677            if node.flag("absolute")? {
678                validator = validator.absolute();
679            }
680            if node.flag("relative")? {
681                validator = validator.relative();
682            }
683            for extension in node.str_list("extensions")? {
684                validator = validator.extension(extension);
685            }
686            if node.flag("must_exist")? {
687                validator = validator.must_exist();
688            }
689            if let Some(kind) = node.opt_str("kind")? {
690                let kind = match kind {
691                    "dir" => PathKind::Dir,
692                    "file" => PathKind::File,
693                    "symlink" => PathKind::Symlink,
694                    other => {
695                        return Err(node.error(SchemaErrorKind::InvalidValue {
696                            field: "kind".to_string(),
697                            message: format!("unknown kind `{other}`"),
698                        }));
699                    }
700                };
701                validator = validator.kind(kind);
702            }
703            if node.flag("readable")? {
704                validator = validator.readable();
705            }
706            if node.flag("writable")? {
707                validator = validator.writable();
708            }
709            Ok(Box::new(validator))
710        });
711
712        #[cfg(feature = "regex")]
713        registry.register("regex_pattern", |_node| {
714            Ok(Box::new(crate::RegexPattern::new()))
715        });
716
717        #[cfg(feature = "url")]
718        registry.register("url", |node| {
719            let mut validator = crate::Url::new();
720            let schemes = node.str_list("schemes")?;
721            if !schemes.is_empty() {
722                validator = validator.schemes(schemes);
723            }
724            if node.flag("require_host")? {
725                validator = validator.require_host();
726            }
727            Ok(Box::new(validator))
728        });
729
730        #[cfg(feature = "cidr")]
731        registry.register("cidr", |_node| Ok(Box::new(crate::Cidr::new())));
732
733        #[cfg(feature = "uuid")]
734        registry.register("uuid", |_node| Ok(Box::new(crate::Uuid::new())));
735
736        #[cfg(feature = "semver")]
737        registry.register("semver", |_node| Ok(Box::new(crate::Semver::new())));
738
739        #[cfg(feature = "encoding")]
740        {
741            registry.register("base64", |_node| Ok(Box::new(crate::Base64::new())));
742            registry.register("hex", |_node| Ok(Box::new(crate::Hex::new())));
743        }
744
745        #[cfg(feature = "duration")]
746        registry.register("duration", |node| {
747            let mut validator = crate::Duration::new();
748            if node.flag("millis")? {
749                validator = validator.millis();
750            }
751            Ok(Box::new(validator))
752        });
753
754        #[cfg(feature = "bytesize")]
755        registry.register("bytesize", |_node| Ok(Box::new(crate::ByteSize::new())));
756
757        #[cfg(feature = "datetime")]
758        {
759            registry.register("datetime", |_node| Ok(Box::new(crate::DateTime::new())));
760            registry.register("date", |_node| Ok(Box::new(crate::Date::new())));
761        }
762
763        registry
764    }
765}
766
767/// Build a validator from a located schema node using a default [`Registry`].
768pub fn build(value: &LocatedValue) -> Result<Box<dyn Validator>, SchemaError> {
769    Registry::with_builtins().build(value)
770}
771
772/// Build a validator from a bare [`Value`] using a default [`Registry`].
773pub fn build_value(value: &Value) -> Result<Box<dyn Validator>, SchemaError> {
774    Registry::with_builtins().build_value(value)
775}
776
777#[cfg(test)]
778mod tests {
779    use super::*;
780
781    fn parse(json: &str) -> Value {
782        let schema: SchemaValue = serde_json::from_str(json).unwrap();
783        schema.into_value()
784    }
785
786    fn build_err(json: &str) -> SchemaError {
787        match build_value(&parse(json)) {
788            Ok(_) => panic!("expected a schema error"),
789            Err(error) => error,
790        }
791    }
792
793    #[test]
794    fn builds_nested_schema_and_validates() {
795        let schema = parse(
796            r#"{
797                "type": "static_map",
798                "fields": {
799                    "host": {"required": true,  "validator": {"type": "host"}},
800                    "port": {"required": false, "validator": {"type": "port"}},
801                    "tags": {"required": false, "validator": {
802                        "type": "list", "unique": true,
803                        "items": {"type": "string", "min_chars": 1}
804                    }},
805                    "mode": {"required": true, "validator": {
806                        "type": "either",
807                        "first":  {"type": "enum", "values": ["auto", "manual"]},
808                        "second": {"type": "integer", "min": 0}
809                    }}
810                }
811            }"#,
812        );
813        let validator = build_value(&schema).unwrap();
814
815        let mut config = LocatedValue::new(
816            parse(r#"{"host": "localhost", "port": "8080", "tags": ["a", "b"], "mode": "auto"}"#),
817            schema_location(),
818        );
819        crate::validate(validator.as_ref(), &mut config).unwrap();
820
821        // port string was coerced to an integer
822        let port = config.value().as_map().unwrap().get("port").unwrap();
823        assert_eq!(*port.value(), Value::Int(8080));
824    }
825
826    #[test]
827    fn unknown_type_is_reported() {
828        let error = build_err(r#"{"type": "nope"}"#);
829        assert!(matches!(error.kind, SchemaErrorKind::UnknownType { .. }));
830    }
831
832    #[test]
833    fn wrong_option_type_is_reported() {
834        let error = build_err(r#"{"type": "integer", "min": "x"}"#);
835        assert!(matches!(error.kind, SchemaErrorKind::WrongType { .. }));
836    }
837
838    #[test]
839    fn missing_type_is_reported() {
840        let error = build_err(r#"{"min": 1}"#);
841        assert!(matches!(error.kind, SchemaErrorKind::MissingField { .. }));
842    }
843
844    #[test]
845    fn nested_error_carries_path() {
846        let error = build_err(r#"{"type": "list", "items": {"type": "integer", "min": "x"}}"#);
847        assert_eq!(error.path, vec![Segment::Key("items".to_string())]);
848    }
849
850    #[test]
851    fn custom_validator_can_be_registered() {
852        let mut registry = Registry::with_builtins();
853        registry.register("yes", |_node| Ok(Box::new(Bool::new())));
854        let validator = registry.build_value(&parse(r#"{"type": "yes"}"#)).unwrap();
855        assert!(validator.validate(&mut Value::Bool(true)).is_ok());
856    }
857
858    #[test]
859    fn empty_registry_knows_nothing() {
860        let error = match Registry::empty().build_value(&parse(r#"{"type": "bool"}"#)) {
861            Ok(_) => panic!("expected a schema error"),
862            Err(error) => error,
863        };
864        assert!(matches!(error.kind, SchemaErrorKind::UnknownType { .. }));
865    }
866
867    #[cfg(feature = "uuid")]
868    #[test]
869    fn feature_gated_tag_round_trips() {
870        let validator = build_value(&parse(r#"{"type": "uuid"}"#)).unwrap();
871        assert!(
872            validator
873                .validate(&mut Value::String(
874                    "67e55044-10b1-426f-9247-bb680e5fe0c8".into()
875                ))
876                .is_ok()
877        );
878    }
879
880    #[test]
881    fn integer_schema_honors_min_and_max() {
882        let validator = build_value(&parse(r#"{"type": "integer", "min": 1, "max": 10}"#)).unwrap();
883        let mut ok = Value::Int(5);
884        validator.validate(&mut ok).unwrap();
885        let mut low = Value::Int(0);
886        assert!(validator.validate(&mut low).is_err());
887    }
888
889    #[test]
890    fn string_schema_honors_min_chars() {
891        let validator = build_value(&parse(r#"{"type": "string", "min_chars": 3}"#)).unwrap();
892        let mut short = Value::String("ab".into());
893        assert!(validator.validate(&mut short).is_err());
894    }
895
896    #[test]
897    fn list_schema_honors_min_len() {
898        let validator = build_value(&parse(r#"{"type": "list", "min_len": 2}"#)).unwrap();
899        let mut short = Value::List(Vec::new());
900        assert!(validator.validate(&mut short).is_err());
901    }
902
903    #[test]
904    fn enum_schema_supports_case_insensitive() {
905        let validator = build_value(&parse(
906            r#"{"type": "enum", "values": ["Auto"], "case_insensitive": true}"#,
907        ))
908        .unwrap();
909        let mut value = Value::String("auto".into());
910        validator.validate(&mut value).unwrap();
911    }
912
913    #[test]
914    fn build_rejects_non_map_root() {
915        let located = LocatedValue::new(Value::String("nope".into()), schema_location());
916        let error = match build(&located) {
917            Ok(_) => panic!("expected a schema error"),
918            Err(error) => error,
919        };
920        assert!(matches!(error.kind, SchemaErrorKind::NotMap));
921    }
922
923    #[test]
924    fn schema_error_display_includes_path_and_location() {
925        let error = build_err(r#"{"type": "list", "min_len": -1}"#);
926        let message = error.to_string();
927        assert!(message.contains("min_len"));
928        assert!(message.contains("must be non-negative"));
929    }
930
931    #[test]
932    fn negative_opt_usize_is_invalid_value() {
933        let error = build_err(r#"{"type": "list", "max_len": -1}"#);
934        assert!(matches!(error.kind, SchemaErrorKind::InvalidValue { .. }));
935    }
936
937    #[test]
938    fn schema_value_deserializes_null() {
939        let value: SchemaValue = serde_json::from_str("null").unwrap();
940        assert!(value.into_value().is_null());
941    }
942
943    #[test]
944    fn schema_value_deserializes_scalar_and_collection_forms() {
945        let float: SchemaValue = serde_json::from_str("1.5").unwrap();
946        assert!(matches!(float.into_value(), Value::Float(_)));
947        let list: SchemaValue = serde_json::from_str("[1, 2]").unwrap();
948        assert!(list.into_value().as_list().is_some());
949        let map: SchemaValue = serde_json::from_str(r#"{"type":"bool"}"#).unwrap();
950        assert!(map.into_value().as_map().is_some());
951    }
952
953    #[test]
954    fn integer_schema_honors_sign_flags() {
955        let validator = build_value(&parse(r#"{"type": "integer", "positive": true}"#)).unwrap();
956        let mut ok = Value::Int(3);
957        validator.validate(&mut ok).unwrap();
958        let mut zero = Value::Int(0);
959        assert!(validator.validate(&mut zero).is_err());
960    }
961
962    #[test]
963    fn float_and_number_schema_honor_bounds() {
964        let float = build_value(&parse(r#"{"type": "float", "min": 0.5, "max": 2.0}"#)).unwrap();
965        let mut ok = Value::Float(1.0);
966        float.validate(&mut ok).unwrap();
967
968        let number = build_value(&parse(
969            r#"{"type": "number", "non_negative": true, "non_positive": false}"#,
970        ))
971        .unwrap();
972        let mut ok = Value::Int(0);
973        number.validate(&mut ok).unwrap();
974    }
975
976    #[test]
977    fn string_schema_honors_max_chars_and_invalid_regex() {
978        let validator = build_value(&parse(r#"{"type": "string", "max_chars": 2}"#)).unwrap();
979        let mut long = Value::String("abc".into());
980        assert!(validator.validate(&mut long).is_err());
981
982        let error = build_err(r#"{"type": "string", "regex": "[invalid"}"#);
983        assert!(matches!(error.kind, SchemaErrorKind::InvalidValue { .. }));
984    }
985
986    #[test]
987    fn list_schema_honors_unique_items_and_max_len() {
988        let validator = build_value(&parse(
989            r#"{"type": "list", "unique": true, "max_len": 1, "items": {"type": "string"}}"#,
990        ))
991        .unwrap();
992        let mut dup = Value::List(vec![
993            LocatedValue::new(Value::String("a".into()), schema_location()),
994            LocatedValue::new(Value::String("a".into()), schema_location()),
995        ]);
996        assert!(validator.validate(&mut dup).is_err());
997    }
998
999    #[test]
1000    fn dynamic_map_schema_honors_values_validator() {
1001        let validator = build_value(&parse(
1002            r#"{"type": "dynamic_map", "values": {"type": "integer"}}"#,
1003        ))
1004        .unwrap();
1005        let mut ok = parse(r#"{"count": "7"}"#);
1006        validator.validate(&mut ok).unwrap();
1007        assert_eq!(
1008            *ok.as_map().unwrap().get("count").unwrap().value(),
1009            Value::Int(7)
1010        );
1011    }
1012
1013    #[test]
1014    fn static_map_schema_supports_optional_and_required_fields() {
1015        let validator = build_value(&parse(
1016            r#"{
1017                "type": "static_map",
1018                "allow_unknown": true,
1019                "fields": {
1020                    "name": {"required": true, "validator": {"type": "string"}},
1021                    "tag": {"required": false},
1022                    "mode": {"required": false, "validator": {"type": "enum", "values": ["a"]}}
1023                }
1024            }"#,
1025        ))
1026        .unwrap();
1027        let mut ok = parse(r#"{"name": "demo", "tag": "x", "extra": 1, "mode": "a"}"#);
1028        validator.validate(&mut ok).unwrap();
1029    }
1030
1031    #[test]
1032    fn static_map_fields_must_be_a_map() {
1033        let error = build_err(r#"{"type": "static_map", "fields": "nope"}"#);
1034        assert!(matches!(error.kind, SchemaErrorKind::WrongType { .. }));
1035    }
1036
1037    #[test]
1038    fn net_schema_constructors_accept_options() {
1039        let domain = build_value(&parse(r#"{"type": "domain", "require_dot": true}"#)).unwrap();
1040        let mut ok = Value::String("example.com".into());
1041        domain.validate(&mut ok).unwrap();
1042
1043        let port = build_value(&parse(
1044            r#"{"type": "port", "allow_zero": true, "privileged_ok": false}"#,
1045        ))
1046        .unwrap();
1047        let mut zero = Value::Int(0);
1048        port.validate(&mut zero).unwrap();
1049
1050        let ip = build_value(&parse(r#"{"type": "ip_addr", "v4_only": true}"#)).unwrap();
1051        let mut v4 = Value::String("127.0.0.1".into());
1052        ip.validate(&mut v4).unwrap();
1053
1054        build_value(&parse(r#"{"type": "host"}"#)).unwrap();
1055        build_value(&parse(r#"{"type": "email"}"#)).unwrap();
1056        build_value(&parse(r#"{"type": "socket_addr"}"#)).unwrap();
1057    }
1058
1059    #[test]
1060    fn path_schema_rejects_unknown_kind() {
1061        let error = build_err(r#"{"type": "path", "kind": "pipe"}"#);
1062        assert!(matches!(error.kind, SchemaErrorKind::InvalidValue { .. }));
1063    }
1064
1065    #[test]
1066    fn path_schema_accepts_extensions_and_flags() {
1067        let validator = build_value(&parse(
1068            r#"{"type": "path", "relative": true, "extensions": ["toml", "json"]}"#,
1069        ))
1070        .unwrap();
1071        let mut ok = Value::String("config.toml".into());
1072        validator.validate(&mut ok).unwrap();
1073    }
1074
1075    #[test]
1076    fn feature_gated_schema_tags_build_successfully() {
1077        build_value(&parse(r#"{"type": "bool"}"#)).unwrap();
1078        build_value(&parse(r#"{"type": "non_empty"}"#)).unwrap();
1079        build_value(&parse(r#"{"type": "percentage"}"#)).unwrap();
1080        build_value(&parse(
1081            r#"{"type": "either", "first": {"type": "string"}, "second": {"type": "integer"}}"#,
1082        ))
1083        .unwrap();
1084        build_value(&parse(r#"{"type": "regex_pattern"}"#)).unwrap();
1085        build_value(&parse(
1086            r#"{"type": "url", "schemes": ["https"], "require_host": true}"#,
1087        ))
1088        .unwrap();
1089        build_value(&parse(r#"{"type": "cidr"}"#)).unwrap();
1090        build_value(&parse(r#"{"type": "semver"}"#)).unwrap();
1091        build_value(&parse(r#"{"type": "base64"}"#)).unwrap();
1092        build_value(&parse(r#"{"type": "hex"}"#)).unwrap();
1093        build_value(&parse(r#"{"type": "duration", "millis": true}"#)).unwrap();
1094        build_value(&parse(r#"{"type": "bytesize"}"#)).unwrap();
1095        build_value(&parse(r#"{"type": "datetime"}"#)).unwrap();
1096        build_value(&parse(r#"{"type": "date"}"#)).unwrap();
1097    }
1098}