Skip to main content

tramlines_core/yaml/
value.rs

1//! The positioned YAML value tree.
2
3use serde::{Deserialize, Serialize};
4
5/// A YAML 1.2 core-schema value.
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub enum Yaml {
8    /// `null`, `~` or an empty plain scalar.
9    Null,
10    /// `true` or `false`.
11    Bool(bool),
12    /// An integer.
13    Int(i64),
14    /// A floating-point number.
15    Float(f64),
16    /// A string.
17    Str(String),
18    /// A sequence.
19    Seq(Vec<Spanned>),
20    /// A mapping, in source order.
21    Map(Vec<(Spanned<String>, Spanned)>),
22}
23
24/// A value with the line it starts on.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct Spanned<T = Yaml> {
27    /// The value.
28    pub value: T,
29    /// The 1-based line of the value in its file.
30    pub line: usize,
31}
32
33impl<T> Spanned<T> {
34    /// Pair a value with its line.
35    pub fn new(value: T, line: usize) -> Self {
36        Spanned { value, line }
37    }
38}
39
40impl Spanned {
41    /// The string value, if this is a string.
42    pub fn as_str(&self) -> Option<&str> {
43        match &self.value {
44            Yaml::Str(s) => Some(s),
45            _ => None,
46        }
47    }
48
49    /// The mapping entries, if this is a mapping.
50    pub fn as_map(&self) -> Option<&[(Spanned<String>, Spanned)]> {
51        match &self.value {
52            Yaml::Map(m) => Some(m),
53            _ => None,
54        }
55    }
56
57    /// The sequence items, if this is a sequence.
58    pub fn as_seq(&self) -> Option<&[Spanned]> {
59        match &self.value {
60            Yaml::Seq(s) => Some(s),
61            _ => None,
62        }
63    }
64
65    /// The integer value, if this is an integer.
66    pub fn as_int(&self) -> Option<i64> {
67        match &self.value {
68            Yaml::Int(i) => Some(*i),
69            _ => None,
70        }
71    }
72
73    /// The boolean value, if this is a boolean.
74    pub fn as_bool(&self) -> Option<bool> {
75        match &self.value {
76            Yaml::Bool(b) => Some(*b),
77            _ => None,
78        }
79    }
80
81    /// The value of `key`, if this is a mapping carrying it.
82    pub fn get(&self, key: &str) -> Option<&Spanned> {
83        self.entry(key).map(|(_, v)| v)
84    }
85
86    /// The line of `key`, if this is a mapping carrying it.
87    pub fn key_line(&self, key: &str) -> Option<usize> {
88        self.entry(key).map(|(k, _)| k.line)
89    }
90
91    /// The entry of `key`, if this is a mapping carrying it.
92    fn entry(&self, key: &str) -> Option<&(Spanned<String>, Spanned)> {
93        self.as_map()?.iter().find(|(k, _)| k.value == key)
94    }
95
96    /// A string, or the items of a sequence of strings (`string-or-list`).
97    pub fn as_str_or_list(&self) -> Option<Vec<String>> {
98        match &self.value {
99            Yaml::Str(s) => Some(vec![s.clone()]),
100            Yaml::Seq(items) => items
101                .iter()
102                .map(|i| i.as_str().map(str::to_string))
103                .collect(),
104            _ => None,
105        }
106    }
107
108    /// The name of the value's kind, for messages.
109    pub fn type_name(&self) -> &'static str {
110        match &self.value {
111            Yaml::Null => "null",
112            Yaml::Bool(_) => "boolean",
113            Yaml::Int(_) => "integer",
114            Yaml::Float(_) => "float",
115            Yaml::Str(_) => "string",
116            Yaml::Seq(_) => "list",
117            Yaml::Map(_) => "map",
118        }
119    }
120
121    /// The scalar as text, for messages and `const` comparisons.
122    pub fn scalar_text(&self) -> Option<String> {
123        match &self.value {
124            Yaml::Null => Some("null".into()),
125            Yaml::Bool(b) => Some(b.to_string()),
126            Yaml::Int(i) => Some(i.to_string()),
127            Yaml::Float(f) => Some(f.to_string()),
128            Yaml::Str(s) => Some(s.clone()),
129            _ => None,
130        }
131    }
132}
133
134// sokf:begin yaml-error
135/// A construct the YAML dialect refuses (TL_14), or a syntax
136/// error, with its line.
137#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
138pub enum YamlError {
139    /// The text is not valid YAML.
140    #[error("YAML does not parse at line {line}: {message}")]
141    Syntax {
142        /// The line of the failure.
143        line: usize,
144        /// The parser's message.
145        message: String,
146    },
147    /// An anchor (`&name`).
148    #[error("YAML anchor at line {line}: anchors are not read")]
149    Anchor {
150        /// The line of the anchor.
151        line: usize,
152    },
153    /// An alias (`*name`).
154    #[error("YAML alias at line {line}: aliases are not read")]
155    Alias {
156        /// The line of the alias.
157        line: usize,
158    },
159    /// A tag (`!tag`).
160    #[error("YAML tag at line {line}: tags are not read")]
161    Tag {
162        /// The line of the tag.
163        line: usize,
164    },
165    /// A mapping key that is not a string.
166    #[error("YAML mapping key at line {line} is not a string")]
167    NonStringKey {
168        /// The line of the key.
169        line: usize,
170    },
171}
172// sokf:end yaml-error
173
174impl YamlError {
175    /// Whether the YAML parses and uses what the engine refuses: an
176    /// anchor, an alias or a tag.
177    // sokf:begin yaml-error
178    pub fn is_refused(&self) -> bool {
179        // sokf:end yaml-error
180        // sokf-implements: CORE-12_AC-1
181        matches!(
182            self,
183            YamlError::Anchor { .. } | YamlError::Alias { .. } | YamlError::Tag { .. }
184        )
185    }
186
187    /// The line the error is at.
188    pub fn line(&self) -> usize {
189        match self {
190            YamlError::Syntax { line, .. }
191            | YamlError::Anchor { line }
192            | YamlError::Alias { line }
193            | YamlError::Tag { line }
194            | YamlError::NonStringKey { line } => *line,
195        }
196    }
197}