1use crate::Deserialize;
18use crate::span::is_single_line;
19use cfg_if::cfg_if;
20use saphyr::{LoadableYamlNode, MarkedYaml, Scalar, YamlData};
21use tanzim_value::{Error, LocatedValue, Location, Map, Value};
22
23#[derive(Default, Copy, Clone)]
24pub struct Yaml;
25
26impl Yaml {
27 pub fn new() -> Self {
28 Self
29 }
30}
31
32impl Deserialize for Yaml {
33 fn name(&self) -> &str {
34 "YAML"
35 }
36
37 fn supported_format_list(&self) -> Vec<String> {
38 vec!["yml".into(), "yaml".into()]
39 }
40
41 fn parse(&self, source: &str, resource: &str, bytes: &[u8]) -> Result<LocatedValue, Error> {
42 cfg_if! {
43 if #[cfg(feature = "tracing")] {
44 tracing::debug!(msg = "Parsing YAML configuration", source = source, resource = resource, bytes = bytes.len());
45 } else if #[cfg(feature = "logging")] {
46 log::debug!("msg=\"Parsing YAML configuration\" source={source} resource={resource} bytes={}", bytes.len());
47 }
48 }
49 let text = match std::str::from_utf8(bytes) {
50 Ok(value) => value,
51 Err(_) => {
52 return Err(Error::InvalidUtf8 {
53 location: Location::at(source, resource, None, None, None),
54 });
55 }
56 };
57 let single_line = is_single_line(bytes);
58 let docs = match MarkedYaml::load_from_str(text) {
59 Ok(value) => value,
60 Err(error) => {
61 let marker = error.marker();
62 return Err(Error::Parse {
63 text: text.to_string(),
64 location: Some(Location::at(
65 source,
66 resource,
67 Some(marker.line()),
68 Some(marker.col() + 1),
69 None,
70 )),
71 message: error.info().to_string(),
72 });
73 }
74 };
75 if docs.is_empty() {
76 cfg_if! {
77 if #[cfg(feature = "tracing")] {
78 tracing::trace!(msg = "Parsed YAML configuration (empty document)", source = source, resource = resource);
79 } else if #[cfg(feature = "logging")] {
80 log::trace!("msg=\"Parsed YAML configuration (empty document)\" source={source} resource={resource}");
81 }
82 }
83 return Ok(LocatedValue {
84 value: Value::Map(Map::new()),
85 location: Location::at(source, resource, None, None, None),
86 });
87 }
88 let result = convert_node(source, resource, text, single_line, &docs[0]);
89 if result.is_ok() {
90 cfg_if! {
91 if #[cfg(feature = "tracing")] {
92 tracing::trace!(msg = "Parsed YAML configuration", source = source, resource = resource);
93 } else if #[cfg(feature = "logging")] {
94 log::trace!("msg=\"Parsed YAML configuration\" source={source} resource={resource}");
95 }
96 }
97 }
98 result
99 }
100
101 fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
102 match std::str::from_utf8(bytes) {
103 Ok(text) => Some(MarkedYaml::load_from_str(text).is_ok()),
104 Err(_) => Some(false),
105 }
106 }
107}
108
109fn convert_node(
110 source: &str,
111 resource: &str,
112 text: &str,
113 single_line: bool,
114 node: &MarkedYaml<'_>,
115) -> Result<LocatedValue, Error> {
116 let location = if single_line {
117 Location::at(source, resource, None, None, None)
118 } else {
119 let marker = node.span.start;
120 let length = if !node.span.is_empty() {
121 Some(node.span.len())
122 } else {
123 None
124 };
125 Location::at(
126 source,
127 resource,
128 Some(marker.line()),
129 Some(marker.col() + 1),
130 length,
131 )
132 };
133 match &node.data {
134 YamlData::Value(scalar) => match scalar {
135 Scalar::Null => Err(Error::UnsupportedNull {
136 text: text.to_string(),
137 location,
138 }),
139 Scalar::Boolean(value) => Ok(LocatedValue {
140 value: Value::Bool(*value),
141 location,
142 }),
143 Scalar::Integer(value) => Ok(LocatedValue {
144 value: Value::Int(*value as isize),
145 location,
146 }),
147 Scalar::FloatingPoint(value) => Ok(LocatedValue {
148 value: Value::Float(value.into_inner()),
149 location,
150 }),
151 Scalar::String(value) => Ok(LocatedValue {
152 value: Value::String(value.to_string()),
153 location,
154 }),
155 },
156 YamlData::Sequence(sequence) => {
157 let mut list = Vec::new();
158 for node in sequence {
159 list.push(convert_node(source, resource, text, single_line, node)?);
160 }
161 Ok(LocatedValue {
162 value: Value::List(list),
163 location,
164 })
165 }
166 YamlData::Mapping(mapping) => {
167 let mut map = Map::new();
168 for (key_node, value_node) in mapping {
169 let key = match &key_node.data {
170 YamlData::Value(Scalar::String(value)) => value.to_string(),
171 YamlData::Representation(value, _, _) => value.to_string(),
172 _ => {
173 return Err(Error::Parse {
174 text: String::new(),
175 location: None,
176 message: "yaml map key must be a string".to_string(),
177 });
178 }
179 };
180 let value = convert_node(source, resource, text, single_line, value_node)?;
181 map.insert(key, value);
182 }
183 Ok(LocatedValue {
184 value: Value::Map(map),
185 location,
186 })
187 }
188 YamlData::Tagged(_, inner) => convert_node(source, resource, text, single_line, inner),
189 YamlData::Representation(representation, _, _) => {
190 if representation == "~" || representation == "null" || representation == "Null" {
191 return Err(Error::UnsupportedNull {
192 text: text.to_string(),
193 location,
194 });
195 }
196 Ok(LocatedValue {
197 value: Value::String(representation.to_string()),
198 location,
199 })
200 }
201 YamlData::Alias(_) | YamlData::BadValue => Err(Error::Parse {
202 text: text.to_string(),
203 location: Some(location),
204 message: "unsupported yaml node".to_string(),
205 }),
206 }
207}
208
209#[cfg(all(test, feature = "yaml"))]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn parses_yaml_map() {
215 let parsed = Yaml::new()
216 .parse("file", "config.yaml", b"hello: world\n")
217 .unwrap();
218 assert_eq!(
219 parsed
220 .value
221 .as_map()
222 .unwrap()
223 .get("hello")
224 .unwrap()
225 .value
226 .as_string()
227 .unwrap(),
228 "world"
229 );
230 }
231
232 #[test]
233 fn parses_yaml_map_with_lines() {
234 let root = Yaml::new()
235 .parse("file", "config.yaml", b"foo: bar\nbaz: qux\n")
236 .unwrap();
237 let map = root.value.as_map().unwrap();
238 let foo = map.get("foo").unwrap();
239 assert_eq!(foo.value.as_string().unwrap(), "bar");
240 assert_eq!(foo.location.line, std::num::NonZeroU32::new(1));
241 let baz = map.get("baz").unwrap();
242 assert_eq!(baz.location.line, std::num::NonZeroU32::new(2));
243 }
244
245 #[test]
246 fn rejects_yaml_null_at_correct_column() {
247 let text = "foo: bar\n\nbaz:\n\n qux: ~\n";
248 let error = Yaml::new()
249 .parse("file", "config.yaml", text.as_bytes())
250 .unwrap_err();
251 if let Error::UnsupportedNull { location, .. } = &error {
252 assert_eq!(location.line, std::num::NonZeroU32::new(5));
253 assert_eq!(location.column, std::num::NonZeroU32::new(8));
254 assert_eq!(location.length, std::num::NonZeroU32::new(1));
255 } else {
256 panic!("expected unsupported null");
257 }
258 let message = format!("{error:#}");
259 let mut source_line = "";
260 for line in message.split('\n') {
261 if line.contains("qux: ~") {
262 source_line = line;
263 break;
264 }
265 }
266 let mut caret_line = "";
267 for line in message.split('\n') {
268 if line.contains('^') {
269 caret_line = line;
270 break;
271 }
272 }
273 let mut tilde_column = 0usize;
274 if let Some(after_pipe) = source_line.split('|').nth(1) {
275 let mut index = 0usize;
276 let mut byte_index = 0usize;
277 while byte_index < after_pipe.len() {
278 let ch = after_pipe[byte_index..]
279 .chars()
280 .next()
281 .expect("valid utf-8");
282 if ch == '~' {
283 tilde_column = index;
284 break;
285 }
286 index += 1;
287 byte_index += ch.len_utf8();
288 }
289 }
290 let mut caret_column = 0usize;
291 if let Some(after_pipe) = caret_line.split('|').nth(1) {
292 let mut index = 0usize;
293 let mut byte_index = 0usize;
294 while byte_index < after_pipe.len() {
295 let ch = after_pipe[byte_index..]
296 .chars()
297 .next()
298 .expect("valid utf-8");
299 if ch == '^' {
300 caret_column = index;
301 break;
302 }
303 index += 1;
304 byte_index += ch.len_utf8();
305 }
306 }
307 assert_eq!(caret_column, tilde_column);
308 }
309
310 #[test]
311 fn syntax_error_has_location() {
312 let error = Yaml::new()
313 .parse("file", "config.yaml", b"foo: [\n")
314 .unwrap_err();
315 if let Error::Parse { location, .. } = &error {
316 let location = location.as_ref().expect("syntax error location");
317 assert!(location.line.is_some());
318 assert!(location.column.is_some());
319 } else {
320 panic!("expected parse error");
321 }
322 let message = format!("{error:#}");
323 assert!(message.contains('^'));
324 }
325}