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