1use itertools::Itertools;
2
3use crate::common_util::format_path;
4use crate::error::Error;
5use crate::spec::{Node, Spec};
6use crate::types::{HashMap, HashSet};
7
8pub fn from_yaml_str(yaml_str: &str) -> Result<Spec, Error> {
9 let yaml_val: serde_yaml::Value = serde_yaml::from_str(yaml_str)?;
10 let root_path = [];
11 Ok(Spec(build_node(
12 &yaml_val,
13 &HashMap::default(),
14 &root_path,
15 )?))
16}
17
18pub fn is_leaf(spec_node: &Node) -> bool {
19 match spec_node {
20 Node::Sub { .. } | Node::AnonMap { .. } | Node::Variant { .. } | Node::Optional { .. } => {
21 false
22 }
23 Node::Bool { .. }
24 | Node::Real { .. }
25 | Node::Int { .. }
26 | Node::Enum { .. }
27 | Node::Const => true,
28 }
29}
30
31const BUILT_IN_TYPE_NAMES: &'static [&'static str] = &[
32 "real", "int", "bool", "sub", "anon map", "variant", "enum", "optional", "const",
33];
34
35fn build_node(
36 yaml_val: &serde_yaml::Value,
37 type_defs: &HashMap<String, Node>,
38 path: &[&str],
39) -> Result<Node, Error> {
40 let mapping = match yaml_val {
41 serde_yaml::Value::Mapping(mapping) => mapping,
42 _ => {
43 return Err(Error::ValueMustBeMap {
44 path_hint: format_path(path),
45 })
46 }
47 };
48
49 let type_name = extract_string(mapping, "type", path, false)?;
50 let type_name = type_name.as_deref().unwrap_or("sub");
51
52 match type_name {
53 "real" => build_real(mapping, path),
54 "int" => build_int(mapping, path),
55 "bool" => build_bool(mapping, path),
56 "sub" => build_sub(mapping, type_defs, path),
57 "anon map" => build_anon_map(mapping, type_defs, path),
58 "variant" => build_variant(mapping, type_defs, path),
59 "enum" => build_enum(mapping, path),
60 "optional" => build_optional(mapping, type_defs, path),
61 "const" => build_const(mapping, path),
62 type_name => match type_defs.get(type_name) {
63 Some(node) => {
64 check_for_unexpected_attributes(mapping, ["type"], path)?;
65 Ok(node.clone())
66 }
67 None => Err(Error::UnknownTypeName {
68 path_hint: format_path(path),
69 unknown_type_name: type_name.to_owned(),
70 }),
71 },
72 }
73}
74
75fn check_bounds_sanity<T: PartialOrd>(
76 min: Option<T>,
77 max: Option<T>,
78 path: &[&str],
79) -> Result<(), Error> {
80 do_check_bounds_sanity(min, max, || Error::InvalidBounds {
81 path_hint: format_path(path),
82 })
83}
84
85fn check_size_bounds_sanity<T: PartialOrd>(
86 min: Option<T>,
87 max: Option<T>,
88 path: &[&str],
89) -> Result<(), Error> {
90 do_check_bounds_sanity(min, max, || Error::InvalidSizeBounds {
91 path_hint: format_path(path),
92 })
93}
94
95fn do_check_bounds_sanity<F, T: PartialOrd>(
96 min: Option<T>,
97 max: Option<T>,
98 error_supplier: F,
99) -> Result<(), Error>
100where
101 F: FnOnce() -> Error,
102{
103 match (min, max) {
104 (Some(min), Some(max)) if min >= max => Err(error_supplier()),
105 _ => Ok(()),
106 }
107}
108
109fn check_finite(num: f64, attribute_name: &str, path: &[&str]) -> Result<(), Error> {
110 if !f64::is_finite(num) {
111 Err(Error::NonFiniteNumber {
112 path_hint: format_path(path),
113 attribute_name: attribute_name.to_string(),
114 })
115 } else {
116 Ok(())
117 }
118}
119
120fn build_real(mapping: &serde_yaml::Mapping, path: &[&str]) -> Result<Node, Error> {
121 check_for_unexpected_attributes(mapping, ["type", "min", "max", "scale", "init"], path)?;
122
123 let min = extract_real(mapping, "min", path, false)?;
124 let max = extract_real(mapping, "max", path, false)?;
125
126 check_bounds_sanity(min, max, path)?;
127
128 let init = extract_real(mapping, "init", path, true)?.unwrap_or({
129 let mut init = 0.0;
130 min.iter().for_each(|min| {
131 init = f64::max(*min, init);
132 });
133 max.iter().for_each(|max| {
134 init = f64::min(*max, init);
135 });
136 init
137 });
138
139 if init < min.unwrap_or(init) || init > max.unwrap_or(init) {
140 return Err(Error::InitNotWithinBounds {
141 path_hint: format_path(path),
142 });
143 }
144
145 let scale = extract_real(mapping, "scale", path, true)?.unwrap_or(1.0);
146 check_scale(scale, path)?;
147
148 Ok(Node::Real {
149 init,
150 scale,
151 min,
152 max,
153 })
154}
155
156fn check_scale(scale: f64, path: &[&str]) -> Result<(), Error> {
157 if scale <= 0. {
158 Err(Error::ScaleMustBeStrictlyPositive {
159 path_hint: format_path(path),
160 })
161 } else {
162 Ok(())
163 }
164}
165
166fn build_int(mapping: &serde_yaml::Mapping, path: &[&str]) -> Result<Node, Error> {
167 check_for_unexpected_attributes(mapping, ["type", "min", "max", "scale", "init"], path)?;
168
169 let min = extract_int(mapping, "min", path, false)?;
170 let max = extract_int(mapping, "max", path, false)?;
171
172 check_bounds_sanity(min, max, path)?;
173
174 let init = extract_int(mapping, "init", path, true)?.unwrap_or({
175 let mut init = 0;
176 min.iter().for_each(|min| {
177 init = i64::max(*min, init);
178 });
179 max.iter().for_each(|max| {
180 init = i64::min(*max, init);
181 });
182 init
183 });
184
185 if init < min.unwrap_or(init) || init > max.unwrap_or(init) {
186 return Err(Error::InitNotWithinBounds {
187 path_hint: format_path(path),
188 });
189 }
190
191 let scale = extract_real(mapping, "scale", path, true)?.unwrap_or(1.0);
192 check_scale(scale, path)?;
193
194 Ok(Node::Int {
195 init,
196 scale,
197 min,
198 max,
199 })
200}
201
202fn build_bool(mapping: &serde_yaml::Mapping, path: &[&str]) -> Result<Node, Error> {
203 check_for_unexpected_attributes(mapping, ["type", "init"], path)?;
204
205 Ok(Node::Bool {
206 init: extract_bool(mapping, "init", path, true)?.unwrap(),
207 })
208}
209
210fn build_sub(
211 mapping: &serde_yaml::Mapping,
212 type_defs: &HashMap<String, Node>,
213 path: &[&str],
214) -> Result<Node, Error> {
215 let mut out_mapping = HashMap::default();
216
217 let mut sub_type_defs = type_defs.clone();
218 for (key, value) in mapping {
219 match key.as_str() {
220 Some(attribute_key) if attribute_key.starts_with("typeDef ") => {
221 let type_name = attribute_key.strip_prefix("typeDef ").unwrap();
222
223 if BUILT_IN_TYPE_NAMES.iter().contains(&type_name) {
224 return Err(Error::IllegalTypeDefName {
225 path_hint: format_path(path),
226 type_def_name: type_name.to_string(),
227 });
228 }
229
230 let path_of_sub = [path, &[attribute_key]].concat();
231
232 sub_type_defs.insert(
233 type_name.to_string(),
234 build_node(value, &sub_type_defs, &path_of_sub)?,
235 );
236 }
237 None => {
238 return Err(Error::InvalidAttributeKeyType {
239 path_hint: format_path(path),
240 formatted_attribute_key: format_attribute_key(key),
241 })
242 }
243 _ => (),
244 }
245 }
246
247 for (key, value) in mapping {
248 match key.as_str() {
249 Some(attribute_key)
250 if !attribute_key.eq("type") && !attribute_key.starts_with("typeDef") =>
251 {
252 let path_of_sub = [path, &[attribute_key]].concat();
253 out_mapping.insert(
254 attribute_key.to_string(),
255 Box::new(build_node(value, &sub_type_defs, &path_of_sub)?),
256 );
257 }
258 _ => (),
259 }
260 }
261
262 if out_mapping.is_empty() {
263 return Err(Error::EmptySub {
264 path_hint: format_path(path),
265 });
266 }
267
268 Ok(Node::Sub { map: out_mapping })
269}
270
271fn extract_value_type_attr_value(
272 mapping: &serde_yaml::Mapping,
273 type_defs: &HashMap<String, Node>,
274 path: &[&str],
275) -> Result<Box<Node>, Error> {
276 return match mapping.get("valueType") {
277 Some(value) => Ok(Box::new(build_node(value, type_defs, path)?)),
278 None => {
279 return Err(Error::MandatoryAttributeMissing {
280 path_hint: format_path(path),
281 missing_attribute_name: "valueType".to_string(),
282 });
283 }
284 };
285}
286
287fn build_anon_map(
288 mapping: &serde_yaml::Mapping,
289 type_defs: &HashMap<String, Node>,
290 path: &[&str],
291) -> Result<Node, Error> {
292 check_for_unexpected_attributes(
293 mapping,
294 ["type", "initSize", "minSize", "maxSize", "valueType"],
295 path,
296 )?;
297
298 let value_type = extract_value_type_attr_value(mapping, type_defs, path)?;
299
300 let min_size = extract_usize_attribute_value(mapping, "minSize", path, false)?;
301 let max_size = extract_usize_attribute_value(mapping, "maxSize", path, false)?;
302
303 check_size_bounds_sanity(min_size, max_size, path)?;
304
305 if max_size.filter(|max_size| *max_size == 0).is_some() {
306 return Err(Error::ZeroMaxSize {
307 path_hint: format_path(path),
308 });
309 }
310
311 let init_size = extract_usize_attribute_value(mapping, "initSize", path, true)?.unwrap();
312
313 let out_of_bounds = match (min_size, max_size) {
314 (Some(min_size), _) if init_size < min_size => true,
315 (_, Some(max_size)) if init_size > max_size => true,
316 _ => false,
317 };
318
319 if out_of_bounds {
320 Err(Error::InitSizeNotWithinBounds {
321 path_hint: format_path(path),
322 })
323 } else {
324 Ok(Node::AnonMap {
325 value_type,
326 init_size,
327 min_size,
328 max_size,
329 })
330 }
331}
332
333fn build_variant(
334 mapping: &serde_yaml::Mapping,
335 type_defs: &HashMap<String, Node>,
336 path: &[&str],
337) -> Result<Node, Error> {
338 let init_variant_name = extract_string(mapping, "init", path, true)?.unwrap();
339
340 let mut out_mapping = HashMap::default();
341 for (key, value) in mapping {
342 match key.as_str() {
343 Some(attribute_key) if !attribute_key.eq("type") && !attribute_key.eq("init") => {
344 let path_of_sub = [path, &[attribute_key]].concat();
345 out_mapping.insert(
346 attribute_key.to_string(),
347 Box::new(build_node(value, type_defs, &path_of_sub)?),
348 );
349 }
350 None => {
351 return Err(Error::InvalidAttributeKeyType {
352 path_hint: format_path(path),
353 formatted_attribute_key: format_attribute_key(key),
354 })
355 }
356 _ => (),
357 }
358 }
359
360 if out_mapping.len() < 2 {
361 return Err(Error::NotEnoughVariantValues {
362 path_hint: format_path(path),
363 });
364 }
365
366 if !out_mapping.contains_key(&init_variant_name) {
367 return Err(Error::InitNotAKnownValue {
368 path_hint: format_path(path),
369 init: init_variant_name,
370 });
371 }
372
373 Ok(Node::Variant {
374 map: out_mapping,
375 init: init_variant_name,
376 })
377}
378
379fn build_enum(mapping: &serde_yaml::Mapping, path: &[&str]) -> Result<Node, Error> {
380 check_for_unexpected_attributes(mapping, ["type", "init", "values"], path)?;
381
382 let mut values = Vec::new();
383 let init_value = extract_string(mapping, "init", path, true)?.unwrap();
384
385 let values_attr_name = "values".to_string();
386 let values_sequence = match mapping.get(&values_attr_name) {
387 None => {
388 return Err(Error::MandatoryAttributeMissing {
389 path_hint: format_path(path),
390 missing_attribute_name: values_attr_name,
391 })
392 }
393 Some(serde_yaml::Value::Sequence(values)) => values,
394 _ => {
395 return Err(Error::InvalidAttributeValueType {
396 path_hint: format_path(path),
397 attribute_name: values_attr_name,
398 expected_type_hint: "a sequence".to_string(),
399 })
400 }
401 };
402
403 if values_sequence.len() < 2 {
404 return Err(Error::NotEnoughEnumValues {
405 path_hint: format_path(path),
406 });
407 }
408
409 for name_value in values_sequence {
410 if let serde_yaml::Value::String(name) = name_value {
411 values.push(name.to_owned());
412 } else {
413 return Err(Error::EnumItemsMustBeString {
414 path_hint: format_path(path),
415 });
416 }
417 }
418
419 if !values.contains(&init_value) {
420 return Err(Error::InitNotAKnownValue {
421 path_hint: format_path(path),
422 init: init_value,
423 });
424 }
425
426 Ok(Node::Enum {
427 values,
428 init: init_value,
429 })
430}
431
432fn build_optional(
433 mapping: &serde_yaml::Mapping,
434 type_defs: &HashMap<String, Node>,
435 path: &[&str],
436) -> Result<Node, Error> {
437 check_for_unexpected_attributes(mapping, ["type", "initPresent", "valueType"], path)?;
438
439 let sub_path = [path, &["optional"]].concat();
440 let value_type = extract_value_type_attr_value(mapping, type_defs, &sub_path)?;
441 let init_present = extract_bool(mapping, "initPresent", path, true)?.unwrap();
442
443 Ok(Node::Optional {
444 value_type,
445 init_present,
446 })
447}
448
449fn build_const(mapping: &serde_yaml::Mapping, path: &[&str]) -> Result<Node, Error> {
450 check_for_unexpected_attributes(mapping, ["type"], path)?;
451 Ok(Node::Const)
452}
453
454fn extract_string(
455 mapping: &serde_yaml::Mapping,
456 attribute_name: &str,
457 path: &[&str],
458 mandatory: bool,
459) -> Result<Option<String>, Error> {
460 extract_attribute_value(
461 mapping,
462 attribute_name,
463 path,
464 |value| value.as_str().map(|s| s.to_string()),
465 "a string",
466 mandatory,
467 )
468}
469
470fn extract_real(
471 mapping: &serde_yaml::Mapping,
472 attribute_name: &str,
473 path: &[&str],
474 mandatory: bool,
475) -> Result<Option<f64>, Error> {
476 let result = extract_attribute_value(
477 mapping,
478 attribute_name,
479 path,
480 |value| value.as_f64(),
481 "a real number",
482 mandatory,
483 );
484
485 match result? {
486 res @ Some(num) => {
487 check_finite(num, attribute_name, path)?;
488 Ok(res)
489 }
490 res => Ok(res),
491 }
492}
493
494fn extract_int(
495 mapping: &serde_yaml::Mapping,
496 attribute_name: &str,
497 path: &[&str],
498 mandatory: bool,
499) -> Result<Option<i64>, Error> {
500 extract_attribute_value(
501 mapping,
502 attribute_name,
503 path,
504 |value| value.as_i64(),
505 "an integer",
506 mandatory,
507 )
508}
509
510fn extract_bool(
511 mapping: &serde_yaml::Mapping,
512 attribute_name: &str,
513 path: &[&str],
514 mandatory: bool,
515) -> Result<Option<bool>, Error> {
516 extract_attribute_value(
517 mapping,
518 attribute_name,
519 path,
520 |value| value.as_bool(),
521 "a boolean",
522 mandatory,
523 )
524}
525
526fn extract_usize_attribute_value(
527 mapping: &serde_yaml::Mapping,
528 attribute_name: &str,
529 path: &[&str],
530 mandatory: bool,
531) -> Result<Option<usize>, Error> {
532 let value_u64 = extract_attribute_value(
533 mapping,
534 attribute_name,
535 path,
536 |value| value.as_u64(),
537 "a positive integer",
538 mandatory,
539 )?;
540
541 match value_u64 {
542 Some(val) => match usize::try_from(val) {
543 Err(_) => Err(Error::UnsignedIntConversionFailed {
544 path_hint: format_path(path),
545 attribute_name: attribute_name.to_string(),
546 }),
547 Ok(res) => Ok(Some(res)),
548 },
549 _ => Ok(None),
550 }
551}
552
553fn extract_attribute_value<F, T>(
554 mapping: &serde_yaml::Mapping,
555 attribute_name: &str,
556 path: &[&str],
557 value_extractor: F,
558 expected_type_hint: &str,
559 mandatory: bool,
560) -> Result<Option<T>, Error>
561where
562 F: FnOnce(&serde_yaml::Value) -> Option<T>,
563{
564 let result = match mapping.get(attribute_name) {
565 Some(value) => match value_extractor(value) {
566 Some(value) => Some(value),
567 None => {
568 return Err(Error::InvalidAttributeValueType {
569 path_hint: format_path(path),
570 attribute_name: attribute_name.to_string(),
571 expected_type_hint: expected_type_hint.to_string(),
572 });
573 }
574 },
575 None => None,
576 };
577
578 match result {
579 None if mandatory => Err(Error::MandatoryAttributeMissing {
580 path_hint: format_path(path),
581 missing_attribute_name: attribute_name.to_string(),
582 }),
583 _ => Ok(result),
584 }
585}
586
587fn check_for_unexpected_attributes<const N: usize>(
588 mapping: &serde_yaml::Mapping,
589 allowed_attributes: [&str; N],
590 path: &[&str],
591) -> Result<(), Error> {
592 let allowed_attributes = HashSet::from_iter(allowed_attributes);
593 for attribute_key in mapping.keys() {
594 match attribute_key {
595 serde_yaml::Value::String(attribute_name) => {
596 if !allowed_attributes.contains(attribute_name.as_str()) {
597 return Err(Error::UnexpectedAttribute {
598 path_hint: format_path(path),
599 unexpected_attribute_name: attribute_name.clone(),
600 });
601 }
602 }
603 _ => {
604 return Err(Error::InvalidAttributeKeyType {
605 path_hint: format_path(path),
606 formatted_attribute_key: format_attribute_key(attribute_key),
607 });
608 }
609 }
610 }
611
612 Ok(())
613}
614
615fn format_attribute_key(key: &serde_yaml::Value) -> String {
616 serde_yaml::to_string(key).unwrap().replace('\n', "")
617}
618
619#[cfg(test)]
620mod tests {
621 use super::*;
622 use float_cmp::approx_eq;
623 use float_cmp::F64Margin;
624
625 #[test]
626 fn invalid_yaml() {
627 let invalid_yaml_str = "{";
628 assert!(matches!(
629 from_yaml_str(invalid_yaml_str),
630 Err(Error::InvalidYaml(_))
631 ));
632 }
633
634 #[test]
635 fn yaml_not_map() {
636 let yaml_str = "foo";
637 assert!(matches!(
638 from_yaml_str(yaml_str),
639 Err(Error::ValueMustBeMap { .. })
640 ));
641 }
642
643 #[test]
644 fn unknown_type_name() {
645 let yaml_str = "
646 type: foo
647 ";
648
649 assert!(matches!(
650 from_yaml_str(yaml_str),
651 Err(Error::UnknownTypeName { path_hint, unknown_type_name })
652 if path_hint == "(root)" && unknown_type_name == "foo"
653 ));
654 }
655
656 #[test]
657 fn const_node() {
658 let yaml_str = "
659 type: const
660 ";
661
662 assert!(matches!(from_yaml_str(yaml_str), Ok(Spec(Node::Const))))
663 }
664
665 #[test]
666 fn const_unexpected_attribute() {
667 let yaml_str = "
668 type: const
669 init: false
670 ";
671
672 assert!(matches!(
673 from_yaml_str(yaml_str),
674 Err(Error::UnexpectedAttribute { path_hint, unexpected_attribute_name })
675 if path_hint == "(root)" && unexpected_attribute_name == "init"
676 ));
677 }
678
679 #[test]
680 fn bool() {
681 let yaml_str = "
682 type: bool
683 init: true
684 ";
685 assert!(matches!(
686 from_yaml_str(yaml_str),
687 Ok(Spec(Node::Bool { init: true }))
688 ));
689 }
690
691 #[test]
692 fn real() {
693 let yaml_str = "
694 type: real
695 init: 0.25
696 scale: 0.1
697 min: -1
698 max: 1.6
699 ";
700 assert!(matches!(
701 from_yaml_str(yaml_str),
702 Ok(Spec(Node::Real {
703 min: Some(min),
704 max: Some(max),
705 init,
706 scale,
707 })) if
708 approx_eq!(f64, min, -1.0, F64Margin::default()) &&
709 approx_eq!(f64, max, 1.6, F64Margin::default()) &&
710 approx_eq!(f64, init, 0.25, F64Margin::default()) &&
711 approx_eq!(f64, scale, 0.1, F64Margin::default())
712 ));
713 }
714
715 #[test]
716 fn real_scale_not_strictly_positive() {
717 let yaml_str = "
718 type: real
719 init: 0
720 scale: 0.0
721 ";
722
723 assert!(matches!(
724 from_yaml_str(yaml_str),
725 Err(Error::ScaleMustBeStrictlyPositive { path_hint })
726 if path_hint == "(root)"
727 ));
728 }
729
730 #[test]
731 fn real_missing_scale() {
732 let yaml_str = "
733 type: real
734 init: 0
735 ";
736
737 assert!(matches!(
738 from_yaml_str(yaml_str),
739 Err(Error::MandatoryAttributeMissing { path_hint, missing_attribute_name })
740 if path_hint == "(root)" && missing_attribute_name == "scale"
741 ));
742 }
743
744 #[test]
745 fn real_missing_init() {
746 let yaml_str = "
747 type: real
748 scale: 1
749 ";
750
751 assert!(matches!(
752 from_yaml_str(yaml_str),
753 Err(Error::MandatoryAttributeMissing { path_hint, missing_attribute_name })
754 if path_hint == "(root)" && missing_attribute_name == "init"
755 ));
756 }
757
758 #[test]
759 fn real_bounds_sanity() {
760 let yaml_str = "
761 type: real
762 init: 0
763 scale: 1
764 min: 1
765 max: 0
766 ";
767
768 assert!(matches!(
769 from_yaml_str(yaml_str),
770 Err(Error::InvalidBounds {path_hint}) if *"(root)" == path_hint
771 ));
772 }
773
774 #[test]
775 fn non_finite_number() {
776 let yaml_str = "
777 type: real
778 init: 0
779 scale: .nan
780 ";
781
782 assert!(matches!(
783 from_yaml_str(yaml_str),
784 Err(Error::NonFiniteNumber {path_hint, attribute_name}) if *"(root)" == path_hint && *"scale" == attribute_name
785 ));
786 }
787
788 #[test]
789 fn real_defaults() {
790 let yaml_str = "
791 type: real
792 init: 0.0
793 scale: 1.0
794 ";
795 assert!(matches!(
796 from_yaml_str(yaml_str),
797 Ok(Spec(Node::Real {
798 min: None,
799 max: None,
800 init,
801 scale,
802 })) if
803 approx_eq!(f64, init, 0.0, F64Margin::default()) &&
804 approx_eq!(f64, scale, 1.0, F64Margin::default())
805 ));
806 }
807
808 #[test]
809 fn init_not_within_bounds() {
810 let yaml_str = "
811 type: real
812 min: 0
813 max: 1
814 init: 2
815 ";
816
817 assert!(matches!(
818 from_yaml_str(yaml_str),
819 Err(Error::InitNotWithinBounds { path_hint })
820 if path_hint == "(root)"
821 ));
822 }
823
824 #[test]
825 fn int() {
826 let yaml_str = "
827 type: int
828 init: 2
829 scale: 0.5
830 min: -1
831 max: 10
832 ";
833 assert!(matches!(
834 from_yaml_str(yaml_str),
835 Ok(Spec(Node::Int {
836 min: Some(-1),
837 max: Some(10),
838 init: 2,
839 scale,
840 })) if
841 approx_eq!(f64, scale, 0.5, F64Margin::default())
842 ));
843 }
844
845 #[test]
846 fn int_missing_scale() {
847 let yaml_str = "
848 type: int
849 init: 0
850 ";
851
852 assert!(matches!(
853 from_yaml_str(yaml_str),
854 Err(Error::MandatoryAttributeMissing { path_hint, missing_attribute_name })
855 if path_hint == "(root)" && missing_attribute_name == "scale"
856 ));
857 }
858
859 #[test]
860 fn int_scale_not_positive() {
861 let yaml_str = "
862 type: int
863 init: 0
864 scale: -0.5
865 ";
866
867 assert!(matches!(
868 from_yaml_str(yaml_str),
869 Err(Error::ScaleMustBeStrictlyPositive { path_hint })
870 if path_hint == "(root)"
871 ));
872 }
873
874 #[test]
875 fn int_missing_init() {
876 let yaml_str = "
877 type: int
878 scale: 1
879 ";
880
881 assert!(matches!(
882 from_yaml_str(yaml_str),
883 Err(Error::MandatoryAttributeMissing { path_hint, missing_attribute_name })
884 if path_hint == "(root)" && missing_attribute_name == "init"
885 ));
886 }
887 #[test]
888 fn int_bounds_sanity() {
889 let yaml_str = "
890 type: int
891 init: 0
892 scale: 1
893 min: 1
894 max: 0
895 ";
896
897 assert!(matches!(
898 from_yaml_str(yaml_str),
899 Err(Error::InvalidBounds {path_hint}) if *"(root)" == path_hint
900 ));
901 }
902
903 #[test]
904 fn int_defaults() {
905 let yaml_str = "
906 type: int
907 init: 0
908 scale: 1.0
909 ";
910
911 assert!(matches!(
912 from_yaml_str(yaml_str),
913 Ok(Spec(Node::Int {
914 min: None,
915 max: None,
916 init: 0,
917 scale,
918 })) if
919 approx_eq!(f64, scale, 1.0, F64Margin::default())
920 ));
921 }
922
923 #[test]
924 fn sub() {
925 let yaml_str = "
926 foo:
927 type: bool
928 init: false
929 ";
930 assert!(matches!(
931 from_yaml_str(yaml_str),
932 Ok(Spec(Node::Sub {
933 map
934 })) if map.len() == 1 &&
935 *map.get("foo").unwrap().as_ref() == Node::Bool {init: false}
936 ));
937 }
938
939 #[test]
940 fn sub_empty() {
941 let yaml_str = "
942 type: sub
943 ";
944 assert!(matches!(
945 from_yaml_str(yaml_str),
946 Err(Error::EmptySub {
947 path_hint
948 }) if path_hint == "(root)"
949 ));
950 }
951
952 #[test]
953 fn anon_map() {
954 let yaml_str = "
955 type: anon map
956 valueType:
957 type: bool
958 init: false
959 initSize: 2
960 minSize: 2
961 maxSize: 4
962 ";
963
964 assert!(matches!(
965 from_yaml_str(yaml_str),
966 Ok(Spec(Node::AnonMap {
967 init_size: 2,
968 value_type,
969 min_size: Some(2),
970 max_size: Some(4)
971 })) if *value_type.as_ref() == Node::Bool {init: false}
972 ));
973 }
974
975 #[test]
976 fn anon_map_defaults() {
977 let yaml_str = "
978 type: anon map
979 initSize: 1
980 valueType:
981 type: bool
982 init: false
983 ";
984
985 assert!(matches!(
986 from_yaml_str(yaml_str),
987 Ok(Spec(Node::AnonMap {
988 init_size: 1,
989 value_type,
990 min_size: None,
991 max_size: None
992 })) if *value_type.as_ref() == Node::Bool {init: false}
993 ));
994 }
995
996 #[test]
997 fn anon_map_zero_max_size() {
998 let yaml_str = "
999 type: anon map
1000 valueType:
1001 type: bool
1002 init: false
1003 maxSize: 0
1004 ";
1005
1006 assert!(matches!(
1007 from_yaml_str(yaml_str),
1008 Err(Error::ZeroMaxSize { path_hint })
1009 if path_hint == "(root)"
1010 ));
1011 }
1012
1013 #[test]
1014 fn anon_map_missing_value_type() {
1015 let yaml_str = "
1016 type: anon map
1017 ";
1018
1019 assert!(matches!(
1020 from_yaml_str(yaml_str),
1021 Err(Error::MandatoryAttributeMissing { path_hint, missing_attribute_name })
1022 if path_hint == "(root)" && missing_attribute_name == "valueType"
1023 ));
1024 }
1025
1026 #[test]
1027 fn anon_map_invalid_size_bounds() {
1028 let yaml_str = "
1029 type: anon map
1030 valueType:
1031 type: bool
1032 init: false
1033 minSize: 3
1034 maxSize: 2
1035 ";
1036
1037 assert!(matches!(
1038 from_yaml_str(yaml_str),
1039 Err(Error::InvalidSizeBounds { path_hint })
1040 if path_hint == "(root)"
1041 ));
1042 }
1043
1044 #[test]
1045 fn anon_map_init_size_out_of_bounds() {
1046 let yaml_str = "
1047 type: anon map
1048 valueType:
1049 type: bool
1050 init: false
1051 minSize: 2
1052 maxSize: 3
1053 initSize: 4
1054 ";
1055
1056 assert!(matches!(
1057 from_yaml_str(yaml_str),
1058 Err(Error::InitSizeNotWithinBounds { path_hint })
1059 if path_hint == "(root)"
1060 ));
1061 }
1062
1063 #[test]
1064 fn variant() {
1065 let yaml_str = "
1066 type: variant
1067 init: bar
1068 foo:
1069 type: bool
1070 init: false
1071 bar:
1072 type: int
1073 init: 0
1074 scale: 1
1075 ";
1076 assert!(matches!(
1077 from_yaml_str(yaml_str),
1078 Ok(Spec(Node::Variant {
1079 map,
1080 init
1081 })) if map.len() == 2 &&
1082 *map.get("foo").unwrap().as_ref() == Node::Bool {init: false} &&
1083 matches!(*map.get("bar").unwrap().as_ref(), Node::Int {init: 0, ..}) &&
1084 init == *"bar"
1085 ));
1086 }
1087
1088 #[test]
1089 fn variant_not_enough_values() {
1090 let yaml_str = "
1091 type: variant
1092 init: foo
1093 foo:
1094 type: bool
1095 init: false
1096 ";
1097 assert!(matches!(
1098 from_yaml_str(yaml_str),
1099 Err(Error::NotEnoughVariantValues {
1100 path_hint
1101 }) if path_hint == "(root)"
1102 ));
1103 }
1104
1105 #[test]
1106 fn variant_unknown_init() {
1107 let yaml_str = "
1108 type: variant
1109 init: bars
1110 foo:
1111 type: bool
1112 init: false
1113 bar:
1114 type: int
1115 init: 0
1116 scale: 1
1117 ";
1118 assert!(matches!(
1119 from_yaml_str(yaml_str),
1120 Err(Error::InitNotAKnownValue {
1121 path_hint,
1122 init
1123 }) if path_hint == "(root)" && init == "bars"
1124 ));
1125 }
1126
1127 #[test]
1128 fn enum_spec() {
1129 let yaml_str = "
1130 type: enum
1131 init: bar
1132 values:
1133 - foo
1134 - bar
1135 ";
1136 assert!(matches!(
1137 from_yaml_str(yaml_str),
1138 Ok(Spec(Node::Enum {
1139 values,
1140 init
1141 })) if values == vec!["foo", "bar"] &&
1142 init == *"bar"
1143 ));
1144 }
1145
1146 #[test]
1147 fn enum_not_enough_values() {
1148 let yaml_str = "
1149 type: enum
1150 init: foo
1151 values:
1152 - foo
1153 ";
1154 assert!(matches!(
1155 from_yaml_str(yaml_str),
1156 Err(Error::NotEnoughEnumValues {
1157 path_hint
1158 }) if path_hint == "(root)"
1159 ));
1160 }
1161
1162 #[test]
1163 fn enum_unknown_init() {
1164 let yaml_str = "
1165 type: enum
1166 init: bars
1167 values:
1168 - foo
1169 - bar
1170 ";
1171 assert!(matches!(
1172 from_yaml_str(yaml_str),
1173 Err(Error::InitNotAKnownValue {
1174 path_hint,
1175 init
1176 }) if path_hint == "(root)" && init == "bars"
1177 ));
1178 }
1179
1180 #[test]
1181 fn enum_items_must_be_string() {
1182 let yaml_str = "
1183 type: enum
1184 init: \"foo\"
1185 values:
1186 - 0
1187 - foo
1188 ";
1189
1190 assert!(matches!(
1191 from_yaml_str(yaml_str),
1192 Err(Error::EnumItemsMustBeString {
1193 path_hint
1194 }) if path_hint == "(root)"
1195 ));
1196 }
1197
1198 #[test]
1199 fn optional() {
1200 let yaml_str = "
1201 type: optional
1202 valueType:
1203 type: bool
1204 init: true
1205 initPresent: true
1206 ";
1207
1208 let expected = Node::Optional {
1209 value_type: Box::new(Node::Bool { init: true }),
1210 init_present: true,
1211 };
1212
1213 assert_eq!(from_yaml_str(yaml_str).unwrap().0, expected);
1214 }
1215
1216 #[test]
1217 fn optional_init_missing() {
1218 let yaml_str = "
1219 type: optional
1220 valueType:
1221 type: bool
1222 init: true
1223 ";
1224
1225 if let Error::MandatoryAttributeMissing {
1226 path_hint,
1227 missing_attribute_name,
1228 } = from_yaml_str(yaml_str).unwrap_err()
1229 {
1230 assert_eq!(path_hint, "(root)");
1231 assert_eq!(missing_attribute_name, "initPresent");
1232 } else {
1233 panic!();
1234 }
1235 }
1236
1237 #[test]
1238 fn unexpected_attribute() {
1239 let yaml_str = "
1240 type: anon map
1241 unexpected: false
1242 ";
1243
1244 assert!(matches!(
1245 from_yaml_str(yaml_str),
1246 Err(Error::UnexpectedAttribute { path_hint, unexpected_attribute_name })
1247 if path_hint == "(root)" && unexpected_attribute_name == "unexpected"
1248 ));
1249 }
1250
1251 #[test]
1252 fn invalid_attribute_value_type() {
1253 let yaml_str = "
1254 type: anon map
1255 valueType:
1256 type: bool
1257 init: true
1258 maxSize: true
1259 ";
1260
1261 assert!(matches!(
1262 from_yaml_str(yaml_str),
1263 Err(Error::InvalidAttributeValueType { path_hint, attribute_name, expected_type_hint })
1264 if path_hint == "(root)" && attribute_name == "maxSize" && expected_type_hint == "a positive integer"
1265 ));
1266 }
1267
1268 #[test]
1269 fn invalid_attribute_key_type() {
1270 let yaml_str = "
1271 1: 1
1272 ";
1273
1274 assert!(matches!(
1275 from_yaml_str(yaml_str),
1276 Err(Error::InvalidAttributeKeyType { path_hint, .. })
1277 if path_hint == "(root)"
1278 ));
1279 }
1280
1281 #[test]
1282 fn path_hint() {
1283 let yaml_str = "
1284 foo:
1285 type: bar
1286 ";
1287
1288 assert!(matches!(
1289 from_yaml_str(yaml_str),
1290 Err(Error::UnknownTypeName { path_hint, .. })
1291 if path_hint == "foo"
1292 ));
1293 }
1294
1295 #[test]
1296 fn type_defs() {
1297 let yaml_str = "
1298 typeDef foo:
1299 type: bool
1300 init: false
1301 typeDef bar:
1302 a:
1303 type: bool
1304 init: true
1305 b:
1306 type: int
1307 init: 1
1308 scale: 1.0
1309 x:
1310 type: foo
1311 y:
1312 h:
1313 type: foo
1314 i:
1315 type: bar
1316 ";
1317
1318 let equivalent_yaml_str = "
1319 x:
1320 type: bool
1321 init: false
1322 y:
1323 h:
1324 type: bool
1325 init: false
1326 i:
1327 a:
1328 type: bool
1329 init: true
1330 b:
1331 type: int
1332 init: 1
1333 scale: 1.0
1334 ";
1335
1336 let spec_with_type_def = from_yaml_str(yaml_str).unwrap().0;
1337 let expected_spec = from_yaml_str(equivalent_yaml_str).unwrap().0;
1338
1339 assert_eq!(spec_with_type_def, expected_spec);
1340 }
1341
1342 #[test]
1343 fn type_def_shadowing() {
1344 let yaml_str = "
1345 typeDef foo:
1346 type: bool
1347 init: false
1348 bar:
1349 typeDef foo:
1350 type: int
1351 init: 1
1352 scale: 1.0
1353 baz:
1354 type: foo
1355
1356 ";
1357
1358 let equivalent_yaml_str = "
1359 bar:
1360 baz:
1361 type: int
1362 init: 1
1363 scale: 1.0
1364 ";
1365
1366 let spec_with_type_def = from_yaml_str(yaml_str).unwrap().0;
1367 let expected_spec = from_yaml_str(equivalent_yaml_str).unwrap().0;
1368
1369 assert_eq!(spec_with_type_def, expected_spec);
1370 }
1371
1372 #[test]
1373 fn illegal_type_def_name() {
1374 let yaml_str = "
1375 foo:
1376 typeDef int:
1377 type: bool
1378 init: false
1379 ";
1380
1381 assert!(matches!(
1382 from_yaml_str(yaml_str),
1383 Err(Error::IllegalTypeDefName { path_hint, type_def_name })
1384 if path_hint == "foo" && type_def_name == "int"
1385 ));
1386 }
1387}