Skip to main content

apollo_saphyr/
yaml_owned.rs

1//! YAML objects manipulation utilities.
2
3#![allow(clippy::module_name_repetitions)]
4
5use alloc::{borrow::Cow, boxed::Box, string::String, vec::Vec};
6use core::{
7    hash::{BuildHasher, Hasher},
8    ops::{Index, IndexMut},
9};
10
11use hashlink::LinkedHashMap;
12use saphyr_parser::{ScalarStyle, Tag};
13
14use crate::{LoadableYamlNode, ScalarOwned, Yaml};
15
16/// A YAML node is stored as this `Yaml` enumeration, which provides an easy way to
17/// access your YAML document.
18///
19/// # Examples
20///
21/// ```
22/// # extern crate apollo_saphyr as saphyr;
23/// use saphyr::{Scalar, Yaml};
24/// let foo = Yaml::value_from_str("-123"); // convert the string to the appropriate YAML type
25/// assert_eq!(foo.as_integer().unwrap(), -123);
26///
27/// // iterate over an Sequence
28/// let vec = Yaml::Sequence(vec![Yaml::Value(Scalar::Integer(1)), Yaml::Value(Scalar::Integer(2))]);
29/// for v in vec.as_vec().unwrap() {
30///     assert!(v.is_integer());
31/// }
32/// ```
33#[derive(Clone, PartialEq, PartialOrd, Debug, Eq, Ord, Hash)]
34pub enum YamlOwned {
35    /// The raw string from the input.
36    ///
37    /// When the field is left in the [`Representation`] variant, methods that rely on the value
38    /// (e.g.: [`is_boolean`], [`as_integer`], [`into_floating_point`], ...) will always return
39    /// [`None`].
40    ///
41    /// This variant is only meant:
42    ///   - As an optimization, when lazy-parsing is preferred.
43    ///   - As a more generic way of handling keys in [`Mapping`]s (if user-defined key duplication
44    ///     detection is required.
45    ///
46    /// [`Mapping`]: Yaml::Mapping
47    /// [`Representation`]: Yaml::Representation
48    /// [`is_boolean`]: Yaml::is_boolean
49    /// [`as_integer`]: Yaml::as_integer
50    /// [`into_floating_point`]: Yaml::into_floating_point
51    Representation(String, ScalarStyle, Option<Tag>),
52    /// The resolved value from the representation.
53    Value(ScalarOwned),
54    /// YAML sequence, can be accessed as a `Vec`.
55    Sequence(SequenceOwned),
56    /// YAML mapping, can be accessed as a [`LinkedHashMap`].
57    ///
58    /// Iteration order will match the order of insertion into the map and that of the document.
59    ///
60    /// If keys use the [`Representation`] variant, equality will be based on their representation.
61    /// When comparing representations for equality, the string, [scalar style] and tags must
62    /// match. This means that `'100'` and `"100"`, although similar in their value, have different
63    /// representations.
64    ///
65    /// If keys use the [`Value`] variant, they will be compared by value. It is discouraged to use
66    /// floating point values as keys. [`ScalarOwned`] uses [`OrderedFloat`] for hash and equality.
67    /// Refer to their documentation for details on float comparisons.
68    ///
69    /// Comparison between [`Representation`] variants and [`Value`] variants will always fail.
70    /// Users must ensure all keys in a map are of the same variant, as well as the query keys.
71    ///
72    /// For complex keys, the [`Mapping`] and [`Sequence`] variants are compared for equality. Both
73    /// these comparisons are sensitive to the order of insertions. For instance, in the following
74    /// mapping, the two complex keys are considered different:
75    ///
76    /// ```yaml
77    /// ? { a: b, c: d }: foo
78    /// ? { c: d, a: b }: bar
79    /// ```
80    ///
81    /// [`Mapping`]: Yaml::Mapping
82    /// [`Representation`]: Yaml::Representation
83    /// [`Sequence`]: Yaml::Sequence
84    /// [`Value`]: Yaml::Value
85    /// [scalar style]: ScalarStyle
86    /// [`OrderedFloat`]: ordered_float::OrderedFloat
87    Mapping(MappingOwned),
88    /// A tagged node.
89    ///
90    /// Tags can be applied to any node, whether a scalar or a collection.
91    Tagged(Tag, Box<YamlOwned>),
92    /// Alias, not fully supported yet.
93    Alias(usize),
94    /// A variant used when parsing the representation of a scalar node fails.
95    ///
96    /// The YAML is syntactically valid, but its contents are incoherent. See
97    /// [`ScalarOwned::parse_from_cow_and_metadata`] for details.
98    /// This variant is also used when stealing the contents of `self`, meaning `self` should no
99    /// longer be used. See [`Self::take`] for details
100    BadValue,
101}
102
103/// The type contained in the `YamlOwned::Sequence` variant.
104pub type SequenceOwned = Vec<YamlOwned>;
105/// The type contained in the `YamlOwned::Mapping` variant.
106pub type MappingOwned = LinkedHashMap<YamlOwned, YamlOwned>;
107
108// This defines most common operations on a YAML object. See macro definition for details.
109define_yaml_object_impl!(
110    YamlOwned,
111    mappingtype = MappingOwned,
112    sequencetype = SequenceOwned,
113    nodetype = Self,
114    scalartype = { ScalarOwned },
115    selfname = "YAMLOwned",
116    owned
117);
118
119impl YamlOwned {
120    /// Implementation detail for [`Self::as_mapping_get`], which is generated from a macro.
121    #[must_use]
122    fn as_mapping_get_impl(&self, key: &str) -> Option<&Self> {
123        match self.as_mapping() {
124            Some(mapping) => {
125                let hash = hash_str_as_yaml_string(key, mapping.hasher().build_hasher());
126                mapping
127                    .raw_entry()
128                    .from_hash(hash, |k| k.as_str() == Some(key))
129                    .map(|(_, v)| v)
130            }
131            _ => None,
132        }
133    }
134
135    /// Implementation detail for [`Self::as_mapping_get_mut`], which is generated from a macro.
136    #[must_use]
137    fn as_mapping_get_mut_impl(&mut self, key: &str) -> Option<&mut Self> {
138        use hashlink::linked_hash_map::RawEntryMut::{Occupied, Vacant};
139        match self.as_mapping_mut() {
140            Some(mapping) => {
141                let hash = hash_str_as_yaml_string(key, mapping.hasher().build_hasher());
142                match mapping
143                    .raw_entry_mut()
144                    .from_hash(hash, |k| k.as_str() == Some(key))
145                {
146                    Occupied(entry) => Some(entry.into_mut()),
147                    Vacant(_) => None,
148                }
149            }
150            _ => None,
151        }
152    }
153}
154
155impl LoadableYamlNode<'_> for YamlOwned {
156    type HashKey = Self;
157
158    fn from_bare_yaml(yaml: Yaml<'_>) -> Self {
159        match yaml {
160            // Sequence and Mapping will always have their container empty.
161            Yaml::Sequence(_) => Self::Sequence(vec![]),
162            Yaml::Mapping(_) => Self::Mapping(MappingOwned::new()),
163
164            Yaml::Representation(cow, scalar_style, tag) => {
165                Self::Representation(cow.into(), scalar_style, tag.map(Cow::into_owned))
166            }
167            Yaml::Value(scalar) => Self::Value(scalar.into_owned()),
168            Yaml::Tagged(tag, mut node) => Self::Tagged(
169                tag.into_owned(),
170                Box::new(Self::from_bare_yaml(node.take())),
171            ),
172            Yaml::Alias(x) => Self::Alias(x),
173            Yaml::BadValue => Self::BadValue,
174        }
175    }
176
177    fn is_sequence(&self) -> bool {
178        self.is_sequence() || self.get_tagged_node().map_or(false, YamlOwned::is_sequence)
179    }
180
181    fn is_mapping(&self) -> bool {
182        self.is_mapping() || self.get_tagged_node().map_or(false, YamlOwned::is_mapping)
183    }
184
185    fn is_badvalue(&self) -> bool {
186        self.is_badvalue()
187    }
188
189    fn into_tagged(self, tag: Cow<'_, Tag>) -> Self {
190        Self::Tagged(tag.into_owned(), Box::new(self))
191    }
192
193    fn sequence_mut(&mut self) -> &mut Vec<Self> {
194        match self {
195            Self::Sequence(vec) => vec,
196            Self::Tagged(_, node) => node.sequence_mut(),
197            _ => panic!("Called sequence_mut on a non-array"),
198        }
199    }
200
201    fn mapping_mut(&mut self) -> &mut LinkedHashMap<Self::HashKey, Self> {
202        match self {
203            Self::Mapping(map) => map,
204            Self::Tagged(_, node) => node.mapping_mut(),
205            _ => panic!("Called mapping_mut on a non-array"),
206        }
207    }
208
209    fn take(&mut self) -> Self {
210        core::mem::replace(self, Self::BadValue)
211    }
212}
213
214impl IntoIterator for YamlOwned {
215    type Item = Self;
216    type IntoIter = YamlOwnedIter;
217
218    fn into_iter(self) -> Self::IntoIter {
219        YamlOwnedIter {
220            yaml: match self {
221                Self::Sequence(vec) => vec.into_iter(),
222                _ => Vec::new().into_iter(),
223            },
224        }
225    }
226}
227
228/// An iterator over a [`YamlOwned`] node.
229pub struct YamlOwnedIter {
230    yaml: alloc::vec::IntoIter<YamlOwned>,
231}
232
233impl Iterator for YamlOwnedIter {
234    type Item = YamlOwned;
235
236    fn next(&mut self) -> Option<YamlOwned> {
237        self.yaml.next()
238    }
239}
240
241/// Hash the given `str` as if it were a [`ScalarOwned::String`] object.
242fn hash_str_as_yaml_string<H: Hasher>(key: &str, mut hasher: H) -> u64 {
243    use core::hash::Hash;
244    let key = YamlOwned::Value(ScalarOwned::String(key.into()));
245    key.hash(&mut hasher);
246    hasher.finish()
247}