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 pub(crate) fn validate_ui_value_first(
292 &self,
293 value: &UiValue,
294 ) -> Result<(), SchemaValidationError> {
295 value.validate().map_err(|error| SchemaValidationError {
296 issues: vec![SchemaIssue::new("$", error.to_string())],
297 })?;
298 self.validate_ui_value_at_first(value, "$")
299 .map_or(Ok(()), |issue| {
300 Err(SchemaValidationError {
301 issues: vec![issue],
302 })
303 })
304 }
305
306 #[allow(clippy::too_many_lines)]
307 fn validate_ui_value_at_first(&self, value: &UiValue, path: &str) -> Option<SchemaIssue> {
308 let expected = |name: &str| {
309 Some(SchemaIssue::new(
310 path,
311 format!("expected {name}, got {}", ui_value_type_name(value)),
312 ))
313 };
314 match self {
315 Self::Null => (!matches!(value, UiValue::Null))
316 .then(|| expected("null"))
317 .flatten(),
318 Self::Bool => (!matches!(value, UiValue::Bool(_)))
319 .then(|| expected("bool"))
320 .flatten(),
321 Self::Integer { min, max } => match value {
322 UiValue::Integer(value) => {
323 if min.is_some_and(|min| *value < min) {
324 Some(SchemaIssue::new(
325 path,
326 format!("expected integer >= {}, got {value}", min.unwrap()),
327 ))
328 } else if max.is_some_and(|max| *value > max) {
329 Some(SchemaIssue::new(
330 path,
331 format!("expected integer <= {}, got {value}", max.unwrap()),
332 ))
333 } else {
334 None
335 }
336 }
337 _ => expected("integer"),
338 },
339 Self::Float {
340 min,
341 max,
342 exclusive_min,
343 exclusive_max,
344 } => match value {
345 UiValue::Float(value) => {
346 first_float_issue(*value, *min, *max, *exclusive_min, *exclusive_max, path)
347 }
348 _ => expected("float"),
349 },
350 Self::Number {
351 min,
352 max,
353 exclusive_min,
354 exclusive_max,
355 } => match value {
356 UiValue::Float(value) => {
357 first_float_issue(*value, *min, *max, *exclusive_min, *exclusive_max, path)
358 }
359 UiValue::Integer(value) => first_float_issue(
360 integer_as_float(*value),
361 *min,
362 *max,
363 *exclusive_min,
364 *exclusive_max,
365 path,
366 ),
367 _ => expected("number"),
368 },
369 Self::String { allowed } => match value {
370 UiValue::String(value)
371 if !allowed.is_empty() && !allowed.iter().any(|item| item == value) =>
372 {
373 Some(SchemaIssue::new(
374 path,
375 format!("expected one of [{}], got `{value}`", allowed.join(", ")),
376 ))
377 }
378 UiValue::String(_) => None,
379 _ => expected("string"),
380 },
381 Self::Array { items, max_items } => match value {
382 UiValue::Array(values) => {
383 if max_items.is_some_and(|max| values.len() > max) {
384 Some(SchemaIssue::new(
385 path,
386 format!(
387 "expected at most {} items, got {}",
388 max_items.unwrap(),
389 values.len()
390 ),
391 ))
392 } else {
393 values.iter().enumerate().find_map(|(index, value)| {
394 items.validate_ui_value_at_first(value, &format!("{path}[{index}]"))
395 })
396 }
397 }
398 _ => expected("array"),
399 },
400 Self::Map { values } => match value {
401 UiValue::Map(items) => items.iter().find_map(|(key, value)| {
402 values.validate_ui_value_at_first(value, &format!("{path}.{key}"))
403 }),
404 _ => expected("map"),
405 },
406 Self::Object {
407 fields,
408 allow_unknown,
409 } => match value {
410 UiValue::Map(values) => {
411 let field_issue = fields.iter().find_map(|(name, field)| {
412 values.get(name).map_or_else(
413 || {
414 field.required.then(|| {
415 SchemaIssue::new(
416 format!("{path}.{name}"),
417 "required field is missing",
418 )
419 })
420 },
421 |value| {
422 field
423 .schema
424 .validate_ui_value_at_first(value, &format!("{path}.{name}"))
425 },
426 )
427 });
428 field_issue.or_else(|| {
429 (!*allow_unknown).then(|| {
430 values
431 .keys()
432 .find(|key| !fields.contains_key(*key))
433 .map(|key| {
434 SchemaIssue::new(format!("{path}.{key}"), "unknown field")
435 })
436 })?
437 })
438 }
439 _ => expected("object"),
440 },
441 Self::Optional { value: schema } => {
442 if matches!(value, UiValue::Null) {
443 None
444 } else {
445 schema.validate_ui_value_at_first(value, path)
446 }
447 }
448 Self::OneOf { variants } => {
449 let failures = variants
450 .iter()
451 .map(|variant| variant.validate_ui_value_at_first(value, path))
452 .collect::<Vec<_>>();
453 if failures.iter().any(Option::is_none) {
454 None
455 } else {
456 Some(SchemaIssue::new(
457 path,
458 format!(
459 "value did not match any one_of variant ({})",
460 failures
461 .into_iter()
462 .enumerate()
463 .filter_map(|(index, issue)| issue
464 .map(|issue| format!("variant {index}: {}", issue.message)))
465 .collect::<Vec<_>>()
466 .join("; ")
467 ),
468 ))
469 }
470 }
471 Self::UiValue => None,
472 Self::Handle { kind } => match value {
473 UiValue::Handle(handle) if handle.kind() == kind => None,
474 UiValue::Handle(handle) => Some(SchemaIssue::new(
475 path,
476 format!("expected `{kind}` handle, got `{}` handle", handle.kind()),
477 )),
478 _ => expected(&format!("{kind} handle")),
479 },
480 Self::Node => expected("UiNode"),
481 Self::Callback => expected("callback or NativeHandlerRef"),
482 Self::Style => expected("Style"),
483 Self::Length => expected("Length"),
484 Self::Asset => expected("AssetId"),
485 Self::Signal => expected("NativeSignal"),
486 Self::Collection => expected("NativeCollection"),
487 Self::Document => expected("NativeTextDocument"),
488 #[cfg(feature = "charts")]
489 Self::ChartData => expected("NativeChartData"),
490 Self::Ref => expected("ElementRef"),
491 }
492 }
493
494 fn validate_at(&self, value: &Dynamic, path: &str, issues: &mut Vec<SchemaIssue>) {
495 match self {
496 Self::Null => expect_type(value.is_unit(), value, path, "null", issues),
497 Self::Bool => expect_type(value.is::<bool>(), value, path, "bool", issues),
498 Self::Integer { min, max } => validate_integer(value, *min, *max, path, issues),
499 Self::Float {
500 min,
501 max,
502 exclusive_min,
503 exclusive_max,
504 } => validate_float(
505 value,
506 *min,
507 *max,
508 *exclusive_min,
509 *exclusive_max,
510 path,
511 issues,
512 ),
513 Self::Number {
514 min,
515 max,
516 exclusive_min,
517 exclusive_max,
518 } => validate_number(
519 value,
520 *min,
521 *max,
522 *exclusive_min,
523 *exclusive_max,
524 path,
525 issues,
526 ),
527 Self::String { allowed } => validate_string(value, allowed, path, issues),
528 Self::Array { items, max_items } => {
529 validate_array(value, items, *max_items, path, issues);
530 }
531 Self::Map { values } => validate_map(value, values, path, issues),
532 Self::Object {
533 fields,
534 allow_unknown,
535 } => validate_object_value(value, fields, *allow_unknown, path, issues),
536 Self::Optional { value: schema } => {
537 if !value.is_unit() {
538 schema.validate_at(value, path, issues);
539 }
540 }
541 Self::OneOf { variants } => validate_one_of(value, variants, path, issues),
542 Self::Node => expect_type(value.is::<UiNode>(), value, path, "UiNode", issues),
543 Self::Callback => expect_type(
544 value.is::<FnPtr>() || value.is::<crate::NativeHandlerRef>(),
545 value,
546 path,
547 "callback or NativeHandlerRef",
548 issues,
549 ),
550 Self::Style => expect_type(value.is::<Style>(), value, path, "Style", issues),
551 Self::Length => validate_length(value, path, issues),
552 Self::UiValue => {
553 if let Err(error) = UiValue::from_dynamic(value.clone()) {
554 issues.push(SchemaIssue::new(path, error.to_string()));
555 }
556 }
557 Self::Asset => expect_type(value.is::<AssetId>(), value, path, "AssetId", issues),
558 Self::Signal => expect_type(
559 value.is::<NativeSignal>(),
560 value,
561 path,
562 "NativeSignal",
563 issues,
564 ),
565 Self::Collection => expect_type(
566 value.is::<crate::NativeCollection>(),
567 value,
568 path,
569 "NativeCollection",
570 issues,
571 ),
572 Self::Document => expect_type(
573 value.is::<crate::NativeTextDocument>(),
574 value,
575 path,
576 "NativeTextDocument",
577 issues,
578 ),
579 #[cfg(feature = "charts")]
580 Self::ChartData => expect_type(
581 value.is::<crate::NativeChartData>(),
582 value,
583 path,
584 "NativeChartData",
585 issues,
586 ),
587 Self::Ref => expect_type(value.is::<ElementRef>(), value, path, "ElementRef", issues),
588 Self::Handle { kind } => validate_handle(value, kind, path, issues),
589 }
590 }
591}
592
593fn ui_value_type_name(value: &UiValue) -> &'static str {
594 match value {
595 UiValue::Null => "null",
596 UiValue::Bool(_) => "bool",
597 UiValue::Integer(_) => "integer",
598 UiValue::Float(_) => "float",
599 UiValue::String(_) => "string",
600 UiValue::Array(_) => "array",
601 UiValue::Map(_) => "map",
602 UiValue::Handle(_) => "handle",
603 }
604}
605
606fn first_float_issue(
607 value: FLOAT,
608 min: Option<FLOAT>,
609 max: Option<FLOAT>,
610 exclusive_min: Option<FLOAT>,
611 exclusive_max: Option<FLOAT>,
612 path: &str,
613) -> Option<SchemaIssue> {
614 if !value.is_finite() {
615 Some(SchemaIssue::new(path, "number must be finite"))
616 } else if min.is_some_and(|min| value < min) {
617 Some(SchemaIssue::new(
618 path,
619 format!("expected number >= {}, got {value}", min.unwrap()),
620 ))
621 } else if max.is_some_and(|max| value > max) {
622 Some(SchemaIssue::new(
623 path,
624 format!("expected number <= {}, got {value}", max.unwrap()),
625 ))
626 } else if exclusive_min.is_some_and(|min| value <= min) {
627 Some(SchemaIssue::new(
628 path,
629 format!("expected number > {}, got {value}", exclusive_min.unwrap()),
630 ))
631 } else if exclusive_max.is_some_and(|max| value >= max) {
632 Some(SchemaIssue::new(
633 path,
634 format!("expected number < {}, got {value}", exclusive_max.unwrap()),
635 ))
636 } else {
637 None
638 }
639}
640
641fn validate_integer(
642 value: &Dynamic,
643 min: Option<INT>,
644 max: Option<INT>,
645 path: &str,
646 issues: &mut Vec<SchemaIssue>,
647) {
648 if value.is::<INT>() {
649 validate_integer_value(value.clone_cast::<INT>(), min, max, path, issues);
650 } else {
651 expect_type(false, value, path, "integer", issues);
652 }
653}
654
655fn validate_float(
656 value: &Dynamic,
657 min: Option<FLOAT>,
658 max: Option<FLOAT>,
659 exclusive_min: Option<FLOAT>,
660 exclusive_max: Option<FLOAT>,
661 path: &str,
662 issues: &mut Vec<SchemaIssue>,
663) {
664 if value.is::<FLOAT>() {
665 validate_float_value(
666 value.clone_cast::<FLOAT>(),
667 min,
668 max,
669 exclusive_min,
670 exclusive_max,
671 path,
672 issues,
673 );
674 } else {
675 expect_type(false, value, path, "float", issues);
676 }
677}
678
679fn validate_number(
680 value: &Dynamic,
681 min: Option<FLOAT>,
682 max: Option<FLOAT>,
683 exclusive_min: Option<FLOAT>,
684 exclusive_max: Option<FLOAT>,
685 path: &str,
686 issues: &mut Vec<SchemaIssue>,
687) {
688 let number = if value.is::<INT>() {
689 Some(integer_as_float(value.clone_cast::<INT>()))
690 } else if value.is::<FLOAT>() {
691 Some(value.clone_cast::<FLOAT>())
692 } else {
693 None
694 };
695 if let Some(number) = number {
696 validate_float_value(number, min, max, exclusive_min, exclusive_max, path, issues);
697 } else {
698 expect_type(false, value, path, "number", issues);
699 }
700}
701
702fn validate_string(value: &Dynamic, allowed: &[String], path: &str, issues: &mut Vec<SchemaIssue>) {
703 if value.is::<ImmutableString>() {
704 let actual = value.clone_cast::<ImmutableString>();
705 if !allowed.is_empty() && !allowed.iter().any(|allowed| allowed == actual.as_str()) {
706 issues.push(SchemaIssue::new(
707 path,
708 format!("expected one of [{}], got `{actual}`", allowed.join(", ")),
709 ));
710 }
711 } else {
712 expect_type(false, value, path, "string", issues);
713 }
714}
715
716fn validate_array(
717 value: &Dynamic,
718 items: &ValueSchema,
719 max_items: Option<usize>,
720 path: &str,
721 issues: &mut Vec<SchemaIssue>,
722) {
723 if !value.is::<Array>() {
724 expect_type(false, value, path, "array", issues);
725 return;
726 }
727 let values = value.clone_cast::<Array>();
728 if let Some(max_items) = max_items
729 && values.len() > max_items
730 {
731 issues.push(SchemaIssue::new(
732 path,
733 format!("expected at most {max_items} items, got {}", values.len()),
734 ));
735 }
736 for (index, item) in values.iter().enumerate() {
737 items.validate_at(item, &format!("{path}[{index}]"), issues);
738 }
739}
740
741fn validate_map(value: &Dynamic, values: &ValueSchema, path: &str, issues: &mut Vec<SchemaIssue>) {
742 if !value.is::<Map>() {
743 expect_type(false, value, path, "map", issues);
744 return;
745 }
746 for (key, item) in value.clone_cast::<Map>() {
747 values.validate_at(&item, &format!("{path}.{key}"), issues);
748 }
749}
750
751fn validate_object_value(
752 value: &Dynamic,
753 fields: &BTreeMap<String, ObjectField>,
754 allow_unknown: bool,
755 path: &str,
756 issues: &mut Vec<SchemaIssue>,
757) {
758 if value.is::<Map>() {
759 validate_object(
760 &value.clone_cast::<Map>(),
761 fields,
762 allow_unknown,
763 path,
764 issues,
765 );
766 } else {
767 expect_type(false, value, path, "object", issues);
768 }
769}
770
771fn validate_one_of(
772 value: &Dynamic,
773 variants: &[ValueSchema],
774 path: &str,
775 issues: &mut Vec<SchemaIssue>,
776) {
777 let mut branch_issues = Vec::with_capacity(variants.len());
778 for variant in variants {
779 let mut candidate = Vec::new();
780 variant.validate_at(value, path, &mut candidate);
781 if candidate.is_empty() {
782 return;
783 }
784 branch_issues.push(candidate);
785 }
786 let summary = branch_issues
787 .iter()
788 .enumerate()
789 .map(|(index, candidate)| {
790 let messages = candidate
791 .iter()
792 .map(|issue| issue.message.as_str())
793 .collect::<Vec<_>>()
794 .join(", ");
795 format!("variant {index}: {messages}")
796 })
797 .collect::<Vec<_>>()
798 .join("; ");
799 issues.push(SchemaIssue::new(
800 path,
801 format!("value did not match any one_of variant ({summary})"),
802 ));
803}
804
805fn validate_length(value: &Dynamic, path: &str, issues: &mut Vec<SchemaIssue>) {
806 if value.is::<Length>() {
807 let length = value.clone_cast::<Length>();
808 if let Err(error) = length.validate() {
809 issues.push(SchemaIssue::new(path, error.to_string()));
810 }
811 } else {
812 expect_type(false, value, path, "Length", issues);
813 }
814}
815
816fn validate_handle(value: &Dynamic, kind: &str, path: &str, issues: &mut Vec<SchemaIssue>) {
817 if value.is::<OpaqueHandle>() {
818 let handle = value.clone_cast::<OpaqueHandle>();
819 if handle.kind() != kind {
820 issues.push(SchemaIssue::new(
821 path,
822 format!("expected `{kind}` handle, got `{}` handle", handle.kind()),
823 ));
824 }
825 } else {
826 expect_type(false, value, path, &format!("{kind} handle"), issues);
827 }
828}
829
830fn validate_integer_bounds(
831 min: Option<INT>,
832 max: Option<INT>,
833 path: &str,
834) -> Result<(), SchemaDefinitionError> {
835 if min.zip(max).is_some_and(|(min, max)| min > max) {
836 Err(SchemaDefinitionError::new(
837 path,
838 format!("integer minimum {min:?} exceeds maximum {max:?}"),
839 ))
840 } else {
841 Ok(())
842 }
843}
844
845fn validate_float_bounds(
846 min: Option<FLOAT>,
847 max: Option<FLOAT>,
848 exclusive_min: Option<FLOAT>,
849 exclusive_max: Option<FLOAT>,
850 path: &str,
851) -> Result<(), SchemaDefinitionError> {
852 if min.is_some_and(|value| !value.is_finite())
853 || max.is_some_and(|value| !value.is_finite())
854 || exclusive_min.is_some_and(|value| !value.is_finite())
855 || exclusive_max.is_some_and(|value| !value.is_finite())
856 {
857 return Err(SchemaDefinitionError::new(
858 path,
859 "numeric bounds must be finite",
860 ));
861 }
862 if min.is_some() && exclusive_min.is_some() {
863 return Err(SchemaDefinitionError::new(
864 path,
865 "numeric schema cannot define both min and exclusive_min",
866 ));
867 }
868 if max.is_some() && exclusive_max.is_some() {
869 return Err(SchemaDefinitionError::new(
870 path,
871 "numeric schema cannot define both max and exclusive_max",
872 ));
873 }
874 let lower = min.or(exclusive_min);
875 let upper = max.or(exclusive_max);
876 let empty = lower.zip(upper).is_some_and(|(lower, upper)| {
877 matches!(lower.total_cmp(&upper), Ordering::Greater)
878 || (matches!(lower.total_cmp(&upper), Ordering::Equal)
879 && (exclusive_min.is_some() || exclusive_max.is_some()))
880 });
881 if empty {
882 Err(SchemaDefinitionError::new(
883 path,
884 format!("numeric lower bound {lower:?} does not precede upper bound {upper:?}"),
885 ))
886 } else {
887 Ok(())
888 }
889}
890
891fn validate_integer_value(
892 value: INT,
893 min: Option<INT>,
894 max: Option<INT>,
895 path: &str,
896 issues: &mut Vec<SchemaIssue>,
897) {
898 if let Some(min) = min
899 && value < min
900 {
901 issues.push(SchemaIssue::new(
902 path,
903 format!("expected integer >= {min}, got {value}"),
904 ));
905 }
906 if let Some(max) = max
907 && value > max
908 {
909 issues.push(SchemaIssue::new(
910 path,
911 format!("expected integer <= {max}, got {value}"),
912 ));
913 }
914}
915
916fn validate_float_value(
917 value: FLOAT,
918 min: Option<FLOAT>,
919 max: Option<FLOAT>,
920 exclusive_min: Option<FLOAT>,
921 exclusive_max: Option<FLOAT>,
922 path: &str,
923 issues: &mut Vec<SchemaIssue>,
924) {
925 if !value.is_finite() {
926 issues.push(SchemaIssue::new(path, "number must be finite"));
927 return;
928 }
929 if let Some(min) = min
930 && value < min
931 {
932 issues.push(SchemaIssue::new(
933 path,
934 format!("expected number >= {min}, got {value}"),
935 ));
936 }
937 if let Some(max) = max
938 && value > max
939 {
940 issues.push(SchemaIssue::new(
941 path,
942 format!("expected number <= {max}, got {value}"),
943 ));
944 }
945 if let Some(min) = exclusive_min
946 && value <= min
947 {
948 issues.push(SchemaIssue::new(
949 path,
950 format!("expected number > {min}, got {value}"),
951 ));
952 }
953 if let Some(max) = exclusive_max
954 && value >= max
955 {
956 issues.push(SchemaIssue::new(
957 path,
958 format!("expected number < {max}, got {value}"),
959 ));
960 }
961}
962
963fn integer_as_float(value: INT) -> FLOAT {
964 value
965 .to_string()
966 .parse()
967 .expect("an integer always has a finite float representation")
968}
969
970#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
971pub struct ObjectField {
972 pub schema: ValueSchema,
973 #[serde(default)]
974 pub required: bool,
975 #[serde(default)]
976 pub sensitive: bool,
977 #[serde(default, skip_serializing_if = "Option::is_none")]
978 pub default: Option<crate::UiValue>,
979 #[serde(default, skip_serializing_if = "Option::is_none")]
981 pub doc: Option<String>,
982}
983
984impl ObjectField {
985 #[must_use]
986 pub fn required(schema: ValueSchema) -> Self {
987 Self {
988 schema,
989 required: true,
990 sensitive: false,
991 default: None,
992 doc: None,
993 }
994 }
995
996 #[must_use]
997 pub fn optional(schema: ValueSchema) -> Self {
998 Self {
999 schema,
1000 required: false,
1001 sensitive: false,
1002 default: None,
1003 doc: None,
1004 }
1005 }
1006
1007 #[must_use]
1008 pub fn sensitive(mut self) -> Self {
1009 self.sensitive = true;
1010 self
1011 }
1012
1013 #[must_use]
1014 pub fn with_doc(mut self, doc: impl Into<String>) -> Self {
1015 self.doc = Some(doc.into());
1016 self
1017 }
1018
1019 #[must_use]
1020 pub fn with_default(mut self, default: crate::UiValue) -> Self {
1021 self.default = Some(default);
1022 self
1023 }
1024}
1025
1026fn validate_object(
1027 value: &Map,
1028 fields: &BTreeMap<String, ObjectField>,
1029 allow_unknown: bool,
1030 path: &str,
1031 issues: &mut Vec<SchemaIssue>,
1032) {
1033 let actual_keys = value
1034 .keys()
1035 .map(ToString::to_string)
1036 .collect::<BTreeSet<_>>();
1037
1038 for (name, field) in fields {
1039 match value.get(name.as_str()) {
1040 Some(value) => field
1041 .schema
1042 .validate_at(value, &format!("{path}.{name}"), issues),
1043 None if field.required => issues.push(SchemaIssue::new(
1044 format!("{path}.{name}"),
1045 "required field is missing",
1046 )),
1047 None => {}
1048 }
1049 }
1050
1051 if !allow_unknown {
1052 for unknown in actual_keys.difference(&fields.keys().cloned().collect()) {
1053 issues.push(SchemaIssue::new(
1054 format!("{path}.{unknown}"),
1055 "unknown field",
1056 ));
1057 }
1058 }
1059}
1060
1061fn expect_type(
1062 valid: bool,
1063 value: &Dynamic,
1064 path: &str,
1065 expected: &str,
1066 issues: &mut Vec<SchemaIssue>,
1067) {
1068 if !valid {
1069 issues.push(SchemaIssue::new(
1070 path,
1071 format!("expected {expected}, got {}", value.type_name()),
1072 ));
1073 }
1074}
1075
1076#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1077pub struct SchemaIssue {
1078 pub path: String,
1079 pub message: String,
1080}
1081
1082impl SchemaIssue {
1083 #[must_use]
1084 pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
1085 Self {
1086 path: path.into(),
1087 message: message.into(),
1088 }
1089 }
1090}
1091
1092#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1093pub struct SchemaValidationError {
1094 pub issues: Vec<SchemaIssue>,
1095}
1096
1097impl fmt::Display for SchemaValidationError {
1098 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1099 for (index, issue) in self.issues.iter().enumerate() {
1100 if index > 0 {
1101 formatter.write_str("; ")?;
1102 }
1103 write!(formatter, "{}: {}", issue.path, issue.message)?;
1104 }
1105 Ok(())
1106 }
1107}
1108
1109impl std::error::Error for SchemaValidationError {}
1110
1111#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1112pub struct SchemaDefinitionError {
1113 pub path: String,
1114 pub message: String,
1115}
1116
1117impl SchemaDefinitionError {
1118 #[must_use]
1119 pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
1120 Self {
1121 path: path.into(),
1122 message: message.into(),
1123 }
1124 }
1125}
1126
1127impl fmt::Display for SchemaDefinitionError {
1128 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1129 write!(formatter, "{}: {}", self.path, self.message)
1130 }
1131}
1132
1133impl std::error::Error for SchemaDefinitionError {}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::*;
1138
1139 fn button_schema() -> ValueSchema {
1140 ValueSchema::object(BTreeMap::from([
1141 (
1142 "text".to_owned(),
1143 ObjectField::required(ValueSchema::string()),
1144 ),
1145 (
1146 "variant".to_owned(),
1147 ObjectField::optional(ValueSchema::enumeration(["primary", "secondary"])),
1148 ),
1149 (
1150 "on_click".to_owned(),
1151 ObjectField::optional(ValueSchema::Callback),
1152 ),
1153 ]))
1154 }
1155
1156 #[test]
1157 fn objects_report_all_precise_paths() {
1158 let value = Dynamic::from_map(Map::from_iter([
1159 ("variant".into(), Dynamic::from("danger")),
1160 ("lable".into(), Dynamic::from("Save")),
1161 ]));
1162 let error = button_schema().validate(&value).unwrap_err();
1163
1164 assert_eq!(
1165 error.issues,
1166 vec![
1167 SchemaIssue::new("$.text", "required field is missing"),
1168 SchemaIssue::new(
1169 "$.variant",
1170 "expected one of [primary, secondary], got `danger`",
1171 ),
1172 SchemaIssue::new("$.lable", "unknown field"),
1173 ]
1174 );
1175 }
1176
1177 #[test]
1178 fn node_callback_and_handle_types_are_distinct() {
1179 ValueSchema::Node
1180 .validate(&Dynamic::from(UiNode::text("content")))
1181 .unwrap();
1182 ValueSchema::Callback
1183 .validate(&Dynamic::from(FnPtr::new("clicked").unwrap()))
1184 .unwrap();
1185 ValueSchema::Handle {
1186 kind: "image".to_owned(),
1187 }
1188 .validate(&Dynamic::from(OpaqueHandle::new("image", 42)))
1189 .unwrap();
1190
1191 let error = ValueSchema::Handle {
1192 kind: "image".to_owned(),
1193 }
1194 .validate(&Dynamic::from(OpaqueHandle::new("task", 42)))
1195 .unwrap_err();
1196 assert_eq!(error.issues[0].path, "$".to_owned());
1197 }
1198
1199 #[test]
1200 fn bounds_and_unions_report_precise_failures() {
1201 let schema = ValueSchema::one_of([
1202 ValueSchema::bounded_integer(Some(1), Some(3)),
1203 ValueSchema::enumeration(["auto"]),
1204 ]);
1205 schema.validate_definition().unwrap();
1206 schema.validate(&Dynamic::from(2_i64)).unwrap();
1207 schema.validate(&Dynamic::from("auto")).unwrap();
1208
1209 let error = schema.validate(&Dynamic::from(9_i64)).unwrap_err();
1210 assert_eq!(error.issues.len(), 1);
1211 assert!(error.issues[0].message.contains("integer <= 3"));
1212 assert!(error.issues[0].message.contains("expected string"));
1213 }
1214
1215 #[test]
1216 fn invalid_definitions_are_rejected() {
1217 assert!(matches!(
1218 ValueSchema::bounded_integer(Some(4), Some(2)).validate_definition(),
1219 Err(SchemaDefinitionError { ref path, .. }) if path == "$"
1220 ));
1221 assert!(ValueSchema::one_of([]).validate_definition().is_err());
1222 assert!(
1223 ValueSchema::bounded_number(Some(f64::NAN), None)
1224 .validate_definition()
1225 .is_err()
1226 );
1227 ValueSchema::positive_number()
1228 .validate(&Dynamic::from(0.5_f64))
1229 .unwrap();
1230 assert!(
1231 ValueSchema::positive_number()
1232 .validate(&Dynamic::from(0_i64))
1233 .is_err()
1234 );
1235 }
1236
1237 #[test]
1238 fn length_ui_value_and_asset_are_distinct() {
1239 ValueSchema::Length
1240 .validate(&Dynamic::from(Length::pixels(12.0).unwrap()))
1241 .unwrap();
1242 ValueSchema::UiValue
1243 .validate(&Dynamic::from_map(Map::from_iter([(
1244 "nested".into(),
1245 Dynamic::from_array(vec![Dynamic::from(1_i64)]),
1246 )])))
1247 .unwrap();
1248 ValueSchema::Asset
1249 .validate(&Dynamic::from(AssetId::parse("app/check").unwrap()))
1250 .unwrap();
1251 assert!(
1252 ValueSchema::UiValue
1253 .validate(&Dynamic::from(UiNode::text("not data")))
1254 .is_err()
1255 );
1256 }
1257
1258 #[test]
1259 fn online_ui_value_validation_stops_after_the_first_issue() {
1260 let value = UiValue::Map(
1261 (0..100_000)
1262 .map(|index| (format!("field-{index}"), UiValue::Null))
1263 .collect(),
1264 );
1265 let error = ValueSchema::Map {
1266 values: Box::new(ValueSchema::integer()),
1267 }
1268 .validate_ui_value_first(&value)
1269 .unwrap_err();
1270 assert_eq!(error.issues.len(), 1);
1271 assert_eq!(error.issues[0].path, "$.field-0");
1272 }
1273
1274 #[test]
1275 fn online_and_complete_durable_validation_agree_on_acceptance() {
1276 let cases = [
1277 (ValueSchema::integer(), UiValue::Integer(2)),
1278 (ValueSchema::integer(), UiValue::Null),
1279 (
1280 ValueSchema::Array {
1281 items: Box::new(ValueSchema::string()),
1282 max_items: Some(2),
1283 },
1284 UiValue::Array(vec![UiValue::String("ok".to_owned())]),
1285 ),
1286 (
1287 ValueSchema::Array {
1288 items: Box::new(ValueSchema::string()),
1289 max_items: Some(2),
1290 },
1291 UiValue::Array(vec![UiValue::Integer(1)]),
1292 ),
1293 (
1294 button_schema(),
1295 UiValue::Map(BTreeMap::from([(
1296 "text".to_owned(),
1297 UiValue::String("Save".to_owned()),
1298 )])),
1299 ),
1300 (
1301 button_schema(),
1302 UiValue::Map(BTreeMap::from([(
1303 "variant".to_owned(),
1304 UiValue::String("danger".to_owned()),
1305 )])),
1306 ),
1307 ];
1308 for (schema, value) in cases {
1309 assert_eq!(
1310 schema.validate_ui_value(&value).is_ok(),
1311 schema.validate_ui_value_first(&value).is_ok(),
1312 "schema={schema:?}, value={value:?}"
1313 );
1314 }
1315 }
1316}