1use serde_json::{Map, Number, Value};
17use std::fmt;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct YamlError {
21 message: String,
22 line: Option<usize>,
24}
25
26impl YamlError {
27 fn new(message: impl Into<String>, line: Option<usize>) -> Self {
28 Self {
29 message: message.into(),
30 line,
31 }
32 }
33}
34
35impl fmt::Display for YamlError {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self.line {
38 Some(l) => write!(f, "line {}: {}", l + 1, self.message),
39 None => write!(f, "{}", self.message),
40 }
41 }
42}
43
44impl std::error::Error for YamlError {}
45
46const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991; fn resolve_scalar(text: &str) -> Result<Value, YamlError> {
53 match text {
54 "" | "~" | "null" | "Null" | "NULL" => return Ok(Value::Null),
55 "true" | "True" | "TRUE" => return Ok(Value::Bool(true)),
56 "false" | "False" | "FALSE" => return Ok(Value::Bool(false)),
57 ".nan" | ".NaN" | ".NAN" => {
58 return Err(YamlError::new("non-finite floats are not supported", None))
59 }
60 _ => {}
61 }
62 let bytes = text.as_bytes();
63 let digits = |s: &[u8]| !s.is_empty() && s.iter().all(u8::is_ascii_digit);
64 let unsigned = bytes.strip_prefix(b"-").or_else(|| bytes.strip_prefix(b"+"));
65 let body = unsigned.unwrap_or(bytes);
66 if digits(body) {
67 if let Ok(n) = text.parse::<i64>() {
68 if n.abs() <= MAX_SAFE_INTEGER {
69 return Ok(Value::Number(n.into()));
70 }
71 }
72 return Ok(Value::String(text.to_string())); }
74 if let Some(hex) = text.strip_prefix("0x") {
75 if !hex.is_empty() && hex.bytes().all(|b| b.is_ascii_hexdigit()) {
76 if let Ok(n) = i64::from_str_radix(hex, 16) {
77 if n <= MAX_SAFE_INTEGER {
78 return Ok(Value::Number(n.into()));
79 }
80 }
81 return Ok(Value::String(text.to_string()));
82 }
83 }
84 if let Some(oct) = text.strip_prefix("0o") {
85 if !oct.is_empty() && oct.bytes().all(|b| (b'0'..=b'7').contains(&b)) {
86 if let Ok(n) = i64::from_str_radix(oct, 8) {
87 if n <= MAX_SAFE_INTEGER {
88 return Ok(Value::Number(n.into()));
89 }
90 }
91 return Ok(Value::String(text.to_string()));
92 }
93 }
94 if is_float_syntax(text) {
95 if let Ok(f) = text.parse::<f64>() {
96 if f.is_finite() {
97 if let Some(n) = Number::from_f64(f) {
98 return Ok(Value::Number(n));
99 }
100 }
101 }
102 }
103 if matches!(
104 text,
105 ".inf" | ".Inf" | ".INF" | "-.inf" | "-.Inf" | "-.INF" | "+.inf" | "+.Inf" | "+.INF"
106 ) {
107 return Err(YamlError::new("non-finite floats are not supported", None));
108 }
109 Ok(Value::String(text.to_string()))
110}
111
112fn is_float_syntax(text: &str) -> bool {
114 let s = text
115 .strip_prefix('-')
116 .or_else(|| text.strip_prefix('+'))
117 .unwrap_or(text);
118 let (mantissa, exponent) = match s.find(['e', 'E']) {
119 Some(i) => (&s[..i], Some(&s[i + 1..])),
120 None => (s, None),
121 };
122 let mantissa_ok = if let Some(frac) = mantissa.strip_prefix('.') {
123 !frac.is_empty() && frac.bytes().all(|b| b.is_ascii_digit())
124 } else if let Some(dot) = mantissa.find('.') {
125 let (int, frac) = (&mantissa[..dot], &mantissa[dot + 1..]);
126 !int.is_empty()
127 && int.bytes().all(|b| b.is_ascii_digit())
128 && frac.bytes().all(|b| b.is_ascii_digit())
129 } else {
130 exponent.is_some() && !mantissa.is_empty() && mantissa.bytes().all(|b| b.is_ascii_digit())
132 };
133 if !mantissa_ok {
134 return false;
135 }
136 match exponent {
137 None => true,
138 Some(e) => {
139 let e = e.strip_prefix('-').or_else(|| e.strip_prefix('+')).unwrap_or(e);
140 !e.is_empty() && e.bytes().all(|b| b.is_ascii_digit())
141 }
142 }
143}
144
145#[derive(Clone)]
150struct Line {
151 indent: usize,
152 content: String,
153 raw: usize,
154}
155
156struct Parser {
157 lines: Vec<String>,
158 items: Vec<Line>,
159 pos: usize,
160}
161
162impl Parser {
163 fn new(input: &str) -> Result<Self, YamlError> {
164 let lines: Vec<String> = input.split('\n').map(|l| l.trim_end_matches('\r').to_string()).collect();
165 let mut items = Vec::new();
166 for (i, raw) in lines.iter().enumerate() {
167 let trimmed = raw.trim_end();
168 let indent = count_indent(trimmed);
169 if raw[..indent.min(raw.len())].contains('\t') || raw.trim_start_matches(' ').starts_with('\t') && raw.trim().is_empty() {
170 return Err(YamlError::new("tabs are not allowed in indentation", Some(i)));
171 }
172 let content = &trimmed[indent..];
173 if content.is_empty() || content.starts_with('#') {
174 continue;
175 }
176 if content == "---" {
177 if items.is_empty() {
178 continue; }
180 return Err(YamlError::new(
181 "multi-document streams are not supported",
182 Some(i),
183 ));
184 }
185 if content == "..." {
186 continue;
187 }
188 items.push(Line {
189 indent,
190 content: content.to_string(),
191 raw: i,
192 });
193 }
194 Ok(Self {
195 lines,
196 items,
197 pos: 0,
198 })
199 }
200
201 fn parse(mut self) -> Result<Value, YamlError> {
202 if self.items.is_empty() {
203 return Ok(Value::Null);
204 }
205 let value = self.parse_node(0)?;
206 if self.pos < self.items.len() {
207 return Err(YamlError::new(
208 "unexpected content",
209 Some(self.items[self.pos].raw),
210 ));
211 }
212 Ok(value)
213 }
214
215 fn peek(&self) -> Option<&Line> {
216 self.items.get(self.pos)
217 }
218
219 fn parse_node(&mut self, min_indent: usize) -> Result<Value, YamlError> {
220 let Some(line) = self.peek() else {
221 return Ok(Value::Null);
222 };
223 if line.indent < min_indent {
224 return Ok(Value::Null);
225 }
226 if line.content == "-" || line.content.starts_with("- ") {
227 let indent = line.indent;
228 return self.parse_sequence(indent);
229 }
230 if find_key(&line.content).is_some() {
231 let indent = line.indent;
232 return self.parse_mapping(indent);
233 }
234 self.parse_scalar_lines()
235 }
236
237 fn parse_sequence(&mut self, indent: usize) -> Result<Value, YamlError> {
238 let mut out = Vec::new();
239 loop {
240 let Some(line) = self.peek() else { break };
241 if line.indent != indent {
242 break;
243 }
244 if line.content == "-" {
245 self.pos += 1;
246 let deeper = self.peek().map(|n| n.indent > indent).unwrap_or(false);
247 out.push(if deeper {
248 self.parse_node(indent + 1)?
249 } else {
250 Value::Null
251 });
252 continue;
253 }
254 if !line.content.starts_with("- ") {
255 break;
256 }
257 let rest = line.content[2..].to_string();
260 let raw = line.raw;
261 let extra = count_indent(&rest);
262 self.items[self.pos] = Line {
263 indent: indent + 2 + extra,
264 content: rest[extra..].to_string(),
265 raw,
266 };
267 out.push(self.parse_node(indent + 1)?);
268 }
269 Ok(Value::Array(out))
270 }
271
272 fn parse_mapping(&mut self, indent: usize) -> Result<Value, YamlError> {
273 let mut out = Map::new();
274 loop {
275 let Some(line) = self.peek() else { break };
276 if line.indent != indent {
277 break;
278 }
279 if line.content == "-" || line.content.starts_with("- ") {
280 break;
281 }
282 let Some((key, rest)) = find_key(&line.content) else {
283 break;
284 };
285 let raw = line.raw;
286 self.pos += 1;
287 let value = if rest.is_empty() {
288 let deeper = self.peek().map(|n| n.indent > indent).unwrap_or(false);
289 if deeper {
290 self.parse_node(indent + 1)?
291 } else {
292 Value::Null
293 }
294 } else if rest.starts_with('|') || rest.starts_with('>') {
295 Value::String(self.parse_block_scalar(&rest, indent, raw)?)
296 } else {
297 self.parse_inline_value(&rest, indent, raw)?
298 };
299 if out.contains_key(&key) {
300 return Err(YamlError::new(
301 format!("duplicate mapping key {key:?}"),
302 Some(raw),
303 ));
304 }
305 out.insert(key, value);
306 }
307 Ok(Value::Object(out))
308 }
309
310 fn parse_inline_value(
311 &mut self,
312 rest: &str,
313 indent: usize,
314 raw_line: usize,
315 ) -> Result<Value, YamlError> {
316 if rest.starts_with('[') || rest.starts_with('{') {
317 let text = self.collect_flow(rest, raw_line)?;
318 let mut flow = FlowParser {
319 s: text.as_bytes(),
320 text: &text,
321 i: 0,
322 raw_line,
323 };
324 let value = flow.parse_value()?;
325 flow.expect_end()?;
326 return Ok(value);
327 }
328 if rest.starts_with('"') || rest.starts_with('\'') {
329 let (value, end) = parse_quoted(rest)
330 .ok_or_else(|| YamlError::new("unterminated quoted scalar", Some(raw_line)))?;
331 let after = strip_comment(rest[end..].trim());
332 if !after.is_empty() {
333 return Err(YamlError::new(
334 "unexpected content after quoted scalar",
335 Some(raw_line),
336 ));
337 }
338 return Ok(Value::String(value));
339 }
340 if rest.starts_with('&') || rest.starts_with('*') {
341 return Err(YamlError::new(
342 "anchors and aliases are not supported (TF-YAML subset)",
343 Some(raw_line),
344 ));
345 }
346 if rest.starts_with('!') {
347 return Err(YamlError::new(
348 "tags are not supported (TF-YAML subset)",
349 Some(raw_line),
350 ));
351 }
352 let mut text = strip_comment(rest).to_string();
354 loop {
355 let Some(next) = self.peek() else { break };
356 if next.indent <= indent {
357 break;
358 }
359 if next.content == "-" || next.content.starts_with("- ") {
360 break;
361 }
362 if find_key(&next.content).is_some() {
363 break;
364 }
365 text.push(' ');
366 text.push_str(strip_comment(&next.content));
367 self.pos += 1;
368 }
369 resolve_scalar(text.trim()).map_err(|e| YamlError::new(e.message, Some(raw_line)))
370 }
371
372 fn parse_scalar_lines(&mut self) -> Result<Value, YamlError> {
373 let first = self.peek().expect("caller checked").clone();
374 self.pos += 1;
375 self.parse_inline_value(&first.content, first.indent.saturating_sub(1), first.raw)
376 }
377
378 fn collect_flow(&mut self, first: &str, raw_line: usize) -> Result<String, YamlError> {
379 let mut text = strip_comment(first.trim()).to_string();
380 loop {
381 if flow_balanced(&text) {
382 return Ok(text);
383 }
384 let Some(next) = self.peek() else {
385 return Err(YamlError::new("unterminated flow collection", Some(raw_line)));
386 };
387 let chunk = strip_comment(next.content.trim()).to_string();
388 self.pos += 1;
389 text.push(' ');
390 text.push_str(&chunk);
391 }
392 }
393
394 fn parse_block_scalar(
395 &mut self,
396 header: &str,
397 key_indent: usize,
398 raw_line: usize,
399 ) -> Result<String, YamlError> {
400 let folded = header.starts_with('>');
401 #[derive(PartialEq)]
402 enum Chomp {
403 Clip,
404 Strip,
405 Keep,
406 }
407 let mut chomp = Chomp::Clip;
408 let mut explicit_indent = None;
409 for c in strip_comment(header[1..].trim()).chars() {
410 match c {
411 '-' => chomp = Chomp::Strip,
412 '+' => chomp = Chomp::Keep,
413 '1'..='9' => explicit_indent = Some(key_indent + c.to_digit(10).unwrap() as usize),
414 _ => {
415 return Err(YamlError::new(
416 format!("bad block scalar header {header:?}"),
417 Some(raw_line),
418 ))
419 }
420 }
421 }
422
423 let start_raw = raw_line + 1;
424 let mut end_raw = start_raw;
425 for i in start_raw..self.lines.len() {
426 let l = &self.lines[i];
427 if l.trim().is_empty() {
428 continue;
429 }
430 if count_indent(l) <= key_indent {
431 break;
432 }
433 end_raw = i + 1;
434 }
435 let raw: Vec<&str> = (start_raw..end_raw)
436 .map(|i| self.lines[i].as_str())
437 .collect();
438 while self.pos < self.items.len() && self.items[self.pos].raw < end_raw {
439 self.pos += 1;
440 }
441
442 let mut block_indent = explicit_indent;
443 if block_indent.is_none() {
444 for l in &raw {
445 if !l.trim().is_empty() {
446 block_indent = Some(count_indent(l));
447 break;
448 }
449 }
450 }
451 let block_indent = block_indent.unwrap_or(key_indent + 1);
452 if block_indent <= key_indent {
453 return Err(YamlError::new(
454 "block scalar body must be indented past its key",
455 Some(raw_line),
456 ));
457 }
458
459 let body: Vec<String> = raw
460 .iter()
461 .map(|l| {
462 if l.trim().is_empty() {
463 String::new()
464 } else {
465 l[block_indent.min(count_indent(l))..].to_string()
466 }
467 })
468 .collect();
469 let mut end = body.len();
470 while end > 0 && body[end - 1].is_empty() {
471 end -= 1;
472 }
473 let kept = &body[..end];
474 let trailing_blank = body.len() - end;
475
476 let text = if !folded {
477 kept.join("\n")
478 } else {
479 let mut text = String::new();
480 let mut prev_was_text = false;
481 let mut prev_was_literal = false;
482 for l in kept {
483 let literal = !l.is_empty() && (l.starts_with(' ') || l.starts_with('\t'));
484 if l.is_empty() {
485 text.push('\n');
486 prev_was_text = false;
487 prev_was_literal = false;
488 continue;
489 }
490 if prev_was_text && !literal && !prev_was_literal {
491 text.push(' ');
492 } else if prev_was_literal || (prev_was_text && literal) {
493 text.push('\n');
494 }
495 text.push_str(l);
496 prev_was_text = true;
497 prev_was_literal = literal;
498 }
499 text
500 };
501 Ok(match chomp {
502 Chomp::Strip => text.trim_end_matches('\n').to_string(),
503 Chomp::Keep => {
504 if kept.is_empty() && trailing_blank == 0 {
505 text
506 } else {
507 text + &"\n".repeat(trailing_blank + 1)
508 }
509 }
510 Chomp::Clip => {
511 if text.is_empty() && trailing_blank == 0 {
512 text
513 } else {
514 text + "\n"
515 }
516 }
517 })
518 }
519}
520
521fn count_indent(s: &str) -> usize {
522 s.bytes().take_while(|&b| b == b' ').count()
523}
524
525fn find_key(content: &str) -> Option<(String, String)> {
527 if content.starts_with('"') || content.starts_with('\'') {
528 let (key, end) = parse_quoted(content)?;
529 let after = content[end..].trim_start();
530 let rest = after.strip_prefix(':')?;
531 if !rest.is_empty() && !rest.starts_with(' ') {
532 return None;
533 }
534 return Some((key, strip_comment(rest.trim()).to_string()));
535 }
536 let bytes = content.as_bytes();
537 let mut depth = 0i32;
538 for i in 0..bytes.len() {
539 match bytes[i] {
540 b'[' | b'{' => depth += 1,
541 b']' | b'}' => depth -= 1,
542 b':' if depth == 0 => {
543 if i + 1 == bytes.len() || bytes[i + 1] == b' ' {
544 let key = content[..i].trim();
545 if key.is_empty() || key.starts_with('#') {
546 return None;
547 }
548 return Some((
549 key.to_string(),
550 strip_comment(content[i + 1..].trim()).to_string(),
551 ));
552 }
553 }
554 b'#' if i > 0 && bytes[i - 1] == b' ' => return None,
555 _ => {}
556 }
557 }
558 None
559}
560
561fn strip_comment(s: &str) -> &str {
563 if s.starts_with('#') {
564 return "";
565 }
566 let bytes = s.as_bytes();
567 let mut in_single = false;
568 let mut in_double = false;
569 let mut i = 0;
570 while i < bytes.len() {
571 match bytes[i] {
572 b'\'' if !in_double => in_single = !in_single,
573 b'"' if !in_single && (i == 0 || bytes[i - 1] != b'\\') => in_double = !in_double,
574 b'#' if !in_single && !in_double && i > 0 && (bytes[i - 1] == b' ' || bytes[i - 1] == b'\t') => {
575 return s[..i].trim_end();
576 }
577 _ => {}
578 }
579 i += 1;
580 }
581 s
582}
583
584fn flow_balanced(s: &str) -> bool {
585 let bytes = s.as_bytes();
586 let mut depth = 0i32;
587 let mut in_single = false;
588 let mut in_double = false;
589 let mut i = 0;
590 while i < bytes.len() {
591 let b = bytes[i];
592 if in_single {
593 if b == b'\'' {
594 in_single = false;
595 }
596 } else if in_double {
597 if b == b'\\' {
598 i += 1;
599 } else if b == b'"' {
600 in_double = false;
601 }
602 } else {
603 match b {
604 b'\'' => in_single = true,
605 b'"' => in_double = true,
606 b'[' | b'{' => depth += 1,
607 b']' | b'}' => depth -= 1,
608 _ => {}
609 }
610 }
611 i += 1;
612 }
613 depth <= 0 && !in_single && !in_double
614}
615
616fn parse_quoted(s: &str) -> Option<(String, usize)> {
618 let mut chars = s.char_indices();
619 let (_, quote) = chars.next()?;
620 if quote == '\'' {
621 let mut out = String::new();
622 let mut iter = chars.peekable();
623 while let Some((i, c)) = iter.next() {
624 if c == '\'' {
625 if matches!(iter.peek(), Some((_, '\''))) {
626 out.push('\'');
627 iter.next();
628 } else {
629 return Some((out, i + 1));
630 }
631 } else {
632 out.push(c);
633 }
634 }
635 return None;
636 }
637 if quote == '"' {
638 let bytes = s.as_bytes();
639 let mut out = String::new();
640 let mut i = 1;
641 while i < s.len() {
642 let c = s[i..].chars().next()?;
643 if c == '"' {
644 return Some((out, i + 1));
645 }
646 if c == '\\' {
647 let esc = s[i + 1..].chars().next()?;
648 i += 1 + esc.len_utf8();
649 match esc {
650 'n' => out.push('\n'),
651 't' => out.push('\t'),
652 'r' => out.push('\r'),
653 '0' => out.push('\0'),
654 'a' => out.push('\x07'),
655 'b' => out.push('\x08'),
656 'f' => out.push('\x0c'),
657 'v' => out.push('\x0b'),
658 'e' => out.push('\x1b'),
659 '"' | '\\' | '/' => out.push(esc),
660 'x' => {
661 let h = s.get(i..i + 2)?;
662 out.push(char::from_u32(u32::from_str_radix(h, 16).ok()?)?);
663 i += 2;
664 }
665 'u' => {
666 let h = s.get(i..i + 4)?;
667 let cp = u32::from_str_radix(h, 16).ok()?;
668 out.push(char::from_u32(cp)?);
672 i += 4;
673 }
674 'U' => {
675 let h = s.get(i..i + 8)?;
676 out.push(char::from_u32(u32::from_str_radix(h, 16).ok()?)?);
677 i += 8;
678 }
679 _ => return None,
680 }
681 let _ = bytes;
682 continue;
683 }
684 out.push(c);
685 i += c.len_utf8();
686 }
687 return None;
688 }
689 None
690}
691
692struct FlowParser<'a> {
697 s: &'a [u8],
698 text: &'a str,
699 i: usize,
700 raw_line: usize,
701}
702
703impl<'a> FlowParser<'a> {
704 fn ws(&mut self) {
705 while self.i < self.s.len() && (self.s[self.i] == b' ' || self.s[self.i] == b'\t') {
706 self.i += 1;
707 }
708 }
709
710 fn fail(&self, msg: &str) -> YamlError {
711 YamlError::new(format!("{msg} in flow collection"), Some(self.raw_line))
712 }
713
714 fn parse_value(&mut self) -> Result<Value, YamlError> {
715 self.ws();
716 let Some(&c) = self.s.get(self.i) else {
717 return Err(self.fail("unexpected end"));
718 };
719 match c {
720 b'[' => self.parse_array(),
721 b'{' => self.parse_map(),
722 b'"' | b'\'' => {
723 let (value, end) = parse_quoted(&self.text[self.i..])
724 .ok_or_else(|| self.fail("unterminated quoted scalar"))?;
725 self.i += end;
726 Ok(Value::String(value))
727 }
728 b'&' | b'*' => Err(self.fail("anchors/aliases are not supported")),
729 b'!' => Err(self.fail("tags are not supported")),
730 _ => {
731 let start = self.i;
732 while self.i < self.s.len() {
733 let ch = self.s[self.i];
734 if ch == b',' || ch == b']' || ch == b'}' {
735 break;
736 }
737 if ch == b':' && (self.i + 1 == self.s.len() || self.s[self.i + 1] == b' ') {
738 break;
739 }
740 self.i += 1;
741 }
742 resolve_scalar(self.text[start..self.i].trim())
743 .map_err(|e| YamlError::new(e.message, Some(self.raw_line)))
744 }
745 }
746 }
747
748 fn parse_array(&mut self) -> Result<Value, YamlError> {
749 self.i += 1;
750 let mut out = Vec::new();
751 self.ws();
752 if self.s.get(self.i) == Some(&b']') {
753 self.i += 1;
754 return Ok(Value::Array(out));
755 }
756 loop {
757 out.push(self.parse_value()?);
758 self.ws();
759 match self.s.get(self.i) {
760 Some(b',') => {
761 self.i += 1;
762 self.ws();
763 if self.s.get(self.i) == Some(&b']') {
764 self.i += 1;
765 return Ok(Value::Array(out));
766 }
767 }
768 Some(b']') => {
769 self.i += 1;
770 return Ok(Value::Array(out));
771 }
772 _ => return Err(self.fail("expected , or ]")),
773 }
774 }
775 }
776
777 fn parse_map(&mut self) -> Result<Value, YamlError> {
778 self.i += 1;
779 let mut out = Map::new();
780 self.ws();
781 if self.s.get(self.i) == Some(&b'}') {
782 self.i += 1;
783 return Ok(Value::Object(out));
784 }
785 loop {
786 self.ws();
787 let key = match self.s.get(self.i) {
788 Some(b'"') | Some(b'\'') => {
789 let (k, end) = parse_quoted(&self.text[self.i..])
790 .ok_or_else(|| self.fail("unterminated quoted key"))?;
791 self.i += end;
792 k
793 }
794 _ => {
795 let start = self.i;
796 while self.i < self.s.len()
797 && self.s[self.i] != b':'
798 && self.s[self.i] != b','
799 && self.s[self.i] != b'}'
800 {
801 self.i += 1;
802 }
803 self.text[start..self.i].trim().to_string()
804 }
805 };
806 self.ws();
807 let mut value = Value::Null;
808 if self.s.get(self.i) == Some(&b':') {
809 self.i += 1;
810 value = self.parse_value()?;
811 }
812 if out.contains_key(&key) {
813 return Err(self.fail(&format!("duplicate key {key:?}")));
814 }
815 out.insert(key, value);
816 self.ws();
817 match self.s.get(self.i) {
818 Some(b',') => {
819 self.i += 1;
820 self.ws();
821 if self.s.get(self.i) == Some(&b'}') {
822 self.i += 1;
823 return Ok(Value::Object(out));
824 }
825 }
826 Some(b'}') => {
827 self.i += 1;
828 return Ok(Value::Object(out));
829 }
830 _ => return Err(self.fail("expected , or }")),
831 }
832 }
833 }
834
835 fn expect_end(&mut self) -> Result<(), YamlError> {
836 self.ws();
837 if self.i < self.s.len() {
838 return Err(self.fail("trailing content"));
839 }
840 Ok(())
841 }
842}
843
844pub fn parse(input: &str) -> Result<Value, YamlError> {
850 Parser::new(input)?.parse()
851}
852
853pub fn from_str<T: serde::de::DeserializeOwned>(input: &str) -> Result<T, YamlError> {
855 let value = parse(input)?;
856 serde_json::from_value(value).map_err(|e| YamlError::new(e.to_string(), None))
857}
858
859pub fn to_string<T: serde::Serialize>(value: &T) -> Result<String, YamlError> {
861 let json = serde_json::to_value(value).map_err(|e| YamlError::new(e.to_string(), None))?;
862 Ok(emit(&json))
863}
864
865fn plain_safe(s: &str) -> bool {
870 let mut chars = s.chars();
871 match chars.next() {
872 Some(c) if c.is_ascii_alphanumeric() || c == '_' => {}
873 _ => return false,
874 }
875 s.chars().all(|c| {
876 c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '/' | '@' | ' ')
877 })
878}
879
880fn needs_quoting(s: &str) -> bool {
881 if s.is_empty() || !plain_safe(s) || s != s.trim() {
882 return true;
883 }
884 match resolve_scalar(s) {
885 Ok(Value::String(_)) => {}
886 _ => return true,
887 }
888 let body = s.strip_prefix('-').or_else(|| s.strip_prefix('+')).unwrap_or(s);
891 if !body.is_empty() && body.bytes().all(|b| b.is_ascii_digit() || b == b'_') && body.bytes().next().unwrap().is_ascii_digit() {
892 return true;
893 }
894 if is_float_syntax(s) {
895 return true;
896 }
897 false
898}
899
900fn quote(s: &str) -> String {
901 serde_json::to_string(s).expect("string serializes")
902}
903
904fn format_scalar(v: &Value) -> String {
905 match v {
906 Value::Null => "null".to_string(),
907 Value::Bool(true) => "true".to_string(),
908 Value::Bool(false) => "false".to_string(),
909 Value::Number(n) => n.to_string(),
910 Value::String(s) => {
911 if needs_quoting(s) {
912 quote(s)
913 } else {
914 s.clone()
915 }
916 }
917 _ => unreachable!("caller checked scalar"),
918 }
919}
920
921fn is_scalar(v: &Value) -> bool {
922 !matches!(v, Value::Array(_) | Value::Object(_))
923}
924
925fn format_key(k: &str) -> String {
926 if needs_quoting(k) || k.contains(':') || k.contains('#') {
927 quote(k)
928 } else {
929 k.to_string()
930 }
931}
932
933fn format_multiline(v: &Value, indent: usize) -> String {
935 if let Value::String(s) = v {
936 let ok = s.contains('\n')
937 && !s.starts_with(char::is_whitespace)
938 && !s.ends_with(char::is_whitespace)
939 && !s.contains("\n\n\n");
940 if ok {
941 let pad = " ".repeat(indent);
942 let body: Vec<String> = s
943 .split('\n')
944 .map(|l| {
945 if l.is_empty() {
946 String::new()
947 } else {
948 format!("{pad}{l}")
949 }
950 })
951 .collect();
952 return format!("|-\n{}", body.join("\n"));
953 }
954 }
955 format_scalar(v)
956}
957
958fn emit_entry(prefix: &str, key: &str, val: &Value, child_indent: usize, out: &mut Vec<String>) {
959 if is_scalar(val) {
960 out.push(format!(
961 "{prefix}{}: {}",
962 format_key(key),
963 format_multiline(val, child_indent)
964 ));
965 } else {
966 out.push(format!("{prefix}{}:", format_key(key)));
967 emit_node(val, child_indent, out);
968 }
969}
970
971fn emit_node(v: &Value, indent: usize, out: &mut Vec<String>) {
972 let pad = " ".repeat(indent);
973 match v {
974 Value::Array(items) => {
975 if items.is_empty() {
976 let last = out.last_mut().expect("array attaches to a line");
977 last.push_str(" []");
978 return;
979 }
980 for item in items {
981 if is_scalar(item) {
982 out.push(format!("{pad}- {}", format_scalar(item)));
983 } else if let Value::Array(_) = item {
984 out.push(format!("{pad}-"));
985 emit_node(item, indent + 1, out);
986 } else if let Value::Object(map) = item {
987 if map.is_empty() {
988 out.push(format!("{pad}- {{}}"));
989 continue;
990 }
991 let mut first = true;
992 for (k, val) in map {
993 let prefix = if first {
994 format!("{pad}- ")
995 } else {
996 format!("{pad} ")
997 };
998 first = false;
999 emit_entry(&prefix, k, val, indent + 2, out);
1000 }
1001 }
1002 }
1003 }
1004 Value::Object(map) => {
1005 if map.is_empty() {
1006 let last = out.last_mut().expect("map attaches to a line");
1007 last.push_str(" {}");
1008 return;
1009 }
1010 for (k, val) in map {
1011 emit_entry(&pad, k, val, indent + 1, out);
1012 }
1013 }
1014 _ => unreachable!("caller checked collection"),
1015 }
1016}
1017
1018fn emit(v: &Value) -> String {
1019 if is_scalar(v) {
1020 return format!("{}\n", format_scalar(v));
1021 }
1022 match v {
1024 Value::Array(items) if items.is_empty() => return "[]\n".to_string(),
1025 Value::Object(map) if map.is_empty() => return "{}\n".to_string(),
1026 _ => {}
1027 }
1028 let mut out = Vec::new();
1029 emit_node(v, 0, &mut out);
1030 format!("{}\n", out.join("\n"))
1031}
1032
1033#[cfg(test)]
1034mod tests {
1035 use super::*;
1036 use serde_json::json;
1037
1038 #[test]
1039 fn scalars_resolve_like_yaml12_core() {
1040 assert_eq!(parse("x: null").unwrap(), json!({"x": null}));
1041 assert_eq!(parse("x: ~").unwrap(), json!({"x": null}));
1042 assert_eq!(parse("x:").unwrap(), json!({"x": null}));
1043 assert_eq!(parse("x: true").unwrap(), json!({"x": true}));
1044 assert_eq!(parse("x: False").unwrap(), json!({"x": false}));
1045 assert_eq!(parse("x: 42").unwrap(), json!({"x": 42}));
1046 assert_eq!(parse("x: -7").unwrap(), json!({"x": -7}));
1047 assert_eq!(parse("x: 0x1f").unwrap(), json!({"x": 31}));
1048 assert_eq!(parse("x: 3.5").unwrap(), json!({"x": 3.5}));
1049 assert_eq!(parse("x: 1e3").unwrap(), json!({"x": 1000.0}));
1050 assert_eq!(parse("x: yes").unwrap(), json!({"x": "yes"})); assert_eq!(parse("x: on").unwrap(), json!({"x": "on"}));
1052 assert_eq!(
1054 parse("x: 070000004041424344454647").unwrap(),
1055 json!({"x": "070000004041424344454647"})
1056 );
1057 }
1058
1059 #[test]
1060 fn block_structures() {
1061 let doc = "top:\n list:\n - a\n - name: n1\n value: 1\n - - nested\n map:\n k: v\n";
1062 assert_eq!(
1063 parse(doc).unwrap(),
1064 json!({"top": {"list": ["a", {"name": "n1", "value": 1}, ["nested"]], "map": {"k": "v"}}})
1065 );
1066 }
1067
1068 #[test]
1069 fn flow_structures() {
1070 assert_eq!(
1071 parse("x: [1, two, {k: v}, [3]]").unwrap(),
1072 json!({"x": [1, "two", {"k": "v"}, [3]]})
1073 );
1074 assert_eq!(parse("x: {a: 1, b: [2]}").unwrap(), json!({"x": {"a": 1, "b": [2]}}));
1075 assert_eq!(
1077 parse("x: [1,\n 2,\n 3]").unwrap(),
1078 json!({"x": [1, 2, 3]})
1079 );
1080 }
1081
1082 #[test]
1083 fn quoted_scalars() {
1084 assert_eq!(parse("x: 'it''s'").unwrap(), json!({"x": "it's"}));
1085 assert_eq!(parse("x: \"a\\nb\"").unwrap(), json!({"x": "a\nb"}));
1086 assert_eq!(parse("x: \"42\"").unwrap(), json!({"x": "42"}));
1087 assert_eq!(parse("\"a: b\": 1").unwrap(), json!({"a: b": 1}));
1088 }
1089
1090 #[test]
1091 fn block_scalars() {
1092 assert_eq!(
1093 parse("x: |\n line1\n line2\ny: 1").unwrap(),
1094 json!({"x": "line1\nline2\n", "y": 1})
1095 );
1096 assert_eq!(
1097 parse("x: |-\n line1\n line2").unwrap(),
1098 json!({"x": "line1\nline2"})
1099 );
1100 assert_eq!(
1101 parse("x: >-\n fold\n ed\n\n para").unwrap(),
1102 json!({"x": "fold ed\npara"})
1103 );
1104 assert_eq!(
1106 parse("x: |\n key: value\n - item\ny: 2").unwrap(),
1107 json!({"x": "key: value\n- item\n", "y": 2})
1108 );
1109 }
1110
1111 #[test]
1112 fn comments_and_blanks() {
1113 let doc = "# header\nx: 1 # trailing\n\ny: \"# not a comment\"\nz: a#b\n";
1114 assert_eq!(
1115 parse(doc).unwrap(),
1116 json!({"x": 1, "y": "# not a comment", "z": "a#b"})
1117 );
1118 }
1119
1120 #[test]
1121 fn subset_violations_rejected() {
1122 assert!(parse("x: &a 1").is_err());
1123 assert!(parse("x: *a").is_err());
1124 assert!(parse("x: !!str 1").is_err());
1125 assert!(parse("a: 1\n---\nb: 2").is_err());
1126 assert!(parse("x: 1\nx: 2").is_err());
1127 }
1128
1129 #[test]
1130 fn emitter_round_trips() {
1131 let doc = json!({
1132 "name": "test",
1133 "count": 42,
1134 "pi": 3.5,
1135 "flag": true,
1136 "nothing": null,
1137 "digits": "0123456789012345678",
1138 "multiline": "first\nsecond",
1139 "list": [1, "two", {"k": "v", "nested": {"deep": [1, 2]}}],
1140 "looks_like_bool": "true",
1141 "empty_list": [],
1142 "empty_map": {},
1143 "weird key: yes": "value",
1144 });
1145 let text = to_string(&doc).unwrap();
1146 assert_eq!(parse(&text).unwrap(), doc, "emitted:\n{text}");
1147 }
1148
1149 #[test]
1150 fn typed_from_str() {
1151 #[derive(serde::Deserialize, PartialEq, Debug)]
1152 struct T {
1153 name: String,
1154 values: Vec<i64>,
1155 }
1156 let t: T = from_str("name: x\nvalues: [1, 2]\n").unwrap();
1157 assert_eq!(
1158 t,
1159 T {
1160 name: "x".into(),
1161 values: vec![1, 2]
1162 }
1163 );
1164 }
1165}