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