irc_bot/util/
yaml.rs

1use smallvec;
2use smallvec::SmallVec;
3use std;
4use std::borrow::Cow;
5use yaml_rust;
6use yaml_rust::yaml;
7use yaml_rust::Yaml;
8
9error_chain! {
10    foreign_links {
11        YamlEmit(yaml_rust::EmitError);
12        YamlScan(yaml_rust::ScanError);
13    }
14
15    errors {
16        NoSingleNode(node_qty: usize) {
17            description("wanted a single YAML node but found zero or multiple nodes")
18            display("While parsing YAML: Wanted a single node, but found {} nodes.", node_qty)
19        }
20        RequiredFieldMissing(name: Cow<'static, str>) {
21            description("a YAML object is missing a required field")
22            display("While handling YAML: An object is missing the required field {:?}.", name)
23        }
24        AliasesNotSupported {
25            description("encountered a YAML alias (which is not supported by `yaml_rust`)")
26            display("While handling YAML: Encountered a YAML alias, which is not supported by \
27                     `yaml_rust`.")
28        }
29        TypeMismatch(path: String, expected_ty: Kind, actual_ty: Kind) {
30            description("encountered a type error while handling YAML")
31            display("While handling YAML: Expected {path} to be of type {expected_ty:?}, but it \
32                     is of type {actual_ty:?}.",
33                     path = path,
34                     expected_ty = expected_ty,
35                     actual_ty = actual_ty)
36        }
37        ExpectedNonEmptyStream {
38            description("expected non-empty YAML stream but found empty stream")
39            display("While handling YAML: Expected a non-empty stream, but found an empty stream.")
40        }
41        ExpectedEmptyStream {
42            description("expected empty YAML stream but found non-empty stream")
43            display("While handling YAML: Expected an empty stream, but found a non-empty stream.")
44        }
45    }
46}
47
48#[derive(Copy, Clone, Debug)]
49pub enum Kind {
50    Scalar,
51    Sequence,
52    Mapping,
53    #[doc(hidden)]
54    __Nonexhaustive,
55}
56
57impl Kind {
58    pub fn of(node: &Yaml) -> Kind {
59        Self::from_aug_ty(&AugmentedTy::of(node))
60    }
61
62    fn from_aug_ty(ty: &AugmentedTy) -> Kind {
63        match ty {
64            &AugmentedTy::Scalar => Kind::Scalar,
65            &AugmentedTy::Sequence => Kind::Sequence,
66            &AugmentedTy::Mapping(_) => Kind::Mapping,
67            &AugmentedTy::Other => Kind::__Nonexhaustive,
68        }
69    }
70}
71
72#[derive(Debug)]
73pub(crate) enum AugmentedTy<'a> {
74    Scalar,
75    Sequence,
76    Mapping(&'a yaml::Hash),
77    Other,
78}
79
80impl<'a> AugmentedTy<'a> {
81    pub(crate) fn of(node: &Yaml) -> AugmentedTy {
82        match node {
83            &Yaml::Real(_)
84            | &Yaml::Integer(_)
85            | &Yaml::String(_)
86            | &Yaml::Boolean(_)
87            | &Yaml::Null => AugmentedTy::Scalar,
88            &Yaml::Array(_) => AugmentedTy::Sequence,
89            &Yaml::Hash(ref data) => AugmentedTy::Mapping(data),
90            &Yaml::Alias(_) | &Yaml::BadValue => AugmentedTy::Other,
91        }
92    }
93}
94
95/// Converts any type of YAML node to a string.
96///
97/// If the `node` is a `Yaml::String`, a `&str` reference to its content it will be passed to
98/// `lt_map` to construct a `Cow` with the desired lifetime. If the `node` is not a `Yaml::String`,
99/// its `Debug` representation will be returned, wrapped in `Cow::Owned`.
100pub fn any_to_str<'a, 'b, F>(node: &'a Yaml, lt_map: F) -> Cow<'b, str>
101where
102    F: Fn(&'a str) -> Cow<'b, str>,
103{
104    node.as_str()
105        .map(lt_map)
106        .unwrap_or_else(|| Cow::Owned(format!("{:?}", node)))
107}
108
109/// Converts a scalar YAML node to a string.
110///
111/// If the `node` is scalar, returns the same value as `any_to_str`, except wrapped in
112/// `Result::Ok`. If the `node` is a sequence, a mapping, or something stranger, returns an `Err`
113/// containing a `Kind` value representing what particular kind of non-scalar `node` is.
114pub fn scalar_to_str<'a, 'b, F>(
115    node: &'a Yaml,
116    lt_map: F,
117) -> std::result::Result<Cow<'b, str>, Kind>
118where
119    F: Fn(&'a str) -> Cow<'b, str>,
120{
121    match Kind::of(node) {
122        Kind::Scalar => Ok(any_to_str(node, lt_map)),
123        kind => Err(kind),
124    }
125}
126
127/// Parses a lone YAML node.
128///
129/// Wraps `yaml_rust::YamlLoader::load_from_str` to parse a single YAML node.
130///
131/// If this function parses a single YAML node `y`, it returns `Ok(Some(y))`. If given an empty
132/// YAML stream, returns `Ok(None)`. If given a stream of multiple YAML documents, returns `Err`.
133pub fn parse_node(src: &str) -> Result<Option<Yaml>> {
134    let mut stream = yaml::YamlLoader::load_from_str(src)?;
135
136    let node = stream.pop();
137
138    match stream.len() {
139        0 => Ok(node),
140        n => {
141            bail!(ErrorKind::NoSingleNode({
142                // This addition should never overflow, because the stream length was previously
143                // greater by one.
144                n + 1
145            }))
146        }
147    }
148}
149
150pub(crate) fn parse_and_check_node<'s, DefaultCtor, S1>(
151    src: &str,
152    expected_syntax: &'s Yaml,
153    subject_label: S1,
154    default: DefaultCtor,
155) -> Result<Yaml>
156where
157    DefaultCtor: Fn() -> Yaml,
158    S1: Into<Cow<'s, str>>,
159{
160    let node = parse_node(src)?.unwrap_or_else(default);
161
162    check_type(expected_syntax, &node, subject_label)?;
163
164    Ok(node)
165}
166
167/// Checks that a YAML object has a given type and structure.
168///
169/// Checks that the `actual` YAML object matches the type and structure of the `expected` YAML
170/// object.
171///
172/// `subject_label` is a string that will identify the `actual` object in any error messages
173/// produced.
174pub(crate) fn check_type<'s, S1>(expected: &'s Yaml, actual: &Yaml, subject_label: S1) -> Result<()>
175where
176    S1: Into<Cow<'s, str>>,
177{
178    let subject_label = subject_label.into();
179
180    let mut path_buf = SmallVec::<[_; 8]>::new();
181
182    check_type_inner(expected, actual, &mut path_buf, subject_label)?;
183
184    debug_assert!(path_buf.is_empty());
185
186    Ok(())
187}
188
189fn check_type_inner<'s, AS>(
190    expected: &'s Yaml,
191    actual: &Yaml,
192    path_buf: &mut SmallVec<AS>,
193    subject_label: Cow<'s, str>,
194) -> Result<()>
195where
196    AS: smallvec::Array<Item = Cow<'s, str>>,
197{
198    trace!(
199        "Checking YAML object's type and structure. Expected: {expected:?}; actual: {actual:?}.",
200        expected = expected,
201        actual = actual
202    );
203
204    use util::yaml::AugmentedTy as Ty;
205
206    path_buf.push(subject_label);
207
208    let expected_ty = Ty::of(expected);
209    let actual_ty = Ty::of(actual);
210
211    match (&expected_ty, &actual_ty) {
212        (&Ty::Scalar, &Ty::Scalar) | (&Ty::Sequence, &Ty::Sequence) => {
213            // Types match trivially.
214        }
215        (&Ty::Mapping(expected_fields), &Ty::Mapping(actual_fields)) => {
216            check_field_types(expected_fields, actual_fields, path_buf)?
217        }
218        (&Ty::Scalar, &Ty::Sequence)
219        | (&Ty::Scalar, &Ty::Mapping(_))
220        | (&Ty::Sequence, &Ty::Scalar)
221        | (&Ty::Sequence, &Ty::Mapping(_))
222        | (&Ty::Mapping(_), &Ty::Scalar)
223        | (&Ty::Mapping(_), &Ty::Sequence) => bail!(ErrorKind::TypeMismatch(
224            path_buf.join("."),
225            Kind::from_aug_ty(&expected_ty),
226            Kind::from_aug_ty(&actual_ty),
227        )),
228        (_, &Ty::Other) | (&Ty::Other, _) => bail!(ErrorKind::AliasesNotSupported),
229    }
230
231    path_buf.pop();
232
233    Ok(())
234}
235
236fn check_field_types<'s, AS>(
237    expected_fields: &'s yaml::Hash,
238    actual_fields: &yaml::Hash,
239    path_buf: &mut SmallVec<AS>,
240) -> Result<()>
241where
242    AS: smallvec::Array<Item = Cow<'s, str>>,
243{
244    for (key, expected_value) in expected_fields {
245        match (expected_value, actual_fields.get(key)) {
246            (_, Some(actual_value)) => check_type_inner(
247                expected_value,
248                actual_value,
249                path_buf,
250                any_to_str(key, Cow::Borrowed),
251            )?,
252            (&Yaml::String(ref s), None) if s.starts_with("[") && s.ends_with("]") => {
253                // This field is optional.
254            }
255            (&Yaml::Array(_), None) => {
256                // All sequence fields are treated as optional.
257            }
258            (&Yaml::Hash(_), None) => {
259                // Treat an absent mapping as were it an empty mapping.
260                check_type_inner(
261                    expected_value,
262                    &Yaml::Hash(Default::default()),
263                    path_buf,
264                    any_to_str(key, Cow::Borrowed),
265                )?
266            }
267            (_, None) => bail!(ErrorKind::RequiredFieldMissing(any_to_str(
268                key,
269                |s| s.to_owned().into()
270            ),)),
271        }
272    }
273
274    Ok(())
275}