1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet};
3use std::fmt;
4
5use rhai::{Array, Dynamic, FLOAT, FnPtr, INT, ImmutableString, Map};
6use serde::{Deserialize, Serialize};
7
8use crate::{AssetId, ElementRef, Length, NativeSignal, OpaqueHandle, Style, UiNode, UiValue};
9
10#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
11#[serde(tag = "type", rename_all = "snake_case")]
12pub enum ValueSchema {
13 Null,
14 Bool,
15 Integer {
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 min: Option<INT>,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 max: Option<INT>,
20 },
21 Float {
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 min: Option<FLOAT>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 max: Option<FLOAT>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 exclusive_min: Option<FLOAT>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 exclusive_max: Option<FLOAT>,
30 },
31 Number {
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 min: Option<FLOAT>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 max: Option<FLOAT>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 exclusive_min: Option<FLOAT>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 exclusive_max: Option<FLOAT>,
40 },
41 String {
42 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 allowed: Vec<String>,
44 },
45 Array {
46 items: Box<Self>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 max_items: Option<usize>,
49 },
50 Map {
51 values: Box<Self>,
52 },
53 Object {
54 fields: BTreeMap<String, ObjectField>,
55 #[serde(default)]
56 allow_unknown: bool,
57 },
58 Optional {
59 value: Box<Self>,
60 },
61 OneOf {
62 variants: Vec<Self>,
63 },
64 Node,
65 Callback,
66 Style,
67 Length,
68 UiValue,
69 Asset,
70 Signal,
71 Collection,
72 Document,
73 #[cfg(feature = "charts")]
74 ChartData,
75 Ref,
76 Handle {
77 kind: String,
78 },
79}
80
81impl ValueSchema {
82 #[must_use]
83 pub fn string() -> Self {
84 Self::String {
85 allowed: Vec::new(),
86 }
87 }
88
89 #[must_use]
90 pub const fn integer() -> Self {
91 Self::Integer {
92 min: None,
93 max: None,
94 }
95 }
96
97 #[must_use]
98 pub const fn bounded_integer(min: Option<INT>, max: Option<INT>) -> Self {
99 Self::Integer { min, max }
100 }
101
102 #[must_use]
103 pub const fn float() -> Self {
104 Self::Float {
105 min: None,
106 max: None,
107 exclusive_min: None,
108 exclusive_max: None,
109 }
110 }
111
112 #[must_use]
113 pub const fn bounded_float(min: Option<FLOAT>, max: Option<FLOAT>) -> Self {
114 Self::Float {
115 min,
116 max,
117 exclusive_min: None,
118 exclusive_max: None,
119 }
120 }
121
122 #[must_use]
123 pub const fn number() -> Self {
124 Self::Number {
125 min: None,
126 max: None,
127 exclusive_min: None,
128 exclusive_max: None,
129 }
130 }
131
132 #[must_use]
133 pub const fn bounded_number(min: Option<FLOAT>, max: Option<FLOAT>) -> Self {
134 Self::Number {
135 min,
136 max,
137 exclusive_min: None,
138 exclusive_max: None,
139 }
140 }
141
142 #[must_use]
143 pub const fn positive_number() -> Self {
144 Self::Number {
145 min: None,
146 max: None,
147 exclusive_min: Some(0.0),
148 exclusive_max: None,
149 }
150 }
151
152 #[must_use]
153 pub fn enumeration(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
154 Self::String {
155 allowed: values.into_iter().map(Into::into).collect(),
156 }
157 }
158
159 #[must_use]
160 pub fn optional(value: Self) -> Self {
161 Self::Optional {
162 value: Box::new(value),
163 }
164 }
165
166 #[must_use]
167 pub fn one_of(variants: impl IntoIterator<Item = Self>) -> Self {
168 Self::OneOf {
169 variants: variants.into_iter().collect(),
170 }
171 }
172
173 #[must_use]
174 pub fn object(fields: BTreeMap<String, ObjectField>) -> Self {
175 Self::Object {
176 fields,
177 allow_unknown: false,
178 }
179 }
180
181 pub fn validate_definition(&self) -> Result<(), SchemaDefinitionError> {
188 self.validate_definition_at("$")
189 }
190
191 fn validate_definition_at(&self, path: &str) -> Result<(), SchemaDefinitionError> {
192 match self {
193 Self::Integer { min, max } => validate_integer_bounds(*min, *max, path),
194 Self::Float {
195 min,
196 max,
197 exclusive_min,
198 exclusive_max,
199 }
200 | Self::Number {
201 min,
202 max,
203 exclusive_min,
204 exclusive_max,
205 } => validate_float_bounds(*min, *max, *exclusive_min, *exclusive_max, path),
206 Self::String { allowed } => {
207 let mut unique = BTreeSet::new();
208 if let Some(duplicate) = allowed.iter().find(|value| !unique.insert(*value)) {
209 return Err(SchemaDefinitionError::new(
210 path,
211 format!("allowed string value `{duplicate}` is duplicated"),
212 ));
213 }
214 Ok(())
215 }
216 Self::Array { items, .. } => items.validate_definition_at(&format!("{path}.items")),
217 Self::Map { values } => values.validate_definition_at(&format!("{path}.values")),
218 Self::Object { fields, .. } => {
219 for (name, field) in fields {
220 field
221 .schema
222 .validate_definition_at(&format!("{path}.fields.{name}"))?;
223 }
224 Ok(())
225 }
226 Self::Optional { value } => value.validate_definition_at(&format!("{path}.value")),
227 Self::OneOf { variants } => {
228 if variants.is_empty() {
229 return Err(SchemaDefinitionError::new(
230 path,
231 "one_of must contain at least one variant",
232 ));
233 }
234 for (index, variant) in variants.iter().enumerate() {
235 variant.validate_definition_at(&format!("{path}.variants[{index}]"))?;
236 }
237 Ok(())
238 }
239 Self::Handle { kind } if kind.trim().is_empty() => Err(SchemaDefinitionError::new(
240 path,
241 "handle kind cannot be empty",
242 )),
243 Self::Null
244 | Self::Bool
245 | Self::Node
246 | Self::Callback
247 | Self::Style
248 | Self::Length
249 | Self::UiValue
250 | Self::Asset
251 | Self::Signal
252 | Self::Collection
253 | Self::Document
254 | Self::Ref
255 | Self::Handle { .. } => Ok(()),
256 #[cfg(feature = "charts")]
257 Self::ChartData => Ok(()),
258 }
259 }
260
261 pub fn validate(&self, value: &Dynamic) -> Result<(), SchemaValidationError> {
268 let mut issues = Vec::new();
269 self.validate_at(value, "$", &mut issues);
270 if issues.is_empty() {
271 Ok(())
272 } else {
273 Err(SchemaValidationError { issues })
274 }
275 }
276
277 pub fn validate_ui_value(&self, value: &UiValue) -> Result<(), SchemaValidationError> {
283 value.validate().map_err(|error| SchemaValidationError {
284 issues: vec![SchemaIssue::new("$", error.to_string())],
285 })?;
286 self.validate(&value.clone().into_dynamic())
287 }
288
289 fn validate_at(&self, value: &Dynamic, path: &str, issues: &mut Vec<SchemaIssue>) {
290 match self {
291 Self::Null => expect_type(value.is_unit(), value, path, "null", issues),
292 Self::Bool => expect_type(value.is::<bool>(), value, path, "bool", issues),
293 Self::Integer { min, max } => validate_integer(value, *min, *max, path, issues),
294 Self::Float {
295 min,
296 max,
297 exclusive_min,
298 exclusive_max,
299 } => validate_float(
300 value,
301 *min,
302 *max,
303 *exclusive_min,
304 *exclusive_max,
305 path,
306 issues,
307 ),
308 Self::Number {
309 min,
310 max,
311 exclusive_min,
312 exclusive_max,
313 } => validate_number(
314 value,
315 *min,
316 *max,
317 *exclusive_min,
318 *exclusive_max,
319 path,
320 issues,
321 ),
322 Self::String { allowed } => validate_string(value, allowed, path, issues),
323 Self::Array { items, max_items } => {
324 validate_array(value, items, *max_items, path, issues);
325 }
326 Self::Map { values } => validate_map(value, values, path, issues),
327 Self::Object {
328 fields,
329 allow_unknown,
330 } => validate_object_value(value, fields, *allow_unknown, path, issues),
331 Self::Optional { value: schema } => {
332 if !value.is_unit() {
333 schema.validate_at(value, path, issues);
334 }
335 }
336 Self::OneOf { variants } => validate_one_of(value, variants, path, issues),
337 Self::Node => expect_type(value.is::<UiNode>(), value, path, "UiNode", issues),
338 Self::Callback => expect_type(
339 value.is::<FnPtr>() || value.is::<crate::NativeHandlerRef>(),
340 value,
341 path,
342 "callback or NativeHandlerRef",
343 issues,
344 ),
345 Self::Style => expect_type(value.is::<Style>(), value, path, "Style", issues),
346 Self::Length => validate_length(value, path, issues),
347 Self::UiValue => {
348 if let Err(error) = UiValue::from_dynamic(value.clone()) {
349 issues.push(SchemaIssue::new(path, error.to_string()));
350 }
351 }
352 Self::Asset => expect_type(value.is::<AssetId>(), value, path, "AssetId", issues),
353 Self::Signal => expect_type(
354 value.is::<NativeSignal>(),
355 value,
356 path,
357 "NativeSignal",
358 issues,
359 ),
360 Self::Collection => expect_type(
361 value.is::<crate::NativeCollection>(),
362 value,
363 path,
364 "NativeCollection",
365 issues,
366 ),
367 Self::Document => expect_type(
368 value.is::<crate::NativeTextDocument>(),
369 value,
370 path,
371 "NativeTextDocument",
372 issues,
373 ),
374 #[cfg(feature = "charts")]
375 Self::ChartData => expect_type(
376 value.is::<crate::NativeChartData>(),
377 value,
378 path,
379 "NativeChartData",
380 issues,
381 ),
382 Self::Ref => expect_type(value.is::<ElementRef>(), value, path, "ElementRef", issues),
383 Self::Handle { kind } => validate_handle(value, kind, path, issues),
384 }
385 }
386}
387
388fn validate_integer(
389 value: &Dynamic,
390 min: Option<INT>,
391 max: Option<INT>,
392 path: &str,
393 issues: &mut Vec<SchemaIssue>,
394) {
395 if value.is::<INT>() {
396 validate_integer_value(value.clone_cast::<INT>(), min, max, path, issues);
397 } else {
398 expect_type(false, value, path, "integer", issues);
399 }
400}
401
402fn validate_float(
403 value: &Dynamic,
404 min: Option<FLOAT>,
405 max: Option<FLOAT>,
406 exclusive_min: Option<FLOAT>,
407 exclusive_max: Option<FLOAT>,
408 path: &str,
409 issues: &mut Vec<SchemaIssue>,
410) {
411 if value.is::<FLOAT>() {
412 validate_float_value(
413 value.clone_cast::<FLOAT>(),
414 min,
415 max,
416 exclusive_min,
417 exclusive_max,
418 path,
419 issues,
420 );
421 } else {
422 expect_type(false, value, path, "float", issues);
423 }
424}
425
426fn validate_number(
427 value: &Dynamic,
428 min: Option<FLOAT>,
429 max: Option<FLOAT>,
430 exclusive_min: Option<FLOAT>,
431 exclusive_max: Option<FLOAT>,
432 path: &str,
433 issues: &mut Vec<SchemaIssue>,
434) {
435 let number = if value.is::<INT>() {
436 Some(integer_as_float(value.clone_cast::<INT>()))
437 } else if value.is::<FLOAT>() {
438 Some(value.clone_cast::<FLOAT>())
439 } else {
440 None
441 };
442 if let Some(number) = number {
443 validate_float_value(number, min, max, exclusive_min, exclusive_max, path, issues);
444 } else {
445 expect_type(false, value, path, "number", issues);
446 }
447}
448
449fn validate_string(value: &Dynamic, allowed: &[String], path: &str, issues: &mut Vec<SchemaIssue>) {
450 if value.is::<ImmutableString>() {
451 let actual = value.clone_cast::<ImmutableString>();
452 if !allowed.is_empty() && !allowed.iter().any(|allowed| allowed == actual.as_str()) {
453 issues.push(SchemaIssue::new(
454 path,
455 format!("expected one of [{}], got `{actual}`", allowed.join(", ")),
456 ));
457 }
458 } else {
459 expect_type(false, value, path, "string", issues);
460 }
461}
462
463fn validate_array(
464 value: &Dynamic,
465 items: &ValueSchema,
466 max_items: Option<usize>,
467 path: &str,
468 issues: &mut Vec<SchemaIssue>,
469) {
470 if !value.is::<Array>() {
471 expect_type(false, value, path, "array", issues);
472 return;
473 }
474 let values = value.clone_cast::<Array>();
475 if let Some(max_items) = max_items
476 && values.len() > max_items
477 {
478 issues.push(SchemaIssue::new(
479 path,
480 format!("expected at most {max_items} items, got {}", values.len()),
481 ));
482 }
483 for (index, item) in values.iter().enumerate() {
484 items.validate_at(item, &format!("{path}[{index}]"), issues);
485 }
486}
487
488fn validate_map(value: &Dynamic, values: &ValueSchema, path: &str, issues: &mut Vec<SchemaIssue>) {
489 if !value.is::<Map>() {
490 expect_type(false, value, path, "map", issues);
491 return;
492 }
493 for (key, item) in value.clone_cast::<Map>() {
494 values.validate_at(&item, &format!("{path}.{key}"), issues);
495 }
496}
497
498fn validate_object_value(
499 value: &Dynamic,
500 fields: &BTreeMap<String, ObjectField>,
501 allow_unknown: bool,
502 path: &str,
503 issues: &mut Vec<SchemaIssue>,
504) {
505 if value.is::<Map>() {
506 validate_object(
507 &value.clone_cast::<Map>(),
508 fields,
509 allow_unknown,
510 path,
511 issues,
512 );
513 } else {
514 expect_type(false, value, path, "object", issues);
515 }
516}
517
518fn validate_one_of(
519 value: &Dynamic,
520 variants: &[ValueSchema],
521 path: &str,
522 issues: &mut Vec<SchemaIssue>,
523) {
524 let mut branch_issues = Vec::with_capacity(variants.len());
525 for variant in variants {
526 let mut candidate = Vec::new();
527 variant.validate_at(value, path, &mut candidate);
528 if candidate.is_empty() {
529 return;
530 }
531 branch_issues.push(candidate);
532 }
533 let summary = branch_issues
534 .iter()
535 .enumerate()
536 .map(|(index, candidate)| {
537 let messages = candidate
538 .iter()
539 .map(|issue| issue.message.as_str())
540 .collect::<Vec<_>>()
541 .join(", ");
542 format!("variant {index}: {messages}")
543 })
544 .collect::<Vec<_>>()
545 .join("; ");
546 issues.push(SchemaIssue::new(
547 path,
548 format!("value did not match any one_of variant ({summary})"),
549 ));
550}
551
552fn validate_length(value: &Dynamic, path: &str, issues: &mut Vec<SchemaIssue>) {
553 if value.is::<Length>() {
554 let length = value.clone_cast::<Length>();
555 if let Err(error) = length.validate() {
556 issues.push(SchemaIssue::new(path, error.to_string()));
557 }
558 } else {
559 expect_type(false, value, path, "Length", issues);
560 }
561}
562
563fn validate_handle(value: &Dynamic, kind: &str, path: &str, issues: &mut Vec<SchemaIssue>) {
564 if value.is::<OpaqueHandle>() {
565 let handle = value.clone_cast::<OpaqueHandle>();
566 if handle.kind() != kind {
567 issues.push(SchemaIssue::new(
568 path,
569 format!("expected `{kind}` handle, got `{}` handle", handle.kind()),
570 ));
571 }
572 } else {
573 expect_type(false, value, path, &format!("{kind} handle"), issues);
574 }
575}
576
577fn validate_integer_bounds(
578 min: Option<INT>,
579 max: Option<INT>,
580 path: &str,
581) -> Result<(), SchemaDefinitionError> {
582 if min.zip(max).is_some_and(|(min, max)| min > max) {
583 Err(SchemaDefinitionError::new(
584 path,
585 format!("integer minimum {min:?} exceeds maximum {max:?}"),
586 ))
587 } else {
588 Ok(())
589 }
590}
591
592fn validate_float_bounds(
593 min: Option<FLOAT>,
594 max: Option<FLOAT>,
595 exclusive_min: Option<FLOAT>,
596 exclusive_max: Option<FLOAT>,
597 path: &str,
598) -> Result<(), SchemaDefinitionError> {
599 if min.is_some_and(|value| !value.is_finite())
600 || max.is_some_and(|value| !value.is_finite())
601 || exclusive_min.is_some_and(|value| !value.is_finite())
602 || exclusive_max.is_some_and(|value| !value.is_finite())
603 {
604 return Err(SchemaDefinitionError::new(
605 path,
606 "numeric bounds must be finite",
607 ));
608 }
609 if min.is_some() && exclusive_min.is_some() {
610 return Err(SchemaDefinitionError::new(
611 path,
612 "numeric schema cannot define both min and exclusive_min",
613 ));
614 }
615 if max.is_some() && exclusive_max.is_some() {
616 return Err(SchemaDefinitionError::new(
617 path,
618 "numeric schema cannot define both max and exclusive_max",
619 ));
620 }
621 let lower = min.or(exclusive_min);
622 let upper = max.or(exclusive_max);
623 let empty = lower.zip(upper).is_some_and(|(lower, upper)| {
624 matches!(lower.total_cmp(&upper), Ordering::Greater)
625 || (matches!(lower.total_cmp(&upper), Ordering::Equal)
626 && (exclusive_min.is_some() || exclusive_max.is_some()))
627 });
628 if empty {
629 Err(SchemaDefinitionError::new(
630 path,
631 format!("numeric lower bound {lower:?} does not precede upper bound {upper:?}"),
632 ))
633 } else {
634 Ok(())
635 }
636}
637
638fn validate_integer_value(
639 value: INT,
640 min: Option<INT>,
641 max: Option<INT>,
642 path: &str,
643 issues: &mut Vec<SchemaIssue>,
644) {
645 if let Some(min) = min
646 && value < min
647 {
648 issues.push(SchemaIssue::new(
649 path,
650 format!("expected integer >= {min}, got {value}"),
651 ));
652 }
653 if let Some(max) = max
654 && value > max
655 {
656 issues.push(SchemaIssue::new(
657 path,
658 format!("expected integer <= {max}, got {value}"),
659 ));
660 }
661}
662
663fn validate_float_value(
664 value: FLOAT,
665 min: Option<FLOAT>,
666 max: Option<FLOAT>,
667 exclusive_min: Option<FLOAT>,
668 exclusive_max: Option<FLOAT>,
669 path: &str,
670 issues: &mut Vec<SchemaIssue>,
671) {
672 if !value.is_finite() {
673 issues.push(SchemaIssue::new(path, "number must be finite"));
674 return;
675 }
676 if let Some(min) = min
677 && value < min
678 {
679 issues.push(SchemaIssue::new(
680 path,
681 format!("expected number >= {min}, got {value}"),
682 ));
683 }
684 if let Some(max) = max
685 && value > max
686 {
687 issues.push(SchemaIssue::new(
688 path,
689 format!("expected number <= {max}, got {value}"),
690 ));
691 }
692 if let Some(min) = exclusive_min
693 && value <= min
694 {
695 issues.push(SchemaIssue::new(
696 path,
697 format!("expected number > {min}, got {value}"),
698 ));
699 }
700 if let Some(max) = exclusive_max
701 && value >= max
702 {
703 issues.push(SchemaIssue::new(
704 path,
705 format!("expected number < {max}, got {value}"),
706 ));
707 }
708}
709
710fn integer_as_float(value: INT) -> FLOAT {
711 value
712 .to_string()
713 .parse()
714 .expect("an integer always has a finite float representation")
715}
716
717#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
718pub struct ObjectField {
719 pub schema: ValueSchema,
720 #[serde(default)]
721 pub required: bool,
722 #[serde(default)]
723 pub sensitive: bool,
724 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub default: Option<crate::UiValue>,
726}
727
728impl ObjectField {
729 #[must_use]
730 pub fn required(schema: ValueSchema) -> Self {
731 Self {
732 schema,
733 required: true,
734 sensitive: false,
735 default: None,
736 }
737 }
738
739 #[must_use]
740 pub fn optional(schema: ValueSchema) -> Self {
741 Self {
742 schema,
743 required: false,
744 sensitive: false,
745 default: None,
746 }
747 }
748
749 #[must_use]
750 pub fn sensitive(mut self) -> Self {
751 self.sensitive = true;
752 self
753 }
754
755 #[must_use]
756 pub fn with_default(mut self, default: crate::UiValue) -> Self {
757 self.default = Some(default);
758 self
759 }
760}
761
762fn validate_object(
763 value: &Map,
764 fields: &BTreeMap<String, ObjectField>,
765 allow_unknown: bool,
766 path: &str,
767 issues: &mut Vec<SchemaIssue>,
768) {
769 let actual_keys = value
770 .keys()
771 .map(ToString::to_string)
772 .collect::<BTreeSet<_>>();
773
774 for (name, field) in fields {
775 match value.get(name.as_str()) {
776 Some(value) => field
777 .schema
778 .validate_at(value, &format!("{path}.{name}"), issues),
779 None if field.required => issues.push(SchemaIssue::new(
780 format!("{path}.{name}"),
781 "required field is missing",
782 )),
783 None => {}
784 }
785 }
786
787 if !allow_unknown {
788 for unknown in actual_keys.difference(&fields.keys().cloned().collect()) {
789 issues.push(SchemaIssue::new(
790 format!("{path}.{unknown}"),
791 "unknown field",
792 ));
793 }
794 }
795}
796
797fn expect_type(
798 valid: bool,
799 value: &Dynamic,
800 path: &str,
801 expected: &str,
802 issues: &mut Vec<SchemaIssue>,
803) {
804 if !valid {
805 issues.push(SchemaIssue::new(
806 path,
807 format!("expected {expected}, got {}", value.type_name()),
808 ));
809 }
810}
811
812#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
813pub struct SchemaIssue {
814 pub path: String,
815 pub message: String,
816}
817
818impl SchemaIssue {
819 #[must_use]
820 pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
821 Self {
822 path: path.into(),
823 message: message.into(),
824 }
825 }
826}
827
828#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
829pub struct SchemaValidationError {
830 pub issues: Vec<SchemaIssue>,
831}
832
833impl fmt::Display for SchemaValidationError {
834 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
835 for (index, issue) in self.issues.iter().enumerate() {
836 if index > 0 {
837 formatter.write_str("; ")?;
838 }
839 write!(formatter, "{}: {}", issue.path, issue.message)?;
840 }
841 Ok(())
842 }
843}
844
845impl std::error::Error for SchemaValidationError {}
846
847#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
848pub struct SchemaDefinitionError {
849 pub path: String,
850 pub message: String,
851}
852
853impl SchemaDefinitionError {
854 #[must_use]
855 pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
856 Self {
857 path: path.into(),
858 message: message.into(),
859 }
860 }
861}
862
863impl fmt::Display for SchemaDefinitionError {
864 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
865 write!(formatter, "{}: {}", self.path, self.message)
866 }
867}
868
869impl std::error::Error for SchemaDefinitionError {}
870
871#[cfg(test)]
872mod tests {
873 use super::*;
874
875 fn button_schema() -> ValueSchema {
876 ValueSchema::object(BTreeMap::from([
877 (
878 "text".to_owned(),
879 ObjectField::required(ValueSchema::string()),
880 ),
881 (
882 "variant".to_owned(),
883 ObjectField::optional(ValueSchema::enumeration(["primary", "secondary"])),
884 ),
885 (
886 "on_click".to_owned(),
887 ObjectField::optional(ValueSchema::Callback),
888 ),
889 ]))
890 }
891
892 #[test]
893 fn objects_report_all_precise_paths() {
894 let value = Dynamic::from_map(Map::from_iter([
895 ("variant".into(), Dynamic::from("danger")),
896 ("lable".into(), Dynamic::from("Save")),
897 ]));
898 let error = button_schema().validate(&value).unwrap_err();
899
900 assert_eq!(
901 error.issues,
902 vec![
903 SchemaIssue::new("$.text", "required field is missing"),
904 SchemaIssue::new(
905 "$.variant",
906 "expected one of [primary, secondary], got `danger`",
907 ),
908 SchemaIssue::new("$.lable", "unknown field"),
909 ]
910 );
911 }
912
913 #[test]
914 fn node_callback_and_handle_types_are_distinct() {
915 ValueSchema::Node
916 .validate(&Dynamic::from(UiNode::text("content")))
917 .unwrap();
918 ValueSchema::Callback
919 .validate(&Dynamic::from(FnPtr::new("clicked").unwrap()))
920 .unwrap();
921 ValueSchema::Handle {
922 kind: "image".to_owned(),
923 }
924 .validate(&Dynamic::from(OpaqueHandle::new("image", 42)))
925 .unwrap();
926
927 let error = ValueSchema::Handle {
928 kind: "image".to_owned(),
929 }
930 .validate(&Dynamic::from(OpaqueHandle::new("task", 42)))
931 .unwrap_err();
932 assert_eq!(error.issues[0].path, "$".to_owned());
933 }
934
935 #[test]
936 fn bounds_and_unions_report_precise_failures() {
937 let schema = ValueSchema::one_of([
938 ValueSchema::bounded_integer(Some(1), Some(3)),
939 ValueSchema::enumeration(["auto"]),
940 ]);
941 schema.validate_definition().unwrap();
942 schema.validate(&Dynamic::from(2_i64)).unwrap();
943 schema.validate(&Dynamic::from("auto")).unwrap();
944
945 let error = schema.validate(&Dynamic::from(9_i64)).unwrap_err();
946 assert_eq!(error.issues.len(), 1);
947 assert!(error.issues[0].message.contains("integer <= 3"));
948 assert!(error.issues[0].message.contains("expected string"));
949 }
950
951 #[test]
952 fn invalid_definitions_are_rejected() {
953 assert!(matches!(
954 ValueSchema::bounded_integer(Some(4), Some(2)).validate_definition(),
955 Err(SchemaDefinitionError { ref path, .. }) if path == "$"
956 ));
957 assert!(ValueSchema::one_of([]).validate_definition().is_err());
958 assert!(
959 ValueSchema::bounded_number(Some(f64::NAN), None)
960 .validate_definition()
961 .is_err()
962 );
963 ValueSchema::positive_number()
964 .validate(&Dynamic::from(0.5_f64))
965 .unwrap();
966 assert!(
967 ValueSchema::positive_number()
968 .validate(&Dynamic::from(0_i64))
969 .is_err()
970 );
971 }
972
973 #[test]
974 fn length_ui_value_and_asset_are_distinct() {
975 ValueSchema::Length
976 .validate(&Dynamic::from(Length::pixels(12.0).unwrap()))
977 .unwrap();
978 ValueSchema::UiValue
979 .validate(&Dynamic::from_map(Map::from_iter([(
980 "nested".into(),
981 Dynamic::from_array(vec![Dynamic::from(1_i64)]),
982 )])))
983 .unwrap();
984 ValueSchema::Asset
985 .validate(&Dynamic::from(AssetId::parse("app/check").unwrap()))
986 .unwrap();
987 assert!(
988 ValueSchema::UiValue
989 .validate(&Dynamic::from(UiNode::text("not data")))
990 .is_err()
991 );
992 }
993}