1use std::fmt;
9
10use serde::de::{Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
11use serde_saphyr::Spanned;
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct Node {
16 pub kind: Kind,
17 pub line: usize,
18 pub column: usize,
19}
20
21#[derive(Debug, Clone, PartialEq)]
22pub enum Kind {
23 Null,
24 Bool(bool),
25 Int(i64),
26 UInt(u64),
27 Float(f64),
28 Str(String),
29 Seq(Vec<Node>),
30 Map(Vec<(Key, Node)>),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Key {
37 pub name: String,
38 pub line: usize,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct SyntaxError {
44 pub line: Option<usize>,
45 pub message: String,
46}
47
48impl Kind {
49 pub fn name(&self) -> &'static str {
51 match self {
52 Kind::Null => "nothing",
53 Kind::Bool(_) => "a boolean",
54 Kind::Int(_) | Kind::UInt(_) | Kind::Float(_) => "a number",
55 Kind::Str(_) => "a string",
56 Kind::Seq(_) => "a list",
57 Kind::Map(_) => "a map",
58 }
59 }
60}
61
62impl Node {
63 pub fn null() -> Node {
64 Node {
65 kind: Kind::Null,
66 line: 0,
67 column: 0,
68 }
69 }
70
71 pub fn as_str(&self) -> Option<&str> {
72 match &self.kind {
73 Kind::Str(s) => Some(s),
74 _ => None,
75 }
76 }
77
78 pub fn as_map(&self) -> Option<&[(Key, Node)]> {
79 match &self.kind {
80 Kind::Map(m) => Some(m),
81 _ => None,
82 }
83 }
84
85 pub fn get(&self, key: &str) -> Option<&Node> {
87 self.as_map()?.iter().find(|(k, _)| k.name == key).map(|(_, v)| v)
88 }
89
90 pub fn entry(&self, key: &str) -> Option<&(Key, Node)> {
92 self.as_map()?.iter().find(|(k, _)| k.name == key)
93 }
94
95 pub fn to_json(&self) -> serde_json::Value {
98 use serde_json::Value as J;
99 match &self.kind {
100 Kind::Null => J::Null,
101 Kind::Bool(b) => J::Bool(*b),
102 Kind::Int(i) => J::from(*i),
103 Kind::UInt(u) => J::from(*u),
104 Kind::Float(f) => {
105 serde_json::Number::from_f64(*f).map_or_else(|| J::String(f.to_string()), J::Number)
106 }
107 Kind::Str(s) => J::String(s.clone()),
108 Kind::Seq(s) => J::Array(s.iter().map(Node::to_json).collect()),
109 Kind::Map(m) => J::Object(m.iter().map(|(k, v)| (k.name.clone(), v.to_json())).collect()),
110 }
111 }
112}
113
114pub fn parse(text: &str) -> Result<Node, SyntaxError> {
116 let mut options = serde_saphyr::options::Options::default();
117 options.strict_booleans = true;
118 options.duplicate_keys = serde_saphyr::options::DuplicateKeyPolicy::Error;
119 options.with_snippet = false;
120 options.non_finite_float_policy = serde_saphyr::options::NonFiniteFloatPolicy::AsString;
123 if text.trim().is_empty() {
124 return Ok(Node::null());
125 }
126 match serde_saphyr::from_str_with_options::<Spanned<Raw>>(text, options) {
127 Ok(raw) => {
128 let mut node: Node = raw.into();
129 if let Kind::Map(m) = &mut node.kind {
132 m.retain(|(k, _)| !k.name.starts_with("x-"));
133 }
134 Ok(node)
135 }
136 Err(e) => Err(syntax_error(&e)),
137 }
138}
139
140fn syntax_error(e: &serde_saphyr::Error) -> SyntaxError {
141 static LOCATION: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
142 regex::Regex::new(r"^(?:error: )?(?:line \d+ column \d+: )?(.*?)(?: at line \d+, column \d+)?$")
143 .unwrap()
144 });
145 let line = e.location().map(|l| l.line() as usize).filter(|l| *l > 0);
146 let text = e.to_string();
147 let first = text.lines().next().unwrap_or_default();
148 let message = LOCATION
150 .captures(first)
151 .map_or(first, |c| c.get(1).map_or(first, |m| m.as_str()));
152 let message = message.replace(", set DuplicateKeyPolicy in Options if acceptable", "");
153 let message = message
154 .split("; use from_")
155 .next()
156 .unwrap_or_default()
157 .to_string();
158 let message = if message.contains("merge value must be") {
159 "invalid YAML merge key (`<<`): `<<` must be a map or a list of maps".to_string()
160 } else {
161 message
162 };
163 SyntaxError { line, message }
164}
165
166enum Raw {
168 Null,
169 Bool(bool),
170 Int(i64),
171 UInt(u64),
172 Float(f64),
173 Str(String),
174 Seq(Vec<Spanned<Raw>>),
175 Map(Vec<(Spanned<KeyRaw>, Spanned<Raw>)>),
176}
177
178struct KeyRaw(String);
180
181impl From<Spanned<Raw>> for Node {
182 fn from(s: Spanned<Raw>) -> Node {
183 let kind = match s.value {
184 Raw::Null => Kind::Null,
185 Raw::Bool(b) => Kind::Bool(b),
186 Raw::Int(i) => Kind::Int(i),
187 Raw::UInt(u) => Kind::UInt(u),
188 Raw::Float(f) => Kind::Float(f),
189 Raw::Str(t) => Kind::Str(t),
190 Raw::Seq(items) => Kind::Seq(items.into_iter().map(Node::from).collect()),
191 Raw::Map(entries) => Kind::Map(
192 entries
193 .into_iter()
194 .map(|(k, v)| {
195 let key = Key {
196 name: k.value.0,
197 line: k.referenced.line() as usize,
198 };
199 (key, Node::from(v))
200 })
201 .collect(),
202 ),
203 };
204 Node {
205 kind,
206 line: s.referenced.line() as usize,
207 column: s.referenced.column() as usize,
208 }
209 }
210}
211
212impl<'de> Deserialize<'de> for Raw {
213 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
214 struct V;
215 impl<'de> Visitor<'de> for V {
216 type Value = Raw;
217 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
218 f.write_str("a YAML value")
219 }
220 fn visit_unit<E>(self) -> Result<Raw, E> {
221 Ok(Raw::Null)
222 }
223 fn visit_none<E>(self) -> Result<Raw, E> {
224 Ok(Raw::Null)
225 }
226 fn visit_some<D: Deserializer<'de>>(self, d: D) -> Result<Raw, D::Error> {
227 Raw::deserialize(d)
228 }
229 fn visit_bool<E>(self, v: bool) -> Result<Raw, E> {
230 Ok(Raw::Bool(v))
231 }
232 fn visit_i64<E>(self, v: i64) -> Result<Raw, E> {
233 Ok(Raw::Int(v))
234 }
235 fn visit_u64<E>(self, v: u64) -> Result<Raw, E> {
236 Ok(i64::try_from(v).map_or(Raw::UInt(v), Raw::Int))
237 }
238 fn visit_f64<E>(self, v: f64) -> Result<Raw, E> {
239 Ok(Raw::Float(v))
240 }
241 fn visit_str<E>(self, v: &str) -> Result<Raw, E> {
242 Ok(Raw::Str(v.to_string()))
243 }
244 fn visit_string<E>(self, v: String) -> Result<Raw, E> {
245 Ok(Raw::Str(v))
246 }
247 fn visit_seq<A: SeqAccess<'de>>(self, mut a: A) -> Result<Raw, A::Error> {
248 let mut items = Vec::new();
249 while let Some(x) = a.next_element()? {
250 items.push(x);
251 }
252 Ok(Raw::Seq(items))
253 }
254 fn visit_map<A: MapAccess<'de>>(self, mut a: A) -> Result<Raw, A::Error> {
255 let mut entries = Vec::new();
256 while let Some(e) = a.next_entry()? {
257 entries.push(e);
258 }
259 Ok(Raw::Map(entries))
260 }
261 }
262 d.deserialize_any(V)
263 }
264}
265
266pub const COMPLEX_KEY: &str = "a map key is a list or a map; keys must be names";
268
269impl<'de> Deserialize<'de> for KeyRaw {
270 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
271 struct V;
272 impl<'de> Visitor<'de> for V {
273 type Value = KeyRaw;
274 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
275 f.write_str("a key")
276 }
277 fn visit_unit<E>(self) -> Result<KeyRaw, E> {
278 Ok(KeyRaw("null".into()))
279 }
280 fn visit_none<E>(self) -> Result<KeyRaw, E> {
281 Ok(KeyRaw("null".into()))
282 }
283 fn visit_bool<E>(self, v: bool) -> Result<KeyRaw, E> {
284 Ok(KeyRaw(v.to_string()))
285 }
286 fn visit_i64<E>(self, v: i64) -> Result<KeyRaw, E> {
287 Ok(KeyRaw(v.to_string()))
288 }
289 fn visit_u64<E>(self, v: u64) -> Result<KeyRaw, E> {
290 Ok(KeyRaw(v.to_string()))
291 }
292 fn visit_f64<E>(self, v: f64) -> Result<KeyRaw, E> {
293 Ok(KeyRaw(v.to_string()))
294 }
295 fn visit_str<E>(self, v: &str) -> Result<KeyRaw, E> {
296 Ok(KeyRaw(v.to_string()))
297 }
298 fn visit_string<E>(self, v: String) -> Result<KeyRaw, E> {
299 Ok(KeyRaw(v))
300 }
301 fn visit_seq<A: SeqAccess<'de>>(self, _: A) -> Result<KeyRaw, A::Error> {
302 Err(serde::de::Error::custom(COMPLEX_KEY))
303 }
304 fn visit_map<A: MapAccess<'de>>(self, _: A) -> Result<KeyRaw, A::Error> {
305 Err(serde::de::Error::custom(COMPLEX_KEY))
306 }
307 }
308 d.deserialize_any(V)
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn values_keep_their_lines() {
318 let n = parse("a:\n b: 1\n c: [x, y]\n").unwrap();
319 let a = n.get("a").unwrap();
320 assert_eq!(n.entry("a").unwrap().0.line, 1);
321 assert_eq!(a.entry("c").unwrap().0.line, 3);
322 assert_eq!(a.get("b").unwrap().kind, Kind::Int(1));
323 assert_eq!(a.get("b").unwrap().line, 2);
324 }
325
326 #[test]
327 fn only_true_and_false_are_booleans() {
328 let n = parse("a: yes\nb: no\nc: on\nd: true\n").unwrap();
329 assert_eq!(n.get("a").unwrap().as_str(), Some("yes"));
330 assert_eq!(n.get("c").unwrap().as_str(), Some("on"));
331 assert_eq!(n.get("d").unwrap().kind, Kind::Bool(true));
332 }
333
334 #[test]
335 fn merge_keys_are_expanded_and_written_keys_win() {
336 let n = parse("base: &b {type: duckdb, path: x}\ndev: {<<: *b, path: y}\n").unwrap();
337 let dev = n.get("dev").unwrap();
338 assert_eq!(dev.get("type").unwrap().as_str(), Some("duckdb"));
339 assert_eq!(dev.get("path").unwrap().as_str(), Some("y"));
340 }
341
342 #[test]
343 fn duplicate_keys_are_errors_with_their_line() {
344 let e = parse("a: 1\na: 2\n").unwrap_err();
345 assert_eq!(e.line, Some(2));
346 assert_eq!(e.message, "duplicate mapping key: a");
347 }
348
349 #[test]
350 fn syntax_errors_have_a_line() {
351 let e = parse("a:\n b: [1\n").unwrap_err();
352 assert_eq!(e.line, Some(2));
353 assert!(!e.message.starts_with("line"), "{}", e.message);
354 }
355
356 #[test]
357 fn scalar_keys_are_strings_and_empty_files_are_null() {
358 assert_eq!(
359 parse("2024: x\n").unwrap().get("2024").unwrap().as_str(),
360 Some("x")
361 );
362 assert_eq!(parse("\n# nothing\n").unwrap().kind, Kind::Null);
363 }
364
365 #[test]
366 fn merged_keys_report_the_merge_line() {
367 let n = parse("base: &b\n x: 1\ndev:\n <<: *b\n z: 3\n").unwrap();
368 let dev = n.get("dev").unwrap();
369 assert_eq!(dev.entry("x").unwrap().0.line, 4);
370 assert_eq!(dev.entry("z").unwrap().0.line, 5);
371 }
372
373 #[test]
374 fn multiple_documents_name_no_library_functions() {
375 let e = parse("a: 1\n---\nb: 2\n").unwrap_err();
376 assert_eq!(e.line, Some(3));
377 assert_eq!(e.message, "multiple YAML documents detected");
378 }
379
380 #[test]
381 fn non_finite_floats_are_strings() {
382 let j = parse("a: .inf\nb: -.inf\nc: .nan\nd: 1e999\n").unwrap().to_json();
383 assert_eq!(
384 j,
385 serde_json::json!({"a": ".inf", "b": "-.inf", "c": ".nan", "d": ".inf"})
386 );
387 }
388
389 #[test]
390 fn relaxed_yaml_forms_parse() {
391 let quoted = parse("a: \"one\n two\"\n").unwrap();
392 assert_eq!(quoted.get("a").unwrap().as_str(), Some("one two"));
393 assert_eq!(parse("?\tk\n").unwrap().to_json(), serde_json::json!({"k": null}));
394 assert!(parse("?").is_ok());
395 let block = parse("s: |\n a\n ").unwrap();
396 assert_eq!(block.get("s").unwrap().as_str(), Some("a\n"));
397 }
398
399 #[test]
400 fn jinja_in_quoted_values_is_a_string() {
401 let n = parse("pw: \"{{ env_var('X') }}\"\n").unwrap();
402 assert_eq!(n.get("pw").unwrap().as_str(), Some("{{ env_var('X') }}"));
403 }
404}
405
406#[cfg(test)]
407mod x_key_tests {
408 use super::*;
409
410 #[test]
411 fn top_level_x_keys_hold_anchors_and_are_dropped() {
412 let n = parse("x-dest: &dest {profile: s3, path: a.csv}\noutput:\n destination:\n <<: *dest\n path: b.csv\n").unwrap();
413 let j = n.to_json();
414 assert!(j.get("x-dest").is_none(), "{j}");
415 assert_eq!(
416 j["output"]["destination"],
417 serde_json::json!({"profile": "s3", "path": "b.csv"})
418 );
419 }
420}