cambrian/
spec_util.rs

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}