1use 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
44fn schema_location() -> Location {
46 Location::at("schema", "", None, None, None)
47}
48
49#[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#[derive(Debug, Clone, PartialEq)]
138pub enum SchemaErrorKind {
139 NotMap,
141 UnknownType { tag: String },
143 MissingField { field: String },
145 WrongType {
147 field: String,
148 expected: &'static str,
149 },
150 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#[derive(Debug, Clone, PartialEq)]
170pub struct SchemaError {
171 pub kind: SchemaErrorKind,
172 pub path: Vec<Segment>,
173 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
203pub struct Node<'a> {
208 registry: &'a Registry,
209 map: &'a Map,
210 location: &'a Location,
211 path: Vec<Segment>,
212}
213
214impl Node<'_> {
215 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 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 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 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 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 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 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 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 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 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 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 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
377pub type Constructor = Box<dyn Fn(&Node) -> Result<Box<dyn Validator>, SchemaError>>;
379
380pub 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 pub fn empty() -> Self {
394 Self {
395 constructors: HashMap::new(),
396 }
397 }
398
399 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 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 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 pub fn with_builtins() -> Self {
452 #[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
767pub fn build(value: &LocatedValue) -> Result<Box<dyn Validator>, SchemaError> {
769 Registry::with_builtins().build(value)
770}
771
772pub 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 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}