1use crate::Document;
24use crate::error::{IncludeError, Result};
25use crate::node::{Node, NodeMap};
26use crate::options::EntryOptions;
27use crate::patch::PatchOptions;
28
29const JS_TAG: &str = "tag:yaml.org,2002:js";
31
32fn core_tag(tag: &str) -> Option<&'static str> {
34 const CORE: [&str; 5] = ["bool", "int", "float", "null", "str"];
35 let local = tag.strip_prefix("tag:yaml.org,2002:")?;
36 CORE.into_iter().find(|candidate| *candidate == local)
37}
38
39#[derive(Debug)]
41struct DialectError {
42 message: String,
43}
44
45impl std::fmt::Display for DialectError {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.write_str(&self.message)
48 }
49}
50
51impl std::error::Error for DialectError {}
52
53fn yaml_error(message: impl Into<String>) -> IncludeError {
54 IncludeError::Parse {
55 format: "yaml",
56 source: Box::new(DialectError {
57 message: message.into(),
58 }),
59 }
60}
61
62mod sys {
67 #![allow(unsafe_code)]
68 #[allow(clippy::unsafe_removed_from_name)]
75 use unsafe_libyaml as sys;
76
77 pub(super) struct Event {
79 pub kind: EventKind,
80 pub anchor: Option<String>,
81 pub tag: Option<String>,
82 pub value: String,
83 pub style: ScalarStyle,
84 pub line: u64,
86 pub column: u64,
87 }
88
89 pub(super) enum EventKind {
90 StreamStart,
91 StreamEnd,
92 DocumentStart,
93 DocumentEnd,
94 Alias,
95 Scalar,
96 SequenceStart,
97 SequenceEnd,
98 MappingStart,
99 MappingEnd,
100 }
101
102 pub(super) enum ScalarStyle {
103 Plain,
104 Quoted,
105 }
106
107 pub(super) struct Error {
109 pub problem: String,
110 pub line: u64,
111 pub column: u64,
112 }
113
114 pub(super) struct Parser {
115 raw: std::boxed::Box<std::mem::MaybeUninit<sys::yaml_parser_t>>,
116 }
117
118 impl Parser {
119 pub(super) fn new(input: &[u8]) -> Parser {
120 let mut raw = std::boxed::Box::new(std::mem::MaybeUninit::uninit());
121 unsafe {
125 let parser = raw.as_mut_ptr();
126 if sys::yaml_parser_initialize(parser).fail {
127 panic!("libyaml parser allocation failed");
128 }
129 sys::yaml_parser_set_encoding(parser, sys::YAML_UTF8_ENCODING);
130 sys::yaml_parser_set_input_string(parser, input.as_ptr(), input.len() as u64);
131 }
132 Parser { raw }
133 }
134
135 pub(super) fn next(&mut self) -> std::result::Result<Event, Error> {
136 let mut event = std::mem::MaybeUninit::<sys::yaml_event_t>::uninit();
137 unsafe {
142 let parser = self.raw.as_mut_ptr();
143 if sys::yaml_parser_parse(parser, event.as_mut_ptr()).fail {
144 return Err(Self::error(parser));
145 }
146 let converted = convert_event(&*event.as_ptr());
147 sys::yaml_event_delete(event.as_mut_ptr());
148 Ok(converted)
149 }
150 }
151
152 unsafe fn error(parser: *mut sys::yaml_parser_t) -> Error {
158 unsafe {
159 let parser: &sys::yaml_parser_t = &*parser;
160 let problem = cstring(std::ptr::addr_of!(parser.problem).cast())
161 .unwrap_or_else(|| "libyaml parser failed".to_owned());
162 let mark = std::ptr::addr_of!(parser.problem_mark).read();
163 Error {
164 problem,
165 line: mark.line,
166 column: mark.column,
167 }
168 }
169 }
170 }
171
172 impl Drop for Parser {
173 fn drop(&mut self) {
174 unsafe {
179 sys::yaml_parser_delete(self.raw.as_mut_ptr());
180 }
181 }
182 }
183
184 unsafe fn cstring(ptr: *const u8) -> Option<String> {
186 unsafe {
187 if ptr.is_null() {
188 return None;
189 }
190 let mut length = 0usize;
191 while *ptr.add(length) != 0 {
192 length += 1;
193 }
194 let bytes = std::slice::from_raw_parts(ptr, length);
195 Some(String::from_utf8_lossy(bytes).into_owned())
196 }
197 }
198
199 unsafe fn convert_event(event: &sys::yaml_event_t) -> Event {
202 let mark = event.start_mark;
203 let base = |kind| Event {
204 kind,
205 anchor: None,
206 tag: None,
207 value: String::new(),
208 style: ScalarStyle::Plain,
209 line: mark.line,
210 column: mark.column,
211 };
212 unsafe {
215 match event.type_ {
216 sys::YAML_STREAM_START_EVENT => base(EventKind::StreamStart),
217 sys::YAML_STREAM_END_EVENT => base(EventKind::StreamEnd),
218 sys::YAML_DOCUMENT_START_EVENT => base(EventKind::DocumentStart),
219 sys::YAML_DOCUMENT_END_EVENT => base(EventKind::DocumentEnd),
220 sys::YAML_ALIAS_EVENT => Event {
221 anchor: cstring(event.data.alias.anchor),
222 ..base(EventKind::Alias)
223 },
224 sys::YAML_SCALAR_EVENT => {
225 let length = event.data.scalar.length as usize;
226 let bytes = if event.data.scalar.value.is_null() {
227 &[][..]
228 } else {
229 std::slice::from_raw_parts(event.data.scalar.value, length)
230 };
231 Event {
232 kind: EventKind::Scalar,
233 anchor: cstring(event.data.scalar.anchor),
234 tag: cstring(event.data.scalar.tag),
235 value: String::from_utf8_lossy(bytes).into_owned(),
236 style: match event.data.scalar.style {
237 sys::YAML_PLAIN_SCALAR_STYLE => ScalarStyle::Plain,
238 _ => ScalarStyle::Quoted,
239 },
240 line: mark.line,
241 column: mark.column,
242 }
243 }
244 sys::YAML_SEQUENCE_START_EVENT => Event {
245 anchor: cstring(event.data.sequence_start.anchor),
246 tag: cstring(event.data.sequence_start.tag),
247 ..base(EventKind::SequenceStart)
248 },
249 sys::YAML_SEQUENCE_END_EVENT => base(EventKind::SequenceEnd),
250 sys::YAML_MAPPING_START_EVENT => Event {
251 anchor: cstring(event.data.mapping_start.anchor),
252 tag: cstring(event.data.mapping_start.tag),
253 ..base(EventKind::MappingStart)
254 },
255 sys::YAML_MAPPING_END_EVENT => base(EventKind::MappingEnd),
256 _ => unreachable!("libyaml produced an event outside the parser state machine"),
257 }
258 }
259 }
260}
261
262pub fn parse_node(source: &str) -> Result<Node> {
272 let mut parser = sys::Parser::new(source.as_bytes());
273 let mut anchors: Vec<(String, Node)> = Vec::new();
274 let mut stack: Vec<Frame> = Vec::new();
275 let mut root: Option<Node> = None;
276 let mut documents = 0usize;
277
278 loop {
279 let event = parser.next().map_err(|error| {
280 yaml_error(format!(
281 "{} at line {} column {}",
282 error.problem,
283 error.line + 1,
284 error.column + 1
285 ))
286 })?;
287 match event.kind {
288 sys::EventKind::StreamStart | sys::EventKind::DocumentEnd => {}
289 sys::EventKind::DocumentStart => {
290 documents += 1;
291 if documents > 1 {
292 return Err(yaml_error(
293 "deserializing from YAML containing more than one document is not supported",
294 ));
295 }
296 }
297 sys::EventKind::StreamEnd => break,
298 sys::EventKind::Scalar => {
299 if let Some(Frame::Map {
302 key: slot @ None, ..
303 }) = stack.last_mut()
304 {
305 *slot = Some(event.value.clone());
306 } else {
307 let node = resolve_scalar(&event)?;
308 register_anchor(&mut anchors, &event.anchor, &node);
309 feed(&mut stack, &mut root, node)?;
310 }
311 }
312 sys::EventKind::Alias => {
313 let Some(anchor) = &event.anchor else {
314 return Err(yaml_error("alias without an anchor"));
315 };
316 let Some(node) = anchors
317 .iter()
318 .rev()
319 .find(|(name, _)| name == anchor)
320 .map(|(_, node)| node.clone())
321 else {
322 return Err(yaml_error(format!(
323 "unknown anchor {anchor:?} at line {} column {}",
324 event.line + 1,
325 event.column + 1
326 )));
327 };
328 feed(&mut stack, &mut root, node)?;
329 }
330 sys::EventKind::SequenceStart => {
331 reject_local_tag(&event)?;
332 stack.push(Frame::Seq {
333 items: Vec::new(),
334 anchor: event.anchor,
335 });
336 }
337 sys::EventKind::SequenceEnd => {
338 let Some(Frame::Seq { items, anchor }) = stack.pop() else {
339 return Err(yaml_error("unbalanced sequence end"));
340 };
341 let node = Node::Array(items);
342 register_anchor(&mut anchors, &anchor, &node);
343 feed(&mut stack, &mut root, node)?;
344 }
345 sys::EventKind::MappingStart => {
346 reject_local_tag(&event)?;
347 stack.push(Frame::Map {
348 map: NodeMap::new(),
349 key: None,
350 anchor: event.anchor,
351 });
352 }
353 sys::EventKind::MappingEnd => {
354 let Some(Frame::Map {
355 map,
356 key: None,
357 anchor,
358 }) = stack.pop()
359 else {
360 return Err(yaml_error("unbalanced mapping end"));
361 };
362 let node = Node::Object(map);
363 register_anchor(&mut anchors, &anchor, &node);
364 feed(&mut stack, &mut root, node)?;
365 }
366 }
367 }
368 Ok(root.unwrap_or(Node::Null))
369}
370
371enum Frame {
373 Seq {
374 items: Vec<Node>,
375 anchor: Option<String>,
376 },
377 Map {
378 map: NodeMap,
379 key: Option<String>,
380 anchor: Option<String>,
381 },
382}
383
384fn register_anchor(anchors: &mut Vec<(String, Node)>, anchor: &Option<String>, node: &Node) {
385 if let Some(anchor) = anchor {
386 anchors.push((anchor.clone(), node.clone()));
387 }
388}
389
390fn feed(stack: &mut [Frame], root: &mut Option<Node>, node: Node) -> Result<()> {
392 match stack.last_mut() {
393 None => {
394 if root.is_some() {
395 return Err(yaml_error("multiple root values in one document"));
396 }
397 *root = Some(node);
398 }
399 Some(Frame::Seq { items, .. }) => items.push(node),
400 Some(Frame::Map { map, key, .. }) => match key.take() {
401 None => {
402 return Err(yaml_error(format!(
403 "mapping keys must be scalars, found {}",
404 node_kind(&node)
405 )));
406 }
407 Some(name) => {
408 map.insert(name, node);
409 }
410 },
411 }
412 Ok(())
413}
414
415fn reject_local_tag(event: &sys::Event) -> Result<()> {
417 if event.tag.as_deref().is_some_and(|tag| tag.starts_with('!')) {
418 return Err(yaml_error(format!(
419 "local tags are not supported: {}",
420 event.tag.as_deref().unwrap_or_default()
421 )));
422 }
423 Ok(())
424}
425
426fn resolve_scalar(event: &sys::Event) -> Result<Node> {
429 let value = &event.value;
430 if let Some(tag) = &event.tag {
431 if tag == JS_TAG {
432 return Ok(Node::Expr(value.clone()));
433 }
434 if let Some(core) = core_tag(tag) {
435 return match core {
436 "bool" => parse_bool(value)
437 .map(Node::Bool)
438 .ok_or_else(|| yaml_error(format!("invalid boolean {value:?}"))),
439 "int" => {
440 try_int(value)?.ok_or_else(|| yaml_error(format!("invalid integer {value:?}")))
441 }
442 "float" => parse_f64(value)
443 .map(Node::Float)
444 .ok_or_else(|| yaml_error(format!("invalid float {value:?}"))),
445 "null" => parse_null(value)
446 .map(|()| Node::Null)
447 .ok_or_else(|| yaml_error(format!("invalid null {value:?}"))),
448 "str" => Ok(Node::String(value.clone())),
449 _ => unreachable!("core_tag filters its output"),
450 };
451 }
452 if tag.starts_with('!') {
453 return Err(yaml_error(format!("local tags are not supported: {tag}")));
456 }
457 return Ok(Node::String(value.clone()));
460 }
461 if matches!(event.style, sys::ScalarStyle::Plain) {
462 resolve_plain(value)
463 } else {
464 Ok(Node::String(value.clone()))
465 }
466}
467
468fn resolve_plain(value: &str) -> Result<Node> {
472 if value.is_empty() || parse_null(value).is_some() {
473 return Ok(Node::Null);
474 }
475 if let Some(boolean) = parse_bool(value) {
476 return Ok(Node::Bool(boolean));
477 }
478 if let Some(node) = try_int(value)? {
479 return Ok(node);
480 }
481 if !digits_but_not_number(value) {
482 if let Some(float) = parse_f64(value) {
483 return Ok(Node::Float(float));
484 }
485 }
486 Ok(Node::String(value.to_owned()))
487}
488
489fn small_uint(value: u64) -> Node {
492 if value <= i64::MAX as u64 {
493 Node::Int(value as i64)
494 } else {
495 Node::UInt(value)
496 }
497}
498
499fn digits_but_not_number(scalar: &str) -> bool {
501 let scalar = scalar.strip_prefix(['-', '+']).unwrap_or(scalar);
502 scalar.len() > 1 && scalar.starts_with('0') && scalar[1..].bytes().all(|b| b.is_ascii_digit())
503}
504
505fn parse_unsigned(scalar: &str, radix_skip: fn(&str, u32) -> Option<u64>) -> Option<u64> {
508 let unpositive = scalar.strip_prefix('+').unwrap_or(scalar);
509 if let Some(rest) = unpositive.strip_prefix("0x") {
510 if !rest.starts_with(['+', '-']) {
511 if let Some(int) = radix_skip(rest, 16) {
512 return Some(int);
513 }
514 }
515 }
516 if let Some(rest) = unpositive.strip_prefix("0o") {
517 if !rest.starts_with(['+', '-']) {
518 if let Some(int) = radix_skip(rest, 8) {
519 return Some(int);
520 }
521 }
522 }
523 if let Some(rest) = unpositive.strip_prefix("0b") {
524 if !rest.starts_with(['+', '-']) {
525 if let Some(int) = radix_skip(rest, 2) {
526 return Some(int);
527 }
528 }
529 }
530 if unpositive.starts_with(['+', '-']) {
531 return None;
532 }
533 if digits_but_not_number(scalar) {
534 return None;
535 }
536 radix_skip(unpositive, 10)
537}
538
539fn parse_negative(scalar: &str, radix_skip: fn(&str, u32) -> Option<i64>) -> Option<i64> {
543 for prefix in ["-0x", "-0o", "-0b"] {
544 if let Some(rest) = scalar.strip_prefix(prefix) {
545 let radix = match prefix {
546 "-0x" => 16,
547 "-0o" => 8,
548 _ => 2,
549 };
550 if let Some(int) = radix_skip(rest, radix) {
551 return Some(-int);
552 }
553 }
554 }
555 if digits_but_not_number(scalar) {
556 return None;
557 }
558 radix_skip(scalar, 10)
559}
560
561fn u64_radix(text: &str, radix: u32) -> Option<u64> {
562 u64::from_str_radix(text, radix).ok()
563}
564
565fn i64_radix(text: &str, radix: u32) -> Option<i64> {
566 i64::from_str_radix(text, radix).ok()
567}
568
569fn try_int(scalar: &str) -> Result<Option<Node>> {
573 if let Some(unsigned) = parse_unsigned(scalar, u64_radix) {
574 return Ok(Some(small_uint(unsigned)));
575 }
576 if let Some(signed) = parse_negative(scalar, i64_radix) {
577 return Ok(Some(Node::Int(signed)));
578 }
579 if parse_unsigned(scalar, |text, radix| {
581 u128::from_str_radix(text, radix).ok().map(|_| 0)
582 })
583 .is_some()
584 || parse_negative(scalar, |text, radix| {
585 i128::from_str_radix(text, radix).ok().map(|_| 0)
586 })
587 .is_some()
588 {
589 return Err(yaml_error(format!("integer out of range: {scalar:?}")));
590 }
591 Ok(None)
592}
593
594fn parse_null(scalar: &str) -> Option<()> {
595 match scalar {
596 "null" | "Null" | "NULL" | "~" => Some(()),
597 _ => None,
598 }
599}
600
601fn parse_bool(scalar: &str) -> Option<bool> {
602 match scalar {
603 "true" | "True" | "TRUE" => Some(true),
604 "false" | "False" | "FALSE" => Some(false),
605 _ => None,
606 }
607}
608
609fn parse_f64(scalar: &str) -> Option<f64> {
610 let unpositive = if let Some(unpositive) = scalar.strip_prefix('+') {
611 if unpositive.starts_with(['+', '-']) {
612 return None;
613 }
614 unpositive
615 } else {
616 scalar
617 };
618 if let ".inf" | ".Inf" | ".INF" = unpositive {
619 return Some(f64::INFINITY);
620 }
621 if let "-.inf" | "-.Inf" | "-.INF" = scalar {
622 return Some(f64::NEG_INFINITY);
623 }
624 if let ".nan" | ".NaN" | ".NAN" = scalar {
625 return Some(f64::NAN.copysign(1.0));
626 }
627 if let Ok(float) = unpositive.parse::<f64>() {
628 if float.is_finite() {
629 return Some(float);
630 }
631 }
632 None
633}
634
635fn node_kind(node: &Node) -> &'static str {
637 match node {
638 Node::Null => "null",
639 Node::Bool(_) => "a boolean",
640 Node::Int(_) | Node::UInt(_) => "an integer",
641 Node::Float(_) => "a float",
642 Node::String(_) => "a string",
643 Node::Expr(_) => "a !!js expression",
644 Node::Array(_) => "a sequence",
645 Node::Object(_) => "a mapping",
646 }
647}
648
649pub fn parse_document(source: &str) -> Result<Document> {
654 document_from_node(parse_node(source)?)
655}
656
657pub fn parse_entry_list(source: &str) -> Result<Vec<EntryOptions>> {
660 entry_list_from_node(parse_node(source)?)
661}
662
663pub fn document_from_node(node: Node) -> Result<Document> {
665 let Node::Object(map) = node else {
666 return Err(yaml_error(format!(
667 "expected a mapping with an `entries` list, found {}",
668 node_kind(&node)
669 )));
670 };
671 let mut document = Document::default();
672 for (key, value) in map {
673 if key == "entries" {
674 document.entries = match value {
677 Node::Null => Vec::new(),
678 other => {
679 entry_list_from_node(other).map_err(|error| prepend(error, "entries: "))?
680 }
681 };
682 } else {
683 document.extra.insert(key, value);
684 }
685 }
686 Ok(document)
687}
688
689pub fn entry_list_from_node(node: Node) -> Result<Vec<EntryOptions>> {
691 let Node::Array(items) = node else {
692 return Err(yaml_error(format!(
693 "expected a sequence of entries, found {}",
694 node_kind(&node)
695 )));
696 };
697 items
698 .into_iter()
699 .enumerate()
700 .map(|(index, node)| {
701 entry_from_node(node).map_err(|error| prepend(error, &format!("entry {}: ", index + 1)))
702 })
703 .collect()
704}
705
706pub fn patch_list_from_node(node: Node) -> Result<Vec<PatchOptions>> {
708 let Node::Array(items) = node else {
709 return Err(yaml_error(format!(
710 "expected a sequence of patch entries, found {}",
711 node_kind(&node)
712 )));
713 };
714 items
715 .into_iter()
716 .enumerate()
717 .map(|(index, node)| {
718 patch_from_node(node).map_err(|error| prepend(error, &format!("patch {}: ", index + 1)))
719 })
720 .collect()
721}
722
723fn prepend(error: IncludeError, context: &str) -> IncludeError {
724 let message = match &error {
725 IncludeError::Parse { source, .. } => source.to_string(),
726 other => other.to_string(),
727 };
728 yaml_error(format!("{context}{message}"))
729}
730
731fn entry_from_node(node: Node) -> Result<EntryOptions> {
733 let Node::Object(map) = node else {
734 return Err(yaml_error(format!(
735 "expected a mapping (an entry), found {}",
736 node_kind(&node)
737 )));
738 };
739 let mut entry = EntryOptions::default();
740 for (key, value) in map {
741 match key.as_str() {
742 "id" => entry.id = optional_string(value, "id")?,
743 "name" => {
744 entry.name = match value {
745 Node::Null => String::new(),
747 other => string_value(other, "name")?,
748 };
749 }
750 "disabled" => {
751 entry.disabled = match value {
752 Node::Bool(flag) => flag,
753 Node::Expr(_) => {
754 return Err(yaml_error(
755 "disabled: !!js expressions are not supported yet",
756 ));
757 }
758 other => {
759 return Err(yaml_error(format!(
760 "disabled: expected a boolean, found {}",
761 node_kind(&other)
762 )));
763 }
764 };
765 }
766 "inject" => {
768 entry.inject = match value {
769 Node::Null => Vec::new(),
770 other => string_list(other, "inject")?,
771 };
772 }
773 "group" => {
774 entry.group = match value {
775 Node::Null => Vec::new(),
776 other => {
777 entry_list_from_node(other).map_err(|error| prepend(error, "group: "))?
778 }
779 };
780 }
781 "config" => entry.config = optional_config(value),
782 _ => {}
784 }
785 }
786 Ok(entry)
787}
788
789pub(crate) fn patch_from_node(node: Node) -> Result<PatchOptions> {
792 let Node::Object(map) = node else {
793 return Err(yaml_error(format!(
794 "expected a mapping (a loader patch entry), found {}",
795 node_kind(&node)
796 )));
797 };
798 let mut patch = PatchOptions::default();
799 for (key, value) in map {
800 match key.as_str() {
801 "id" => patch.id = optional_string(value, "id")?,
802 "insert" => {
803 patch.insert = match value {
804 Node::Null => None,
805 Node::Array(_) => Some(
806 entry_list_from_node(value).map_err(|error| prepend(error, "insert: "))?,
807 ),
808 other => {
809 return Err(yaml_error(format!(
810 "insert: expected a sequence of entries, found {}",
811 node_kind(&other)
812 )));
813 }
814 };
815 }
816 "name" => patch.name = optional_string(value, "name")?,
817 "config" => patch.config = optional_config(value),
818 "disabled" => {
819 patch.disabled = match value {
820 Node::Null => None,
821 Node::Bool(flag) => Some(flag),
822 Node::Expr(_) => {
823 return Err(yaml_error(
824 "disabled: !!js expressions are not supported yet",
825 ));
826 }
827 other => {
828 return Err(yaml_error(format!(
829 "disabled: expected a boolean, found {}",
830 node_kind(&other)
831 )));
832 }
833 };
834 }
835 "inject" => {
836 patch.inject = match value {
837 Node::Null => None,
838 Node::Array(_) => Some(string_list(value, "inject")?),
839 other => {
840 return Err(yaml_error(format!(
841 "inject: expected a list of strings, found {}",
842 node_kind(&other)
843 )));
844 }
845 };
846 }
847 other => {
848 patch.extra.insert(other.to_owned(), value);
849 }
850 }
851 }
852 Ok(patch)
853}
854
855fn optional_string(node: Node, field: &str) -> Result<Option<String>> {
856 match node {
857 Node::Null => Ok(None),
858 Node::String(value) => Ok(Some(value)),
859 other => Err(yaml_error(format!(
860 "{field}: expected a string, found {}",
861 node_kind(&other)
862 ))),
863 }
864}
865
866fn string_value(node: Node, field: &str) -> Result<String> {
867 match node {
868 Node::String(value) => Ok(value),
869 other => Err(yaml_error(format!(
870 "{field}: expected a string, found {}",
871 node_kind(&other)
872 ))),
873 }
874}
875
876fn string_list(node: Node, field: &str) -> Result<Vec<String>> {
877 let Node::Array(items) = node else {
878 return Err(yaml_error(format!(
879 "{field}: expected a list of strings, found {}",
880 node_kind(&node)
881 )));
882 };
883 items
884 .into_iter()
885 .enumerate()
886 .map(|(index, item)| {
887 string_value(item, &format!("{field}[{}]", index))
888 .map_err(|error| prepend(error, &format!("{field}: entry {}: ", index + 1)))
889 })
890 .collect()
891}
892
893fn optional_config(node: Node) -> Option<Node> {
895 match node {
896 Node::Null => None,
897 other => Some(other),
898 }
899}
900
901pub fn emit_document(document: &Document) -> String {
906 let mut map = NodeMap::new();
907 if !document.entries.is_empty() {
908 map.insert("entries".to_owned(), entries_to_node(&document.entries));
909 }
910 for (key, value) in &document.extra {
911 map.insert(key.clone(), value.clone());
912 }
913 let mut out = String::new();
914 if map.is_empty() {
915 out.push_str("{}\n");
916 } else {
917 emit_map(&map, 0, "", &mut out);
918 }
919 out
920}
921
922pub fn emit_entry_list(entries: &[EntryOptions]) -> String {
924 let mut out = String::new();
925 if entries.is_empty() {
926 out.push_str("[]\n");
927 } else {
928 emit_node(&entries_to_node(entries), 0, "", &mut out);
929 }
930 out
931}
932
933fn entries_to_node(entries: &[EntryOptions]) -> Node {
937 Node::Array(entries.iter().map(entry_to_node).collect())
938}
939
940fn entry_to_node(entry: &EntryOptions) -> Node {
941 let mut map = NodeMap::new();
942 if let Some(id) = &entry.id {
943 map.insert("id".to_owned(), Node::String(id.clone()));
944 }
945 map.insert("name".to_owned(), Node::String(entry.name.clone()));
946 if entry.disabled {
947 map.insert("disabled".to_owned(), Node::Bool(true));
948 }
949 if !entry.inject.is_empty() {
950 map.insert(
951 "inject".to_owned(),
952 Node::Array(
953 entry
954 .inject
955 .iter()
956 .map(|name| Node::String(name.clone()))
957 .collect(),
958 ),
959 );
960 }
961 if !entry.group.is_empty() {
962 map.insert("group".to_owned(), entries_to_node(&entry.group));
963 }
964 if let Some(config) = &entry.config {
965 map.insert("config".to_owned(), config.clone());
966 }
967 Node::Object(map)
968}
969
970fn spaces(indent: usize) -> String {
971 " ".repeat(indent)
972}
973
974fn emit_node(node: &Node, indent: usize, first_lead: &str, out: &mut String) {
977 match node {
978 Node::Array(items) => emit_seq(items, indent, first_lead, out),
979 Node::Object(map) => emit_map(map, indent, first_lead, out),
980 scalar => {
981 out.push_str(first_lead);
982 out.push_str(&scalar_text(scalar));
983 out.push('\n');
984 }
985 }
986}
987
988fn emit_map(map: &NodeMap, indent: usize, first_lead: &str, out: &mut String) {
989 let indented = spaces(indent);
990 for (position, (key, value)) in map.iter().enumerate() {
991 let lead: &str = if position == 0 { first_lead } else { &indented };
992 let key_text = scalar_text(&Node::String(key.clone()));
993 match value {
994 Node::Array(items) if items.is_empty() => {
995 out.push_str(&format!("{lead}{key_text}: []\n"));
996 }
997 Node::Object(inner) if inner.is_empty() => {
998 out.push_str(&format!("{lead}{key_text}: {{}}\n"));
999 }
1000 Node::Object(inner) => {
1001 out.push_str(&format!("{lead}{key_text}:\n"));
1002 emit_map(inner, indent + 2, &spaces(indent + 2), out);
1003 }
1004 Node::Array(items) => {
1007 out.push_str(&format!("{lead}{key_text}:\n"));
1008 emit_seq(items, indent, &spaces(indent), out);
1009 }
1010 scalar => {
1011 out.push_str(&format!("{lead}{key_text}: {}\n", scalar_text(scalar)));
1012 }
1013 }
1014 }
1015}
1016
1017fn emit_seq(items: &[Node], indent: usize, first_lead: &str, out: &mut String) {
1018 let indented = spaces(indent);
1019 for (position, item) in items.iter().enumerate() {
1020 let lead: &str = if position == 0 { first_lead } else { &indented };
1021 match item {
1022 Node::Object(map) if map.is_empty() => {
1023 out.push_str(&format!("{lead}- {{}}\n"));
1024 }
1025 Node::Object(map) => emit_map(map, indent + 2, &format!("{lead}- "), out),
1026 Node::Array(inner) if inner.is_empty() => {
1027 out.push_str(&format!("{lead}- []\n"));
1028 }
1029 Node::Array(inner) => {
1030 out.push_str(&format!("{lead}-\n"));
1031 emit_seq(inner, indent + 2, &spaces(indent + 2), out);
1032 }
1033 scalar => {
1034 out.push_str(&format!("{lead}- {}\n", scalar_text(scalar)));
1035 }
1036 }
1037 }
1038}
1039
1040fn scalar_text(node: &Node) -> String {
1044 match node {
1045 Node::Null => "null".to_owned(),
1046 Node::Bool(value) => value.to_string(),
1047 Node::Int(value) => value.to_string(),
1048 Node::UInt(value) => value.to_string(),
1049 Node::Float(value) => float_text(*value),
1050 Node::String(value) => quote_scalar(value),
1051 Node::Expr(value) => format!("!!js {}", quote_scalar(value)),
1052 Node::Array(_) | Node::Object(_) => unreachable!("scalars only"),
1053 }
1054}
1055
1056fn float_text(value: f64) -> String {
1059 if value.is_nan() {
1060 ".nan".to_owned()
1061 } else if value == f64::INFINITY {
1062 ".inf".to_owned()
1063 } else if value == f64::NEG_INFINITY {
1064 "-.inf".to_owned()
1065 } else {
1066 let mut buffer = ryu::Buffer::new();
1067 buffer.format_finite(value).to_owned()
1068 }
1069}
1070
1071enum Quote {
1073 Plain,
1074 Single,
1075 Double,
1076}
1077
1078fn classify(text: &str) -> Quote {
1079 if text.is_empty() {
1080 return Quote::Single;
1081 }
1082 if text.chars().any(|c| (c as u32) < 0x20 || c as u32 == 0x7f) {
1083 return Quote::Double;
1084 }
1085 let first = text.chars().next().expect("non-empty");
1086 if "#%,[]{}&*!|>'\"`@".contains(first) {
1087 return Quote::Single;
1088 }
1089 if (first == '-' || first == '?') && text.chars().nth(1).is_none_or(|next| next == ' ') {
1090 return Quote::Single;
1091 }
1092 if text == "---" || text == "..." {
1093 return Quote::Single;
1094 }
1095 if text.ends_with(':')
1096 || text.contains(": ")
1097 || text.contains(" #")
1098 || text.starts_with(' ')
1099 || text.ends_with(' ')
1100 {
1101 return Quote::Single;
1102 }
1103 if parse_null(text).is_some()
1106 || parse_bool(text).is_some()
1107 || parse_f64(text).is_some()
1108 || matches!(try_int(text), Ok(Some(_)))
1109 {
1110 return Quote::Single;
1111 }
1112 Quote::Plain
1113}
1114
1115fn quote_scalar(text: &str) -> String {
1116 match classify(text) {
1117 Quote::Plain => text.to_owned(),
1118 Quote::Single => format!("'{}'", text.replace('\'', "''")),
1119 Quote::Double => double_quote(text),
1120 }
1121}
1122
1123fn double_quote(text: &str) -> String {
1124 let mut out = String::with_capacity(text.len() + 2);
1125 out.push('"');
1126 for character in text.chars() {
1127 match character {
1128 '"' => out.push_str("\\\""),
1129 '\\' => out.push_str("\\\\"),
1130 '\n' => out.push_str("\\n"),
1131 '\r' => out.push_str("\\r"),
1132 '\t' => out.push_str("\\t"),
1133 '\0' => out.push_str("\\0"),
1134 other if (other as u32) < 0x20 || other as u32 == 0x7f => {
1135 out.push_str(&format!("\\x{:02x}", other as u32));
1136 }
1137 other => out.push(other),
1138 }
1139 }
1140 out.push('"');
1141 out
1142}
1143
1144#[cfg(test)]
1145mod tests {
1146 use super::*;
1147
1148 fn parity(source: &str) {
1151 let mine = parse_node(source).expect("dialect parse");
1152 let oracle: Node = serde_yaml_ng::from_str(source).expect("oracle parse");
1153 assert_eq!(
1154 format!("{mine:?}"),
1155 format!("{oracle:?}"),
1156 "dialect and serde disagree on {source:?}"
1157 );
1158 }
1159
1160 #[test]
1161 fn scalar_resolution_matches_the_serde_oracle() {
1162 for value in [
1163 "null",
1164 "Null",
1165 "NULL",
1166 "~",
1167 "true",
1168 "True",
1169 "TRUE",
1170 "false",
1171 "False",
1172 "FALSE",
1173 "0",
1174 "-0",
1175 "42",
1176 "+5",
1177 "-17",
1178 "9223372036854775807",
1179 "9223372036854775808",
1180 "18446744073709551615",
1181 "0x1A",
1182 "0o17",
1183 "0b101",
1184 "-0x10",
1185 "007",
1186 "1_000",
1187 "1.5",
1188 "-0.0",
1189 "1e300",
1190 ".inf",
1191 "-.inf",
1192 ".nan",
1193 "yes",
1194 "No",
1195 "text",
1196 "http://x",
1197 "a b",
1198 "${{ env.X }}",
1199 ] {
1200 parity(&format!("key: {value}\n"));
1201 parity(&format!("- {value}\n"));
1202 }
1203 for value in ["", "0", "true", "42", " x "] {
1204 parity(&format!("key: '{value}'\n"));
1205 parity(&format!("key: \"{value}\"\n"));
1206 }
1207 parity("key:\n");
1208 parity("key: |\n line1\n line2\n");
1209 parity("key: >\n folded text\n");
1210 parity("");
1211 parity("---\n");
1212 }
1213
1214 #[test]
1215 fn structural_shapes_match_the_serde_oracle() {
1216 parity("[]\n");
1217 parity("{}\n");
1218 parity("- 1\n- a\n- [1, 2]\n- {x: 1}\n");
1219 parity("a:\n b:\n c: 1\n");
1220 parity("a: &anchor 1\nb: *anchor\n");
1221 parity("base: &b\n x: 1\nover: *b\n");
1222 parity("list:\n- 1\n- 2\n");
1223 parity("- id: a\n name: n\n group:\n - id: c\n");
1224 parity("m:\n 1: value\n");
1226 }
1227
1228 #[test]
1229 fn core_tags_match_the_serde_oracle() {
1230 parity("a: !!str 5\n");
1231 parity("a: !!bool 'true'\n");
1232 parity("a: !!int 0x1f\n");
1233 parity("a: !!float 5\n");
1234 parity("a: !!null ~\n");
1235 parity("a: !!python/object 'x'\n");
1236 }
1237
1238 #[test]
1241 fn local_tags_fail_like_the_serde_path() {
1242 for source in ["a: !local 5\n", "a: !local 'x'\n"] {
1243 assert!(serde_yaml_ng::from_str::<Node>(source).is_err(), "{source}");
1244 let error = parse_node(source).unwrap_err().to_string();
1245 assert!(error.contains("local tags are not supported"), "{error}");
1246 }
1247 }
1248
1249 #[test]
1251 fn complex_mapping_keys_fail_like_the_serde_path() {
1252 let source = "? [a]\n: v\n";
1253 assert!(serde_yaml_ng::from_str::<Node>(source).is_err());
1254 let error = parse_node(source).unwrap_err().to_string();
1255 assert!(error.contains("mapping keys must be scalars"), "{error}");
1256 }
1257
1258 #[test]
1259 fn js_tag_becomes_an_expression_node() {
1260 let node = parse_node("- id: a\n config:\n model: !!js process.env.MODEL\n").unwrap();
1261 let entry = &node.as_array().unwrap()[0];
1262 let config = entry.as_object().unwrap()["config"].as_object().unwrap();
1263 assert_eq!(config["model"], Node::Expr("process.env.MODEL".to_owned()));
1264 let node = parse_node("k: !!js 'quoted expression'\n").unwrap();
1267 assert_eq!(
1268 node.as_object().unwrap()["k"],
1269 Node::Expr("quoted expression".to_owned())
1270 );
1271 }
1272
1273 #[test]
1274 fn integer_overflow_fails_like_the_serde_path() {
1275 let error = parse_node("a: 18446744073709551616\n")
1276 .unwrap_err()
1277 .to_string();
1278 assert!(error.contains("integer out of range"), "{error}");
1279 assert!(serde_yaml_ng::from_str::<Node>("a: 18446744073709551616\n").is_err());
1281 }
1282
1283 #[test]
1284 fn syntax_errors_carry_line_and_column() {
1285 let error = parse_node("a: [unclosed\n").unwrap_err().to_string();
1288 assert!(error.contains("line 2"), "{error}");
1289 let error = parse_node("valid: 1\nbroken: [x\n")
1290 .unwrap_err()
1291 .to_string();
1292 assert!(error.contains("line 3"), "{error}");
1293 }
1294
1295 #[test]
1296 fn multiple_documents_and_unknown_anchors_fail() {
1297 let error = parse_node("---\na: 1\n---\nb: 2\n")
1298 .unwrap_err()
1299 .to_string();
1300 assert!(error.contains("more than one document"), "{error}");
1301 let error = parse_node("a: *missing\n").unwrap_err().to_string();
1302 assert!(error.contains("unknown anchor"), "{error}");
1303 }
1304
1305 #[test]
1306 fn converters_reject_wrong_shapes_with_field_context() {
1307 let error = parse_document("- id: a\n").unwrap_err().to_string();
1308 assert!(
1309 error.contains("expected a mapping with an `entries` list"),
1310 "{error}"
1311 );
1312 let error = parse_document("entries: 5\n").unwrap_err().to_string();
1313 assert!(error.contains("entries:"), "{error}");
1314 let error = parse_entry_list("entries:\n - id: a\n")
1315 .unwrap_err()
1316 .to_string();
1317 assert!(error.contains("expected a sequence of entries"), "{error}");
1318 let error = parse_entry_list("- name: 5\n").unwrap_err().to_string();
1319 assert!(
1320 error.contains("entry 1: name: expected a string"),
1321 "{error}"
1322 );
1323 let error = parse_entry_list("- inject: [1]\n").unwrap_err().to_string();
1324 assert!(error.contains("inject"), "{error}");
1325 let error = parse_entry_list("- disabled: maybe\n")
1326 .unwrap_err()
1327 .to_string();
1328 assert!(error.contains("disabled: expected a boolean"), "{error}");
1329 let error = parse_entry_list("- disabled: !!js process.platform\n")
1330 .unwrap_err()
1331 .to_string();
1332 assert!(
1333 error.contains("disabled: !!js expressions are not supported yet"),
1334 "{error}"
1335 );
1336 }
1337
1338 #[test]
1339 fn converters_keep_unknown_entry_keys_dropped_and_patch_extras() {
1340 let entries = parse_entry_list("- id: a\n name: n\n mystery: value\n").unwrap();
1341 assert_eq!(entries.len(), 1);
1342 assert_eq!(entries[0].name, "n");
1343
1344 let patches = crate::yaml::patch_list_from_node(
1345 parse_node("- id: a\n intercept: db\n group: []\n").unwrap(),
1346 )
1347 .unwrap();
1348 assert_eq!(patches[0].extra.len(), 2);
1349 assert!(patches[0].extra.contains_key("intercept"));
1350 assert!(patches[0].extra.contains_key("group"));
1351
1352 let patches = crate::yaml::patch_list_from_node(
1353 parse_node("- insert: [{id: x, name: n}]\n").unwrap(),
1354 )
1355 .unwrap();
1356 assert_eq!(
1357 patches[0].insert.as_ref().unwrap()[0].id.as_deref(),
1358 Some("x")
1359 );
1360 }
1361
1362 #[test]
1363 fn document_round_trips_with_extras_in_order() {
1364 let document = parse_document(
1365 "entries:\n - id: a\n name: n\n config:\n x: 1\nmeta: kept\n",
1366 )
1367 .unwrap();
1368 assert_eq!(document.entries.len(), 1);
1369 assert_eq!(document.extra.len(), 1);
1370 let text = emit_document(&document);
1371 assert_eq!(parse_document(&text).unwrap(), document, "{text}");
1372 assert!(text.contains("meta: kept"), "{text}");
1373 }
1374
1375 #[test]
1376 fn emitter_matches_the_previous_writer_layout() {
1377 let entries = parse_entry_list(
1378 "- id: w1\n name: worker\n config: 8080\n- id: g\n name: group\n group:\n - id: c1\n name: adapter-http\n config:\n host: localhost\n empty: ''\n list:\n - 1\n - 2.5\n",
1379 )
1380 .unwrap();
1381 let text = emit_entry_list(&entries);
1382 let expected = "\
1383- id: w1
1384 name: worker
1385 config: 8080
1386- id: g
1387 name: group
1388 group:
1389 - id: c1
1390 name: adapter-http
1391 config:
1392 host: localhost
1393 empty: ''
1394 list:
1395 - 1
1396 - 2.5
1397";
1398 assert_eq!(text, expected);
1399 assert_eq!(parse_entry_list(&text).unwrap(), entries);
1400 }
1401
1402 #[test]
1403 fn emitter_quotes_and_numbers_match_the_previous_writer() {
1404 for (text, expected) in [
1405 ("x: -foo\n", "x: -foo\n"),
1406 ("x: '#hash'\n", "x: '#hash'\n"),
1407 ("x: 'trailing:'\n", "x: 'trailing:'\n"),
1408 ("x: 'true'\n", "x: 'true'\n"),
1409 ("x: '0x1A'\n", "x: '0x1A'\n"),
1410 ("x: it's\n", "x: it's\n"),
1411 ("x: qu\"ote\n", "x: qu\"ote\n"),
1412 ("x: http://x\n", "x: http://x\n"),
1413 ("x: ${{ env.X }}\n", "x: ${{ env.X }}\n"),
1414 ("x: 0.5\n", "x: 0.5\n"),
1415 ("x: -0.0\n", "x: -0.0\n"),
1416 ("x: 1e300\n", "x: 1e300\n"),
1417 ("x: 8080\n", "x: 8080\n"),
1418 ] {
1419 let node = parse_node(text).unwrap();
1420 let emitted = emit_node_test(&node);
1421 assert_eq!(emitted, expected, "input {text:?}");
1422 assert_eq!(parse_node(&emitted).unwrap(), node, "round trip {text:?}");
1423 }
1424 assert_eq!(emit_document(&Document::default()), "{}\n");
1426 let node = parse_node("a: []\nb: {}\nc: \"tab\\there\"\n").unwrap();
1429 let emitted = emit_node_test(&node);
1430 assert_eq!(emitted, "a: []\nb: {}\nc: \"tab\\there\"\n");
1431 assert_eq!(parse_node(&emitted).unwrap(), node);
1432 }
1433
1434 fn emit_node_test(node: &Node) -> String {
1435 let mut out = String::new();
1436 emit_node(node, 0, "", &mut out);
1437 out
1438 }
1439
1440 #[test]
1441 fn expressions_emit_and_round_trip() {
1442 let entries = parse_entry_list(
1443 "- id: a\n name: n\n config:\n model: !!js process.env.MODEL || 'default'\n",
1444 )
1445 .unwrap();
1446 let text = emit_entry_list(&entries);
1447 assert!(
1448 text.contains("model: !!js process.env.MODEL || 'default'\n"),
1449 "{text}"
1450 );
1451 assert_eq!(parse_entry_list(&text).unwrap(), entries);
1452 }
1453
1454 #[test]
1459 fn bundle_shaped_fixture_round_trips() {
1460 let fixture = "\
1461- id: base-sandbox
1462 name: '@dsh/base'
1463 inject: [database]
1464 config:
1465 level: !!js process.env.DSH_LOG_LEVEL || 'info'
1466 retries: 3
1467 nested:
1468 keep: true
1469- insert:
1470 - id: web-app
1471 name: '@dsh/web-app'
1472 config:
1473 theme: dark
1474 gate: !!js process.platform === 'darwin'
1475";
1476 let node = parse_node(fixture).unwrap();
1477 let emitted = {
1478 let mut out = String::new();
1479 emit_node(&node, 0, "", &mut out);
1480 out
1481 };
1482 assert_eq!(parse_node(&emitted).unwrap(), node, "{emitted}");
1483 assert!(
1484 emitted.contains("!!js process.platform === 'darwin'"),
1485 "{emitted}"
1486 );
1487
1488 let patches = crate::yaml::patch_list_from_node(parse_node(fixture).unwrap()).unwrap();
1489 assert_eq!(patches.len(), 2);
1490 assert_eq!(
1491 patches[1].insert.as_ref().unwrap()[0].id.as_deref(),
1492 Some("web-app")
1493 );
1494 }
1495
1496 #[test]
1497 fn anchors_resolve_into_shared_clones() {
1498 let node = parse_node("a: &x {v: 1}\nb: *x\n").unwrap();
1499 let map = node.as_object().unwrap();
1500 assert_eq!(map["a"], map["b"]);
1501 }
1502}