biodivine_lib_param_bn/_impl_annotations/
_impl_writer.rs

1use crate::_impl_annotations::{ALPHANUMERIC, validate_path_segment};
2use crate::ModelAnnotation;
3use std::fmt::{Display, Formatter};
4
5impl Display for ModelAnnotation {
6    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
7        self.fmt_with_path(&mut Vec::new(), f)
8    }
9}
10
11/// A helper function used by the `Display` implementation to print annotation paths with
12/// proper escaping.
13fn fmt_path(mut path: &[&str], f: &mut Formatter<'_>) -> std::fmt::Result {
14    while !path.is_empty() {
15        // Only print path segment if it is valid.
16        validate_path_segment(path[0]);
17        if ALPHANUMERIC.is_match(path[0]) {
18            // Alphanumeric names are printed directly.
19            write!(f, "{}:", path[0])?;
20        } else {
21            // Everything else is escaped using backticks.
22            write!(f, "`{}`:", path[0])?;
23        }
24        path = &path[1..];
25    }
26    Ok(())
27}
28
29impl ModelAnnotation {
30    /// A helper function used by the `Display` implementation to recursively print the whole
31    /// annotation tree.
32    ///
33    /// Arguments:
34    ///  - absolute `path` to this specific annotation. The recursive function will push/pop
35    ///    items into path as needed (hence a mutable vector instead of a slice).
36    ///  - A formatter from the `Display::fmt` method.
37    fn fmt_with_path<'a>(
38        &'a self,
39        path: &mut Vec<&'a str>,
40        f: &mut Formatter<'_>,
41    ) -> std::fmt::Result {
42        if let Some(value) = self.value.as_ref() {
43            if value.is_empty() {
44                // Empty value has an empty lines iterator,
45                // hence we need to print it explicitly.
46                write!(f, "#!")?;
47                fmt_path(path, f)?;
48                writeln!(f)?;
49            } else {
50                // Otherwise the value is printed line-by-line:
51                for line in value.lines() {
52                    write!(f, "#!")?; // Preamble.
53                    fmt_path(path, f)?; // Path.
54
55                    // Escape the whole line if there is a colon, or if it by some weird coincidence
56                    // matches our escape sequence.
57                    let has_escape_sequence = line.starts_with("#`") && line.ends_with("`#");
58                    let mut chars = line.chars();
59                    // Also escape the line if it is surrounded by whitespace that would be stripped
60                    // away by our parser.
61                    let first_whitespace =
62                        chars.next().map(|it| it.is_whitespace()).unwrap_or(false);
63                    let last_whitespace =
64                        chars.last().map(|it| it.is_whitespace()).unwrap_or(false);
65                    let has_whitespace = first_whitespace || last_whitespace;
66                    if line.contains(':') || has_escape_sequence || has_whitespace {
67                        writeln!(f, "#`{}`#", line)?;
68                    } else {
69                        writeln!(f, "{}", line)?;
70                    }
71                }
72            }
73        }
74
75        // Keys are explored in deterministic order:
76        let mut keys = self.inner.keys().collect::<Vec<_>>();
77        keys.sort();
78        for key in keys {
79            path.push(key.as_str());
80            let child = self.inner.get(key).unwrap();
81            child.fmt_with_path(path, f)?;
82            path.pop();
83        }
84        Ok(())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use crate::ModelAnnotation;
91
92    #[test]
93    pub fn annotation_read_write_test() {
94        let mut annotations = ModelAnnotation::new();
95        // "root" annotation
96        annotations.ensure_value::<&str>(&[], "hello");
97        // normal annotation
98        annotations.ensure_value(&["name"], "Model name");
99        // Empty key and value
100        annotations.ensure_value(&["var", "", "x"], "");
101        // Escaped names and values
102        annotations.ensure_value(&["var {list}"], "Hello: there");
103        // Multiline values
104        annotations.ensure_value(&["multi_line"], " Hello\n\nWorld: #`Some escaped text`#.");
105
106        let string = annotations.to_string();
107        let parsed = ModelAnnotation::from_model_string(string.as_str());
108
109        assert_eq!(annotations, parsed);
110    }
111}