1use saphyr::{ScalarOwned, YamlOwned};
2use saphyr_parser::{ScalarStyle, Tag};
3use std::borrow::Cow;
4use std::str::FromStr;
5use tstring_syntax::{
6 BackendError, BackendResult, NormalizedDocument, NormalizedEntry, NormalizedFloat,
7 NormalizedKey, NormalizedKeyEntry, NormalizedStream, NormalizedValue, SourcePosition,
8 SourceSpan, StreamItem, TemplateInput,
9};
10
11#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
12pub enum YamlProfile {
13 V1_2_2,
14}
15
16impl YamlProfile {
17 #[must_use]
18 pub const fn as_str(self) -> &'static str {
19 match self {
20 Self::V1_2_2 => "1.2.2",
21 }
22 }
23}
24
25impl Default for YamlProfile {
26 fn default() -> Self {
27 Self::V1_2_2
28 }
29}
30
31impl FromStr for YamlProfile {
32 type Err = String;
33
34 fn from_str(value: &str) -> Result<Self, Self::Err> {
35 match value {
36 "1.2.2" => Ok(Self::V1_2_2),
37 other => Err(format!(
38 "Unsupported YAML profile {other:?}. Supported profiles: \"1.2.2\"."
39 )),
40 }
41 }
42}
43
44#[derive(Clone, Debug)]
45pub struct YamlInterpolationNode {
46 pub span: SourceSpan,
47 pub interpolation_index: usize,
48 pub role: String,
49}
50
51#[derive(Clone, Debug)]
52pub struct YamlTextChunkNode {
53 pub span: SourceSpan,
54 pub value: String,
55}
56
57#[derive(Clone, Debug)]
58pub enum YamlChunk {
59 Text(YamlTextChunkNode),
60 Interpolation(YamlInterpolationNode),
61}
62
63#[derive(Clone, Debug)]
64pub struct YamlTagNode {
65 pub span: SourceSpan,
66 pub chunks: Vec<YamlChunk>,
67}
68
69#[derive(Clone, Debug)]
70pub struct YamlAnchorNode {
71 pub span: SourceSpan,
72 pub chunks: Vec<YamlChunk>,
73}
74
75#[derive(Clone, Debug)]
76pub struct YamlPlainScalarNode {
77 pub span: SourceSpan,
78 pub chunks: Vec<YamlChunk>,
79}
80
81#[derive(Clone, Debug)]
82pub struct YamlDoubleQuotedScalarNode {
83 pub span: SourceSpan,
84 pub chunks: Vec<YamlChunk>,
85}
86
87#[derive(Clone, Debug)]
88pub struct YamlSingleQuotedScalarNode {
89 pub span: SourceSpan,
90 pub chunks: Vec<YamlChunk>,
91}
92
93#[derive(Clone, Debug)]
94pub struct YamlBlockScalarNode {
95 pub span: SourceSpan,
96 pub style: String,
97 pub chomping: Option<String>,
98 pub indent_indicator: Option<usize>,
99 pub chunks: Vec<YamlChunk>,
100}
101
102#[derive(Clone, Debug)]
103pub struct YamlAliasNode {
104 pub span: SourceSpan,
105 pub chunks: Vec<YamlChunk>,
106}
107
108#[derive(Clone, Debug)]
109pub enum YamlKeyValue {
110 Scalar(YamlScalarNode),
111 Interpolation(YamlInterpolationNode),
112 Complex(Box<YamlValueNode>),
113}
114
115#[derive(Clone, Debug)]
116pub struct YamlKeyNode {
117 pub span: SourceSpan,
118 pub value: YamlKeyValue,
119}
120
121#[derive(Clone, Debug)]
122pub struct YamlMappingEntryNode {
123 pub span: SourceSpan,
124 pub key: YamlKeyNode,
125 pub value: YamlValueNode,
126}
127
128#[derive(Clone, Debug)]
129pub struct YamlMappingNode {
130 pub span: SourceSpan,
131 pub entries: Vec<YamlMappingEntryNode>,
132 pub flow: bool,
133}
134
135#[derive(Clone, Debug)]
136pub struct YamlSequenceNode {
137 pub span: SourceSpan,
138 pub items: Vec<YamlValueNode>,
139 pub flow: bool,
140}
141
142#[derive(Clone, Debug)]
143pub struct YamlDecoratedNode {
144 pub span: SourceSpan,
145 pub value: Box<YamlValueNode>,
146 pub tag: Option<YamlTagNode>,
147 pub anchor: Option<YamlAnchorNode>,
148}
149
150#[derive(Clone, Debug)]
151pub struct YamlDocumentNode {
152 pub span: SourceSpan,
153 pub directives: Vec<String>,
154 pub explicit_start: bool,
155 pub explicit_end: bool,
156 pub value: YamlValueNode,
157}
158
159#[derive(Clone, Debug)]
160pub struct YamlStreamNode {
161 pub span: SourceSpan,
162 pub documents: Vec<YamlDocumentNode>,
163}
164
165#[derive(Clone, Debug)]
166pub enum YamlScalarNode {
167 Plain(YamlPlainScalarNode),
168 DoubleQuoted(YamlDoubleQuotedScalarNode),
169 SingleQuoted(YamlSingleQuotedScalarNode),
170 Block(YamlBlockScalarNode),
171 Alias(YamlAliasNode),
172}
173
174#[derive(Clone, Debug)]
175pub enum YamlValueNode {
176 Scalar(YamlScalarNode),
177 Interpolation(YamlInterpolationNode),
178 Mapping(YamlMappingNode),
179 Sequence(YamlSequenceNode),
180 Decorated(YamlDecoratedNode),
181}
182
183pub struct YamlParser {
184 items: Vec<StreamItem>,
185 index: usize,
186}
187
188impl YamlParser {
189 #[must_use]
190 pub fn new(template: &TemplateInput) -> Self {
191 Self {
192 items: template.flatten(),
193 index: 0,
194 }
195 }
196
197 #[must_use]
198 pub fn from_items(items: Vec<StreamItem>) -> Self {
199 Self { items, index: 0 }
200 }
201
202 pub fn parse(&mut self) -> BackendResult<YamlDocumentNode> {
203 let start = self.mark();
204 self.skip_blank_lines();
205 self.consume_line_start_tabs()?;
206 let value = self.parse_block_node(self.current_line_indent())?;
207 self.skip_trailing_document_space();
208 self.skip_blank_lines();
209 if self.current_kind() != "eof" {
210 return Err(self.error("Unexpected trailing YAML content."));
211 }
212 Ok(YamlDocumentNode {
213 span: self.span_from(start),
214 directives: Vec::new(),
215 explicit_start: false,
216 explicit_end: false,
217 value,
218 })
219 }
220
221 fn current(&self) -> &StreamItem {
222 &self.items[self.index]
223 }
224
225 fn current_kind(&self) -> &'static str {
226 self.current().kind()
227 }
228
229 fn current_char(&self) -> Option<char> {
230 self.current().char()
231 }
232
233 fn mark(&self) -> SourcePosition {
234 self.current().span().start.clone()
235 }
236
237 fn previous_end(&self) -> SourcePosition {
238 if self.index == 0 {
239 return self.current().span().start.clone();
240 }
241 self.items[self.index - 1].span().end.clone()
242 }
243
244 fn span_from(&self, start: SourcePosition) -> SourceSpan {
245 SourceSpan::between(start, self.previous_end())
246 }
247
248 fn error(&self, message: impl Into<String>) -> BackendError {
249 BackendError::parse_at("yaml.parse", message, Some(self.current().span().clone()))
250 }
251
252 fn advance(&mut self) {
253 if self.current_kind() != "eof" {
254 self.index += 1;
255 }
256 }
257
258 fn is_line_start(&self) -> bool {
259 self.index == 0 || self.items[self.index - 1].char() == Some('\n')
260 }
261
262 fn current_line_indent(&self) -> usize {
263 let mut probe = self.index;
264 while probe > 0 && self.items[probe - 1].char() != Some('\n') {
265 probe -= 1;
266 }
267 let mut indent = 0;
268 while matches!(self.items[probe].char(), Some(' ')) {
269 indent += 1;
270 probe += 1;
271 }
272 indent
273 }
274
275 fn skip_blank_lines(&mut self) {
276 loop {
277 if self.current_kind() == "eof" || !self.is_line_start() {
278 return;
279 }
280 let mut probe = self.index;
281 while matches!(self.items[probe].char(), Some(' ')) {
282 probe += 1;
283 }
284 match &self.items[probe] {
285 StreamItem::Eof { .. } => {
286 self.index = probe;
287 return;
288 }
289 StreamItem::Char { ch: '#', .. } => {
290 self.index = probe;
291 while !matches!(self.current_char(), None | Some('\n')) {
292 self.advance();
293 }
294 if self.current_char() == Some('\n') {
295 self.advance();
296 }
297 }
298 _ if self.items[probe].char() == Some('\n') => {
299 self.index = probe;
300 self.advance();
301 }
302 _ => return,
303 }
304 }
305 }
306
307 fn skip_trailing_document_space(&mut self) {
308 loop {
309 while self.current_char() == Some(' ') {
310 self.advance();
311 }
312 if self.current_starts_comment() {
313 while !matches!(self.current_char(), None | Some('\n')) {
314 self.advance();
315 }
316 }
317 if self.current_char() == Some('\n') {
318 self.advance();
319 continue;
320 }
321 return;
322 }
323 }
324
325 fn consume_indent(&mut self, indent: usize) -> BackendResult<()> {
326 if !self.is_line_start() {
327 return Ok(());
328 }
329 for _ in 0..indent {
330 if self.current_char() != Some(' ') {
331 return Err(self.error("Incorrect YAML indentation."));
332 }
333 self.advance();
334 }
335 Ok(())
336 }
337
338 fn parse_block_node(&mut self, indent: usize) -> BackendResult<YamlValueNode> {
339 self.skip_blank_lines();
340 self.consume_line_start_tabs()?;
341 if self.current_kind() == "eof" || self.current_line_indent() < indent {
342 return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
343 null_plain_scalar(),
344 )));
345 }
346 self.consume_indent(indent)?;
347 if self.starts_sequence_item() {
348 return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
349 }
350 if self.line_has_mapping_key() {
351 return Ok(YamlValueNode::Mapping(self.parse_block_mapping(indent)?));
352 }
353 let decorated = self.parse_decorators()?;
354 if decorated.is_some() && self.starts_sequence_item() {
355 return Err(self.error("Unexpected trailing YAML content."));
356 }
357 let value = if decorated.is_some() && self.decorator_line_break_ahead() {
358 self.consume_inline_comment_and_line_break();
359 self.skip_blank_lines();
360 self.consume_line_start_tabs()?;
361 let next_indent = self.current_line_indent();
362 let has_nested_value = self.current_kind() != "eof"
363 && (next_indent >= indent || (indent > 0 && next_indent + 1 == indent));
364 if has_nested_value {
365 self.parse_block_node(next_indent)?
366 } else {
367 YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
368 }
369 } else {
370 self.parse_inline_value(None, indent, false)?
371 };
372 wrap_decorators(decorated, value)
373 }
374
375 fn value_owns_following_lines(value: &YamlValueNode) -> bool {
376 match value {
377 YamlValueNode::Mapping(node) => !node.flow,
378 YamlValueNode::Sequence(node) => !node.flow,
379 YamlValueNode::Scalar(YamlScalarNode::Block(_)) => true,
380 YamlValueNode::Decorated(node) => Self::value_owns_following_lines(node.value.as_ref()),
381 _ => false,
382 }
383 }
384
385 fn parse_decorators(
386 &mut self,
387 ) -> BackendResult<Option<(Option<YamlTagNode>, Option<YamlAnchorNode>)>> {
388 let mut tag = None;
389 let mut anchor = None;
390 let mut consumed = false;
391 loop {
392 if self.current_char() == Some('!') {
393 tag = Some(self.parse_tag()?);
394 consumed = true;
395 self.skip_inline_spaces();
396 continue;
397 }
398 if self.current_char() == Some('&') {
399 anchor = Some(self.parse_anchor()?);
400 consumed = true;
401 self.skip_inline_spaces();
402 continue;
403 }
404 break;
405 }
406 Ok(consumed.then_some((tag, anchor)))
407 }
408
409 fn parse_tag(&mut self) -> BackendResult<YamlTagNode> {
410 let start = self.mark();
411 self.advance();
412 let chunks = if self.current_char() == Some('<') {
413 self.parse_verbatim_tag_chunks()?
414 } else {
415 self.parse_symbol_chunks(&[' ', '\t', '\n', '[', ']', '{', '}', ',', ':'])?
416 };
417 Ok(YamlTagNode {
418 span: self.span_from(start),
419 chunks,
420 })
421 }
422
423 fn parse_anchor(&mut self) -> BackendResult<YamlAnchorNode> {
424 let start = self.mark();
425 self.advance();
426 let chunks = self.parse_symbol_chunks(&[' ', '\t', '\n', '[', ']', '{', '}', ','])?;
427 Ok(YamlAnchorNode {
428 span: self.span_from(start),
429 chunks,
430 })
431 }
432
433 fn parse_alias(&mut self) -> BackendResult<YamlAliasNode> {
434 let start = self.mark();
435 self.advance();
436 let chunks = self.parse_symbol_chunks(&[' ', '\t', '\n', '[', ']', '{', '}', ','])?;
437 Ok(YamlAliasNode {
438 span: self.span_from(start),
439 chunks,
440 })
441 }
442
443 fn parse_symbol_chunks(&mut self, stop_chars: &[char]) -> BackendResult<Vec<YamlChunk>> {
444 let mut chunks = Vec::new();
445 let mut buffer = String::new();
446 while self.current_kind() != "eof" {
447 if self.current_kind() == "interpolation" {
448 self.flush_buffer(&mut buffer, &mut chunks);
449 chunks.push(YamlChunk::Interpolation(
450 self.consume_interpolation("metadata_fragment")?,
451 ));
452 continue;
453 }
454 let Some(ch) = self.current_char() else {
455 break;
456 };
457 if stop_chars.contains(&ch) {
458 break;
459 }
460 buffer.push(ch);
461 self.advance();
462 }
463 self.flush_buffer(&mut buffer, &mut chunks);
464 Ok(chunks)
465 }
466
467 fn parse_verbatim_tag_chunks(&mut self) -> BackendResult<Vec<YamlChunk>> {
468 let mut chunks = Vec::new();
469 let mut buffer = String::new();
470 buffer.push('<');
471 self.advance();
472
473 while self.current_kind() != "eof" {
474 if self.current_kind() == "interpolation" {
475 self.flush_buffer(&mut buffer, &mut chunks);
476 chunks.push(YamlChunk::Interpolation(
477 self.consume_interpolation("metadata_fragment")?,
478 ));
479 continue;
480 }
481
482 let Some(ch) = self.current_char() else {
483 break;
484 };
485 buffer.push(ch);
486 self.advance();
487 if ch == '>' {
488 self.flush_buffer(&mut buffer, &mut chunks);
489 return Ok(chunks);
490 }
491 }
492
493 Err(self.error("Unterminated YAML verbatim tag."))
494 }
495
496 fn skip_inline_spaces(&mut self) {
497 while matches!(self.current_char(), Some(' ' | '\t')) {
498 self.advance();
499 }
500 }
501
502 fn skip_flow_separation(&mut self) {
503 self.skip_flow_separation_with_breaks();
504 }
505
506 fn skip_flow_separation_with_breaks(&mut self) -> bool {
507 let mut saw_line_break = false;
508 loop {
509 while matches!(self.current_char(), Some(' ' | '\t' | '\r')) {
510 self.advance();
511 }
512 if self.current_starts_comment() {
513 while !matches!(self.current_char(), None | Some('\n')) {
514 self.advance();
515 }
516 }
517 if self.current_char() == Some('\n') {
518 saw_line_break = true;
519 self.advance();
520 continue;
521 }
522 break;
523 }
524 saw_line_break
525 }
526
527 fn starts_sequence_item(&self) -> bool {
528 self.starts_sequence_item_at(self.index)
529 }
530
531 fn starts_sequence_item_at(&self, mut probe: usize) -> bool {
532 while matches!(self.items.get(probe).and_then(StreamItem::char), Some(' ')) {
533 probe += 1;
534 }
535 matches!(self.items.get(probe).and_then(StreamItem::char), Some('-'))
536 && matches!(
537 self.items.get(probe + 1).and_then(StreamItem::char),
538 Some(' ' | '\t' | '\n')
539 )
540 }
541
542 fn peek_char(&self, offset: usize) -> Option<char> {
543 self.items
544 .get(self.index + offset)
545 .and_then(StreamItem::char)
546 }
547
548 fn line_has_mapping_key(&self) -> bool {
549 self.line_has_mapping_key_at(self.index)
550 }
551
552 fn line_has_mapping_key_at(&self, probe: usize) -> bool {
553 if matches!(self.items.get(probe).and_then(StreamItem::char), Some('?'))
554 && matches!(
555 self.items.get(probe + 1).and_then(StreamItem::char),
556 Some(' ' | '\t' | '\n')
557 )
558 {
559 return true;
560 }
561 let mut parser = Self {
562 items: self.items.clone(),
563 index: probe,
564 };
565 if parser.parse_key().is_err() {
566 return false;
567 }
568 parser.skip_key_value_spaces();
569 if parser.current_char() != Some(':') {
570 return false;
571 }
572 matches!(parser.peek_char(1), Some(' ' | '\t' | '\n') | None)
573 }
574
575 fn skip_key_value_spaces(&mut self) {
576 while matches!(self.current_char(), Some(' ' | '\t')) {
577 self.advance();
578 }
579 }
580
581 fn decorator_line_break_ahead(&self) -> bool {
582 let mut probe = self.index;
583 while matches!(
584 self.items.get(probe).and_then(StreamItem::char),
585 Some(' ' | '\t')
586 ) {
587 probe += 1;
588 }
589 if self.items.get(probe).and_then(StreamItem::char) == Some('#') {
590 while !matches!(
591 self.items.get(probe).and_then(StreamItem::char),
592 None | Some('\n')
593 ) {
594 probe += 1;
595 }
596 }
597 matches!(self.items.get(probe).and_then(StreamItem::char), Some('\n'))
598 || matches!(self.items.get(probe), Some(StreamItem::Eof { .. }))
599 }
600
601 fn consume_inline_comment_and_line_break(&mut self) {
602 self.skip_inline_spaces();
603 if self.current_starts_comment() {
604 while !matches!(self.current_char(), None | Some('\n')) {
605 self.advance();
606 }
607 }
608 if self.current_char() == Some('\n') {
609 self.advance();
610 }
611 }
612
613 fn consume_line_end_required(&mut self) -> BackendResult<()> {
614 if self.current_kind() == "eof" || self.is_line_start() {
615 return Ok(());
616 }
617 self.skip_inline_spaces();
618 if self.current_starts_comment() {
619 while !matches!(self.current_char(), None | Some('\n')) {
620 self.advance();
621 }
622 }
623 match self.current_char() {
624 Some('\n') => {
625 self.advance();
626 Ok(())
627 }
628 None => Ok(()),
629 _ if self.current_kind() == "eof" => Ok(()),
630 _ => Err(self.error("Unexpected trailing YAML content.")),
631 }
632 }
633
634 fn consume_line_start_tabs(&mut self) -> BackendResult<()> {
635 if !self.is_line_start() || self.current_char() != Some('\t') {
636 return Ok(());
637 }
638
639 let mut probe = self.index;
640 while self.items.get(probe).and_then(StreamItem::char) == Some('\t') {
641 probe += 1;
642 }
643
644 if self.starts_sequence_item_at(probe) || self.line_has_mapping_key_at(probe) {
645 return Err(self.error("Tabs are not allowed as YAML indentation."));
646 }
647
648 self.index = probe;
649 Ok(())
650 }
651
652 fn current_starts_comment(&self) -> bool {
653 self.current_char() == Some('#')
654 && (self.index == 0
655 || matches!(
656 self.items
657 .get(self.index.saturating_sub(1))
658 .and_then(StreamItem::char),
659 Some(' ' | '\t' | '\n' | '\r')
660 ))
661 }
662
663 fn parse_key_value_separator(&mut self) -> BackendResult<()> {
664 self.skip_key_value_spaces();
665 self.consume_char(':')?;
666 if self.current_char() == Some('\t') {
667 let mut probe = self.index;
668 while self.items.get(probe).and_then(StreamItem::char) == Some('\t') {
669 probe += 1;
670 }
671 if !matches!(
672 self.items.get(probe).and_then(StreamItem::char),
673 Some(
674 ' ' | '\n'
675 | '\r'
676 | '#'
677 | '['
678 | '{'
679 | '"'
680 | '\''
681 | '!'
682 | '&'
683 | '*'
684 | '|'
685 | '>'
686 | '?'
687 ) | None
688 ) {
689 return Err(self.error("Tabs are not allowed as YAML indentation."));
690 }
691 }
692 Ok(())
693 }
694
695 fn classify_key_value(&self, start: SourcePosition, value: YamlValueNode) -> YamlKeyNode {
696 let value = match value {
697 YamlValueNode::Interpolation(node) => YamlKeyValue::Interpolation(node),
698 YamlValueNode::Scalar(
699 node @ (YamlScalarNode::Plain(_)
700 | YamlScalarNode::DoubleQuoted(_)
701 | YamlScalarNode::SingleQuoted(_)),
702 ) => YamlKeyValue::Scalar(node),
703 other => YamlKeyValue::Complex(Box::new(other)),
704 };
705 YamlKeyNode {
706 span: self.span_from(start),
707 value,
708 }
709 }
710
711 fn parse_block_sequence(&mut self, indent: usize) -> BackendResult<YamlSequenceNode> {
712 let start = self.mark();
713 let mut items = Vec::new();
714 let mut first_item = true;
715 loop {
716 if !first_item || self.is_line_start() {
717 self.consume_indent(indent)?;
718 }
719 first_item = false;
720 if !self.starts_sequence_item() {
721 break;
722 }
723 self.advance();
724 if matches!(self.current_char(), Some(' ' | '\t')) {
725 self.skip_inline_spaces();
726 }
727 if self.current_starts_comment() {
728 while !matches!(self.current_char(), None | Some('\n')) {
729 self.advance();
730 }
731 if self.current_char() == Some('\n') {
732 self.advance();
733 }
734 self.skip_blank_lines();
735 if self.is_line_start() && self.current_char() == Some('\t') {
736 self.consume_line_start_tabs()?;
737 }
738 let item = if self.current_kind() == "eof" || self.current_line_indent() <= indent {
739 YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
740 } else {
741 self.parse_block_node(self.current_line_indent())?
742 };
743 let owns_following_lines = Self::value_owns_following_lines(&item);
744 items.push(item);
745 if !owns_following_lines && self.current_kind() != "eof" {
746 self.consume_line_end_required()?;
747 }
748 } else if self.current_kind() == "eof" || self.current_char() == Some('\n') {
749 if self.current_char() == Some('\n') {
750 self.advance();
751 }
752 self.skip_blank_lines();
753 let item = self.parse_block_node(self.current_line_indent())?;
754 let owns_following_lines = Self::value_owns_following_lines(&item);
755 items.push(item);
756 if !owns_following_lines {
757 self.consume_line_end_required()?;
758 }
759 } else if self.starts_sequence_item() {
760 items.push(YamlValueNode::Sequence(
761 self.parse_compact_sequence(indent + 2)?,
762 ));
763 } else if self.line_has_mapping_key() {
764 items.push(YamlValueNode::Mapping(
765 self.parse_compact_mapping(indent + 2)?,
766 ));
767 } else {
768 let item = self.parse_inline_value(None, indent, true)?;
769 let owns_following_lines = Self::value_owns_following_lines(&item);
770 items.push(item);
771 if !owns_following_lines {
772 self.consume_line_end_required()?;
773 }
774 }
775 self.skip_blank_lines();
776 if self.current_kind() == "eof"
777 || self.current_line_indent() != indent
778 || !self.starts_sequence_item()
779 {
780 break;
781 }
782 }
783 Ok(YamlSequenceNode {
784 span: self.span_from(start),
785 items,
786 flow: false,
787 })
788 }
789
790 fn parse_compact_sequence(&mut self, indent: usize) -> BackendResult<YamlSequenceNode> {
791 let start = self.mark();
792 let mut items = Vec::new();
793 let mut first_item = true;
794 loop {
795 if !first_item || self.is_line_start() {
796 self.consume_indent(indent)?;
797 }
798 first_item = false;
799 if !self.starts_sequence_item() {
800 break;
801 }
802 self.advance();
803 if matches!(self.current_char(), Some(' ' | '\t')) {
804 self.skip_inline_spaces();
805 }
806 if self.current_starts_comment() {
807 while !matches!(self.current_char(), None | Some('\n')) {
808 self.advance();
809 }
810 if self.current_char() == Some('\n') {
811 self.advance();
812 }
813 self.skip_blank_lines();
814 if self.is_line_start() && self.current_char() == Some('\t') {
815 self.consume_line_start_tabs()?;
816 }
817 let item = if self.current_kind() == "eof" || self.current_line_indent() <= indent {
818 YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
819 } else {
820 self.parse_block_node(self.current_line_indent())?
821 };
822 let owns_following_lines = Self::value_owns_following_lines(&item);
823 items.push(item);
824 if !owns_following_lines && self.current_kind() != "eof" {
825 self.consume_line_end_required()?;
826 }
827 } else if self.current_kind() == "eof" || self.current_char() == Some('\n') {
828 if self.current_char() == Some('\n') {
829 self.advance();
830 }
831 self.skip_blank_lines();
832 let item = self.parse_block_node(self.current_line_indent())?;
833 let owns_following_lines = Self::value_owns_following_lines(&item);
834 items.push(item);
835 if !owns_following_lines {
836 self.consume_line_end_required()?;
837 }
838 } else if self.starts_sequence_item() {
839 items.push(YamlValueNode::Sequence(
840 self.parse_compact_sequence(indent + 2)?,
841 ));
842 } else if self.line_has_mapping_key() {
843 items.push(YamlValueNode::Mapping(
844 self.parse_compact_mapping(indent + 2)?,
845 ));
846 } else {
847 let item = self.parse_inline_value(None, indent, true)?;
848 let owns_following_lines = Self::value_owns_following_lines(&item);
849 items.push(item);
850 if !owns_following_lines {
851 self.consume_line_end_required()?;
852 }
853 }
854 self.skip_blank_lines();
855 if self.current_kind() == "eof"
856 || self.current_line_indent() < indent
857 || !self.starts_sequence_item()
858 {
859 break;
860 }
861 }
862 Ok(YamlSequenceNode {
863 span: self.span_from(start),
864 items,
865 flow: false,
866 })
867 }
868
869 fn parse_compact_mapping(&mut self, indent: usize) -> BackendResult<YamlMappingNode> {
870 let start = self.mark();
871 let mut entries = Vec::new();
872 loop {
873 let entry_start = self.mark();
874 let explicit_key =
875 self.current_char() == Some('?') && matches!(self.peek_char(1), Some(' ' | '\n'));
876 let key = if explicit_key {
877 self.parse_explicit_key(indent)?
878 } else {
879 self.parse_key()?
880 };
881 let value = if explicit_key {
882 self.parse_explicit_mapping_entry_value(indent)?
883 } else {
884 self.parse_key_value_separator()?;
885 self.parse_mapping_value_after_separator(indent, false)?
886 };
887 if !Self::value_owns_following_lines(&value) {
888 self.consume_line_end_required()?;
889 }
890 entries.push(YamlMappingEntryNode {
891 span: self.span_from(entry_start),
892 key,
893 value,
894 });
895 self.skip_blank_lines();
896 if self.current_kind() == "eof"
897 || self.current_line_indent() != indent
898 || self.starts_sequence_item()
899 || !self.line_has_mapping_key()
900 {
901 break;
902 }
903 self.consume_indent(indent)?;
904 }
905 Ok(YamlMappingNode {
906 span: self.span_from(start),
907 entries,
908 flow: false,
909 })
910 }
911
912 fn parse_block_mapping(&mut self, indent: usize) -> BackendResult<YamlMappingNode> {
913 let start = self.mark();
914 let mut entries = Vec::new();
915 let mut first_entry = true;
916 loop {
917 if !first_entry {
918 self.consume_indent(indent)?;
919 }
920 first_entry = false;
921 let entry_start = self.mark();
922 let explicit_key =
923 self.current_char() == Some('?') && matches!(self.peek_char(1), Some(' ' | '\n'));
924 let key = if explicit_key {
925 self.parse_explicit_key(indent)?
926 } else {
927 self.parse_key()?
928 };
929 let value = if explicit_key {
930 self.parse_explicit_mapping_entry_value(indent)?
931 } else {
932 self.parse_key_value_separator()?;
933 self.parse_mapping_value_after_separator(indent, false)?
934 };
935 if !Self::value_owns_following_lines(&value) {
936 self.consume_line_end_required()?;
937 }
938 entries.push(YamlMappingEntryNode {
939 span: self.span_from(entry_start),
940 key,
941 value,
942 });
943 self.skip_blank_lines();
944 if self.current_kind() == "eof"
945 || self.current_line_indent() != indent
946 || self.starts_sequence_item()
947 || !self.line_has_mapping_key()
948 {
949 break;
950 }
951 }
952 Ok(YamlMappingNode {
953 span: self.span_from(start),
954 entries,
955 flow: false,
956 })
957 }
958
959 fn parse_explicit_mapping_entry_value(
960 &mut self,
961 indent: usize,
962 ) -> BackendResult<YamlValueNode> {
963 if self.current_kind() == "eof"
964 || (self.is_line_start() && self.current_line_indent() < indent)
965 {
966 return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
967 null_plain_scalar(),
968 )));
969 }
970
971 self.consume_indent(indent)?;
972 if self.current_char() != Some(':') {
973 return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
974 null_plain_scalar(),
975 )));
976 }
977
978 self.parse_key_value_separator()?;
979 self.parse_mapping_value_after_separator(indent, true)
980 }
981
982 fn parse_mapping_value_after_separator(
983 &mut self,
984 indent: usize,
985 allow_inline_compact_sequence: bool,
986 ) -> BackendResult<YamlValueNode> {
987 if matches!(self.current_char(), Some(' ' | '\t')) {
988 if matches!(self.current_char(), Some(' ' | '\t')) {
989 self.skip_inline_spaces();
990 }
991 return self.parse_mapping_value(indent, allow_inline_compact_sequence);
992 }
993 if matches!(self.current_kind(), "eof") || self.current_char() == Some('\n') {
994 if self.current_char() == Some('\n') {
995 self.advance();
996 }
997 self.skip_blank_lines();
998 self.consume_line_start_tabs()?;
999 if self.starts_sequence_item() && self.current_line_indent() == indent {
1000 return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
1001 }
1002 if self.current_kind() == "eof" || self.current_line_indent() <= indent {
1003 return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
1004 null_plain_scalar(),
1005 )));
1006 }
1007 return self.parse_block_node(self.current_line_indent());
1008 }
1009 self.parse_inline_value(None, indent, true)
1010 }
1011
1012 fn parse_mapping_value(
1013 &mut self,
1014 indent: usize,
1015 allow_inline_compact_sequence: bool,
1016 ) -> BackendResult<YamlValueNode> {
1017 if self.current_starts_comment() {
1018 while !matches!(self.current_char(), None | Some('\n')) {
1019 self.advance();
1020 }
1021 if self.current_char() == Some('\n') {
1022 self.advance();
1023 }
1024 self.skip_blank_lines();
1025 if self.is_line_start() && self.current_char() == Some('\t') {
1026 self.consume_line_start_tabs()?;
1027 }
1028 if self.starts_sequence_item() && self.current_line_indent() == indent {
1029 return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
1030 }
1031 if self.current_kind() == "eof" || self.current_line_indent() <= indent {
1032 return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
1033 null_plain_scalar(),
1034 )));
1035 }
1036 return self.parse_block_node(self.current_line_indent());
1037 }
1038 if self.starts_sequence_item() {
1039 if self.is_line_start() && self.current_line_indent() == indent {
1040 return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
1041 }
1042 if allow_inline_compact_sequence {
1043 return Ok(YamlValueNode::Sequence(
1044 self.parse_compact_sequence(indent + 2)?,
1045 ));
1046 }
1047 return Err(self.error("Unexpected trailing YAML content."));
1048 }
1049 if allow_inline_compact_sequence && self.line_has_mapping_key() {
1050 return Ok(YamlValueNode::Mapping(
1051 self.parse_compact_mapping(indent + 2)?,
1052 ));
1053 }
1054 if matches!(self.current_char(), Some(' ' | '\t')) {
1055 self.skip_inline_spaces();
1056 if self.current_starts_comment() {
1057 while !matches!(self.current_char(), None | Some('\n')) {
1058 self.advance();
1059 }
1060 if self.current_char() == Some('\n') {
1061 self.advance();
1062 }
1063 self.skip_blank_lines();
1064 if self.is_line_start() && self.current_char() == Some('\t') {
1065 self.consume_line_start_tabs()?;
1066 }
1067 if self.starts_sequence_item() && self.current_line_indent() == indent {
1068 return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
1069 }
1070 if self.current_kind() == "eof" || self.current_line_indent() <= indent {
1071 return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
1072 null_plain_scalar(),
1073 )));
1074 }
1075 return self.parse_block_node(self.current_line_indent());
1076 }
1077 if self.starts_sequence_item() {
1078 if self.is_line_start() && self.current_line_indent() == indent {
1079 return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
1080 }
1081 if allow_inline_compact_sequence {
1082 return Ok(YamlValueNode::Sequence(
1083 self.parse_compact_sequence(indent + 2)?,
1084 ));
1085 }
1086 return Err(self.error("Unexpected trailing YAML content."));
1087 }
1088 if allow_inline_compact_sequence && self.line_has_mapping_key() {
1089 return Ok(YamlValueNode::Mapping(
1090 self.parse_compact_mapping(indent + 2)?,
1091 ));
1092 }
1093 return self.parse_inline_value(None, indent, true);
1094 }
1095 if self.current_kind() == "eof" || self.current_char() == Some('\n') {
1096 if self.current_char() == Some('\n') {
1097 self.advance();
1098 }
1099 self.skip_blank_lines();
1100 if self.is_line_start() && self.current_char() == Some('\t') {
1101 self.consume_line_start_tabs()?;
1102 }
1103 if self.starts_sequence_item() && self.current_line_indent() == indent {
1104 return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
1105 }
1106 if self.current_kind() == "eof" || self.current_line_indent() <= indent {
1107 return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
1108 null_plain_scalar(),
1109 )));
1110 }
1111 return self.parse_block_node(self.current_line_indent());
1112 }
1113 self.parse_inline_value(None, indent, true)
1114 }
1115
1116 fn parse_explicit_key(&mut self, indent: usize) -> BackendResult<YamlKeyNode> {
1117 let start = self.mark();
1118 self.consume_char('?')?;
1119 if matches!(self.current_char(), Some(' ' | '\t')) {
1120 self.skip_inline_spaces();
1121 }
1122 let value = if self.current_starts_comment() {
1123 while !matches!(self.current_char(), None | Some('\n')) {
1124 self.advance();
1125 }
1126 if self.current_char() == Some('\n') {
1127 self.advance();
1128 }
1129 self.skip_blank_lines();
1130 if self.is_line_start() && self.current_char() == Some('\t') {
1131 self.consume_line_start_tabs()?;
1132 }
1133 if self.current_kind() == "eof" || self.current_char() == Some(':') {
1134 YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
1135 } else {
1136 self.parse_block_node(self.current_line_indent())?
1137 }
1138 } else if self.current_kind() == "eof" || self.current_char() == Some('\n') {
1139 if self.current_char() == Some('\n') {
1140 self.advance();
1141 }
1142 self.skip_blank_lines();
1143 if self.current_kind() == "eof" || self.current_char() == Some(':') {
1144 YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
1145 } else {
1146 self.parse_block_node(self.current_line_indent())?
1147 }
1148 } else if self.starts_sequence_item() {
1149 YamlValueNode::Sequence(self.parse_compact_sequence(indent + 2)?)
1150 } else if self.line_has_mapping_key() {
1151 YamlValueNode::Mapping(self.parse_compact_mapping(indent + 2)?)
1152 } else {
1153 let value = self.parse_inline_value(None, indent, true)?;
1154 self.consume_line_end_required()?;
1155 value
1156 };
1157 Ok(self.classify_key_value(start, value))
1158 }
1159
1160 fn parse_key(&mut self) -> BackendResult<YamlKeyNode> {
1161 let start = self.mark();
1162 let key_start = self.index;
1163 let value = self.parse_inline_value(Some(&[':']), 0, false)?;
1164 self.ensure_single_line_implicit_key(key_start)?;
1165 Ok(self.classify_key_value(start, value))
1166 }
1167
1168 fn parse_inline_value(
1169 &mut self,
1170 stop_chars: Option<&[char]>,
1171 parent_indent: usize,
1172 requires_indented_continuation: bool,
1173 ) -> BackendResult<YamlValueNode> {
1174 let decorated = self.parse_decorators()?;
1175 if decorated.is_some() && self.starts_sequence_item() {
1176 return Err(self.error("Unexpected trailing YAML content."));
1177 }
1178 if decorated.is_some() && self.decorator_line_break_ahead() {
1179 self.consume_inline_comment_and_line_break();
1180 self.skip_blank_lines();
1181 self.consume_line_start_tabs()?;
1182 let has_nested_value = self.current_kind() != "eof"
1183 && (self.current_line_indent() > parent_indent
1184 || (self.current_line_indent() == parent_indent
1185 && self.starts_sequence_item()));
1186 let value = if has_nested_value {
1187 self.parse_block_node(self.current_line_indent())?
1188 } else {
1189 YamlValueNode::Scalar(YamlScalarNode::Plain(YamlPlainScalarNode {
1190 span: SourceSpan::point(0, 0),
1191 chunks: vec![YamlChunk::Text(YamlTextChunkNode {
1192 span: SourceSpan::point(0, 0),
1193 value: "null".to_owned(),
1194 })],
1195 }))
1196 };
1197 return wrap_decorators(decorated, value);
1198 }
1199 if decorated.is_some() && self.inline_value_terminator(stop_chars) {
1200 return wrap_decorators(
1201 decorated,
1202 YamlValueNode::Scalar(YamlScalarNode::Plain(empty_plain_scalar())),
1203 );
1204 }
1205 let value = if self.current_kind() == "interpolation" {
1206 let interpolation = self.consume_interpolation("value")?;
1207 if self.inline_value_terminator(stop_chars) {
1208 YamlValueNode::Interpolation(interpolation)
1209 } else {
1210 YamlValueNode::Scalar(YamlScalarNode::Plain(self.parse_plain_scalar(
1211 stop_chars,
1212 Some(vec![YamlChunk::Interpolation(interpolation)]),
1213 false,
1214 parent_indent,
1215 requires_indented_continuation,
1216 )?))
1217 }
1218 } else if self.current_char() == Some('[') {
1219 YamlValueNode::Sequence(self.parse_flow_sequence()?)
1220 } else if self.current_char() == Some('{') {
1221 YamlValueNode::Mapping(self.parse_flow_mapping()?)
1222 } else if self.current_char() == Some('"') {
1223 YamlValueNode::Scalar(YamlScalarNode::DoubleQuoted(
1224 self.parse_double_quoted(parent_indent, requires_indented_continuation)?,
1225 ))
1226 } else if self.current_char() == Some('\'') {
1227 YamlValueNode::Scalar(YamlScalarNode::SingleQuoted(self.parse_single_quoted()?))
1228 } else if self.current_char() == Some('*') {
1229 YamlValueNode::Scalar(YamlScalarNode::Alias(self.parse_alias()?))
1230 } else if matches!(self.current_char(), Some('|' | '>')) {
1231 YamlValueNode::Scalar(YamlScalarNode::Block(
1232 self.parse_block_scalar(parent_indent)?,
1233 ))
1234 } else if matches!(self.current_char(), Some(',' | ']' | '}')) {
1235 return Err(self.error("Expected a YAML value."));
1236 } else {
1237 YamlValueNode::Scalar(YamlScalarNode::Plain(self.parse_plain_scalar(
1238 stop_chars,
1239 None,
1240 false,
1241 parent_indent,
1242 requires_indented_continuation,
1243 )?))
1244 };
1245 wrap_decorators(decorated, value)
1246 }
1247
1248 fn inline_value_terminator(&self, stop_chars: Option<&[char]>) -> bool {
1249 let mut probe = self.index;
1250 while matches!(self.items[probe].char(), Some(' ' | '\t')) {
1251 probe += 1;
1252 }
1253 match &self.items[probe] {
1254 StreamItem::Eof { .. } => true,
1255 StreamItem::Char { ch, .. } => {
1256 stop_chars.is_some_and(|chars| chars.contains(ch)) || matches!(ch, '\n' | '#')
1257 }
1258 _ => false,
1259 }
1260 }
1261
1262 fn parse_flow_sequence(&mut self) -> BackendResult<YamlSequenceNode> {
1263 let start = self.mark();
1264 let line_value_start = self.is_line_value_start();
1265 self.consume_char('[')?;
1266 let mut items = Vec::new();
1267 self.skip_flow_separation();
1268 if self.current_char() == Some(']') {
1269 self.advance();
1270 return Ok(YamlSequenceNode {
1271 span: self.span_from(start),
1272 items,
1273 flow: true,
1274 });
1275 }
1276 loop {
1277 if self.current_char() == Some(',') {
1278 return Err(self.error("Expected a YAML value."));
1279 }
1280 if self.current_char() == Some('-') && matches!(self.peek_char(1), Some(',' | ']')) {
1281 return Err(self.error("Expected a YAML value."));
1282 }
1283 items.push(self.parse_flow_sequence_item()?);
1284 self.skip_flow_separation();
1285 if self.current_char() == Some(']') {
1286 self.advance();
1287 break;
1288 }
1289 self.consume_char(',')?;
1290 let saw_line_break = self.skip_flow_separation_with_breaks();
1291 if saw_line_break
1292 && !line_value_start
1293 && self.current_char() != Some(']')
1294 && self.current_line_indent() == 0
1295 {
1296 return Err(self.error("Unexpected trailing YAML content."));
1297 }
1298 if self.current_char() == Some(']') {
1299 self.advance();
1300 break;
1301 }
1302 }
1303 Ok(YamlSequenceNode {
1304 span: self.span_from(start),
1305 items,
1306 flow: true,
1307 })
1308 }
1309
1310 fn is_line_value_start(&self) -> bool {
1311 let mut probe = self.index;
1312 while probe > 0 && self.items[probe - 1].char() != Some('\n') {
1313 probe -= 1;
1314 }
1315 while probe < self.index {
1316 match self.items[probe].char() {
1317 Some(' ' | '\t') => probe += 1,
1318 _ => return false,
1319 }
1320 }
1321 true
1322 }
1323
1324 fn parse_flow_sequence_item(&mut self) -> BackendResult<YamlValueNode> {
1325 let entry_start = self.mark();
1326 let explicit =
1327 self.current_char() == Some('?') && matches!(self.peek_char(1), Some(' ' | '[' | '{'));
1328 if explicit {
1329 self.consume_char('?')?;
1330 self.skip_inline_spaces();
1331 }
1332
1333 let key_value = self.parse_inline_value(Some(&[':', ',', ']']), 0, false)?;
1334 let saw_line_break = self.skip_flow_separation_with_breaks();
1335 if self.current_char() != Some(':') {
1336 if explicit {
1337 return Err(self.error("Expected ':' in YAML template."));
1338 }
1339 return Ok(key_value);
1340 }
1341 if saw_line_break {
1342 return Err(self.error("Expected ':' in YAML template."));
1343 }
1344
1345 let key = self.classify_key_value(entry_start.clone(), key_value);
1346 self.parse_key_value_separator()?;
1347 self.skip_flow_separation();
1348 let value = if matches!(self.current_char(), Some(',' | ']')) {
1349 YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
1350 } else {
1351 self.parse_inline_value(Some(&[',', ']']), 0, false)?
1352 };
1353 let pair_span = self.span_from(entry_start);
1354 Ok(YamlValueNode::Mapping(YamlMappingNode {
1355 span: pair_span.clone(),
1356 entries: vec![YamlMappingEntryNode {
1357 span: pair_span,
1358 key,
1359 value,
1360 }],
1361 flow: true,
1362 }))
1363 }
1364
1365 fn parse_flow_mapping(&mut self) -> BackendResult<YamlMappingNode> {
1366 let start = self.mark();
1367 self.consume_char('{')?;
1368 let mut entries = Vec::new();
1369 self.skip_flow_separation();
1370 if self.current_char() == Some(',') {
1371 return Err(self.error("Expected ':' in YAML template."));
1372 }
1373 if self.current_char() == Some('}') {
1374 self.advance();
1375 return Ok(YamlMappingNode {
1376 span: self.span_from(start),
1377 entries,
1378 flow: true,
1379 });
1380 }
1381 loop {
1382 let entry_start = self.mark();
1383 let key = self.parse_flow_key()?;
1384 self.skip_flow_separation();
1385 let value = if matches!(self.current_char(), Some(',' | '}')) {
1386 if self.current_char() == Some('}') && flow_implicit_plain_key_needs_separator(&key)
1387 {
1388 return Err(self.error("Expected ':' in YAML template."));
1389 }
1390 YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
1391 } else {
1392 self.parse_key_value_separator()?;
1393 self.skip_flow_separation();
1394 if matches!(self.current_char(), Some(',' | '}')) {
1395 YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
1396 } else {
1397 self.parse_inline_value(Some(&[',', '}']), 0, false)?
1398 }
1399 };
1400 entries.push(YamlMappingEntryNode {
1401 span: self.span_from(entry_start),
1402 key,
1403 value,
1404 });
1405 self.skip_flow_separation();
1406 if self.current_char() == Some('}') {
1407 self.advance();
1408 break;
1409 }
1410 self.consume_char(',')?;
1411 self.skip_flow_separation();
1412 if self.current_char() == Some(',') {
1413 return Err(self.error("Expected ':' in YAML template."));
1414 }
1415 if self.current_char() == Some('}') {
1416 self.advance();
1417 break;
1418 }
1419 }
1420 Ok(YamlMappingNode {
1421 span: self.span_from(start),
1422 entries,
1423 flow: true,
1424 })
1425 }
1426
1427 fn parse_flow_key(&mut self) -> BackendResult<YamlKeyNode> {
1428 let start = self.mark();
1429 let explicit =
1430 self.current_char() == Some('?') && matches!(self.peek_char(1), Some(' ' | '[' | '{'));
1431 if explicit {
1432 self.consume_char('?')?;
1433 self.skip_inline_spaces();
1434 }
1435 let value = self.parse_inline_value(Some(&[':', ',', '}']), 0, false)?;
1436 Ok(self.classify_key_value(start, value))
1437 }
1438
1439 fn ensure_single_line_implicit_key(&self, start_index: usize) -> BackendResult<()> {
1440 let saw_line_break = self.items[start_index..self.index]
1441 .iter()
1442 .any(|item| item.char() == Some('\n'));
1443 if saw_line_break {
1444 return Err(BackendError::parse_at(
1445 "yaml.parse",
1446 "Implicit YAML keys must be on a single line.",
1447 Some(self.current().span().clone()),
1448 ));
1449 }
1450 Ok(())
1451 }
1452
1453 fn parse_double_quoted(
1454 &mut self,
1455 parent_indent: usize,
1456 requires_indented_continuation: bool,
1457 ) -> BackendResult<YamlDoubleQuotedScalarNode> {
1458 let start = self.mark();
1459 self.consume_char('"')?;
1460 let mut chunks = Vec::new();
1461 let mut buffer = String::new();
1462 loop {
1463 if self.current_kind() == "eof" {
1464 return Err(self.error("Unterminated YAML double-quoted scalar."));
1465 }
1466 if self.current_kind() == "interpolation" {
1467 self.flush_buffer(&mut buffer, &mut chunks);
1468 chunks.push(YamlChunk::Interpolation(
1469 self.consume_interpolation("string_fragment")?,
1470 ));
1471 continue;
1472 }
1473 if self.current_char() == Some('"') {
1474 self.flush_buffer(&mut buffer, &mut chunks);
1475 self.advance();
1476 break;
1477 }
1478 if matches!(self.current_char(), Some('\r' | '\n')) {
1479 buffer.push_str(&self.parse_quoted_line_folding(
1480 '"',
1481 parent_indent,
1482 requires_indented_continuation,
1483 )?);
1484 continue;
1485 }
1486 if self.current_char() == Some('\\') {
1487 buffer.push_str(&self.parse_double_quoted_escape()?);
1488 continue;
1489 }
1490 if let Some(ch) = self.current_char() {
1491 buffer.push(ch);
1492 self.advance();
1493 }
1494 }
1495 Ok(YamlDoubleQuotedScalarNode {
1496 span: self.span_from(start),
1497 chunks,
1498 })
1499 }
1500
1501 fn parse_quoted_line_folding(
1502 &mut self,
1503 terminator: char,
1504 parent_indent: usize,
1505 requires_indented_continuation: bool,
1506 ) -> BackendResult<String> {
1507 let mut breaks = 0usize;
1508 loop {
1509 if self.current_char() == Some('\r') {
1510 self.advance();
1511 }
1512 if self.current_char() != Some('\n') {
1513 return Err(self.error("Invalid YAML quoted-scalar line break."));
1514 }
1515 self.advance();
1516 breaks += 1;
1517 let mut indent = 0usize;
1518 while matches!(self.current_char(), Some(' ' | '\t')) {
1519 indent += 1;
1520 self.advance();
1521 }
1522 let has_required_indent = if requires_indented_continuation {
1523 indent > parent_indent
1524 } else {
1525 indent >= parent_indent
1526 };
1527 if !has_required_indent
1528 && !matches!(self.current_char(), Some('\r' | '\n'))
1529 && self.current_char() != Some(terminator)
1530 {
1531 return Err(self.error("Invalid YAML quoted-scalar line break."));
1532 }
1533 if !matches!(self.current_char(), Some('\r' | '\n')) {
1534 break;
1535 }
1536 }
1537 if breaks == 1 {
1538 Ok(" ".to_owned())
1539 } else {
1540 Ok("\n".repeat(breaks - 1))
1541 }
1542 }
1543
1544 fn parse_single_quoted(&mut self) -> BackendResult<YamlSingleQuotedScalarNode> {
1545 let start = self.mark();
1546 self.consume_char('\'')?;
1547 let mut chunks = Vec::new();
1548 let mut buffer = String::new();
1549 loop {
1550 if self.current_kind() == "eof" {
1551 return Err(self.error("Unterminated YAML single-quoted scalar."));
1552 }
1553 if self.current_kind() == "interpolation" {
1554 self.flush_buffer(&mut buffer, &mut chunks);
1555 chunks.push(YamlChunk::Interpolation(
1556 self.consume_interpolation("string_fragment")?,
1557 ));
1558 continue;
1559 }
1560 if self.current_char() == Some('\'') {
1561 if self.peek_char(1) == Some('\'') {
1562 buffer.push('\'');
1563 self.advance();
1564 self.advance();
1565 continue;
1566 }
1567 self.flush_buffer(&mut buffer, &mut chunks);
1568 self.advance();
1569 break;
1570 }
1571 if let Some(ch) = self.current_char() {
1572 buffer.push(ch);
1573 self.advance();
1574 }
1575 }
1576 Ok(YamlSingleQuotedScalarNode {
1577 span: self.span_from(start),
1578 chunks,
1579 })
1580 }
1581
1582 fn parse_plain_scalar(
1583 &mut self,
1584 stop_chars: Option<&[char]>,
1585 leading: Option<Vec<YamlChunk>>,
1586 key_mode: bool,
1587 parent_indent: usize,
1588 requires_indented_continuation: bool,
1589 ) -> BackendResult<YamlPlainScalarNode> {
1590 let start = self.mark();
1591 let mut chunks = leading.unwrap_or_default();
1592 let mut buffer = String::new();
1593 while self.current_kind() != "eof" {
1594 if self.current_kind() == "interpolation" {
1595 self.flush_buffer(&mut buffer, &mut chunks);
1596 chunks.push(YamlChunk::Interpolation(
1597 self.consume_interpolation("string_fragment")?,
1598 ));
1599 continue;
1600 }
1601 let Some(ch) = self.current_char() else {
1602 break;
1603 };
1604 if ch == '#' && buffer.is_empty() {
1605 return Err(self.error("Expected a YAML value."));
1606 }
1607 if ch == '#'
1608 && self
1609 .items
1610 .get(self.index.wrapping_sub(1))
1611 .and_then(StreamItem::char)
1612 .is_none_or(char::is_whitespace)
1613 {
1614 break;
1615 }
1616 if ch == '\n' {
1617 if !key_mode {
1618 if stop_chars.is_none()
1619 && self.consume_plain_scalar_continuation(
1620 &mut buffer,
1621 parent_indent,
1622 requires_indented_continuation,
1623 )
1624 {
1625 continue;
1626 }
1627 if let Some(stop_chars) = stop_chars {
1628 if self.consume_flow_plain_scalar_continuation(
1629 &mut buffer,
1630 stop_chars,
1631 parent_indent,
1632 ) {
1633 continue;
1634 }
1635 }
1636 }
1637 break;
1638 }
1639 if stop_chars.is_some_and(|chars| chars.contains(&ch)) {
1640 let colon_without_separator = ch == ':'
1641 && !matches!(self.peek_char(1), Some(' ' | '\t' | '\n' | ',' | ']' | '}'));
1642 if !colon_without_separator
1643 && (!key_mode || matches!(self.peek_char(1), Some(' ' | '\t' | '\n')))
1644 {
1645 break;
1646 }
1647 }
1648 if !key_mode
1649 && stop_chars.is_some()
1650 && ch == ':'
1651 && matches!(self.peek_char(1), Some(' ' | '\n' | ',' | ']' | '}'))
1652 {
1653 break;
1654 }
1655 if !key_mode
1656 && stop_chars.is_none()
1657 && ch == ':'
1658 && matches!(self.peek_char(1), Some(' ' | '\t'))
1659 {
1660 break;
1661 }
1662 if ch == '\t' && stop_chars.is_none() && buffer.contains(':') {
1663 return Err(self.error("Tabs are not allowed as YAML indentation."));
1664 }
1665 buffer.push(ch);
1666 self.advance();
1667 }
1668 self.flush_buffer(&mut buffer, &mut chunks);
1669 Ok(YamlPlainScalarNode {
1670 span: self.span_from(start),
1671 chunks,
1672 })
1673 }
1674
1675 fn consume_plain_scalar_continuation(
1676 &mut self,
1677 buffer: &mut String,
1678 parent_indent: usize,
1679 requires_indented_continuation: bool,
1680 ) -> bool {
1681 let mut probe = self.index;
1682 let mut blank_lines = 0usize;
1683 while self.items.get(probe).and_then(StreamItem::char) == Some('\n') {
1684 probe += 1;
1685 let mut indent = 0usize;
1686 while self.items.get(probe).and_then(StreamItem::char) == Some(' ') {
1687 indent += 1;
1688 probe += 1;
1689 }
1690 match self.items.get(probe).and_then(StreamItem::char) {
1691 Some('\n') => blank_lines += 1,
1692 Some('#') | None => return false,
1693 Some(_)
1694 if if requires_indented_continuation {
1695 indent > parent_indent
1696 } else {
1697 indent >= parent_indent
1698 } =>
1699 {
1700 if indent == parent_indent
1701 && (self.starts_sequence_item_at(probe)
1702 || self.line_has_mapping_key_at(probe))
1703 {
1704 return false;
1705 }
1706 self.index = probe;
1707 if blank_lines == 0 {
1708 if !buffer.ends_with(' ') && !buffer.is_empty() {
1709 buffer.push(' ');
1710 }
1711 } else {
1712 for _ in 0..blank_lines {
1713 buffer.push('\n');
1714 }
1715 }
1716 return true;
1717 }
1718 _ => return false,
1719 }
1720 }
1721 false
1722 }
1723
1724 fn consume_flow_plain_scalar_continuation(
1725 &mut self,
1726 buffer: &mut String,
1727 stop_chars: &[char],
1728 parent_indent: usize,
1729 ) -> bool {
1730 let mut probe = self.index;
1731 let mut breaks = 0usize;
1732 let continuation_indent;
1733
1734 loop {
1735 if self.items.get(probe).and_then(StreamItem::char) != Some('\n') {
1736 return false;
1737 }
1738 probe += 1;
1739 breaks += 1;
1740
1741 let mut indent = 0usize;
1742 while matches!(
1743 self.items.get(probe).and_then(StreamItem::char),
1744 Some(' ' | '\t')
1745 ) {
1746 indent += 1;
1747 probe += 1;
1748 }
1749
1750 if self.items.get(probe).and_then(StreamItem::char) == Some('#') {
1751 return false;
1752 }
1753
1754 if self.items.get(probe).and_then(StreamItem::char) == Some('\n') {
1755 continue;
1756 }
1757
1758 continuation_indent = indent;
1759 break;
1760 }
1761
1762 let Some(next_char) = self.items.get(probe).and_then(StreamItem::char) else {
1763 return false;
1764 };
1765 if continuation_indent < parent_indent {
1766 return false;
1767 }
1768 if stop_chars.contains(&next_char) {
1769 return false;
1770 }
1771
1772 self.index = probe;
1773 if breaks == 1 {
1774 if !buffer.ends_with(' ') && !buffer.is_empty() {
1775 buffer.push(' ');
1776 }
1777 } else {
1778 for _ in 0..(breaks - 1) {
1779 buffer.push('\n');
1780 }
1781 }
1782 true
1783 }
1784
1785 fn parse_block_scalar(&mut self, parent_indent: usize) -> BackendResult<YamlBlockScalarNode> {
1786 let start = self.mark();
1787 let style = self.current_char().unwrap_or('|').to_string();
1788 self.advance();
1789 let mut chomping = None;
1790 let mut indent_indicator = None;
1791 for _ in 0..2 {
1792 if chomping.is_none() && matches!(self.current_char(), Some('+' | '-')) {
1793 chomping = self.current_char().map(|ch| ch.to_string());
1794 self.advance();
1795 continue;
1796 }
1797 if indent_indicator.is_none()
1798 && self.current_char().is_some_and(|ch| ch.is_ascii_digit())
1799 {
1800 indent_indicator = self
1801 .current_char()
1802 .and_then(|ch| ch.to_digit(10))
1803 .map(|value| value as usize);
1804 self.advance();
1805 continue;
1806 }
1807 break;
1808 }
1809 self.consume_line_end_required()?;
1810 let block_indent = if let Some(indent_indicator) = indent_indicator {
1811 indent_indicator
1812 } else {
1813 self.infer_block_scalar_indent(parent_indent)?
1814 };
1815 let mut chunks = Vec::new();
1816 let mut buffer = String::new();
1817 while self.current_kind() != "eof" {
1818 if self.line_is_blank() {
1819 while self.current_char() == Some(' ') {
1820 self.advance();
1821 }
1822 if self.current_char() == Some('\n') {
1823 buffer.push('\n');
1824 self.advance();
1825 continue;
1826 }
1827 }
1828 if self.current_line_indent() < block_indent {
1829 break;
1830 }
1831 self.consume_indent(block_indent)?;
1832 while self.current_kind() != "eof" && self.current_char() != Some('\n') {
1833 if self.current_kind() == "interpolation" {
1834 self.flush_buffer(&mut buffer, &mut chunks);
1835 chunks.push(YamlChunk::Interpolation(
1836 self.consume_interpolation("string_fragment")?,
1837 ));
1838 continue;
1839 }
1840 if let Some(ch) = self.current_char() {
1841 buffer.push(ch);
1842 self.advance();
1843 }
1844 }
1845 if self.current_char() == Some('\n') {
1846 buffer.push('\n');
1847 self.advance();
1848 }
1849 }
1850 self.flush_buffer(&mut buffer, &mut chunks);
1851 Ok(YamlBlockScalarNode {
1852 span: self.span_from(start),
1853 style,
1854 chomping,
1855 indent_indicator,
1856 chunks,
1857 })
1858 }
1859
1860 fn infer_block_scalar_indent(&self, parent_indent: usize) -> BackendResult<usize> {
1861 let mut probe = self.index;
1862 let mut leading_blank_indent = 0usize;
1863 while probe < self.items.len() {
1864 let mut indent = 0usize;
1865 while self.items.get(probe).and_then(StreamItem::char) == Some(' ') {
1866 indent += 1;
1867 probe += 1;
1868 }
1869 match self.items.get(probe).and_then(StreamItem::char) {
1870 Some('\n') => {
1871 leading_blank_indent = leading_blank_indent.max(indent);
1872 probe += 1;
1873 }
1874 Some(_) => {
1875 let is_zero_indented_comment_like_content = parent_indent == 0
1876 && indent == 0
1877 && self.items.get(probe).and_then(StreamItem::char) == Some('#');
1878 if !is_zero_indented_comment_like_content && leading_blank_indent > indent {
1879 return Err(self.error("Incorrect YAML indentation."));
1880 }
1881 return Ok(if parent_indent == 0 {
1882 indent
1883 } else {
1884 indent.max(parent_indent + 1)
1885 });
1886 }
1887 None => break,
1888 }
1889 }
1890 if leading_blank_indent > parent_indent {
1891 return Err(self.error("Incorrect YAML indentation."));
1892 }
1893 if parent_indent == 0 {
1894 Ok(0)
1895 } else {
1896 Ok(parent_indent + 1)
1897 }
1898 }
1899
1900 fn line_is_blank(&self) -> bool {
1901 let mut probe = self.index;
1902 while self.items.get(probe).and_then(StreamItem::char) == Some(' ') {
1903 probe += 1;
1904 }
1905 matches!(
1906 self.items.get(probe),
1907 Some(StreamItem::Char { ch: '\n', .. }) | Some(StreamItem::Eof { .. })
1908 )
1909 }
1910
1911 fn flush_buffer(&self, buffer: &mut String, chunks: &mut Vec<YamlChunk>) {
1912 if buffer.is_empty() {
1913 return;
1914 }
1915 chunks.push(YamlChunk::Text(YamlTextChunkNode {
1916 span: SourceSpan::point(0, 0),
1917 value: std::mem::take(buffer),
1918 }));
1919 }
1920
1921 fn parse_double_quoted_escape(&mut self) -> BackendResult<String> {
1922 self.consume_char('\\')?;
1923 if self.current_char() == Some('\r') {
1924 self.advance();
1925 }
1926 if self.current_char() == Some('\n') {
1927 self.advance();
1928 while matches!(self.current_char(), Some(' ' | '\t')) {
1929 self.advance();
1930 }
1931 return Ok(String::new());
1932 }
1933 let escape = self
1934 .current_char()
1935 .ok_or_else(|| self.error("Incomplete YAML escape sequence."))?;
1936 self.advance();
1937 let mapped = match escape {
1938 '0' => Some("\0".to_owned()),
1939 'a' => Some("\u{0007}".to_owned()),
1940 'b' => Some("\u{0008}".to_owned()),
1941 't' | '\t' => Some("\t".to_owned()),
1942 'n' => Some("\n".to_owned()),
1943 'v' => Some("\u{000b}".to_owned()),
1944 'f' => Some("\u{000c}".to_owned()),
1945 'r' => Some("\r".to_owned()),
1946 'e' => Some("\u{001b}".to_owned()),
1947 ' ' => Some(" ".to_owned()),
1948 '"' => Some("\"".to_owned()),
1949 '/' => Some("/".to_owned()),
1950 '\\' => Some("\\".to_owned()),
1951 'N' => Some("\u{0085}".to_owned()),
1952 '_' => Some("\u{00a0}".to_owned()),
1953 'L' => Some("\u{2028}".to_owned()),
1954 'P' => Some("\u{2029}".to_owned()),
1955 _ => None,
1956 };
1957 if let Some(value) = mapped {
1958 return Ok(value);
1959 }
1960 let (digits, radix_name) = match escape {
1961 'x' => (2, "hex"),
1962 'u' => (4, "unicode"),
1963 'U' => (8, "unicode"),
1964 _ => {
1965 return Err(self.error("Invalid YAML escape sequence."));
1966 }
1967 };
1968 let chars = self.collect_exact_chars(digits)?;
1969 let codepoint = u32::from_str_radix(&chars, 16)
1970 .map_err(|_| self.error(format!("Invalid YAML {radix_name} escape.")))?;
1971 char::from_u32(codepoint)
1972 .map(|value| value.to_string())
1973 .ok_or_else(|| self.error("Invalid YAML unicode escape."))
1974 }
1975
1976 fn collect_exact_chars(&mut self, count: usize) -> BackendResult<String> {
1977 let mut chars = String::new();
1978 for _ in 0..count {
1979 let ch = self
1980 .current_char()
1981 .ok_or_else(|| self.error("Unexpected end of YAML escape sequence."))?;
1982 chars.push(ch);
1983 self.advance();
1984 }
1985 Ok(chars)
1986 }
1987
1988 fn consume_char(&mut self, expected: char) -> BackendResult<()> {
1989 if self.current_char() != Some(expected) {
1990 return Err(self.error(format!("Expected {expected:?} in YAML template.")));
1991 }
1992 self.advance();
1993 Ok(())
1994 }
1995
1996 fn consume_interpolation(&mut self, role: &str) -> BackendResult<YamlInterpolationNode> {
1997 let (interpolation_index, span) = match self.current() {
1998 StreamItem::Interpolation {
1999 interpolation_index,
2000 span,
2001 ..
2002 } => (*interpolation_index, span.clone()),
2003 _ => return Err(self.error("Expected an interpolation.")),
2004 };
2005 self.advance();
2006 Ok(YamlInterpolationNode {
2007 span,
2008 interpolation_index,
2009 role: role.to_owned(),
2010 })
2011 }
2012}
2013
2014#[derive(Clone, Debug)]
2015struct YamlDocumentFragment {
2016 directives: Vec<String>,
2017 explicit_start: bool,
2018 explicit_end: bool,
2019 items: Vec<StreamItem>,
2020}
2021
2022pub fn parse_template_with_profile(
2023 template: &TemplateInput,
2024 _profile: YamlProfile,
2025) -> BackendResult<YamlStreamNode> {
2026 let fragments = split_stream(template)?;
2029 let mut documents = Vec::new();
2030 for fragment in fragments {
2031 let mut parser = YamlParser::from_items(fragment.items);
2032 let mut document = parser.parse()?;
2033 document.directives = fragment.directives;
2034 document.explicit_start = fragment.explicit_start;
2035 document.explicit_end = fragment.explicit_end;
2036 documents.push(document);
2037 }
2038 Ok(YamlStreamNode {
2039 span: documents
2040 .first()
2041 .map(|document| document.span.clone())
2042 .unwrap_or_else(|| SourceSpan::point(0, 0)),
2043 documents,
2044 })
2045}
2046
2047pub fn parse_template(template: &TemplateInput) -> BackendResult<YamlStreamNode> {
2048 parse_template_with_profile(template, YamlProfile::default())
2049}
2050
2051pub fn parse_validated_template_with_profile(
2052 template: &TemplateInput,
2053 profile: YamlProfile,
2054) -> BackendResult<YamlStreamNode> {
2055 let stream = parse_template_with_profile(template, profile)?;
2056 validate_template_stream(&stream)?;
2057 Ok(stream)
2058}
2059
2060pub fn parse_validated_template(template: &TemplateInput) -> BackendResult<YamlStreamNode> {
2061 parse_validated_template_with_profile(template, YamlProfile::default())
2062}
2063
2064pub fn validate_template_with_profile(
2065 template: &TemplateInput,
2066 profile: YamlProfile,
2067) -> BackendResult<()> {
2068 let stream = parse_template_with_profile(template, profile)?;
2069 validate_template_stream(&stream)
2070}
2071
2072pub fn validate_template(template: &TemplateInput) -> BackendResult<()> {
2073 validate_template_with_profile(template, YamlProfile::default())
2074}
2075
2076pub fn check_template_with_profile(
2077 template: &TemplateInput,
2078 profile: YamlProfile,
2079) -> BackendResult<()> {
2080 validate_template_with_profile(template, profile)
2081}
2082
2083pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
2084 check_template_with_profile(template, YamlProfile::default())
2085}
2086
2087pub fn format_template_with_profile(
2088 template: &TemplateInput,
2089 profile: YamlProfile,
2090) -> BackendResult<String> {
2091 let stream = parse_validated_template_with_profile(template, profile)?;
2092 format_yaml_stream(template, &stream)
2093}
2094
2095pub fn format_template(template: &TemplateInput) -> BackendResult<String> {
2096 format_template_with_profile(template, YamlProfile::default())
2097}
2098
2099fn validate_template_stream(stream: &YamlStreamNode) -> BackendResult<()> {
2100 for document in &stream.documents {
2101 validate_value_node(&document.value)?;
2102 }
2103 Ok(())
2104}
2105
2106fn validate_value_node(node: &YamlValueNode) -> BackendResult<()> {
2107 match node {
2108 YamlValueNode::Scalar(YamlScalarNode::Plain(node)) => validate_plain_scalar_node(node),
2109 YamlValueNode::Mapping(node) => {
2110 for entry in &node.entries {
2111 validate_key_node(&entry.key)?;
2112 validate_value_node(&entry.value)?;
2113 }
2114 Ok(())
2115 }
2116 YamlValueNode::Sequence(node) => {
2117 for item in &node.items {
2118 validate_value_node(item)?;
2119 }
2120 Ok(())
2121 }
2122 YamlValueNode::Decorated(node) => validate_value_node(&node.value),
2123 YamlValueNode::Interpolation(_)
2124 | YamlValueNode::Scalar(
2125 YamlScalarNode::DoubleQuoted(_)
2126 | YamlScalarNode::SingleQuoted(_)
2127 | YamlScalarNode::Block(_)
2128 | YamlScalarNode::Alias(_),
2129 ) => Ok(()),
2130 }
2131}
2132
2133fn validate_key_node(node: &YamlKeyNode) -> BackendResult<()> {
2134 match &node.value {
2135 YamlKeyValue::Scalar(YamlScalarNode::Plain(node)) => validate_plain_scalar_node(node),
2136 YamlKeyValue::Complex(node) => validate_value_node(node),
2137 YamlKeyValue::Interpolation(_)
2138 | YamlKeyValue::Scalar(
2139 YamlScalarNode::DoubleQuoted(_)
2140 | YamlScalarNode::SingleQuoted(_)
2141 | YamlScalarNode::Block(_)
2142 | YamlScalarNode::Alias(_),
2143 ) => Ok(()),
2144 }
2145}
2146
2147fn validate_plain_scalar_node(node: &YamlPlainScalarNode) -> BackendResult<()> {
2148 let has_interpolation = node
2149 .chunks
2150 .iter()
2151 .any(|chunk| matches!(chunk, YamlChunk::Interpolation(_)));
2152 let has_whitespace_text = node.chunks.iter().any(|chunk| {
2153 matches!(chunk, YamlChunk::Text(text) if text.value.chars().any(char::is_whitespace))
2154 });
2155
2156 if has_interpolation && has_whitespace_text {
2157 return Err(BackendError::parse_at(
2158 "yaml.parse",
2159 "Quote YAML plain scalars that mix whitespace and interpolations.",
2160 Some(node.span.clone()),
2161 ));
2162 }
2163
2164 Ok(())
2165}
2166
2167pub fn normalize_documents_with_profile(
2168 documents: &[YamlOwned],
2169 _profile: YamlProfile,
2170) -> BackendResult<NormalizedStream> {
2171 documents
2174 .iter()
2175 .map(normalize_document)
2176 .collect::<BackendResult<Vec<_>>>()
2177 .map(NormalizedStream::new)
2178}
2179
2180pub fn normalize_documents(documents: &[YamlOwned]) -> BackendResult<NormalizedStream> {
2181 normalize_documents_with_profile(documents, YamlProfile::default())
2182}
2183
2184pub fn align_normalized_stream_with_ast(
2185 stream: &YamlStreamNode,
2186 normalized: &mut NormalizedStream,
2187) {
2188 for (document_node, document) in stream.documents.iter().zip(normalized.documents.iter_mut()) {
2189 align_document_with_ast(document_node, document);
2190 }
2191}
2192
2193#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2194enum CollectionRenderContext {
2195 BlockAllowed,
2196 FlowRequired,
2197}
2198
2199#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2200enum RenderedLayout {
2201 Inline,
2202 Block,
2203 Flow,
2204}
2205
2206struct RenderedYamlValue {
2207 text: String,
2208 layout: RenderedLayout,
2209 empty_collection: bool,
2210}
2211
2212impl RenderedYamlValue {
2213 fn inline(text: String) -> Self {
2214 Self {
2215 text,
2216 layout: RenderedLayout::Inline,
2217 empty_collection: false,
2218 }
2219 }
2220
2221 fn block(text: String) -> Self {
2222 Self {
2223 text,
2224 layout: RenderedLayout::Block,
2225 empty_collection: false,
2226 }
2227 }
2228
2229 fn flow(text: String, empty_collection: bool) -> Self {
2230 Self {
2231 text,
2232 layout: RenderedLayout::Flow,
2233 empty_collection,
2234 }
2235 }
2236}
2237
2238fn format_yaml_stream(template: &TemplateInput, stream: &YamlStreamNode) -> BackendResult<String> {
2239 let mut parts = Vec::with_capacity(stream.documents.len());
2240 for document in &stream.documents {
2241 let mut lines = Vec::new();
2242 lines.extend(document.directives.iter().cloned());
2243 if document.explicit_start || !document.directives.is_empty() || stream.documents.len() > 1
2244 {
2245 lines.push("---".to_owned());
2246 }
2247 lines.push(
2248 format_yaml_value(
2249 template,
2250 &document.value,
2251 0,
2252 CollectionRenderContext::BlockAllowed,
2253 )?
2254 .text,
2255 );
2256 if document.explicit_end {
2257 lines.push("...".to_owned());
2258 }
2259 parts.push(lines.join("\n"));
2260 }
2261 Ok(parts.join("\n"))
2262}
2263
2264fn format_yaml_value(
2265 template: &TemplateInput,
2266 node: &YamlValueNode,
2267 indent: usize,
2268 context: CollectionRenderContext,
2269) -> BackendResult<RenderedYamlValue> {
2270 match node {
2271 YamlValueNode::Decorated(node) => {
2272 let mut prefix = String::new();
2273 if let Some(tag) = &node.tag {
2274 prefix.push('!');
2275 prefix.push_str(&assemble_yaml_chunks(template, &tag.chunks, true)?);
2276 }
2277 if let Some(anchor) = &node.anchor {
2278 if !prefix.is_empty() {
2279 prefix.push(' ');
2280 }
2281 prefix.push('&');
2282 prefix.push_str(&assemble_yaml_chunks(template, &anchor.chunks, true)?);
2283 }
2284 let rendered = format_yaml_value(template, node.value.as_ref(), indent, context)?;
2285 if prefix.is_empty() {
2286 Ok(rendered)
2287 } else {
2288 Ok(apply_rendered_prefix(prefix, rendered))
2289 }
2290 }
2291 YamlValueNode::Mapping(node) => format_yaml_mapping(template, node, indent, context),
2292 YamlValueNode::Sequence(node) => format_yaml_sequence(template, node, indent, context),
2293 YamlValueNode::Interpolation(node) => Ok(RenderedYamlValue::inline(
2294 interpolation_raw_source(template, node.interpolation_index, &node.span, "YAML value")?,
2295 )),
2296 YamlValueNode::Scalar(YamlScalarNode::Alias(node)) => Ok(RenderedYamlValue::inline(
2297 format!("*{}", assemble_yaml_chunks(template, &node.chunks, true)?),
2298 )),
2299 YamlValueNode::Scalar(YamlScalarNode::Block(node)) => Ok(RenderedYamlValue::inline(
2300 format_yaml_block_scalar(template, node, indent)?,
2301 )),
2302 YamlValueNode::Scalar(YamlScalarNode::DoubleQuoted(node)) => Ok(RenderedYamlValue::inline(
2303 serde_json::to_string(&assemble_yaml_chunks(template, &node.chunks, false)?).unwrap(),
2304 )),
2305 YamlValueNode::Scalar(YamlScalarNode::SingleQuoted(node)) => {
2306 Ok(RenderedYamlValue::inline(format!(
2307 "'{}'",
2308 assemble_yaml_chunks(template, &node.chunks, false)?.replace('\'', "''")
2309 )))
2310 }
2311 YamlValueNode::Scalar(YamlScalarNode::Plain(node)) => Ok(RenderedYamlValue::inline(
2312 format_yaml_plain_scalar(template, node)?,
2313 )),
2314 }
2315}
2316
2317fn format_yaml_mapping(
2318 template: &TemplateInput,
2319 node: &YamlMappingNode,
2320 indent: usize,
2321 context: CollectionRenderContext,
2322) -> BackendResult<RenderedYamlValue> {
2323 if node.flow || context == CollectionRenderContext::FlowRequired {
2324 let mut entries = Vec::with_capacity(node.entries.len());
2325 for entry in &node.entries {
2326 let rendered_key = match &entry.key.value {
2327 YamlKeyValue::Complex(key) => {
2328 format_yaml_value(template, key, indent, CollectionRenderContext::FlowRequired)?
2329 .text
2330 }
2331 _ => format_yaml_key(template, &entry.key)?,
2332 };
2333 let rendered_key = normalize_flow_key_text(rendered_key);
2334 let rendered_value = format_yaml_value(
2335 template,
2336 &entry.value,
2337 indent,
2338 CollectionRenderContext::FlowRequired,
2339 )?;
2340 entries.push(format!("{rendered_key}: {}", rendered_value.text));
2341 }
2342 return Ok(RenderedYamlValue::flow(
2343 format!("{{ {} }}", entries.join(", ")),
2344 node.entries.is_empty(),
2345 ));
2346 }
2347
2348 let mut rendered = String::new();
2349 for entry in &node.entries {
2350 if let YamlKeyValue::Complex(key) = &entry.key.value {
2351 let rendered_key = format_yaml_value(
2352 template,
2353 key,
2354 indent + 2,
2355 CollectionRenderContext::FlowRequired,
2356 )?;
2357 let rendered_value = format_yaml_value(
2358 template,
2359 &entry.value,
2360 indent + 2,
2361 CollectionRenderContext::BlockAllowed,
2362 )?;
2363 push_yaml_section(
2364 &mut rendered,
2365 format!("{}? {}", " ".repeat(indent), rendered_key.text),
2366 );
2367 push_rendered_value_with_prefix(
2368 &mut rendered,
2369 format!("{}:", " ".repeat(indent)),
2370 rendered_value,
2371 );
2372 continue;
2373 }
2374 let key = format_yaml_key(template, &entry.key)?;
2375 let rendered_value = format_yaml_value(
2376 template,
2377 &entry.value,
2378 indent + 2,
2379 CollectionRenderContext::BlockAllowed,
2380 )?;
2381 push_rendered_value_with_prefix(
2382 &mut rendered,
2383 format!("{}{}:", " ".repeat(indent), key),
2384 rendered_value,
2385 );
2386 }
2387 Ok(RenderedYamlValue::block(rendered))
2388}
2389
2390fn format_yaml_sequence(
2391 template: &TemplateInput,
2392 node: &YamlSequenceNode,
2393 indent: usize,
2394 context: CollectionRenderContext,
2395) -> BackendResult<RenderedYamlValue> {
2396 if node.flow || context == CollectionRenderContext::FlowRequired {
2397 let items = node
2398 .items
2399 .iter()
2400 .map(|item| {
2401 format_yaml_value(
2402 template,
2403 item,
2404 indent,
2405 CollectionRenderContext::FlowRequired,
2406 )
2407 .map(|item| item.text)
2408 })
2409 .collect::<BackendResult<Vec<_>>>()?;
2410 return Ok(RenderedYamlValue::flow(
2411 format!("[ {} ]", items.join(", ")),
2412 node.items.is_empty(),
2413 ));
2414 }
2415
2416 let mut rendered = String::new();
2417 for item in &node.items {
2418 let rendered_item = format_yaml_value(
2419 template,
2420 item,
2421 indent + 2,
2422 CollectionRenderContext::BlockAllowed,
2423 )?;
2424 push_rendered_value_with_prefix(
2425 &mut rendered,
2426 format!("{}-", " ".repeat(indent)),
2427 rendered_item,
2428 );
2429 }
2430 Ok(RenderedYamlValue::block(rendered))
2431}
2432
2433fn format_yaml_key(template: &TemplateInput, node: &YamlKeyNode) -> BackendResult<String> {
2434 match &node.value {
2435 YamlKeyValue::Interpolation(value) => {
2436 interpolation_raw_source(template, value.interpolation_index, &value.span, "YAML key")
2437 }
2438 YamlKeyValue::Scalar(YamlScalarNode::DoubleQuoted(value)) => Ok(serde_json::to_string(
2439 &assemble_yaml_chunks(template, &value.chunks, false)?,
2440 )
2441 .unwrap()),
2442 YamlKeyValue::Scalar(YamlScalarNode::SingleQuoted(value)) => Ok(format!(
2443 "'{}'",
2444 assemble_yaml_chunks(template, &value.chunks, false)?.replace('\'', "''")
2445 )),
2446 YamlKeyValue::Scalar(YamlScalarNode::Plain(value)) => {
2447 format_yaml_plain_scalar(template, value)
2448 }
2449 YamlKeyValue::Scalar(YamlScalarNode::Alias(node)) => Ok(format!(
2450 "*{}",
2451 assemble_yaml_chunks(template, &node.chunks, true)?
2452 )),
2453 YamlKeyValue::Scalar(YamlScalarNode::Block(node)) => {
2454 format_yaml_block_scalar(template, node, 0)
2455 }
2456 YamlKeyValue::Complex(value) => {
2457 format_yaml_value(template, value, 0, CollectionRenderContext::FlowRequired)
2458 .map(|value| value.text)
2459 }
2460 }
2461}
2462
2463fn format_yaml_plain_scalar(
2464 template: &TemplateInput,
2465 node: &YamlPlainScalarNode,
2466) -> BackendResult<String> {
2467 let text = assemble_yaml_chunks(template, &node.chunks, false)?
2468 .trim()
2469 .to_owned();
2470 if text.is_empty() {
2471 return Ok("null".to_owned());
2472 }
2473 if text.contains('\n') {
2474 return Ok(serde_json::to_string(&text).unwrap());
2475 }
2476 Ok(text)
2477}
2478
2479fn format_yaml_block_scalar(
2480 template: &TemplateInput,
2481 node: &YamlBlockScalarNode,
2482 indent: usize,
2483) -> BackendResult<String> {
2484 let mut header = node.style.clone();
2485 if let Some(chomping) = &node.chomping {
2486 header.push_str(chomping);
2487 }
2488 if let Some(indent_indicator) = node.indent_indicator {
2489 header.push_str(&indent_indicator.to_string());
2490 }
2491 let content = assemble_yaml_chunks(template, &node.chunks, false)?;
2492 if content.is_empty() {
2493 let mut rendered = header;
2494 rendered.push('\n');
2495 return Ok(rendered);
2496 }
2497
2498 let indentation_width = node
2499 .indent_indicator
2500 .map_or(indent, |indicator| indent.saturating_sub(2) + indicator);
2501 let indentation = " ".repeat(indentation_width);
2502 let trailing_breaks = content.chars().rev().take_while(|&ch| ch == '\n').count();
2503 let body_content = content.trim_end_matches('\n');
2504 let body = if body_content.is_empty() {
2505 String::new()
2506 } else {
2507 body_content
2508 .split('\n')
2509 .map(|line| format!("{indentation}{line}"))
2510 .collect::<Vec<_>>()
2511 .join("\n")
2512 };
2513 let trailing = match node.chomping.as_deref() {
2514 Some("-") => 0,
2515 Some("+") => trailing_breaks.max(1),
2516 _ => usize::from(!content.is_empty()),
2517 };
2518
2519 let mut rendered = header;
2520 rendered.push('\n');
2521 rendered.push_str(&body);
2522 for _ in 0..trailing {
2523 rendered.push('\n');
2524 }
2525 Ok(rendered)
2526}
2527
2528fn assemble_yaml_chunks(
2529 template: &TemplateInput,
2530 chunks: &[YamlChunk],
2531 _metadata: bool,
2532) -> BackendResult<String> {
2533 let mut text = String::new();
2534 for chunk in chunks {
2535 match chunk {
2536 YamlChunk::Text(chunk) => text.push_str(&chunk.value),
2537 YamlChunk::Interpolation(chunk) => text.push_str(&interpolation_raw_source(
2538 template,
2539 chunk.interpolation_index,
2540 &chunk.span,
2541 "YAML fragment",
2542 )?),
2543 }
2544 }
2545 Ok(text)
2546}
2547
2548fn apply_rendered_prefix(prefix: String, rendered: RenderedYamlValue) -> RenderedYamlValue {
2549 if rendered.layout == RenderedLayout::Block {
2550 return RenderedYamlValue {
2551 text: format!("{prefix}\n{}", rendered.text),
2552 layout: RenderedLayout::Block,
2553 empty_collection: rendered.empty_collection,
2554 };
2555 }
2556 RenderedYamlValue {
2557 text: format!("{prefix} {}", rendered.text),
2558 layout: rendered.layout,
2559 empty_collection: rendered.empty_collection,
2560 }
2561}
2562
2563fn push_rendered_value_with_prefix(
2564 output: &mut String,
2565 prefix: String,
2566 rendered: RenderedYamlValue,
2567) {
2568 if rendered.layout == RenderedLayout::Block && !rendered.empty_collection {
2569 if let Some((first, rest)) = rendered.text.split_once('\n')
2570 && !first.starts_with(' ')
2571 {
2572 push_yaml_section(output, format!("{prefix} {first}"));
2573 if !rest.is_empty() {
2574 push_yaml_section(output, rest.to_owned());
2575 }
2576 return;
2577 }
2578 push_yaml_section(output, prefix);
2579 push_yaml_section(output, rendered.text);
2580 return;
2581 }
2582
2583 push_yaml_section(output, format!("{prefix} {}", rendered.text));
2584}
2585
2586fn push_yaml_section(output: &mut String, section: String) {
2587 if output.is_empty() {
2588 output.push_str(§ion);
2589 return;
2590 }
2591 if !output.ends_with('\n') {
2592 output.push('\n');
2593 }
2594 output.push_str(§ion);
2595}
2596
2597fn normalize_flow_key_text(rendered_key: String) -> String {
2598 if rendered_key.starts_with('?') && !rendered_key.starts_with("? ") {
2599 return format!("? {}", &rendered_key[1..]);
2600 }
2601 rendered_key
2602}
2603
2604fn interpolation_raw_source(
2605 template: &TemplateInput,
2606 interpolation_index: usize,
2607 span: &SourceSpan,
2608 context: &str,
2609) -> BackendResult<String> {
2610 template
2611 .interpolation_raw_source(interpolation_index)
2612 .map(str::to_owned)
2613 .ok_or_else(|| {
2614 let expression = template.interpolation(interpolation_index).map_or_else(
2615 || format!("slot {interpolation_index}"),
2616 |value| value.expression_label().to_owned(),
2617 );
2618 BackendError::semantic_at(
2619 "yaml.format",
2620 format!(
2621 "Cannot format {context} interpolation {expression:?} without raw source text."
2622 ),
2623 Some(span.clone()),
2624 )
2625 })
2626}
2627
2628fn normalize_document(document: &YamlOwned) -> BackendResult<NormalizedDocument> {
2629 if matches!(document, YamlOwned::BadValue) {
2630 return Ok(NormalizedDocument::Empty);
2631 }
2632 Ok(NormalizedDocument::Value(normalize_value(document)?))
2633}
2634
2635fn align_document_with_ast(node: &YamlDocumentNode, document: &mut NormalizedDocument) {
2636 if let NormalizedDocument::Value(value) = document {
2637 align_value_with_ast(&node.value, value);
2638 }
2639}
2640
2641fn normalize_value(value: &YamlOwned) -> BackendResult<NormalizedValue> {
2642 match value {
2643 YamlOwned::Value(value) => normalize_scalar(value),
2644 YamlOwned::Representation(value, style, tag) => {
2645 normalize_representation_value(value, *style, tag.as_ref())
2646 }
2647 YamlOwned::Sequence(values) => values
2648 .iter()
2649 .map(normalize_value)
2650 .collect::<BackendResult<Vec<_>>>()
2651 .map(NormalizedValue::Sequence),
2652 YamlOwned::Mapping(values) => normalize_mapping(values),
2653 YamlOwned::Tagged(tag, value) => {
2654 normalize_tagged_value(tag.handle.as_str(), &tag.suffix, value)
2655 }
2656 YamlOwned::Alias(_) => Err(BackendError::semantic(
2657 "Rendered YAML still contains an unresolved alias after validation.",
2658 )),
2659 YamlOwned::BadValue => Ok(NormalizedValue::Null),
2660 }
2661}
2662
2663fn normalize_representation_value(
2664 value: &str,
2665 style: ScalarStyle,
2666 tag: Option<&Tag>,
2667) -> BackendResult<NormalizedValue> {
2668 let parsed = ScalarOwned::parse_from_cow_and_metadata(
2669 Cow::Borrowed(value),
2670 style,
2671 tag.map(Cow::Borrowed).as_ref(),
2672 )
2673 .ok_or_else(|| {
2674 BackendError::semantic(format!(
2675 "Rendered YAML representation {value:?} could not be normalized with the active tag/schema."
2676 ))
2677 })?;
2678 normalize_scalar(&parsed)
2679}
2680
2681fn normalize_scalar(value: &saphyr::ScalarOwned) -> BackendResult<NormalizedValue> {
2682 match value {
2683 saphyr::ScalarOwned::Null => Ok(NormalizedValue::Null),
2684 saphyr::ScalarOwned::Boolean(value) => Ok(NormalizedValue::Bool(*value)),
2685 saphyr::ScalarOwned::Integer(value) => Ok(NormalizedValue::Integer((*value).into())),
2686 saphyr::ScalarOwned::FloatingPoint(value) => {
2687 let value = value.into_inner();
2688 if !value.is_finite() {
2689 return Err(BackendError::semantic(
2690 "Rendered YAML contained a non-finite float, which this backend does not normalize.",
2691 ));
2692 }
2693 Ok(NormalizedValue::Float(NormalizedFloat::finite(value)))
2694 }
2695 saphyr::ScalarOwned::String(value) => Ok(NormalizedValue::String(value.clone())),
2696 }
2697}
2698
2699fn normalize_mapping(values: &saphyr::MappingOwned) -> BackendResult<NormalizedValue> {
2700 let mut entries = Vec::new();
2701 for (key, value) in values {
2702 if is_merge_key(key) {
2703 apply_merge_entries(value, &mut entries)?;
2704 continue;
2705 }
2706 insert_mapping_entry(
2707 &mut entries,
2708 normalize_key(key)?,
2709 normalize_value(value)?,
2710 true,
2711 );
2712 }
2713 Ok(NormalizedValue::Mapping(entries))
2714}
2715
2716fn align_value_with_ast(node: &YamlValueNode, normalized: &mut NormalizedValue) {
2717 match node {
2718 YamlValueNode::Decorated(decorated) => {
2719 if decorated.tag.as_ref().is_some_and(is_set_tag_literal)
2720 && let NormalizedValue::Mapping(entries) = normalized
2721 {
2722 let keys = entries.iter().map(|entry| entry.key.clone()).collect();
2723 *normalized = NormalizedValue::Set(keys);
2724 return;
2725 }
2726 align_value_with_ast(&decorated.value, normalized);
2727 }
2728 YamlValueNode::Mapping(mapping) => {
2729 if let NormalizedValue::Mapping(entries) = normalized {
2730 for (entry_node, entry) in mapping.entries.iter().zip(entries.iter_mut()) {
2731 align_key_with_ast(mapping.flow, &entry_node.key, &mut entry.key);
2732 align_value_with_ast(&entry_node.value, &mut entry.value);
2733 }
2734 }
2735 }
2736 YamlValueNode::Sequence(sequence) => {
2737 if let NormalizedValue::Sequence(values) = normalized {
2738 for (item_node, value) in sequence.items.iter().zip(values.iter_mut()) {
2739 align_value_with_ast(item_node, value);
2740 }
2741 }
2742 }
2743 YamlValueNode::Scalar(_) | YamlValueNode::Interpolation(_) => {}
2744 }
2745}
2746
2747fn align_key_with_ast(flow_mapping: bool, key: &YamlKeyNode, normalized: &mut NormalizedKey) {
2748 match &key.value {
2749 YamlKeyValue::Scalar(YamlScalarNode::Plain(node))
2750 if flow_mapping && plain_scalar_starts_with_question(node) =>
2751 {
2752 if let NormalizedKey::String(value) = normalized
2753 && let Some(stripped) = value.strip_prefix('?')
2754 {
2755 *value = stripped.to_owned();
2756 }
2757 }
2758 YamlKeyValue::Complex(value) => align_key_value_with_ast(value, normalized),
2759 YamlKeyValue::Scalar(_) | YamlKeyValue::Interpolation(_) => {}
2760 }
2761}
2762
2763fn align_key_value_with_ast(node: &YamlValueNode, normalized: &mut NormalizedKey) {
2764 match node {
2765 YamlValueNode::Decorated(decorated) => {
2766 align_key_value_with_ast(&decorated.value, normalized)
2767 }
2768 YamlValueNode::Sequence(sequence) => {
2769 if let NormalizedKey::Sequence(keys) = normalized {
2770 for (item_node, key) in sequence.items.iter().zip(keys.iter_mut()) {
2771 align_key_value_with_ast(item_node, key);
2772 }
2773 }
2774 }
2775 YamlValueNode::Mapping(mapping) => {
2776 if let NormalizedKey::Mapping(entries) = normalized {
2777 for (entry_node, entry) in mapping.entries.iter().zip(entries.iter_mut()) {
2778 align_key_with_ast(mapping.flow, &entry_node.key, &mut entry.key);
2779 align_key_value_with_ast(&entry_node.value, &mut entry.value);
2780 }
2781 }
2782 }
2783 YamlValueNode::Scalar(_) | YamlValueNode::Interpolation(_) => {}
2784 }
2785}
2786
2787fn plain_scalar_starts_with_question(node: &YamlPlainScalarNode) -> bool {
2788 let Some(YamlChunk::Text(text)) = node.chunks.first() else {
2789 return false;
2790 };
2791 text.value.starts_with('?')
2792}
2793
2794fn is_set_tag_literal(tag: &YamlTagNode) -> bool {
2795 let mut literal = String::new();
2796 for chunk in &tag.chunks {
2797 let YamlChunk::Text(text) = chunk else {
2798 return false;
2799 };
2800 literal.push_str(&text.value);
2801 }
2802 matches!(
2803 literal.as_str(),
2804 "!set" | "!!set" | "<tag:yaml.org,2002:set>" | "tag:yaml.org,2002:set"
2805 )
2806}
2807
2808fn normalize_key(value: &YamlOwned) -> BackendResult<NormalizedKey> {
2809 match value {
2810 YamlOwned::Value(value) => match value {
2811 saphyr::ScalarOwned::Null => Ok(NormalizedKey::Null),
2812 saphyr::ScalarOwned::Boolean(value) => Ok(NormalizedKey::Bool(*value)),
2813 saphyr::ScalarOwned::Integer(value) => Ok(NormalizedKey::Integer((*value).into())),
2814 saphyr::ScalarOwned::FloatingPoint(value) => {
2815 let value = value.into_inner();
2816 if !value.is_finite() {
2817 return Err(BackendError::semantic(
2818 "Rendered YAML contained a non-finite float mapping key, which this backend does not normalize.",
2819 ));
2820 }
2821 Ok(NormalizedKey::Float(NormalizedFloat::finite(value)))
2822 }
2823 saphyr::ScalarOwned::String(value) => Ok(NormalizedKey::String(value.clone())),
2824 },
2825 YamlOwned::Representation(value, style, tag) => {
2826 normalize_representation_key(value, *style, tag.as_ref())
2827 }
2828 YamlOwned::Sequence(values) => values
2829 .iter()
2830 .map(normalize_key)
2831 .collect::<BackendResult<Vec<_>>>()
2832 .map(NormalizedKey::Sequence),
2833 YamlOwned::Mapping(values) => values
2834 .iter()
2835 .map(|(key, value)| {
2836 Ok(NormalizedKeyEntry {
2837 key: normalize_key(key)?,
2838 value: normalize_key(value)?,
2839 })
2840 })
2841 .collect::<BackendResult<Vec<_>>>()
2842 .map(NormalizedKey::Mapping),
2843 YamlOwned::Tagged(tag, value) => {
2844 normalize_tagged_key(tag.handle.as_str(), &tag.suffix, value)
2845 }
2846 YamlOwned::Alias(_) => Err(BackendError::semantic(
2847 "Rendered YAML still contains an unresolved alias after validation.",
2848 )),
2849 YamlOwned::BadValue => Ok(NormalizedKey::Null),
2850 }
2851}
2852
2853fn normalize_representation_key(
2854 value: &str,
2855 style: ScalarStyle,
2856 tag: Option<&Tag>,
2857) -> BackendResult<NormalizedKey> {
2858 let parsed = ScalarOwned::parse_from_cow_and_metadata(
2859 Cow::Borrowed(value),
2860 style,
2861 tag.map(Cow::Borrowed).as_ref(),
2862 )
2863 .ok_or_else(|| {
2864 BackendError::semantic(format!(
2865 "Rendered YAML key representation {value:?} could not be normalized with the active tag/schema."
2866 ))
2867 })?;
2868 match parsed {
2869 ScalarOwned::Null => Ok(NormalizedKey::Null),
2870 ScalarOwned::Boolean(value) => Ok(NormalizedKey::Bool(value)),
2871 ScalarOwned::Integer(value) => Ok(NormalizedKey::Integer(value.into())),
2872 ScalarOwned::FloatingPoint(value) => {
2873 let value = value.into_inner();
2874 if !value.is_finite() {
2875 return Err(BackendError::semantic(
2876 "Rendered YAML contained a non-finite float mapping key, which this backend does not normalize.",
2877 ));
2878 }
2879 Ok(NormalizedKey::Float(NormalizedFloat::finite(value)))
2880 }
2881 ScalarOwned::String(value) => Ok(NormalizedKey::String(value)),
2882 }
2883}
2884
2885fn normalize_tagged_value(
2886 handle: &str,
2887 suffix: &str,
2888 value: &YamlOwned,
2889) -> BackendResult<NormalizedValue> {
2890 if is_yaml_core_tag(handle, suffix, "set") {
2891 let YamlOwned::Mapping(values) = value else {
2892 return Err(BackendError::semantic(
2893 "YAML !!set nodes must normalize from a mapping value.",
2894 ));
2895 };
2896 return values
2897 .iter()
2898 .map(|(key, _)| normalize_key(key))
2899 .collect::<BackendResult<Vec<_>>>()
2900 .map(NormalizedValue::Set);
2901 }
2902 if let Some(normalized) = normalize_core_tagged_scalar_value(handle, suffix, value)? {
2903 return Ok(normalized);
2904 }
2905 normalize_value(value)
2906}
2907
2908fn normalize_tagged_key(
2909 handle: &str,
2910 suffix: &str,
2911 value: &YamlOwned,
2912) -> BackendResult<NormalizedKey> {
2913 if is_yaml_core_tag(handle, suffix, "set") {
2914 return Err(BackendError::semantic(
2915 "YAML set values cannot be used as mapping keys in normalized output.",
2916 ));
2917 }
2918 if let Some(normalized) = normalize_core_tagged_scalar_key(handle, suffix, value)? {
2919 return Ok(normalized);
2920 }
2921 normalize_key(value)
2922}
2923
2924fn normalize_core_tagged_scalar_value(
2925 handle: &str,
2926 suffix: &str,
2927 value: &YamlOwned,
2928) -> BackendResult<Option<NormalizedValue>> {
2929 if !matches!(suffix, "null" | "bool" | "int" | "float" | "str")
2930 || !matches!(handle, "" | "!!" | "tag:yaml.org,2002:")
2931 {
2932 return Ok(None);
2933 }
2934 if is_empty_tagged_scalar(value) {
2935 return Ok(match suffix {
2936 "null" => Some(NormalizedValue::Null),
2937 "str" => Some(NormalizedValue::String(String::new())),
2938 _ => None,
2939 });
2940 }
2941 let Some(text) = scalar_text_for_core_tagged_value(value) else {
2942 return Ok(None);
2943 };
2944 normalize_representation_value(
2945 text.as_ref(),
2946 ScalarStyle::Plain,
2947 Some(&Tag {
2948 handle: canonical_core_tag_handle(handle).to_owned(),
2949 suffix: suffix.to_owned(),
2950 }),
2951 )
2952 .map(Some)
2953}
2954
2955fn normalize_core_tagged_scalar_key(
2956 handle: &str,
2957 suffix: &str,
2958 value: &YamlOwned,
2959) -> BackendResult<Option<NormalizedKey>> {
2960 if !matches!(suffix, "null" | "bool" | "int" | "float" | "str")
2961 || !matches!(handle, "" | "!!" | "tag:yaml.org,2002:")
2962 {
2963 return Ok(None);
2964 }
2965 if is_empty_tagged_scalar(value) {
2966 return Ok(match suffix {
2967 "null" => Some(NormalizedKey::Null),
2968 "str" => Some(NormalizedKey::String(String::new())),
2969 _ => None,
2970 });
2971 }
2972 let Some(text) = scalar_text_for_core_tagged_value(value) else {
2973 return Ok(None);
2974 };
2975 normalize_representation_key(
2976 text.as_ref(),
2977 ScalarStyle::Plain,
2978 Some(&Tag {
2979 handle: canonical_core_tag_handle(handle).to_owned(),
2980 suffix: suffix.to_owned(),
2981 }),
2982 )
2983 .map(Some)
2984}
2985
2986fn canonical_core_tag_handle(handle: &str) -> &str {
2987 match handle {
2988 "" | "!!" | "tag:yaml.org,2002:" => "tag:yaml.org,2002:",
2989 other => other,
2990 }
2991}
2992
2993fn scalar_text_for_tagged_value(value: &YamlOwned) -> Option<Cow<'_, str>> {
2994 match value {
2995 YamlOwned::Representation(value, _, _) => Some(Cow::Borrowed(value.as_str())),
2996 YamlOwned::Value(ScalarOwned::Null) => Some(Cow::Borrowed("null")),
2997 YamlOwned::Value(ScalarOwned::Boolean(value)) => {
2998 Some(Cow::Borrowed(if *value { "true" } else { "false" }))
2999 }
3000 YamlOwned::Value(ScalarOwned::Integer(value)) => Some(Cow::Owned(value.to_string())),
3001 YamlOwned::Value(ScalarOwned::FloatingPoint(value)) => {
3002 Some(Cow::Owned(value.into_inner().to_string()))
3003 }
3004 YamlOwned::Value(ScalarOwned::String(value)) => Some(Cow::Borrowed(value.as_str())),
3005 YamlOwned::Tagged(_, value) => scalar_text_for_tagged_value(value),
3006 YamlOwned::Sequence(_)
3007 | YamlOwned::Mapping(_)
3008 | YamlOwned::Alias(_)
3009 | YamlOwned::BadValue => None,
3010 }
3011}
3012
3013fn scalar_text_for_core_tagged_value(value: &YamlOwned) -> Option<Cow<'_, str>> {
3014 match value {
3015 YamlOwned::Value(ScalarOwned::Null) => Some(Cow::Borrowed("")),
3016 YamlOwned::Tagged(_, value) => scalar_text_for_core_tagged_value(value),
3017 other => scalar_text_for_tagged_value(other),
3018 }
3019}
3020
3021fn is_empty_tagged_scalar(value: &YamlOwned) -> bool {
3022 match value {
3023 YamlOwned::Value(ScalarOwned::Null) => true,
3024 YamlOwned::Representation(value, _, _) => value.is_empty(),
3025 YamlOwned::Tagged(_, value) => is_empty_tagged_scalar(value),
3026 _ => false,
3027 }
3028}
3029
3030fn is_yaml_core_tag(handle: &str, suffix: &str, expected_suffix: &str) -> bool {
3031 suffix == expected_suffix && matches!(handle, "" | "!!" | "tag:yaml.org,2002:")
3032}
3033
3034fn is_merge_key(value: &YamlOwned) -> bool {
3035 match value {
3036 YamlOwned::Value(saphyr::ScalarOwned::String(value)) => value == "<<",
3037 YamlOwned::Representation(value, _, _) => value == "<<",
3038 YamlOwned::Tagged(_, value) => is_merge_key(value),
3039 _ => false,
3040 }
3041}
3042
3043fn apply_merge_entries(value: &YamlOwned, entries: &mut Vec<NormalizedEntry>) -> BackendResult<()> {
3044 match value {
3045 YamlOwned::Mapping(values) => merge_mapping_entries(values, entries),
3046 YamlOwned::Sequence(values) => {
3047 for value in values {
3048 let YamlOwned::Mapping(values) = value else {
3049 return Err(BackendError::semantic(
3050 "YAML merge sequences must contain only mappings.",
3051 ));
3052 };
3053 merge_mapping_entries(values, entries)?;
3054 }
3055 Ok(())
3056 }
3057 YamlOwned::Tagged(_, value) => apply_merge_entries(value, entries),
3058 _ => Err(BackendError::semantic(
3059 "YAML merge values must be a mapping or sequence of mappings.",
3060 )),
3061 }
3062}
3063
3064fn merge_mapping_entries(
3065 values: &saphyr::MappingOwned,
3066 entries: &mut Vec<NormalizedEntry>,
3067) -> BackendResult<()> {
3068 for (key, value) in values {
3069 insert_mapping_entry(entries, normalize_key(key)?, normalize_value(value)?, false);
3070 }
3071 Ok(())
3072}
3073
3074fn insert_mapping_entry(
3075 entries: &mut Vec<NormalizedEntry>,
3076 key: NormalizedKey,
3077 value: NormalizedValue,
3078 override_existing: bool,
3079) {
3080 if let Some(existing) = entries.iter_mut().find(|entry| entry.key == key) {
3081 if override_existing {
3082 existing.value = value;
3083 }
3084 return;
3085 }
3086 entries.push(NormalizedEntry { key, value });
3087}
3088
3089fn wrap_decorators(
3090 decorated: Option<(Option<YamlTagNode>, Option<YamlAnchorNode>)>,
3091 value: YamlValueNode,
3092) -> BackendResult<YamlValueNode> {
3093 if let Some((tag, anchor)) = decorated {
3094 let decorator_span = tag
3095 .as_ref()
3096 .map(|tag| tag.span.clone())
3097 .or_else(|| anchor.as_ref().map(|anchor| anchor.span.clone()))
3098 .unwrap_or_else(|| value_span(&value).clone());
3099 let span = decorator_span.merge(value_span(&value));
3100
3101 match value {
3102 YamlValueNode::Decorated(inner) => {
3103 if anchor.is_some() && inner.anchor.is_some() {
3104 return Err(BackendError::parse_at(
3105 "yaml.parse",
3106 "YAML nodes cannot define more than one anchor.",
3107 Some(span),
3108 ));
3109 }
3110 if tag.is_some() && inner.tag.is_some() {
3111 return Err(BackendError::parse_at(
3112 "yaml.parse",
3113 "YAML nodes cannot define more than one tag.",
3114 Some(span),
3115 ));
3116 }
3117
3118 Ok(YamlValueNode::Decorated(YamlDecoratedNode {
3119 span,
3120 value: inner.value,
3121 tag: tag.or(inner.tag),
3122 anchor: anchor.or(inner.anchor),
3123 }))
3124 }
3125 YamlValueNode::Scalar(YamlScalarNode::Alias(_))
3126 if tag.is_some() || anchor.is_some() =>
3127 {
3128 Err(BackendError::parse_at(
3129 "yaml.parse",
3130 "YAML aliases cannot define tags or anchors.",
3131 Some(span),
3132 ))
3133 }
3134 value => Ok(YamlValueNode::Decorated(YamlDecoratedNode {
3135 span,
3136 value: Box::new(value),
3137 tag,
3138 anchor,
3139 })),
3140 }
3141 } else {
3142 Ok(value)
3143 }
3144}
3145
3146fn null_plain_scalar() -> YamlPlainScalarNode {
3147 YamlPlainScalarNode {
3148 span: SourceSpan::point(0, 0),
3149 chunks: vec![YamlChunk::Text(YamlTextChunkNode {
3150 span: SourceSpan::point(0, 0),
3151 value: "null".to_owned(),
3152 })],
3153 }
3154}
3155
3156fn empty_plain_scalar() -> YamlPlainScalarNode {
3157 YamlPlainScalarNode {
3158 span: SourceSpan::point(0, 0),
3159 chunks: Vec::new(),
3160 }
3161}
3162
3163fn flow_implicit_plain_key_needs_separator(key: &YamlKeyNode) -> bool {
3164 matches!(
3165 &key.value,
3166 YamlKeyValue::Scalar(YamlScalarNode::Plain(node))
3167 if node.chunks.iter().any(|chunk| match chunk {
3168 YamlChunk::Text(text) => text.value.chars().any(char::is_whitespace),
3169 YamlChunk::Interpolation(_) => true,
3170 })
3171 )
3172}
3173
3174fn split_stream(template: &TemplateInput) -> BackendResult<Vec<YamlDocumentFragment>> {
3175 let items = template.flatten();
3176 let mut fragments = Vec::new();
3177 let mut directives = Vec::new();
3178 let mut current_start = None;
3179 let mut explicit_start = false;
3180 let mut explicit_end = false;
3181 let mut line_start = 0;
3182
3183 while line_start < items.len() {
3184 let (line_end, next_line) = line_bounds(&items, line_start);
3185 let line = collect_line(&items[line_start..line_end]);
3186 let trimmed = line.trim_end_matches(['\r', '\n']);
3187 let document_start_payload = document_start_payload_start(&items, line_start, line_end);
3188 let document_end = is_document_end_marker(trimmed);
3189 let malformed_document_end = trimmed.starts_with("...") && !document_end;
3190
3191 if malformed_document_end {
3192 let span = items
3193 .get(line_start)
3194 .map(|item| item.span().clone())
3195 .unwrap_or_else(|| SourceSpan::point(0, 0));
3196 return Err(BackendError::parse_at(
3197 "yaml.parse",
3198 "Unexpected trailing YAML content.",
3199 Some(span),
3200 ));
3201 }
3202
3203 if current_start.is_none() {
3204 if trimmed.is_empty() || trimmed.starts_with('#') {
3205 line_start = next_line;
3206 continue;
3207 }
3208 if trimmed.starts_with('%') {
3209 if !is_valid_directive(trimmed) {
3210 let span = items
3211 .get(line_start)
3212 .map(|item| item.span().clone())
3213 .unwrap_or_else(|| SourceSpan::point(0, 0));
3214 return Err(BackendError::parse_at(
3215 "yaml.parse",
3216 "Unexpected trailing YAML content.",
3217 Some(span),
3218 ));
3219 }
3220 if duplicates_existing_directive(&directives, trimmed) {
3221 let span = items
3222 .get(line_start)
3223 .map(|item| item.span().clone())
3224 .unwrap_or_else(|| SourceSpan::point(0, 0));
3225 return Err(BackendError::parse_at(
3226 "yaml.parse",
3227 "Unexpected trailing YAML content.",
3228 Some(span),
3229 ));
3230 }
3231 directives.push(trimmed.to_owned());
3232 line_start = next_line;
3233 continue;
3234 }
3235 if trimmed == "---" {
3236 current_start = Some(next_line);
3237 explicit_start = true;
3238 line_start = next_line;
3239 continue;
3240 }
3241 if let Some(payload_start) = document_start_payload {
3242 if explicit_start_payload_looks_like_block_mapping(trimmed) {
3243 let span = items
3244 .get(line_start)
3245 .map(|item| item.span().clone())
3246 .unwrap_or_else(|| SourceSpan::point(0, 0));
3247 return Err(BackendError::parse_at(
3248 "yaml.parse",
3249 "Unexpected trailing YAML content.",
3250 Some(span),
3251 ));
3252 }
3253 current_start = Some(payload_start.unwrap_or(next_line));
3254 explicit_start = true;
3255 line_start = next_line;
3256 continue;
3257 }
3258 if document_end {
3259 line_start = next_line;
3260 continue;
3261 }
3262 current_start = Some(line_start);
3263 } else if trimmed == "---" {
3264 fragments.push(build_fragment(
3265 &items,
3266 current_start.unwrap_or(line_start),
3267 line_start,
3268 std::mem::take(&mut directives),
3269 explicit_start,
3270 explicit_end,
3271 ));
3272 current_start = Some(next_line);
3273 explicit_start = true;
3274 explicit_end = false;
3275 line_start = next_line;
3276 continue;
3277 } else if let Some(payload_start) = document_start_payload {
3278 fragments.push(build_fragment(
3279 &items,
3280 current_start.unwrap_or(line_start),
3281 line_start,
3282 std::mem::take(&mut directives),
3283 explicit_start,
3284 explicit_end,
3285 ));
3286 current_start = Some(payload_start.unwrap_or(next_line));
3287 explicit_start = true;
3288 explicit_end = false;
3289 line_start = next_line;
3290 continue;
3291 } else if document_end {
3292 fragments.push(build_fragment(
3293 &items,
3294 current_start.unwrap_or(line_start),
3295 line_start,
3296 std::mem::take(&mut directives),
3297 explicit_start,
3298 true,
3299 ));
3300 current_start = None;
3301 explicit_start = false;
3302 explicit_end = false;
3303 line_start = next_line;
3304 continue;
3305 }
3306
3307 line_start = next_line;
3308 }
3309
3310 if let Some(start) = current_start {
3311 fragments.push(build_fragment(
3312 &items,
3313 start,
3314 items.len().saturating_sub(1),
3315 std::mem::take(&mut directives),
3316 explicit_start,
3317 explicit_end,
3318 ));
3319 }
3320
3321 if current_start.is_none() && !directives.is_empty() {
3322 let span = items
3323 .iter()
3324 .rev()
3325 .find(|item| item.kind() != "eof")
3326 .map(|item| item.span().clone())
3327 .unwrap_or_else(|| SourceSpan::point(0, 0));
3328 return Err(BackendError::parse_at(
3329 "yaml.parse",
3330 "Unexpected trailing YAML content.",
3331 Some(span),
3332 ));
3333 }
3334
3335 if fragments.is_empty() {
3336 fragments.push(YamlDocumentFragment {
3337 directives: Vec::new(),
3338 explicit_start: false,
3339 explicit_end: false,
3340 items: vec![StreamItem::Eof {
3341 span: SourceSpan::point(0, 0),
3342 }],
3343 });
3344 }
3345
3346 Ok(fragments)
3347}
3348
3349fn build_fragment(
3350 items: &[StreamItem],
3351 start: usize,
3352 end: usize,
3353 directives: Vec<String>,
3354 explicit_start: bool,
3355 explicit_end: bool,
3356) -> YamlDocumentFragment {
3357 let mut fragment_items = items[start..end].to_vec();
3358 let eof_span = fragment_items
3359 .last()
3360 .map_or_else(|| SourceSpan::point(0, 0), |item| item.span().clone());
3361 fragment_items.push(StreamItem::Eof { span: eof_span });
3362 YamlDocumentFragment {
3363 directives,
3364 explicit_start,
3365 explicit_end,
3366 items: fragment_items,
3367 }
3368}
3369
3370fn line_bounds(items: &[StreamItem], start: usize) -> (usize, usize) {
3371 let mut probe = start;
3372 while probe < items.len() {
3373 if items[probe].char() == Some('\n') {
3374 return (probe, probe + 1);
3375 }
3376 if items[probe].kind() == "eof" {
3377 return (probe, probe + 1);
3378 }
3379 probe += 1;
3380 }
3381 (items.len(), items.len())
3382}
3383
3384fn document_start_payload_start(
3385 items: &[StreamItem],
3386 line_start: usize,
3387 line_end: usize,
3388) -> Option<Option<usize>> {
3389 let mut probe = line_start;
3390 for expected in ['-', '-', '-'] {
3391 if items.get(probe).and_then(StreamItem::char) != Some(expected) {
3392 return None;
3393 }
3394 probe += 1;
3395 }
3396 if probe >= line_end {
3397 return Some(None);
3398 }
3399 if !matches!(
3400 items.get(probe).and_then(StreamItem::char),
3401 Some(' ' | '\t')
3402 ) {
3403 return None;
3404 }
3405 while probe < line_end
3406 && matches!(
3407 items.get(probe).and_then(StreamItem::char),
3408 Some(' ' | '\t')
3409 )
3410 {
3411 probe += 1;
3412 }
3413 if probe >= line_end || items.get(probe).and_then(StreamItem::char) == Some('#') {
3414 return Some(None);
3415 }
3416 Some(Some(probe))
3417}
3418
3419fn is_document_end_marker(trimmed: &str) -> bool {
3420 if !trimmed.starts_with("...") {
3421 return false;
3422 }
3423 let Some(remainder) = trimmed.get(3..) else {
3424 return true;
3425 };
3426 let remainder = remainder.trim_start_matches([' ', '\t']);
3427 remainder.is_empty() || remainder.starts_with('#')
3428}
3429
3430fn explicit_start_payload_looks_like_block_mapping(trimmed: &str) -> bool {
3431 let Some(rest) = trimmed.strip_prefix("---") else {
3432 return false;
3433 };
3434 let rest = rest.trim_start_matches([' ', '\t']);
3435 if !(rest.starts_with('&') || rest.starts_with('!')) {
3436 return false;
3437 }
3438 if rest.starts_with('[') || rest.starts_with('{') {
3439 return false;
3440 }
3441 let Some((before_colon, after_colon)) = rest.split_once(':') else {
3442 return false;
3443 };
3444 !before_colon.contains('[')
3445 && !before_colon.contains('{')
3446 && !before_colon.ends_with(':')
3447 && matches!(after_colon.chars().next(), Some(' ' | '\t') | None)
3448}
3449
3450fn is_valid_directive(trimmed: &str) -> bool {
3451 let directive = trimmed
3452 .split_once('#')
3453 .map(|(directive, _)| directive)
3454 .unwrap_or(trimmed)
3455 .trim_end();
3456 let mut parts = directive.split_whitespace();
3457 match parts.next() {
3458 Some("%YAML") => matches!(
3459 (parts.next(), parts.next(), parts.next()),
3460 (Some(_version), None, None)
3461 ),
3462 Some("%TAG") => matches!(
3463 (parts.next(), parts.next(), parts.next(), parts.next()),
3464 (Some(_handle), Some(_prefix), None, None)
3465 ),
3466 Some(_) => true,
3467 None => false,
3468 }
3469}
3470
3471fn duplicates_existing_directive(existing: &[String], candidate: &str) -> bool {
3472 let Some((name, handle)) = directive_identity(candidate) else {
3473 return false;
3474 };
3475 existing.iter().any(|directive| {
3476 directive_identity(directive).is_some_and(|(existing_name, existing_handle)| {
3477 existing_name == name && existing_handle == handle
3478 })
3479 })
3480}
3481
3482fn directive_identity(trimmed: &str) -> Option<(&str, Option<&str>)> {
3483 let directive = trimmed
3484 .split_once('#')
3485 .map(|(directive, _)| directive)
3486 .unwrap_or(trimmed)
3487 .trim_end();
3488 let mut parts = directive.split_whitespace();
3489 match parts.next() {
3490 Some("%YAML") => Some(("%YAML", None)),
3491 Some("%TAG") => parts.next().map(|handle| ("%TAG", Some(handle))),
3492 _ => None,
3493 }
3494}
3495
3496fn collect_line(items: &[StreamItem]) -> String {
3497 let mut line = String::new();
3498 for item in items {
3499 if let Some(ch) = item.char() {
3500 line.push(ch);
3501 } else {
3502 line.push('\u{fffc}');
3503 }
3504 }
3505 line
3506}
3507
3508fn value_span(value: &YamlValueNode) -> &SourceSpan {
3509 match value {
3510 YamlValueNode::Scalar(YamlScalarNode::Plain(node)) => &node.span,
3511 YamlValueNode::Scalar(YamlScalarNode::DoubleQuoted(node)) => &node.span,
3512 YamlValueNode::Scalar(YamlScalarNode::SingleQuoted(node)) => &node.span,
3513 YamlValueNode::Scalar(YamlScalarNode::Block(node)) => &node.span,
3514 YamlValueNode::Scalar(YamlScalarNode::Alias(node)) => &node.span,
3515 YamlValueNode::Interpolation(node) => &node.span,
3516 YamlValueNode::Mapping(node) => &node.span,
3517 YamlValueNode::Sequence(node) => &node.span,
3518 YamlValueNode::Decorated(node) => &node.span,
3519 }
3520}
3521
3522#[cfg(test)]
3523mod tests {
3524 use super::{YamlValueNode, parse_template};
3525 use pyo3::prelude::*;
3526 use saphyr::{LoadableYamlNode, ScalarOwned, YamlOwned};
3527 use tstring_pyo3_bindings::{extract_template, yaml::render_document};
3528 use tstring_syntax::{BackendError, BackendResult, ErrorKind};
3529
3530 fn parse_rendered_yaml(text: &str) -> BackendResult<Vec<YamlOwned>> {
3531 YamlOwned::load_from_str(text).map_err(|err| {
3532 BackendError::parse(format!(
3533 "Rendered YAML could not be reparsed during test verification: {err}"
3534 ))
3535 })
3536 }
3537
3538 fn yaml_scalar_text(value: &YamlOwned) -> Option<&str> {
3539 match value {
3540 YamlOwned::Value(value) => value.as_str(),
3541 YamlOwned::Representation(value, _, _) => Some(value.as_str()),
3542 YamlOwned::Tagged(_, value) => yaml_scalar_text(value),
3543 _ => None,
3544 }
3545 }
3546
3547 fn yaml_integer(value: &YamlOwned) -> Option<i64> {
3548 match value {
3549 YamlOwned::Value(value) => value.as_integer(),
3550 YamlOwned::Tagged(_, value) => yaml_integer(value),
3551 _ => None,
3552 }
3553 }
3554
3555 fn yaml_float(value: &YamlOwned) -> Option<f64> {
3556 match value {
3557 YamlOwned::Value(ScalarOwned::FloatingPoint(value)) => Some(value.into_inner()),
3558 YamlOwned::Tagged(_, value) => yaml_float(value),
3559 _ => None,
3560 }
3561 }
3562
3563 fn yaml_sequence_len(value: &YamlOwned) -> Option<usize> {
3564 match value {
3565 YamlOwned::Sequence(value) => Some(value.len()),
3566 YamlOwned::Tagged(_, value) => yaml_sequence_len(value),
3567 _ => None,
3568 }
3569 }
3570
3571 fn yaml_mapping(value: &YamlOwned) -> Option<&saphyr::MappingOwned> {
3572 match value {
3573 YamlOwned::Mapping(value) => Some(value),
3574 YamlOwned::Tagged(_, value) => yaml_mapping(value),
3575 _ => None,
3576 }
3577 }
3578
3579 fn assert_string_entry(document: &YamlOwned, key: &str, expected: &str) {
3580 let mapping = yaml_mapping(document).expect("expected YAML mapping");
3581 let value = mapping
3582 .iter()
3583 .find_map(|(entry_key, entry_value)| {
3584 (yaml_scalar_text(entry_key) == Some(key)).then_some(entry_value)
3585 })
3586 .expect("expected YAML mapping entry");
3587 assert_eq!(yaml_scalar_text(value), Some(expected));
3588 }
3589
3590 fn assert_integer_entry(document: &YamlOwned, key: &str, expected: i64) {
3591 let mapping = yaml_mapping(document).expect("expected YAML mapping");
3592 let value = mapping
3593 .iter()
3594 .find_map(|(entry_key, entry_value)| {
3595 (yaml_scalar_text(entry_key) == Some(key)).then_some(entry_value)
3596 })
3597 .expect("expected YAML mapping entry");
3598 assert_eq!(yaml_integer(value), Some(expected));
3599 }
3600
3601 fn yaml_mapping_entry<'a>(document: &'a YamlOwned, key: &str) -> Option<&'a YamlOwned> {
3602 yaml_mapping(document).and_then(|mapping| {
3603 mapping.iter().find_map(|(entry_key, entry_value)| {
3604 (yaml_scalar_text(entry_key) == Some(key)).then_some(entry_value)
3605 })
3606 })
3607 }
3608
3609 #[test]
3610 fn parses_yaml_flow_and_scalar_nodes() {
3611 Python::with_gil(|py| {
3612 let module = PyModule::from_code(
3613 py,
3614 pyo3::ffi::c_str!(
3615 "user='Alice'\ntemplate=t'name: \"hi-{user}\"\\nitems: [1, {user}]'\n"
3616 ),
3617 pyo3::ffi::c_str!("test_yaml.py"),
3618 pyo3::ffi::c_str!("test_yaml"),
3619 )
3620 .unwrap();
3621 let template = module.getattr("template").unwrap();
3622 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3623 let stream = parse_template(&template).unwrap();
3624 let YamlValueNode::Mapping(mapping) = &stream.documents[0].value else {
3625 panic!("expected mapping");
3626 };
3627 assert_eq!(mapping.entries.len(), 2);
3628 });
3629 }
3630
3631 #[test]
3632 fn parses_tags_and_anchors_on_scalars() {
3633 Python::with_gil(|py| {
3634 let module = PyModule::from_code(
3635 py,
3636 pyo3::ffi::c_str!(
3637 "tag='str'\nanchor='user'\ntemplate=t'value: !{tag} &{anchor} \"hi\"\\n'\n"
3638 ),
3639 pyo3::ffi::c_str!("test_yaml_decorators.py"),
3640 pyo3::ffi::c_str!("test_yaml_decorators"),
3641 )
3642 .unwrap();
3643 let template = module.getattr("template").unwrap();
3644 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3645 let stream = parse_template(&template).unwrap();
3646 let YamlValueNode::Mapping(mapping) = &stream.documents[0].value else {
3647 panic!("expected mapping");
3648 };
3649 let YamlValueNode::Decorated(node) = &mapping.entries[0].value else {
3650 panic!("expected decorated scalar value");
3651 };
3652 assert!(node.tag.is_some());
3653 assert!(node.anchor.is_some());
3654 });
3655 }
3656
3657 #[test]
3658 fn renders_nested_yaml_values_and_validates() {
3659 Python::with_gil(|py| {
3660 let module = PyModule::from_code(
3661 py,
3662 pyo3::ffi::c_str!(
3663 "name='Ada'\nmeta={'active': True, 'count': 2}\ntemplate=t'name: {name}\\nmeta: {meta}\\n'\n"
3664 ),
3665 pyo3::ffi::c_str!("test_yaml_render.py"),
3666 pyo3::ffi::c_str!("test_yaml_render"),
3667 )
3668 .unwrap();
3669 let template = module.getattr("template").unwrap();
3670 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3671 let stream = parse_template(&template).unwrap();
3672 let rendered = render_document(py, &stream).unwrap();
3673 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3674
3675 assert!(rendered.text.contains("name: \"Ada\""));
3676 assert_string_entry(&documents[0], "name", "Ada");
3677 });
3678 }
3679
3680 #[test]
3681 fn rejects_metadata_with_whitespace() {
3682 Python::with_gil(|py| {
3683 let module = PyModule::from_code(
3684 py,
3685 pyo3::ffi::c_str!("anchor='bad anchor'\ntemplate=t'value: &{anchor} \"hi\"\\n'\n"),
3686 pyo3::ffi::c_str!("test_yaml_error.py"),
3687 pyo3::ffi::c_str!("test_yaml_error"),
3688 )
3689 .unwrap();
3690 let template = module.getattr("template").unwrap();
3691 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3692 let stream = parse_template(&template).unwrap();
3693 let err = match render_document(py, &stream) {
3694 Ok(_) => panic!("expected YAML render failure"),
3695 Err(err) => err,
3696 };
3697
3698 assert_eq!(err.kind, ErrorKind::Unrepresentable);
3699 assert!(err.message.contains("YAML metadata"));
3700 });
3701 }
3702
3703 #[test]
3704 fn rejects_flow_mappings_missing_commas_during_parse() {
3705 Python::with_gil(|py| {
3706 let module = PyModule::from_code(
3707 py,
3708 pyo3::ffi::c_str!(
3709 "from string.templatelib import Template\nmapping=Template('{a: 1 b: 2}\\n')\nmissing_colon=Template('value: {a b}\\n')\nleading_comma_mapping=Template('value: {, a: 1}\\n')\nsequence=Template('[1,,2]\\n')\ntrailing_sequence=Template('value: [1, 2,,]\\n')\nempty_sequence=Template('[,]\\n')\nempty_mapping=Template('value: {,}\\n')\n"
3710 ),
3711 pyo3::ffi::c_str!("test_yaml_flow_error.py"),
3712 pyo3::ffi::c_str!("test_yaml_flow_error"),
3713 )
3714 .unwrap();
3715 for name in [
3716 "mapping",
3717 "missing_colon",
3718 "leading_comma_mapping",
3719 "sequence",
3720 "trailing_sequence",
3721 "empty_sequence",
3722 "empty_mapping",
3723 ] {
3724 let template = module.getattr(name).unwrap();
3725 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3726 let err = parse_template(&template).expect_err("expected YAML parse failure");
3727 assert_eq!(err.kind, ErrorKind::Parse);
3728 assert!(err.message.contains("Expected"));
3729 }
3730 });
3731 }
3732
3733 #[test]
3734 fn rejects_tabs_as_mapping_separation_whitespace() {
3735 Python::with_gil(|py| {
3736 let module = PyModule::from_code(
3737 py,
3738 pyo3::ffi::c_str!(
3739 "from string.templatelib import Template\nmapping=Template('a:\\t1\\n')\nplain=Template('url: a:b\\t\\n')\nindent=Template('a:\\n\\t- 1\\n')\n"
3740 ),
3741 pyo3::ffi::c_str!("test_yaml_tab_error.py"),
3742 pyo3::ffi::c_str!("test_yaml_tab_error"),
3743 )
3744 .unwrap();
3745 for name in ["mapping", "plain", "indent"] {
3746 let template = module.getattr(name).unwrap();
3747 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3748 let err = parse_template(&template).expect_err("expected YAML parse failure");
3749 assert_eq!(err.kind, ErrorKind::Parse);
3750 assert!(
3751 err.message.contains("Tabs are not allowed"),
3752 "{name}: {}",
3753 err.message
3754 );
3755 }
3756 });
3757 }
3758
3759 #[test]
3760 fn splits_explicit_document_streams() {
3761 Python::with_gil(|py| {
3762 let module = PyModule::from_code(
3763 py,
3764 pyo3::ffi::c_str!("template=t'---\\nname: Alice\\n---\\nname: Bob\\n'\n"),
3765 pyo3::ffi::c_str!("test_yaml_stream.py"),
3766 pyo3::ffi::c_str!("test_yaml_stream"),
3767 )
3768 .unwrap();
3769 let template = module.getattr("template").unwrap();
3770 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3771 let stream = parse_template(&template).unwrap();
3772 assert_eq!(stream.documents.len(), 2);
3773 });
3774 }
3775
3776 #[test]
3777 fn parses_and_renders_flow_complex_keys_with_interpolation() {
3778 Python::with_gil(|py| {
3779 let module = PyModule::from_code(
3780 py,
3781 pyo3::ffi::c_str!(
3782 "left='Alice'\nright='Bob'\ntemplate=t'{{ {{name: [{left}, {right}]}}: 1, [{left}, {right}]: 2 }}'\n"
3783 ),
3784 pyo3::ffi::c_str!("test_yaml_flow_complex_keys.py"),
3785 pyo3::ffi::c_str!("test_yaml_flow_complex_keys"),
3786 )
3787 .unwrap();
3788 let template = module.getattr("template").unwrap();
3789 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3790 let stream = parse_template(&template).unwrap();
3791 let YamlValueNode::Mapping(mapping) = &stream.documents[0].value else {
3792 panic!("expected mapping");
3793 };
3794 assert!(matches!(
3795 mapping.entries[0].key.value,
3796 super::YamlKeyValue::Complex(_)
3797 ));
3798 assert!(matches!(
3799 mapping.entries[1].key.value,
3800 super::YamlKeyValue::Complex(_)
3801 ));
3802
3803 let rendered = render_document(py, &stream).unwrap();
3804 assert_eq!(
3805 rendered.text,
3806 "{ { name: [ \"Alice\", \"Bob\" ] }: 1, [ \"Alice\", \"Bob\" ]: 2 }"
3807 );
3808 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3809 assert_eq!(
3810 documents[0]
3811 .as_mapping()
3812 .expect("expected YAML mapping")
3813 .len(),
3814 2
3815 );
3816 });
3817 }
3818
3819 #[test]
3820 fn parses_explicit_mapping_keys_with_nested_collections() {
3821 Python::with_gil(|py| {
3822 let module = PyModule::from_code(
3823 py,
3824 pyo3::ffi::c_str!(
3825 "left='Alice'\nright='Bob'\ntemplate=t'? {{name: [{left}, {right}]}}\\n: 1\\n'\n"
3826 ),
3827 pyo3::ffi::c_str!("test_yaml_explicit_complex_key.py"),
3828 pyo3::ffi::c_str!("test_yaml_explicit_complex_key"),
3829 )
3830 .unwrap();
3831 let template = module.getattr("template").unwrap();
3832 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3833 let stream = parse_template(&template).unwrap();
3834 let YamlValueNode::Mapping(mapping) = &stream.documents[0].value else {
3835 panic!("expected mapping");
3836 };
3837 assert!(matches!(
3838 mapping.entries[0].key.value,
3839 super::YamlKeyValue::Complex(_)
3840 ));
3841 let rendered = render_document(py, &stream).unwrap();
3842 assert!(rendered.text.contains("? { name: [ \"Alice\", \"Bob\" ] }"));
3843 });
3844 }
3845
3846 #[test]
3847 fn renders_explicit_complex_keys_text_and_validated_shape() {
3848 Python::with_gil(|py| {
3849 let module = PyModule::from_code(
3850 py,
3851 pyo3::ffi::c_str!(
3852 "left='Alice'\nright='Bob'\ncomplex_key=t'? {{name: [{left}, {right}]}}\\n: 1\\n'\nempty_key=t'?\\n: 1\\n'\n"
3853 ),
3854 pyo3::ffi::c_str!("test_yaml_explicit_key_render.py"),
3855 pyo3::ffi::c_str!("test_yaml_explicit_key_render"),
3856 )
3857 .unwrap();
3858
3859 let complex_key = module.getattr("complex_key").unwrap();
3860 let complex_key = extract_template(py, &complex_key, "yaml_t/yaml_t_str").unwrap();
3861 let rendered = render_document(py, &parse_template(&complex_key).unwrap()).unwrap();
3862 assert!(
3863 rendered
3864 .text
3865 .contains("? { name: [ \"Alice\", \"Bob\" ] }\n: 1")
3866 );
3867 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3868 assert_eq!(
3869 documents[0]
3870 .as_mapping()
3871 .expect("expected YAML mapping")
3872 .len(),
3873 1
3874 );
3875
3876 let empty_key = module.getattr("empty_key").unwrap();
3877 let empty_key = extract_template(py, &empty_key, "yaml_t/yaml_t_str").unwrap();
3878 let rendered = render_document(py, &parse_template(&empty_key).unwrap()).unwrap();
3879 assert_eq!(rendered.text, "? null\n: 1");
3880 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3881 assert_eq!(
3882 documents[0]
3883 .as_mapping()
3884 .expect("expected YAML mapping")
3885 .len(),
3886 1
3887 );
3888 });
3889 }
3890
3891 #[test]
3892 fn parses_yaml_quoted_scalar_escapes() {
3893 Python::with_gil(|py| {
3894 let module = PyModule::from_code(
3895 py,
3896 pyo3::ffi::c_str!(
3897 "from string.templatelib import Template\ntemplate=t'value: \"line\\\\nnext \\\\u03B1 \\\\x41\"\\nquote: \\'it\\'\\'s ok\\''\nunicode=t'value: \"\\\\U0001D11E\"\\n'\ncrlf_join=Template('value: \"a\\\\\\r\\n b\"\\n')\nnel=t'value: \"\\\\N\"\\n'\nnbsp=t'value: \"\\\\_\"\\n'\nempty_single=Template(\"value: ''\\n\")\nempty_double=t'value: \"\"\\n'\nsingle_blank=Template(\"value: 'a\\n\\n b\\n\\n c'\\n\")\n"
3898 ),
3899 pyo3::ffi::c_str!("test_yaml_quoted_scalars.py"),
3900 pyo3::ffi::c_str!("test_yaml_quoted_scalars"),
3901 )
3902 .unwrap();
3903 let template = module.getattr("template").unwrap();
3904 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3905 let stream = parse_template(&template).unwrap();
3906 let rendered = render_document(py, &stream).unwrap();
3907 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3908 assert_string_entry(&documents[0], "value", "line\nnext α A");
3909 assert_string_entry(&documents[0], "quote", "it's ok");
3910
3911 for (name, expected) in [
3912 ("unicode", "𝄞"),
3913 ("crlf_join", "ab"),
3914 ("nel", "\u{0085}"),
3915 ("nbsp", "\u{00a0}"),
3916 ("empty_single", ""),
3917 ("empty_double", ""),
3918 ("single_blank", "a\nb\nc"),
3919 ] {
3920 let template = module.getattr(name).unwrap();
3921 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3922 let stream = parse_template(&template).unwrap();
3923 let rendered = render_document(py, &stream).unwrap();
3924 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3925 assert_string_entry(&documents[0], "value", expected);
3926 }
3927 });
3928 }
3929
3930 #[test]
3931 fn renders_quoted_scalar_escape_and_folding_families() {
3932 Python::with_gil(|py| {
3933 let module = PyModule::from_code(
3934 py,
3935 pyo3::ffi::c_str!(
3936 "from string.templatelib import Template\nbase=t'value: \"line\\\\nnext \\\\u03B1 \\\\x41\"\\nquote: \\'it\\'\\'s ok\\''\nmultiline_double=t'value: \"a\\n b\"\\n'\nmultiline_double_blank=t'value: \"a\\n\\n b\"\\n'\nmultiline_double_more_blank=t'value: \"a\\n\\n\\n b\"\\n'\nunicode=t'value: \"\\\\U0001D11E\"\\n'\ncrlf_join=Template('value: \"a\\\\\\r\\n b\"\\n')\nnel=t'value: \"\\\\N\"\\n'\nnbsp=t'value: \"\\\\_\"\\n'\nspace=t'value: \"\\\\ \"\\n'\nslash=t'value: \"\\\\/\"\\n'\ntab=t'value: \"\\\\t\"\\n'\nempty_single=Template(\"value: ''\\n\")\nempty_double=t'value: \"\"\\n'\nsingle_blank=Template(\"value: 'a\\n\\n b\\n\\n c'\\n\")\n"
3937 ),
3938 pyo3::ffi::c_str!("test_yaml_quoted_scalar_families.py"),
3939 pyo3::ffi::c_str!("test_yaml_quoted_scalar_families"),
3940 )
3941 .unwrap();
3942
3943 let base = module.getattr("base").unwrap();
3944 let base = extract_template(py, &base, "yaml_t/yaml_t_str").unwrap();
3945 let rendered = render_document(py, &parse_template(&base).unwrap()).unwrap();
3946 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3947 assert_eq!(
3948 rendered.text,
3949 "value: \"line\\nnext α A\"\nquote: 'it''s ok'"
3950 );
3951 assert_string_entry(&documents[0], "value", "line\nnext α A");
3952 assert_string_entry(&documents[0], "quote", "it's ok");
3953
3954 for (name, expected_text, expected) in [
3955 ("multiline_double", "value: \"a b\"", "a b"),
3956 ("multiline_double_blank", "value: \"a\\nb\"", "a\nb"),
3957 (
3958 "multiline_double_more_blank",
3959 "value: \"a\\n\\nb\"",
3960 "a\n\nb",
3961 ),
3962 ("unicode", "value: \"𝄞\"", "𝄞"),
3963 ("crlf_join", "value: \"ab\"", "ab"),
3964 ("nel", "value: \"\u{0085}\"", "\u{0085}"),
3965 ("nbsp", "value: \"\u{00a0}\"", "\u{00a0}"),
3966 ("space", "value: \" \"", " "),
3967 ("slash", "value: \"/\"", "/"),
3968 ("tab", "value: \"\\t\"", "\t"),
3969 ("empty_single", "value: ''", ""),
3970 ("empty_double", "value: \"\"", ""),
3971 ("single_blank", "value: 'a\n\n b\n\n c'", "a\nb\nc"),
3972 ] {
3973 let template = module.getattr(name).unwrap();
3974 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3975 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
3976 assert_eq!(rendered.text, expected_text, "{name}");
3977 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3978 assert_string_entry(&documents[0], "value", expected);
3979 }
3980 });
3981 }
3982
3983 #[test]
3984 fn renders_spec_quoted_scalar_examples_round_trip() {
3985 Python::with_gil(|py| {
3986 let module = PyModule::from_code(
3987 py,
3988 pyo3::ffi::c_str!(
3989 "unicode=t'unicode: \"Sosa did fine.\\\\u263A\"'\ncontrol=t'control: \"\\\\b1998\\\\t1999\\\\t2000\\\\n\"'\nsingle=t'''single: '\"Howdy!\" he cried.' '''\nquoted=t'''quoted: ' # Not a ''comment''.' '''\ntie=t'''tie: '|\\\\-*-/|' '''\n"
3990 ),
3991 pyo3::ffi::c_str!("test_yaml_spec_quoted_examples.py"),
3992 pyo3::ffi::c_str!("test_yaml_spec_quoted_examples"),
3993 )
3994 .unwrap();
3995
3996 for (name, key, expected_text, expected_value) in [
3997 (
3998 "unicode",
3999 "unicode",
4000 "unicode: \"Sosa did fine.☺\"",
4001 "Sosa did fine.\u{263a}",
4002 ),
4003 (
4004 "control",
4005 "control",
4006 "control: \"\\b1998\\t1999\\t2000\\n\"",
4007 "\u{0008}1998\t1999\t2000\n",
4008 ),
4009 (
4010 "single",
4011 "single",
4012 "single: '\"Howdy!\" he cried.'",
4013 "\"Howdy!\" he cried.",
4014 ),
4015 (
4016 "quoted",
4017 "quoted",
4018 "quoted: ' # Not a ''comment''.'",
4019 " # Not a 'comment'.",
4020 ),
4021 ("tie", "tie", "tie: '|\\-*-/|'", "|\\-*-/|"),
4022 ] {
4023 let template = module.getattr(name).unwrap();
4024 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4025 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
4026 assert_eq!(rendered.text, expected_text, "{name}");
4027 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4028 assert_string_entry(&documents[0], key, expected_value);
4029 }
4030 });
4031 }
4032
4033 #[test]
4034 fn folds_multiline_double_quoted_scalars() {
4035 Python::with_gil(|py| {
4036 let module = PyModule::from_code(
4037 py,
4038 pyo3::ffi::c_str!(
4039 "single=t'value: \"a\\n b\"\\n'\nblank=t'value: \"a\\n\\n b\"\\n'\n"
4040 ),
4041 pyo3::ffi::c_str!("test_yaml_multiline_double_quoted.py"),
4042 pyo3::ffi::c_str!("test_yaml_multiline_double_quoted"),
4043 )
4044 .unwrap();
4045
4046 for (name, expected) in [("single", "a b"), ("blank", "a\nb")] {
4047 let template = module.getattr(name).unwrap();
4048 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4049 let stream = parse_template(&template).unwrap();
4050 let rendered = render_document(py, &stream).unwrap();
4051 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4052 assert_string_entry(&documents[0], "value", expected);
4053 }
4054 });
4055 }
4056
4057 #[test]
4058 fn renders_block_chomping_and_indent_indicator_families() {
4059 Python::with_gil(|py| {
4060 let module = PyModule::from_code(
4061 py,
4062 pyo3::ffi::c_str!(
4063 "literal_strip=t'value: |-\\n a\\n b\\n'\nliteral_keep=t'value: |+\\n a\\n b\\n'\nliteral_keep_leading_blank=t'value: |+\\n\\n a\\n'\nfolded_strip=t'value: >-\\n a\\n b\\n'\nfolded_keep=t'value: >+\\n a\\n b\\n'\nfolded_more=t'value: >\\n a\\n b\\n c\\n'\nindent_indicator=t'value: |2\\n a\\n b\\n'\nliteral_blank_keep=t'value: |+\\n a\\n\\n b\\n'\nfolded_blank_keep=t'value: >+\\n a\\n\\n b\\n'\n"
4064 ),
4065 pyo3::ffi::c_str!("test_yaml_block_chomping_families.py"),
4066 pyo3::ffi::c_str!("test_yaml_block_chomping_families"),
4067 )
4068 .unwrap();
4069
4070 for (name, expected_text, key, expected_value) in [
4071 ("literal_strip", "value: |-\n a\n b", "value", "a\nb"),
4072 ("literal_keep", "value: |+\n a\n b\n", "value", "a\nb\n"),
4073 (
4074 "literal_keep_leading_blank",
4075 "value: |+\n \n a\n",
4076 "value",
4077 "\na\n",
4078 ),
4079 ("folded_strip", "value: >-\n a\n b", "value", "a b"),
4080 ("folded_keep", "value: >+\n a\n b\n", "value", "a b\n"),
4081 (
4082 "folded_more",
4083 "value: >\n a\n b\n c\n",
4084 "value",
4085 "a\n b\nc\n",
4086 ),
4087 (
4088 "indent_indicator",
4089 "value: |2\n a\n b\n",
4090 "value",
4091 "a\nb\n",
4092 ),
4093 (
4094 "literal_blank_keep",
4095 "value: |+\n a\n \n b\n",
4096 "value",
4097 "a\n\nb\n",
4098 ),
4099 (
4100 "folded_blank_keep",
4101 "value: >+\n a\n \n b\n",
4102 "value",
4103 "a\nb\n",
4104 ),
4105 ] {
4106 let template = module.getattr(name).unwrap();
4107 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4108 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
4109 assert_eq!(rendered.text, expected_text, "{name}");
4110 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4111 assert_string_entry(&documents[0], key, expected_value);
4112 }
4113 });
4114 }
4115
4116 #[test]
4117 fn folds_multiline_plain_scalars() {
4118 Python::with_gil(|py| {
4119 let module = PyModule::from_code(
4120 py,
4121 pyo3::ffi::c_str!("template=t'value: a\\n b\\n\\n c\\n'\n"),
4122 pyo3::ffi::c_str!("test_yaml_multiline_plain.py"),
4123 pyo3::ffi::c_str!("test_yaml_multiline_plain"),
4124 )
4125 .unwrap();
4126 let template = module.getattr("template").unwrap();
4127 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4128 let stream = parse_template(&template).unwrap();
4129 let rendered = render_document(py, &stream).unwrap();
4130 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4131 assert_string_entry(&documents[0], "value", "a b\nc");
4132 assert!(rendered.text.contains("\"a b\\nc\""));
4133 });
4134 }
4135
4136 #[test]
4137 fn accepts_top_level_flow_collections_with_trailing_newlines() {
4138 Python::with_gil(|py| {
4139 let module = PyModule::from_code(
4140 py,
4141 pyo3::ffi::c_str!(
4142 "from string.templatelib import Template\ntemplate=Template('{a: 1, b: [2, 3]}\\n')\n"
4143 ),
4144 pyo3::ffi::c_str!("test_yaml_flow_trailing_newline.py"),
4145 pyo3::ffi::c_str!("test_yaml_flow_trailing_newline"),
4146 )
4147 .unwrap();
4148 let template = module.getattr("template").unwrap();
4149 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4150 let stream = parse_template(&template).unwrap();
4151 let rendered = render_document(py, &stream).unwrap();
4152 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4153 assert_eq!(
4154 documents[0]
4155 .as_mapping()
4156 .expect("expected YAML mapping")
4157 .len(),
4158 2
4159 );
4160 });
4161 }
4162
4163 #[test]
4164 fn accepts_line_wrapped_flow_collections() {
4165 Python::with_gil(|py| {
4166 let module = PyModule::from_code(
4167 py,
4168 pyo3::ffi::c_str!(
4169 "from string.templatelib import Template\nsequence=Template('key: [a,\\n b]\\n')\nmapping=Template('key: {a: 1,\\n b: 2}\\n')\n"
4170 ),
4171 pyo3::ffi::c_str!("test_yaml_wrapped_flow.py"),
4172 pyo3::ffi::c_str!("test_yaml_wrapped_flow"),
4173 )
4174 .unwrap();
4175
4176 for name in ["sequence", "mapping"] {
4177 let template = module.getattr(name).unwrap();
4178 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4179 let stream = parse_template(&template).unwrap();
4180 let rendered = render_document(py, &stream).unwrap();
4181 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4182 assert_eq!(
4183 documents[0]
4184 .as_mapping()
4185 .expect("expected YAML mapping")
4186 .len(),
4187 1
4188 );
4189 }
4190 });
4191 }
4192
4193 #[test]
4194 fn accepts_flow_collections_with_comments() {
4195 Python::with_gil(|py| {
4196 let module = PyModule::from_code(
4197 py,
4198 pyo3::ffi::c_str!(
4199 "from string.templatelib import Template\nsequence=Template('key: [a, # first\\n b]\\n')\nmapping=Template('key: {a: 1, # first\\n b: 2}\\n')\n"
4200 ),
4201 pyo3::ffi::c_str!("test_yaml_flow_comments.py"),
4202 pyo3::ffi::c_str!("test_yaml_flow_comments"),
4203 )
4204 .unwrap();
4205
4206 for name in ["sequence", "mapping"] {
4207 let template = module.getattr(name).unwrap();
4208 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4209 let stream = parse_template(&template).unwrap();
4210 let rendered = render_document(py, &stream).unwrap();
4211 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4212 assert_eq!(
4213 documents[0]
4214 .as_mapping()
4215 .expect("expected YAML mapping")
4216 .len(),
4217 1
4218 );
4219 }
4220 });
4221 }
4222
4223 #[test]
4224 fn treats_empty_documents_as_null() {
4225 Python::with_gil(|py| {
4226 let module = PyModule::from_code(
4227 py,
4228 pyo3::ffi::c_str!(
4229 "from string.templatelib import Template\nempty=Template('---\\n...\\n')\nstream=Template('---\\n\\n---\\na: 1\\n')\n"
4230 ),
4231 pyo3::ffi::c_str!("test_yaml_empty_docs.py"),
4232 pyo3::ffi::c_str!("test_yaml_empty_docs"),
4233 )
4234 .unwrap();
4235
4236 let empty = module.getattr("empty").unwrap();
4237 let empty = extract_template(py, &empty, "yaml_t/yaml_t_str").unwrap();
4238 let rendered = render_document(py, &parse_template(&empty).unwrap()).unwrap();
4239 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4240 assert!(documents.as_slice()[0].is_null());
4241
4242 let stream = module.getattr("stream").unwrap();
4243 let stream = extract_template(py, &stream, "yaml_t/yaml_t_str").unwrap();
4244 let rendered = render_document(py, &parse_template(&stream).unwrap()).unwrap();
4245 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4246 assert!(documents[0].is_null());
4247 assert!(documents[1].as_mapping().is_some());
4248 });
4249 }
4250
4251 #[test]
4252 fn supports_indentless_sequence_values_and_empty_explicit_keys() {
4253 Python::with_gil(|py| {
4254 let module = PyModule::from_code(
4255 py,
4256 pyo3::ffi::c_str!(
4257 "from string.templatelib import Template\nindentless=Template('a:\\n- 1\\n- 2\\n')\nempty_key=Template('?\\n: 1\\n')\n"
4258 ),
4259 pyo3::ffi::c_str!("test_yaml_indentless_and_empty_key.py"),
4260 pyo3::ffi::c_str!("test_yaml_indentless_and_empty_key"),
4261 )
4262 .unwrap();
4263
4264 let indentless = module.getattr("indentless").unwrap();
4265 let indentless = extract_template(py, &indentless, "yaml_t/yaml_t_str").unwrap();
4266 let rendered = render_document(py, &parse_template(&indentless).unwrap()).unwrap();
4267 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4268 let mapping = documents[0].as_mapping().expect("expected YAML mapping");
4269 let value = mapping
4270 .iter()
4271 .find_map(|(key, value)| (key.as_str() == Some("a")).then_some(value))
4272 .expect("expected key a");
4273 assert_eq!(value.as_vec().expect("expected YAML sequence").len(), 2);
4274
4275 let empty_key = module.getattr("empty_key").unwrap();
4276 let empty_key = extract_template(py, &empty_key, "yaml_t/yaml_t_str").unwrap();
4277 let rendered = render_document(py, &parse_template(&empty_key).unwrap()).unwrap();
4278 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4279 let mapping = documents[0].as_mapping().expect("expected YAML mapping");
4280 let value = mapping
4281 .iter()
4282 .find_map(|(key, value)| key.is_null().then_some(value))
4283 .expect("expected null key");
4284 assert_eq!(value.as_integer(), Some(1));
4285 });
4286 }
4287
4288 #[test]
4289 fn supports_compact_mappings_in_sequences_and_plain_hash_chars() {
4290 Python::with_gil(|py| {
4291 let module = PyModule::from_code(
4292 py,
4293 pyo3::ffi::c_str!(
4294 "from string.templatelib import Template\nseq=Template('- a: 1\\n b: 2\\n- c: 3\\n')\nmapped=Template('items:\\n- a: 1\\n b: 2\\n- c: 3\\n')\nseqs=Template('- - 1\\n - 2\\n- - 3\\n')\nhash_value=Template('value: a#b\\n')\n"
4295 ),
4296 pyo3::ffi::c_str!("test_yaml_compact_sequence_maps.py"),
4297 pyo3::ffi::c_str!("test_yaml_compact_sequence_maps"),
4298 )
4299 .unwrap();
4300
4301 for name in ["seq", "mapped", "seqs", "hash_value"] {
4302 let template = module.getattr(name).unwrap();
4303 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4304 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
4305 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4306 assert_eq!(documents.len(), 1);
4307 }
4308 });
4309 }
4310
4311 #[test]
4312 fn preserves_explicit_document_end_markers_in_streams() {
4313 Python::with_gil(|py| {
4314 let module = PyModule::from_code(
4315 py,
4316 pyo3::ffi::c_str!(
4317 "from string.templatelib import Template\ntemplate=Template('---\\na: 1\\n...\\n---\\nb: 2\\n')\ncommented=Template('---\\na: 1\\n... # end\\n---\\nb: 2\\n')\n"
4318 ),
4319 pyo3::ffi::c_str!("test_yaml_explicit_end.py"),
4320 pyo3::ffi::c_str!("test_yaml_explicit_end"),
4321 )
4322 .unwrap();
4323 let template = module.getattr("template").unwrap();
4324 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4325 let stream = parse_template(&template).unwrap();
4326 let rendered = render_document(py, &stream).unwrap();
4327
4328 assert!(rendered.text.contains("...\n---"));
4329
4330 let commented = module.getattr("commented").unwrap();
4331 let commented = extract_template(py, &commented, "yaml_t/yaml_t_str").unwrap();
4332 let rendered = render_document(py, &parse_template(&commented).unwrap()).unwrap();
4333 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4334 assert_eq!(documents.len(), 2);
4335 });
4336 }
4337
4338 #[test]
4339 fn parses_verbatim_tags() {
4340 Python::with_gil(|py| {
4341 let module = PyModule::from_code(
4342 py,
4343 pyo3::ffi::c_str!(
4344 "from string.templatelib import Template\ntemplate=Template('value: !<tag:yaml.org,2002:str> hello\\n')\n"
4345 ),
4346 pyo3::ffi::c_str!("test_yaml_verbatim_tag.py"),
4347 pyo3::ffi::c_str!("test_yaml_verbatim_tag"),
4348 )
4349 .unwrap();
4350 let template = module.getattr("template").unwrap();
4351 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4352 let stream = parse_template(&template).unwrap();
4353 let rendered = render_document(py, &stream).unwrap();
4354 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4355
4356 assert!(rendered.text.contains("!<tag:yaml.org,2002:str>"));
4357 assert_string_entry(&documents[0], "value", "hello");
4358 });
4359 }
4360
4361 #[test]
4362 fn validates_user_defined_tags_via_saphyr() {
4363 Python::with_gil(|py| {
4364 let module = PyModule::from_code(
4365 py,
4366 pyo3::ffi::c_str!(
4367 "from string.templatelib import Template\nscalar=Template('!custom 3\\n')\ncustom_tag_scalar=Template('value: !custom 3\\n')\ncustom_tag_sequence=Template('value: !custom [1, 2]\\n')\ncommented_root=Template('--- # comment\\n!custom [1, 2]\\n')\n"
4368 ),
4369 pyo3::ffi::c_str!("test_yaml_custom_tags.py"),
4370 pyo3::ffi::c_str!("test_yaml_custom_tags"),
4371 )
4372 .unwrap();
4373
4374 let scalar = module.getattr("scalar").unwrap();
4375 let scalar = extract_template(py, &scalar, "yaml_t/yaml_t_str").unwrap();
4376 let rendered = render_document(py, &parse_template(&scalar).unwrap()).unwrap();
4377 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4378 assert_eq!(rendered.text.trim_end(), "!custom 3");
4379 assert!(rendered.text.contains("!custom 3"));
4380 assert_eq!(yaml_integer(&documents[0]), Some(3));
4381
4382 let custom_tag_scalar = module.getattr("custom_tag_scalar").unwrap();
4383 let custom_tag_scalar =
4384 extract_template(py, &custom_tag_scalar, "yaml_t/yaml_t_str").unwrap();
4385 let rendered =
4386 render_document(py, &parse_template(&custom_tag_scalar).unwrap()).unwrap();
4387 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4388 assert_eq!(rendered.text.trim_end(), "value: !custom 3");
4389 assert!(rendered.text.contains("value: !custom 3"));
4390 assert_integer_entry(&documents[0], "value", 3);
4391
4392 let custom_tag_sequence = module.getattr("custom_tag_sequence").unwrap();
4393 let custom_tag_sequence =
4394 extract_template(py, &custom_tag_sequence, "yaml_t/yaml_t_str").unwrap();
4395 let rendered =
4396 render_document(py, &parse_template(&custom_tag_sequence).unwrap()).unwrap();
4397 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4398 assert!(rendered.text.contains("value: !custom [ 1, 2 ]"));
4399 let value = documents[0]
4400 .as_mapping()
4401 .expect("expected YAML mapping")
4402 .iter()
4403 .find_map(|(key, value)| (yaml_scalar_text(key) == Some("value")).then_some(value))
4404 .expect("expected value key");
4405 assert_eq!(yaml_sequence_len(value), Some(2));
4406
4407 let commented_root = module.getattr("commented_root").unwrap();
4408 let commented_root =
4409 extract_template(py, &commented_root, "yaml_t/yaml_t_str").unwrap();
4410 let rendered = render_document(py, &parse_template(&commented_root).unwrap()).unwrap();
4411 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4412 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
4413 });
4414 }
4415
4416 #[test]
4417 fn rejects_aliases_that_cross_document_boundaries() {
4418 Python::with_gil(|py| {
4419 let module = PyModule::from_code(
4420 py,
4421 pyo3::ffi::c_str!(
4422 "from string.templatelib import Template\nstream=Template('--- &a\\n- 1\\n- 2\\n---\\n*a\\n')\n"
4423 ),
4424 pyo3::ffi::c_str!("test_yaml_cross_doc_alias.py"),
4425 pyo3::ffi::c_str!("test_yaml_cross_doc_alias"),
4426 )
4427 .unwrap();
4428
4429 let stream = module.getattr("stream").unwrap();
4430 let stream = extract_template(py, &stream, "yaml_t/yaml_t_str").unwrap();
4431 let rendered = render_document(py, &parse_template(&stream).unwrap()).unwrap();
4432 let err = parse_rendered_yaml(&rendered.text).expect_err("expected YAML parse failure");
4433 assert_eq!(err.kind, ErrorKind::Parse);
4434 assert!(err.message.contains("unknown anchor"));
4435 });
4436 }
4437
4438 #[test]
4439 fn preserves_tag_directives_and_handle_tags() {
4440 Python::with_gil(|py| {
4441 let module = PyModule::from_code(
4442 py,
4443 pyo3::ffi::c_str!(
4444 "from string.templatelib import Template\nscalar=Template('%TAG !e! tag:example.com,2020:\\n---\\nvalue: !e!foo 1\\n')\ndoc_start_comment=Template('--- # comment\\nvalue: 1\\n')\ndoc_start_tag_comment=Template('--- !!str true # comment\\n')\nroot=Template('%YAML 1.2\\n%TAG !e! tag:example.com,2020:\\n---\\n!e!root {value: !e!leaf 1}\\n')\nblock_map=Template('--- !!map\\na: 1\\n')\nblock_seq=Template('--- !!seq\\n- 1\\n- 2\\n')\nanchor_map=Template('--- &root\\n a: 1\\n')\nanchor_seq=Template('--- !custom &root\\n - 1\\n - 2\\n')\n"
4445 ),
4446 pyo3::ffi::c_str!("test_yaml_tag_directives.py"),
4447 pyo3::ffi::c_str!("test_yaml_tag_directives"),
4448 )
4449 .unwrap();
4450
4451 let scalar = module.getattr("scalar").unwrap();
4452 let scalar = extract_template(py, &scalar, "yaml_t/yaml_t_str").unwrap();
4453 let stream = parse_template(&scalar).unwrap();
4454 let rendered = render_document(py, &stream).unwrap();
4455 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4456 assert_eq!(
4457 rendered.text,
4458 "%TAG !e! tag:example.com,2020:\n---\nvalue: !e!foo 1"
4459 );
4460 assert_integer_entry(&documents[0], "value", 1);
4461
4462 let doc_start_comment = module.getattr("doc_start_comment").unwrap();
4463 let doc_start_comment =
4464 extract_template(py, &doc_start_comment, "yaml_t/yaml_t_str").unwrap();
4465 let stream = parse_template(&doc_start_comment).unwrap();
4466 let rendered = render_document(py, &stream).unwrap();
4467 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4468 assert_eq!(rendered.text, "---\nvalue: 1");
4469 assert_integer_entry(&documents[0], "value", 1);
4470
4471 let doc_start_tag_comment = module.getattr("doc_start_tag_comment").unwrap();
4472 let doc_start_tag_comment =
4473 extract_template(py, &doc_start_tag_comment, "yaml_t/yaml_t_str").unwrap();
4474 let stream = parse_template(&doc_start_tag_comment).unwrap();
4475 let rendered = render_document(py, &stream).unwrap();
4476 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4477 assert_eq!(rendered.text, "---\n!!str true");
4478 assert_eq!(yaml_scalar_text(&documents[0]), Some("true"));
4479
4480 let root = module.getattr("root").unwrap();
4481 let root = extract_template(py, &root, "yaml_t/yaml_t_str").unwrap();
4482 let stream = parse_template(&root).unwrap();
4483 let rendered = render_document(py, &stream).unwrap();
4484 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4485 assert_eq!(
4486 rendered.text,
4487 "%YAML 1.2\n%TAG !e! tag:example.com,2020:\n---\n!e!root { value: !e!leaf 1 }"
4488 );
4489 assert_integer_entry(&documents[0], "value", 1);
4490
4491 let block_map = module.getattr("block_map").unwrap();
4492 let block_map = extract_template(py, &block_map, "yaml_t/yaml_t_str").unwrap();
4493 let stream = parse_template(&block_map).unwrap();
4494 let rendered = render_document(py, &stream).unwrap();
4495 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4496 assert_eq!(rendered.text, "---\n!!map\na: 1");
4497 assert_integer_entry(&documents[0], "a", 1);
4498
4499 let block_seq = module.getattr("block_seq").unwrap();
4500 let block_seq = extract_template(py, &block_seq, "yaml_t/yaml_t_str").unwrap();
4501 let stream = parse_template(&block_seq).unwrap();
4502 let rendered = render_document(py, &stream).unwrap();
4503 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4504 assert_eq!(rendered.text, "---\n!!seq\n- 1\n- 2");
4505 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
4506
4507 let anchor_map = module.getattr("anchor_map").unwrap();
4508 let anchor_map = extract_template(py, &anchor_map, "yaml_t/yaml_t_str").unwrap();
4509 let stream = parse_template(&anchor_map).unwrap();
4510 let rendered = render_document(py, &stream).unwrap();
4511 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4512 assert_eq!(rendered.text, "---\n&root\na: 1");
4513 assert_integer_entry(&documents[0], "a", 1);
4514
4515 let anchor_seq = module.getattr("anchor_seq").unwrap();
4516 let anchor_seq = extract_template(py, &anchor_seq, "yaml_t/yaml_t_str").unwrap();
4517 let stream = parse_template(&anchor_seq).unwrap();
4518 let rendered = render_document(py, &stream).unwrap();
4519 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4520 assert_eq!(rendered.text, "---\n!custom &root\n- 1\n- 2");
4521 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
4522 });
4523 }
4524
4525 #[test]
4526 fn preserves_explicit_core_schema_tags() {
4527 Python::with_gil(|py| {
4528 let module = PyModule::from_code(
4529 py,
4530 pyo3::ffi::c_str!(
4531 "from string.templatelib import Template\nmapping=Template('value_bool: !!bool true\\nvalue_str: !!str true\\nvalue_float: !!float 1\\nvalue_null: !!null null\\n')\nroot_int=Template('--- !!int 3\\n')\nroot_str=Template('--- !!str true\\n')\nroot_bool=Template('--- !!bool true\\n')\n"
4532 ),
4533 pyo3::ffi::c_str!("test_yaml_core_tags.py"),
4534 pyo3::ffi::c_str!("test_yaml_core_tags"),
4535 )
4536 .unwrap();
4537
4538 let mapping = module.getattr("mapping").unwrap();
4539 let mapping = extract_template(py, &mapping, "yaml_t/yaml_t_str").unwrap();
4540 let rendered = render_document(py, &parse_template(&mapping).unwrap()).unwrap();
4541 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4542 let mapping = yaml_mapping(&documents[0]).expect("expected YAML mapping");
4543 let float_value = mapping
4544 .iter()
4545 .find_map(|(key, value)| {
4546 (yaml_scalar_text(key) == Some("value_float")).then_some(value)
4547 })
4548 .expect("expected value_float key");
4549 assert_eq!(yaml_float(float_value), Some(1.0));
4550 assert_string_entry(&documents[0], "value_str", "true");
4551
4552 let root_int = module.getattr("root_int").unwrap();
4553 let root_int = extract_template(py, &root_int, "yaml_t/yaml_t_str").unwrap();
4554 let rendered = render_document(py, &parse_template(&root_int).unwrap()).unwrap();
4555 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4556 assert_eq!(yaml_integer(&documents[0]), Some(3));
4557
4558 let root_str = module.getattr("root_str").unwrap();
4559 let root_str = extract_template(py, &root_str, "yaml_t/yaml_t_str").unwrap();
4560 let rendered = render_document(py, &parse_template(&root_str).unwrap()).unwrap();
4561 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4562 assert_eq!(yaml_scalar_text(&documents[0]), Some("true"));
4563
4564 let root_bool = module.getattr("root_bool").unwrap();
4565 let root_bool = extract_template(py, &root_bool, "yaml_t/yaml_t_str").unwrap();
4566 let rendered = render_document(py, &parse_template(&root_bool).unwrap()).unwrap();
4567 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4568 assert!(documents[0].is_boolean());
4569 });
4570 }
4571
4572 #[test]
4573 fn supports_flow_trailing_commas_sequence_values_and_indent_indicators() {
4574 Python::with_gil(|py| {
4575 let module = PyModule::from_code(
4576 py,
4577 pyo3::ffi::c_str!(
4578 "from string.templatelib import Template\ncomment_only=Template('# comment\\n')\ncomment_only_explicit=Template('--- # comment\\n')\ncomment_only_explicit_end=Template('--- # comment\\n...\\n')\ncomment_only_explicit_end_stream=Template('--- # comment\\n...\\n---\\na: 1\\n')\ncomment_only_mid_stream=Template('---\\na: 1\\n--- # comment\\n...\\n---\\nb: 2\\n')\ncomment_only_tail_stream=Template('---\\na: 1\\n--- # comment\\n...\\n')\nflow_seq=Template('[1, 2,]\\n')\nempty_flow_seq=Template('value: []\\n')\nflow_map=Template('{a: 1,}\\n')\nempty_flow_map=Template('value: {}\\n')\nflow_scalar_mix=Template('value: [\"\", \\'\\', plain]\\n')\nflow_plain_scalar=Template('value: [1 2]\\n')\nflow_hash_plain_mapping_value=Template('value: {a: b#c}\\n')\nflow_hash_plain_mapping_values=Template('value: {a: b#c, d: e#f}\\n')\nflow_hash_plain_scalars=Template('value: [a#b, c#d]\\n')\nflow_hash_value_sequence=Template('value: [a#b, c#d, e#f]\\n')\nflow_hash_long_sequence=Template('value: [a#b, c#d, e#f, g#h]\\n')\nflow_hash_five_sequence=Template('value: [a#b, c#d, e#f, g#h, i#j]\\n')\nflow_hash_seq_six=Template('value: [a#b, c#d, e#f, g#h, i#j, k#l]\\n')\nflow_hash_seq_seven=Template('value: [a#b, c#d, e#f, g#h, i#j, k#l, m#n]\\n')\nflow_mapping_hash_key=Template('value: {a#b: 1}\\n')\nflow_sequence_comments_value=Template('value: [1, # c\\n 2]\\n')\nflow_mapping_comments_value=Template('value: {a: 1, # c\\n b: 2}\\n')\ncomment_after_value=Template('value: a # c\\n')\nplain_colon_hash=Template('value: a:b#c\\n')\nplain_colon_hash_deeper=Template('value: a:b:c#d\\n')\nplain_hash_chain=Template('value: a#b#c\\n')\nplain_hash_chain_deeper=Template('value: a#b#c#d\\n')\nplain_hash_chain_deeper_comment=Template('value: a#b#c#d # comment\\n')\nflow_hash_mapping_long=Template('value: {a: b#c, d: e#f, g: h#i}\\n')\nflow_hash_mapping_four=Template('value: {a: b#c, d: e#f, g: h#i, j: k#l}\\n')\nflow_hash_mapping_five=Template('value: {a: b#c, d: e#f, g: h#i, j: k#l, m: n#o}\\n')\nflow_hash_mapping_six=Template('value: {a: b#c, d: e#f, g: h#i, j: k#l, m: n#o, p: q#r}\\n')\ncomment_after_plain_colon=Template('value: a:b # c\\n')\ncomment_after_flow_plain_colon=Template('value: [a:b # c\\n]\\n')\nflow_plain_hash_chain=Template('value: [a#b#c, d#e#f]\\n')\nflow_plain_hash_chain_single_deeper=Template('value: [a#b#c#d]\\n')\nflow_plain_hash_chain_single_deeper_comment=Template('value: [a#b#c#d # comment\\n]\\n')\nflow_plain_hash_chain_long=Template('value: [a#b#c, d#e#f, g#h#i]\\n')\nflow_plain_hash_chain_four=Template('value: [a#b#c, d#e#f, g#h#i, j#k#l]\\n')\nblock_plain_comment_after_colon_long=Template('value: a:b:c # comment\\n')\nblock_plain_comment_after_colon_deeper=Template('value: a:b:c:d # comment\\n')\nflow_plain_comment_after_colon_long=Template('value: [a:b:c # comment\\n]\\n')\nflow_plain_comment_after_colon_deeper=Template('value: [a:b:c:d # comment\\n]\\n')\nflow_plain_colon_hash_deeper=Template('value: [a:b:c#d]\\n')\nflow_mapping_plain_key_question=Template('value: {?x: 1}\\n')\nflow_mapping_plain_key_questions=Template('value: {?x: 1, ?y: 2}\\n')\nmapping_empty_flow_values=Template('value: {a: [], b: {}}\\n')\nflow_mapping_empty_key=Template('{\"\": 1}\\n')\nflow_mapping_empty_key_and_values=Template('{\"\": [], foo: {}}\\n')\nflow_mapping_nested_empty=Template('{a: {}, b: []}\\n')\nflow_null_key=Template('{null: 1, \"\": 2}\\n')\nflow_sequence_nested_empty=Template('[[], {}]\\n')\nplain_scalar_colon_no_space=Template('value: a:b\\n')\nplain_question_mark_scalar=Template('value: ?x\\n')\nplain_colon_scalar_flow=Template('value: [a:b, c:d]\\n')\nflow_mapping_colon_plain_key=Template('value: {a:b: c}\\n')\nflow_mapping_colon_and_hash=Template('value: {a:b: c#d}\\n')\nblock_plain_colon_no_space=Template('value: a:b:c\\n')\nblock_null_key=Template('? null\\n: 1\\n')\nquoted_null_key=Template('? \"\"\\n: 1\\n')\nalias_in_flow_mapping_value=Template('base: &a {x: 1}\\nvalue: {ref: *a}\\n')\nflow_null_and_alias=Template('base: &a {x: 1}\\nvalue: {null: *a}\\n')\nalias_seq_value=Template('a: &x [1, 2]\\nb: *x\\n')\nflow_mapping_missing_value=Template('value: {a: }\\n')\nflow_seq_missing_value_before_end=Template('value: [1, 2, ]\\n')\nflow_alias_map=Template('value: {left: &a 1, right: *a}\\n')\nflow_alias_seq=Template('value: [&a 1, *a]\\n')\nflow_merge=Template('value: {<<: &base {a: 1}, b: 2}\\n')\nnested_flow_alias_merge=Template('value: [{<<: &base {a: 1}, b: 2}, *base]\\n')\nexplicit_seq=Template('? a\\n: - 1\\n - 2\\n')\nindented_block=Template('value: |1\\n a\\n b\\n')\n"
4579 ),
4580 pyo3::ffi::c_str!("test_yaml_edge_cases.py"),
4581 pyo3::ffi::c_str!("test_yaml_edge_cases"),
4582 )
4583 .unwrap();
4584
4585 let flow_seq = module.getattr("flow_seq").unwrap();
4586 let flow_seq = extract_template(py, &flow_seq, "yaml_t/yaml_t_str").unwrap();
4587 let rendered = render_document(py, &parse_template(&flow_seq).unwrap()).unwrap();
4588 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4589 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
4590
4591 let comment_only = module.getattr("comment_only").unwrap();
4592 let comment_only = extract_template(py, &comment_only, "yaml_t/yaml_t_str").unwrap();
4593 let rendered = render_document(py, &parse_template(&comment_only).unwrap()).unwrap();
4594 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4595 assert!(documents[0].is_null());
4596
4597 let comment_only_explicit = module.getattr("comment_only_explicit").unwrap();
4598 let comment_only_explicit =
4599 extract_template(py, &comment_only_explicit, "yaml_t/yaml_t_str").unwrap();
4600 let rendered =
4601 render_document(py, &parse_template(&comment_only_explicit).unwrap()).unwrap();
4602 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4603 assert!(documents[0].is_null());
4604
4605 let comment_only_explicit_end = module.getattr("comment_only_explicit_end").unwrap();
4606 let comment_only_explicit_end =
4607 extract_template(py, &comment_only_explicit_end, "yaml_t/yaml_t_str").unwrap();
4608 let rendered =
4609 render_document(py, &parse_template(&comment_only_explicit_end).unwrap()).unwrap();
4610 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4611 assert!(documents[0].is_null());
4612
4613 let comment_only_explicit_end_stream =
4614 module.getattr("comment_only_explicit_end_stream").unwrap();
4615 let comment_only_explicit_end_stream =
4616 extract_template(py, &comment_only_explicit_end_stream, "yaml_t/yaml_t_str")
4617 .unwrap();
4618 let rendered = render_document(
4619 py,
4620 &parse_template(&comment_only_explicit_end_stream).unwrap(),
4621 )
4622 .unwrap();
4623 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4624 assert_eq!(documents.len(), 2);
4625 assert!(documents[0].is_null());
4626 assert_integer_entry(&documents[1], "a", 1);
4627
4628 let comment_only_mid_stream = module.getattr("comment_only_mid_stream").unwrap();
4629 let comment_only_mid_stream =
4630 extract_template(py, &comment_only_mid_stream, "yaml_t/yaml_t_str").unwrap();
4631 let rendered =
4632 render_document(py, &parse_template(&comment_only_mid_stream).unwrap()).unwrap();
4633 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4634 assert_eq!(documents.len(), 3);
4635 assert_integer_entry(&documents[0], "a", 1);
4636 assert!(documents[1].is_null());
4637 assert_integer_entry(&documents[2], "b", 2);
4638
4639 let comment_only_tail_stream = module.getattr("comment_only_tail_stream").unwrap();
4640 let comment_only_tail_stream =
4641 extract_template(py, &comment_only_tail_stream, "yaml_t/yaml_t_str").unwrap();
4642 let rendered =
4643 render_document(py, &parse_template(&comment_only_tail_stream).unwrap()).unwrap();
4644 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4645 assert_eq!(documents.len(), 2);
4646 assert_integer_entry(&documents[0], "a", 1);
4647 assert!(documents[1].is_null());
4648
4649 let flow_map = module.getattr("flow_map").unwrap();
4650 let flow_map = extract_template(py, &flow_map, "yaml_t/yaml_t_str").unwrap();
4651 let rendered = render_document(py, &parse_template(&flow_map).unwrap()).unwrap();
4652 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4653 assert_integer_entry(&documents[0], "a", 1);
4654
4655 let empty_flow_seq = module.getattr("empty_flow_seq").unwrap();
4656 let empty_flow_seq =
4657 extract_template(py, &empty_flow_seq, "yaml_t/yaml_t_str").unwrap();
4658 let rendered = render_document(py, &parse_template(&empty_flow_seq).unwrap()).unwrap();
4659 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4660 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4661 assert_eq!(yaml_sequence_len(value), Some(0));
4662
4663 let empty_flow_map = module.getattr("empty_flow_map").unwrap();
4664 let empty_flow_map =
4665 extract_template(py, &empty_flow_map, "yaml_t/yaml_t_str").unwrap();
4666 let rendered = render_document(py, &parse_template(&empty_flow_map).unwrap()).unwrap();
4667 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4668 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4669 assert!(yaml_mapping(value).is_some());
4670
4671 let flow_scalar_mix = module.getattr("flow_scalar_mix").unwrap();
4672 let flow_scalar_mix =
4673 extract_template(py, &flow_scalar_mix, "yaml_t/yaml_t_str").unwrap();
4674 let rendered = render_document(py, &parse_template(&flow_scalar_mix).unwrap()).unwrap();
4675 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4676 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4677 match value {
4678 YamlOwned::Sequence(sequence) => {
4679 assert_eq!(sequence.len(), 3);
4680 assert_eq!(yaml_scalar_text(&sequence[0]), Some(""));
4681 assert_eq!(yaml_scalar_text(&sequence[1]), Some(""));
4682 assert_eq!(yaml_scalar_text(&sequence[2]), Some("plain"));
4683 }
4684 _ => panic!("expected YAML sequence"),
4685 }
4686
4687 let flow_plain_scalar = module.getattr("flow_plain_scalar").unwrap();
4688 let flow_plain_scalar =
4689 extract_template(py, &flow_plain_scalar, "yaml_t/yaml_t_str").unwrap();
4690 let rendered =
4691 render_document(py, &parse_template(&flow_plain_scalar).unwrap()).unwrap();
4692 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4693 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4694 match value {
4695 YamlOwned::Sequence(sequence) => {
4696 assert_eq!(sequence.len(), 1);
4697 assert_eq!(yaml_scalar_text(&sequence[0]), Some("1 2"));
4698 }
4699 _ => panic!("expected YAML sequence"),
4700 }
4701
4702 let flow_hash_plain_mapping_value =
4703 module.getattr("flow_hash_plain_mapping_value").unwrap();
4704 let flow_hash_plain_mapping_value =
4705 extract_template(py, &flow_hash_plain_mapping_value, "yaml_t/yaml_t_str").unwrap();
4706 let rendered =
4707 render_document(py, &parse_template(&flow_hash_plain_mapping_value).unwrap())
4708 .unwrap();
4709 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4710 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4711 assert_string_entry(value, "a", "b#c");
4712
4713 let flow_hash_plain_mapping_values =
4714 module.getattr("flow_hash_plain_mapping_values").unwrap();
4715 let flow_hash_plain_mapping_values =
4716 extract_template(py, &flow_hash_plain_mapping_values, "yaml_t/yaml_t_str").unwrap();
4717 let rendered = render_document(
4718 py,
4719 &parse_template(&flow_hash_plain_mapping_values).unwrap(),
4720 )
4721 .unwrap();
4722 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4723 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4724 assert_string_entry(value, "a", "b#c");
4725 assert_string_entry(value, "d", "e#f");
4726
4727 let flow_hash_plain_scalars = module.getattr("flow_hash_plain_scalars").unwrap();
4728 let flow_hash_plain_scalars =
4729 extract_template(py, &flow_hash_plain_scalars, "yaml_t/yaml_t_str").unwrap();
4730 let rendered =
4731 render_document(py, &parse_template(&flow_hash_plain_scalars).unwrap()).unwrap();
4732 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4733 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4734 match value {
4735 YamlOwned::Sequence(sequence) => {
4736 assert_eq!(sequence.len(), 2);
4737 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b"));
4738 assert_eq!(yaml_scalar_text(&sequence[1]), Some("c#d"));
4739 }
4740 _ => panic!("expected YAML sequence"),
4741 }
4742
4743 let flow_hash_value_sequence = module.getattr("flow_hash_value_sequence").unwrap();
4744 let flow_hash_value_sequence =
4745 extract_template(py, &flow_hash_value_sequence, "yaml_t/yaml_t_str").unwrap();
4746 let rendered =
4747 render_document(py, &parse_template(&flow_hash_value_sequence).unwrap()).unwrap();
4748 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4749 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4750 match value {
4751 YamlOwned::Sequence(sequence) => {
4752 assert_eq!(sequence.len(), 3);
4753 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b"));
4754 assert_eq!(yaml_scalar_text(&sequence[1]), Some("c#d"));
4755 assert_eq!(yaml_scalar_text(&sequence[2]), Some("e#f"));
4756 }
4757 _ => panic!("expected YAML sequence"),
4758 }
4759
4760 let flow_hash_long_sequence = module.getattr("flow_hash_long_sequence").unwrap();
4761 let flow_hash_long_sequence =
4762 extract_template(py, &flow_hash_long_sequence, "yaml_t/yaml_t_str").unwrap();
4763 let rendered =
4764 render_document(py, &parse_template(&flow_hash_long_sequence).unwrap()).unwrap();
4765 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4766 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4767 match value {
4768 YamlOwned::Sequence(sequence) => {
4769 assert_eq!(sequence.len(), 4);
4770 assert_eq!(yaml_scalar_text(&sequence[3]), Some("g#h"));
4771 }
4772 _ => panic!("expected YAML sequence"),
4773 }
4774
4775 let flow_hash_five_sequence = module.getattr("flow_hash_five_sequence").unwrap();
4776 let flow_hash_five_sequence =
4777 extract_template(py, &flow_hash_five_sequence, "yaml_t/yaml_t_str").unwrap();
4778 let rendered =
4779 render_document(py, &parse_template(&flow_hash_five_sequence).unwrap()).unwrap();
4780 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4781 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4782 match value {
4783 YamlOwned::Sequence(sequence) => {
4784 assert_eq!(sequence.len(), 5);
4785 assert_eq!(yaml_scalar_text(&sequence[4]), Some("i#j"));
4786 }
4787 _ => panic!("expected YAML sequence"),
4788 }
4789
4790 let flow_mapping_hash_key = module.getattr("flow_mapping_hash_key").unwrap();
4791 let flow_mapping_hash_key =
4792 extract_template(py, &flow_mapping_hash_key, "yaml_t/yaml_t_str").unwrap();
4793 let rendered =
4794 render_document(py, &parse_template(&flow_mapping_hash_key).unwrap()).unwrap();
4795 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4796 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4797 assert_integer_entry(value, "a#b", 1);
4798
4799 let flow_sequence_comments_value =
4800 module.getattr("flow_sequence_comments_value").unwrap();
4801 let flow_sequence_comments_value =
4802 extract_template(py, &flow_sequence_comments_value, "yaml_t/yaml_t_str").unwrap();
4803 let rendered =
4804 render_document(py, &parse_template(&flow_sequence_comments_value).unwrap())
4805 .unwrap();
4806 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4807 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4808 match value {
4809 YamlOwned::Sequence(sequence) => {
4810 assert_eq!(sequence.len(), 2);
4811 assert_eq!(yaml_integer(&sequence[0]), Some(1));
4812 assert_eq!(yaml_integer(&sequence[1]), Some(2));
4813 }
4814 _ => panic!("expected YAML sequence"),
4815 }
4816
4817 let flow_mapping_comments_value =
4818 module.getattr("flow_mapping_comments_value").unwrap();
4819 let flow_mapping_comments_value =
4820 extract_template(py, &flow_mapping_comments_value, "yaml_t/yaml_t_str").unwrap();
4821 let rendered =
4822 render_document(py, &parse_template(&flow_mapping_comments_value).unwrap())
4823 .unwrap();
4824 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4825 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4826 assert_integer_entry(value, "a", 1);
4827 assert_integer_entry(value, "b", 2);
4828
4829 let comment_after_value = module.getattr("comment_after_value").unwrap();
4830 let comment_after_value =
4831 extract_template(py, &comment_after_value, "yaml_t/yaml_t_str").unwrap();
4832 let rendered =
4833 render_document(py, &parse_template(&comment_after_value).unwrap()).unwrap();
4834 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4835 assert_string_entry(&documents[0], "value", "a");
4836
4837 let plain_colon_hash = module.getattr("plain_colon_hash").unwrap();
4838 let plain_colon_hash =
4839 extract_template(py, &plain_colon_hash, "yaml_t/yaml_t_str").unwrap();
4840 let rendered =
4841 render_document(py, &parse_template(&plain_colon_hash).unwrap()).unwrap();
4842 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4843 assert_string_entry(&documents[0], "value", "a:b#c");
4844
4845 let plain_colon_hash_deeper = module.getattr("plain_colon_hash_deeper").unwrap();
4846 let plain_colon_hash_deeper =
4847 extract_template(py, &plain_colon_hash_deeper, "yaml_t/yaml_t_str").unwrap();
4848 let rendered =
4849 render_document(py, &parse_template(&plain_colon_hash_deeper).unwrap()).unwrap();
4850 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4851 assert_string_entry(&documents[0], "value", "a:b:c#d");
4852
4853 let plain_hash_chain = module.getattr("plain_hash_chain").unwrap();
4854 let plain_hash_chain =
4855 extract_template(py, &plain_hash_chain, "yaml_t/yaml_t_str").unwrap();
4856 let rendered =
4857 render_document(py, &parse_template(&plain_hash_chain).unwrap()).unwrap();
4858 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4859 assert_string_entry(&documents[0], "value", "a#b#c");
4860
4861 let plain_hash_chain_deeper = module.getattr("plain_hash_chain_deeper").unwrap();
4862 let plain_hash_chain_deeper =
4863 extract_template(py, &plain_hash_chain_deeper, "yaml_t/yaml_t_str").unwrap();
4864 let rendered =
4865 render_document(py, &parse_template(&plain_hash_chain_deeper).unwrap()).unwrap();
4866 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4867 assert_string_entry(&documents[0], "value", "a#b#c#d");
4868
4869 let plain_hash_chain_deeper_comment =
4870 module.getattr("plain_hash_chain_deeper_comment").unwrap();
4871 let plain_hash_chain_deeper_comment =
4872 extract_template(py, &plain_hash_chain_deeper_comment, "yaml_t/yaml_t_str")
4873 .unwrap();
4874 let rendered = render_document(
4875 py,
4876 &parse_template(&plain_hash_chain_deeper_comment).unwrap(),
4877 )
4878 .unwrap();
4879 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4880 assert_string_entry(&documents[0], "value", "a#b#c#d");
4881
4882 let flow_hash_mapping_long = module.getattr("flow_hash_mapping_long").unwrap();
4883 let flow_hash_mapping_long =
4884 extract_template(py, &flow_hash_mapping_long, "yaml_t/yaml_t_str").unwrap();
4885 let rendered =
4886 render_document(py, &parse_template(&flow_hash_mapping_long).unwrap()).unwrap();
4887 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4888 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4889 assert_string_entry(value, "a", "b#c");
4890 assert_string_entry(value, "d", "e#f");
4891 assert_string_entry(value, "g", "h#i");
4892
4893 let flow_hash_mapping_four = module.getattr("flow_hash_mapping_four").unwrap();
4894 let flow_hash_mapping_four =
4895 extract_template(py, &flow_hash_mapping_four, "yaml_t/yaml_t_str").unwrap();
4896 let rendered =
4897 render_document(py, &parse_template(&flow_hash_mapping_four).unwrap()).unwrap();
4898 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4899 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4900 assert_string_entry(value, "a", "b#c");
4901 assert_string_entry(value, "d", "e#f");
4902 assert_string_entry(value, "g", "h#i");
4903 assert_string_entry(value, "j", "k#l");
4904
4905 let flow_hash_mapping_five = module.getattr("flow_hash_mapping_five").unwrap();
4906 let flow_hash_mapping_five =
4907 extract_template(py, &flow_hash_mapping_five, "yaml_t/yaml_t_str").unwrap();
4908 let rendered =
4909 render_document(py, &parse_template(&flow_hash_mapping_five).unwrap()).unwrap();
4910 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4911 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4912 assert_string_entry(value, "m", "n#o");
4913
4914 let flow_hash_mapping_six = module.getattr("flow_hash_mapping_six").unwrap();
4915 let flow_hash_mapping_six =
4916 extract_template(py, &flow_hash_mapping_six, "yaml_t/yaml_t_str").unwrap();
4917 let rendered =
4918 render_document(py, &parse_template(&flow_hash_mapping_six).unwrap()).unwrap();
4919 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4920 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4921 assert_string_entry(value, "p", "q#r");
4922
4923 let comment_after_plain_colon = module.getattr("comment_after_plain_colon").unwrap();
4924 let comment_after_plain_colon =
4925 extract_template(py, &comment_after_plain_colon, "yaml_t/yaml_t_str").unwrap();
4926 let rendered =
4927 render_document(py, &parse_template(&comment_after_plain_colon).unwrap()).unwrap();
4928 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4929 assert_string_entry(&documents[0], "value", "a:b");
4930
4931 let comment_after_flow_plain_colon =
4932 module.getattr("comment_after_flow_plain_colon").unwrap();
4933 let comment_after_flow_plain_colon =
4934 extract_template(py, &comment_after_flow_plain_colon, "yaml_t/yaml_t_str").unwrap();
4935 let rendered = render_document(
4936 py,
4937 &parse_template(&comment_after_flow_plain_colon).unwrap(),
4938 )
4939 .unwrap();
4940 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4941 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4942 match value {
4943 YamlOwned::Sequence(sequence) => {
4944 assert_eq!(sequence.len(), 1);
4945 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a:b"));
4946 }
4947 _ => panic!("expected YAML sequence"),
4948 }
4949
4950 let flow_plain_hash_chain = module.getattr("flow_plain_hash_chain").unwrap();
4951 let flow_plain_hash_chain =
4952 extract_template(py, &flow_plain_hash_chain, "yaml_t/yaml_t_str").unwrap();
4953 let rendered =
4954 render_document(py, &parse_template(&flow_plain_hash_chain).unwrap()).unwrap();
4955 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4956 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4957 match value {
4958 YamlOwned::Sequence(sequence) => {
4959 assert_eq!(sequence.len(), 2);
4960 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b#c"));
4961 assert_eq!(yaml_scalar_text(&sequence[1]), Some("d#e#f"));
4962 }
4963 _ => panic!("expected YAML sequence"),
4964 }
4965
4966 let flow_plain_hash_chain_single_deeper = module
4967 .getattr("flow_plain_hash_chain_single_deeper")
4968 .unwrap();
4969 let flow_plain_hash_chain_single_deeper = extract_template(
4970 py,
4971 &flow_plain_hash_chain_single_deeper,
4972 "yaml_t/yaml_t_str",
4973 )
4974 .unwrap();
4975 let rendered = render_document(
4976 py,
4977 &parse_template(&flow_plain_hash_chain_single_deeper).unwrap(),
4978 )
4979 .unwrap();
4980 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4981 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4982 match value {
4983 YamlOwned::Sequence(sequence) => {
4984 assert_eq!(sequence.len(), 1);
4985 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b#c#d"));
4986 }
4987 _ => panic!("expected YAML sequence"),
4988 }
4989
4990 let flow_plain_hash_chain_single_deeper_comment = module
4991 .getattr("flow_plain_hash_chain_single_deeper_comment")
4992 .unwrap();
4993 let flow_plain_hash_chain_single_deeper_comment = extract_template(
4994 py,
4995 &flow_plain_hash_chain_single_deeper_comment,
4996 "yaml_t/yaml_t_str",
4997 )
4998 .unwrap();
4999 let rendered = render_document(
5000 py,
5001 &parse_template(&flow_plain_hash_chain_single_deeper_comment).unwrap(),
5002 )
5003 .unwrap();
5004 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5005 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5006 match value {
5007 YamlOwned::Sequence(sequence) => {
5008 assert_eq!(sequence.len(), 1);
5009 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b#c#d"));
5010 }
5011 _ => panic!("expected YAML sequence"),
5012 }
5013
5014 let flow_hash_seq_six = module.getattr("flow_hash_seq_six").unwrap();
5015 let flow_hash_seq_six =
5016 extract_template(py, &flow_hash_seq_six, "yaml_t/yaml_t_str").unwrap();
5017 let rendered =
5018 render_document(py, &parse_template(&flow_hash_seq_six).unwrap()).unwrap();
5019 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5020 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5021 match value {
5022 YamlOwned::Sequence(sequence) => {
5023 assert_eq!(sequence.len(), 6);
5024 assert_eq!(yaml_scalar_text(&sequence[5]), Some("k#l"));
5025 }
5026 _ => panic!("expected YAML sequence"),
5027 }
5028
5029 let flow_hash_seq_seven = module.getattr("flow_hash_seq_seven").unwrap();
5030 let flow_hash_seq_seven =
5031 extract_template(py, &flow_hash_seq_seven, "yaml_t/yaml_t_str").unwrap();
5032 let rendered =
5033 render_document(py, &parse_template(&flow_hash_seq_seven).unwrap()).unwrap();
5034 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5035 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5036 match value {
5037 YamlOwned::Sequence(sequence) => {
5038 assert_eq!(sequence.len(), 7);
5039 assert_eq!(yaml_scalar_text(&sequence[6]), Some("m#n"));
5040 }
5041 _ => panic!("expected YAML sequence"),
5042 }
5043
5044 let flow_plain_hash_chain_four = module.getattr("flow_plain_hash_chain_four").unwrap();
5045 let flow_plain_hash_chain_four =
5046 extract_template(py, &flow_plain_hash_chain_four, "yaml_t/yaml_t_str").unwrap();
5047 let rendered =
5048 render_document(py, &parse_template(&flow_plain_hash_chain_four).unwrap()).unwrap();
5049 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5050 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5051 match value {
5052 YamlOwned::Sequence(sequence) => {
5053 assert_eq!(sequence.len(), 4);
5054 assert_eq!(yaml_scalar_text(&sequence[3]), Some("j#k#l"));
5055 }
5056 _ => panic!("expected YAML sequence"),
5057 }
5058
5059 let block_plain_comment_after_colon_deeper = module
5060 .getattr("block_plain_comment_after_colon_deeper")
5061 .unwrap();
5062 let block_plain_comment_after_colon_deeper = extract_template(
5063 py,
5064 &block_plain_comment_after_colon_deeper,
5065 "yaml_t/yaml_t_str",
5066 )
5067 .unwrap();
5068 let rendered = render_document(
5069 py,
5070 &parse_template(&block_plain_comment_after_colon_deeper).unwrap(),
5071 )
5072 .unwrap();
5073 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5074 assert_string_entry(&documents[0], "value", "a:b:c:d");
5075
5076 let flow_plain_comment_after_colon_deeper = module
5077 .getattr("flow_plain_comment_after_colon_deeper")
5078 .unwrap();
5079 let flow_plain_comment_after_colon_deeper = extract_template(
5080 py,
5081 &flow_plain_comment_after_colon_deeper,
5082 "yaml_t/yaml_t_str",
5083 )
5084 .unwrap();
5085 let rendered = render_document(
5086 py,
5087 &parse_template(&flow_plain_comment_after_colon_deeper).unwrap(),
5088 )
5089 .unwrap();
5090 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5091 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5092 match value {
5093 YamlOwned::Sequence(sequence) => {
5094 assert_eq!(sequence.len(), 1);
5095 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a:b:c:d"));
5096 }
5097 _ => panic!("expected YAML sequence"),
5098 }
5099
5100 let flow_mapping_plain_key_question =
5101 module.getattr("flow_mapping_plain_key_question").unwrap();
5102 let flow_mapping_plain_key_question =
5103 extract_template(py, &flow_mapping_plain_key_question, "yaml_t/yaml_t_str")
5104 .unwrap();
5105 let rendered = render_document(
5106 py,
5107 &parse_template(&flow_mapping_plain_key_question).unwrap(),
5108 )
5109 .unwrap();
5110 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5111 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5112 let mapping = yaml_mapping(value).expect("expected YAML mapping");
5113 assert_eq!(mapping.len(), 1);
5114 let (_, entry_value) = mapping.iter().next().expect("expected YAML mapping entry");
5115 assert_eq!(yaml_integer(entry_value), Some(1));
5116
5117 let flow_mapping_plain_key_questions =
5118 module.getattr("flow_mapping_plain_key_questions").unwrap();
5119 let flow_mapping_plain_key_questions =
5120 extract_template(py, &flow_mapping_plain_key_questions, "yaml_t/yaml_t_str")
5121 .unwrap();
5122 let rendered = render_document(
5123 py,
5124 &parse_template(&flow_mapping_plain_key_questions).unwrap(),
5125 )
5126 .unwrap();
5127 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5128 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5129 let mapping = yaml_mapping(value).expect("expected YAML mapping");
5130 assert_eq!(mapping.len(), 2);
5131
5132 let mapping_empty_flow_values = module.getattr("mapping_empty_flow_values").unwrap();
5133 let mapping_empty_flow_values =
5134 extract_template(py, &mapping_empty_flow_values, "yaml_t/yaml_t_str").unwrap();
5135 let rendered =
5136 render_document(py, &parse_template(&mapping_empty_flow_values).unwrap()).unwrap();
5137 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5138 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5139 let mapping = yaml_mapping(value).expect("expected YAML mapping");
5140 let left = mapping
5141 .iter()
5142 .find_map(|(key, value)| (yaml_scalar_text(key) == Some("a")).then_some(value))
5143 .expect("expected key a");
5144 let right = mapping
5145 .iter()
5146 .find_map(|(key, value)| (yaml_scalar_text(key) == Some("b")).then_some(value))
5147 .expect("expected key b");
5148 assert_eq!(yaml_sequence_len(left), Some(0));
5149 assert!(yaml_mapping(right).is_some());
5150
5151 let flow_mapping_empty_key = module.getattr("flow_mapping_empty_key").unwrap();
5152 let flow_mapping_empty_key =
5153 extract_template(py, &flow_mapping_empty_key, "yaml_t/yaml_t_str").unwrap();
5154 let rendered =
5155 render_document(py, &parse_template(&flow_mapping_empty_key).unwrap()).unwrap();
5156 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5157 assert_integer_entry(&documents[0], "", 1);
5158
5159 let flow_mapping_empty_key_and_values =
5160 module.getattr("flow_mapping_empty_key_and_values").unwrap();
5161 let flow_mapping_empty_key_and_values =
5162 extract_template(py, &flow_mapping_empty_key_and_values, "yaml_t/yaml_t_str")
5163 .unwrap();
5164 let rendered = render_document(
5165 py,
5166 &parse_template(&flow_mapping_empty_key_and_values).unwrap(),
5167 )
5168 .unwrap();
5169 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5170 assert!(yaml_mapping_entry(&documents[0], "").is_some());
5171 let foo = yaml_mapping_entry(&documents[0], "foo").expect("expected foo");
5172 assert!(yaml_mapping(foo).is_some());
5173
5174 let flow_mapping_nested_empty = module.getattr("flow_mapping_nested_empty").unwrap();
5175 let flow_mapping_nested_empty =
5176 extract_template(py, &flow_mapping_nested_empty, "yaml_t/yaml_t_str").unwrap();
5177 let rendered =
5178 render_document(py, &parse_template(&flow_mapping_nested_empty).unwrap()).unwrap();
5179 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5180 let a = yaml_mapping_entry(&documents[0], "a").expect("expected key a");
5181 let b = yaml_mapping_entry(&documents[0], "b").expect("expected key b");
5182 assert!(yaml_mapping(a).is_some());
5183 assert_eq!(yaml_sequence_len(b), Some(0));
5184
5185 let flow_null_key = module.getattr("flow_null_key").unwrap();
5186 let flow_null_key = extract_template(py, &flow_null_key, "yaml_t/yaml_t_str").unwrap();
5187 let rendered = render_document(py, &parse_template(&flow_null_key).unwrap()).unwrap();
5188 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5189 let mapping = yaml_mapping(&documents[0]).expect("expected YAML mapping");
5190 assert!(
5191 mapping
5192 .iter()
5193 .any(|(key, value)| key.is_null() && yaml_integer(value) == Some(1))
5194 );
5195 assert!(mapping.iter().any(|(key, value)| {
5196 yaml_scalar_text(key) == Some("") && yaml_integer(value) == Some(2)
5197 }));
5198
5199 let block_null_key = module.getattr("block_null_key").unwrap();
5200 let block_null_key =
5201 extract_template(py, &block_null_key, "yaml_t/yaml_t_str").unwrap();
5202 let rendered = render_document(py, &parse_template(&block_null_key).unwrap()).unwrap();
5203 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5204 let mapping = yaml_mapping(&documents[0]).expect("expected YAML mapping");
5205 assert!(
5206 mapping
5207 .iter()
5208 .any(|(key, value)| key.is_null() && yaml_integer(value) == Some(1))
5209 );
5210
5211 let quoted_null_key = module.getattr("quoted_null_key").unwrap();
5212 let quoted_null_key =
5213 extract_template(py, "ed_null_key, "yaml_t/yaml_t_str").unwrap();
5214 let rendered = render_document(py, &parse_template("ed_null_key).unwrap()).unwrap();
5215 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5216 assert_integer_entry(&documents[0], "", 1);
5217
5218 let flow_sequence_nested_empty = module.getattr("flow_sequence_nested_empty").unwrap();
5219 let flow_sequence_nested_empty =
5220 extract_template(py, &flow_sequence_nested_empty, "yaml_t/yaml_t_str").unwrap();
5221 let rendered =
5222 render_document(py, &parse_template(&flow_sequence_nested_empty).unwrap()).unwrap();
5223 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5224 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
5225
5226 let plain_scalar_colon_no_space =
5227 module.getattr("plain_scalar_colon_no_space").unwrap();
5228 let plain_scalar_colon_no_space =
5229 extract_template(py, &plain_scalar_colon_no_space, "yaml_t/yaml_t_str").unwrap();
5230 let rendered =
5231 render_document(py, &parse_template(&plain_scalar_colon_no_space).unwrap())
5232 .unwrap();
5233 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5234 assert_string_entry(&documents[0], "value", "a:b");
5235
5236 let plain_question_mark_scalar = module.getattr("plain_question_mark_scalar").unwrap();
5237 let plain_question_mark_scalar =
5238 extract_template(py, &plain_question_mark_scalar, "yaml_t/yaml_t_str").unwrap();
5239 let rendered =
5240 render_document(py, &parse_template(&plain_question_mark_scalar).unwrap()).unwrap();
5241 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5242 assert_string_entry(&documents[0], "value", "?x");
5243
5244 let plain_colon_scalar_flow = module.getattr("plain_colon_scalar_flow").unwrap();
5245 let plain_colon_scalar_flow =
5246 extract_template(py, &plain_colon_scalar_flow, "yaml_t/yaml_t_str").unwrap();
5247 let rendered =
5248 render_document(py, &parse_template(&plain_colon_scalar_flow).unwrap()).unwrap();
5249 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5250 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5251 assert_eq!(yaml_sequence_len(value), Some(2));
5252
5253 let flow_mapping_colon_plain_key =
5254 module.getattr("flow_mapping_colon_plain_key").unwrap();
5255 let flow_mapping_colon_plain_key =
5256 extract_template(py, &flow_mapping_colon_plain_key, "yaml_t/yaml_t_str").unwrap();
5257 let rendered =
5258 render_document(py, &parse_template(&flow_mapping_colon_plain_key).unwrap())
5259 .unwrap();
5260 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5261 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5262 assert_string_entry(value, "a:b", "c");
5263
5264 let flow_mapping_colon_and_hash =
5265 module.getattr("flow_mapping_colon_and_hash").unwrap();
5266 let flow_mapping_colon_and_hash =
5267 extract_template(py, &flow_mapping_colon_and_hash, "yaml_t/yaml_t_str").unwrap();
5268 let rendered =
5269 render_document(py, &parse_template(&flow_mapping_colon_and_hash).unwrap())
5270 .unwrap();
5271 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5272 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5273 assert_string_entry(value, "a:b", "c#d");
5274
5275 let block_plain_colon_no_space = module.getattr("block_plain_colon_no_space").unwrap();
5276 let block_plain_colon_no_space =
5277 extract_template(py, &block_plain_colon_no_space, "yaml_t/yaml_t_str").unwrap();
5278 let rendered =
5279 render_document(py, &parse_template(&block_plain_colon_no_space).unwrap()).unwrap();
5280 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5281 assert_string_entry(&documents[0], "value", "a:b:c");
5282
5283 let flow_plain_colon_hash_deeper =
5284 module.getattr("flow_plain_colon_hash_deeper").unwrap();
5285 let flow_plain_colon_hash_deeper =
5286 extract_template(py, &flow_plain_colon_hash_deeper, "yaml_t/yaml_t_str").unwrap();
5287 let rendered =
5288 render_document(py, &parse_template(&flow_plain_colon_hash_deeper).unwrap())
5289 .unwrap();
5290 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5291 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5292 match value {
5293 YamlOwned::Sequence(sequence) => {
5294 assert_eq!(sequence.len(), 1);
5295 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a:b:c#d"));
5296 }
5297 _ => panic!("expected YAML sequence"),
5298 }
5299
5300 let alias_in_flow_mapping_value =
5301 module.getattr("alias_in_flow_mapping_value").unwrap();
5302 let alias_in_flow_mapping_value =
5303 extract_template(py, &alias_in_flow_mapping_value, "yaml_t/yaml_t_str").unwrap();
5304 let rendered =
5305 render_document(py, &parse_template(&alias_in_flow_mapping_value).unwrap())
5306 .unwrap();
5307 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5308 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5309 let reference = yaml_mapping_entry(value, "ref").expect("expected ref");
5310 assert_integer_entry(reference, "x", 1);
5311
5312 let flow_null_and_alias = module.getattr("flow_null_and_alias").unwrap();
5313 let flow_null_and_alias =
5314 extract_template(py, &flow_null_and_alias, "yaml_t/yaml_t_str").unwrap();
5315 let rendered =
5316 render_document(py, &parse_template(&flow_null_and_alias).unwrap()).unwrap();
5317 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5318 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5319 let null_value = yaml_mapping(value)
5320 .and_then(|entries| {
5321 entries
5322 .iter()
5323 .find_map(|(key, value)| key.is_null().then_some(value))
5324 })
5325 .expect("expected null key");
5326 assert_integer_entry(null_value, "x", 1);
5327
5328 let alias_seq_value = module.getattr("alias_seq_value").unwrap();
5329 let alias_seq_value =
5330 extract_template(py, &alias_seq_value, "yaml_t/yaml_t_str").unwrap();
5331 let rendered = render_document(py, &parse_template(&alias_seq_value).unwrap()).unwrap();
5332 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5333 assert_eq!(
5334 yaml_sequence_len(yaml_mapping_entry(&documents[0], "a").expect("expected key a")),
5335 Some(2)
5336 );
5337 assert_eq!(
5338 yaml_sequence_len(yaml_mapping_entry(&documents[0], "b").expect("expected key b")),
5339 Some(2)
5340 );
5341
5342 let flow_mapping_missing_value = module.getattr("flow_mapping_missing_value").unwrap();
5343 let flow_mapping_missing_value =
5344 extract_template(py, &flow_mapping_missing_value, "yaml_t/yaml_t_str").unwrap();
5345 let rendered =
5346 render_document(py, &parse_template(&flow_mapping_missing_value).unwrap()).unwrap();
5347 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5348 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5349 let a = yaml_mapping_entry(value, "a").expect("expected key a");
5350 assert!(a.is_null());
5351
5352 let flow_seq_missing_value_before_end =
5353 module.getattr("flow_seq_missing_value_before_end").unwrap();
5354 let flow_seq_missing_value_before_end =
5355 extract_template(py, &flow_seq_missing_value_before_end, "yaml_t/yaml_t_str")
5356 .unwrap();
5357 let rendered = render_document(
5358 py,
5359 &parse_template(&flow_seq_missing_value_before_end).unwrap(),
5360 )
5361 .unwrap();
5362 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5363 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5364 assert_eq!(yaml_sequence_len(value), Some(2));
5365
5366 let flow_alias_map = module.getattr("flow_alias_map").unwrap();
5367 let flow_alias_map =
5368 extract_template(py, &flow_alias_map, "yaml_t/yaml_t_str").unwrap();
5369 let rendered = render_document(py, &parse_template(&flow_alias_map).unwrap()).unwrap();
5370 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5371 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5372 assert_integer_entry(value, "left", 1);
5373 assert_integer_entry(value, "right", 1);
5374
5375 let flow_alias_seq = module.getattr("flow_alias_seq").unwrap();
5376 let flow_alias_seq =
5377 extract_template(py, &flow_alias_seq, "yaml_t/yaml_t_str").unwrap();
5378 let rendered = render_document(py, &parse_template(&flow_alias_seq).unwrap()).unwrap();
5379 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5380 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5381 assert_eq!(yaml_sequence_len(value), Some(2));
5382
5383 let flow_merge = module.getattr("flow_merge").unwrap();
5384 let flow_merge = extract_template(py, &flow_merge, "yaml_t/yaml_t_str").unwrap();
5385 let rendered = render_document(py, &parse_template(&flow_merge).unwrap()).unwrap();
5386 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5387 assert!(rendered.text.contains("<<: &base"));
5388 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5389 assert_integer_entry(value, "b", 2);
5390
5391 let template = module.getattr("nested_flow_alias_merge").unwrap();
5392 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5393 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5394 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5395 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5396 let sequence = match value {
5397 YamlOwned::Sequence(sequence) => sequence,
5398 _ => panic!("expected YAML sequence"),
5399 };
5400 assert_eq!(sequence.len(), 2);
5401 assert_integer_entry(&sequence[0], "b", 2);
5402 assert_integer_entry(&sequence[1], "a", 1);
5403
5404 let explicit_seq = module.getattr("explicit_seq").unwrap();
5405 let explicit_seq = extract_template(py, &explicit_seq, "yaml_t/yaml_t_str").unwrap();
5406 let rendered = render_document(py, &parse_template(&explicit_seq).unwrap()).unwrap();
5407 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5408 let mapping = documents[0].as_mapping().expect("expected YAML mapping");
5409 let value = mapping
5410 .iter()
5411 .find_map(|(key, value)| (yaml_scalar_text(key) == Some("a")).then_some(value))
5412 .expect("expected key a");
5413 assert_eq!(yaml_sequence_len(value), Some(2));
5414
5415 let indented_block = module.getattr("indented_block").unwrap();
5416 let indented_block =
5417 extract_template(py, &indented_block, "yaml_t/yaml_t_str").unwrap();
5418 let rendered = render_document(py, &parse_template(&indented_block).unwrap()).unwrap();
5419 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5420 assert_string_entry(&documents[0], "value", "a\nb\n");
5421 assert_eq!(rendered.text, "value: |1\n a\n b\n");
5422 });
5423 }
5424
5425 #[test]
5426 fn rejects_parse_render_and_validation_contracts() {
5427 Python::with_gil(|py| {
5428 let module = PyModule::from_code(
5429 py,
5430 pyo3::ffi::c_str!(
5431 "from string.templatelib import Template\nclass BadStringValue:\n def __str__(self):\n raise ValueError('cannot stringify')\nbad=BadStringValue()\ntag='bad tag'\nparse_open=t'value: [1, 2'\nparse_tab=Template('a:\\t1\\n')\nparse_nested_tab=Template('a:\\n b:\\n\\t- 1\\n')\nparse_trailing=Template('value: *not alias\\n')\nparse_empty_flow=Template('[1,,2]\\n')\nparse_trailing_entry=Template('value: [1, 2,,]\\n')\nparse_empty_mapping=Template('value: {,}\\n')\nparse_missing_colon=Template('value: {a b}\\n')\nparse_extra_comma=Template('value: {a: 1,, b: 2}\\n')\nunknown_anchor=Template('value: *not_alias\\n')\ncross_doc_anchor=Template('--- &a\\n- 1\\n- 2\\n---\\n*a\\n')\nfragment_template=t'label: \"hi-{bad}\"'\nmetadata_template=t'value: !{tag} ok'\nfloat_template=t'value: {float(\"inf\")}'\n"
5432 ),
5433 pyo3::ffi::c_str!("test_yaml_error_contracts.py"),
5434 pyo3::ffi::c_str!("test_yaml_error_contracts"),
5435 )
5436 .unwrap();
5437
5438 for (name, expected) in [
5439 ("parse_open", "Expected"),
5440 ("parse_tab", "Tabs are not allowed"),
5441 ("parse_nested_tab", "Tabs are not allowed"),
5442 ("parse_trailing", "Unexpected trailing YAML content"),
5443 ("parse_empty_flow", "Expected a YAML value"),
5444 ("parse_trailing_entry", "Expected"),
5445 ("parse_empty_mapping", "Expected ':' in YAML template"),
5446 ("parse_missing_colon", "Expected ':' in YAML template"),
5447 ("parse_extra_comma", "Expected ':' in YAML template"),
5448 ] {
5449 let template = module.getattr(name).unwrap();
5450 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5451 let err = parse_template(&template).expect_err("expected YAML parse failure");
5452 assert_eq!(err.kind, ErrorKind::Parse);
5453 assert!(err.message.contains(expected), "{name}: {}", err.message);
5454 }
5455
5456 let unknown_anchor = module.getattr("unknown_anchor").unwrap();
5457 let unknown_anchor =
5458 extract_template(py, &unknown_anchor, "yaml_t/yaml_t_str").unwrap();
5459 let rendered = render_document(py, &parse_template(&unknown_anchor).unwrap()).unwrap();
5460 let err = parse_rendered_yaml(&rendered.text)
5461 .expect_err("expected YAML unknown-anchor parse failure");
5462 assert_eq!(err.kind, ErrorKind::Parse);
5463 assert!(err.message.contains("unknown anchor"));
5464
5465 let cross_doc_anchor = module.getattr("cross_doc_anchor").unwrap();
5466 let cross_doc_anchor =
5467 extract_template(py, &cross_doc_anchor, "yaml_t/yaml_t_str").unwrap();
5468 let rendered =
5469 render_document(py, &parse_template(&cross_doc_anchor).unwrap()).unwrap();
5470 let err = parse_rendered_yaml(&rendered.text)
5471 .expect_err("expected YAML cross-document anchor parse failure");
5472 assert_eq!(err.kind, ErrorKind::Parse);
5473 assert!(err.message.contains("unknown anchor"));
5474
5475 for (name, expected) in [
5476 ("fragment_template", "fragment"),
5477 ("metadata_template", "metadata"),
5478 ("float_template", "non-finite float"),
5479 ] {
5480 let template = module.getattr(name).unwrap();
5481 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5482 let document = parse_template(&template).unwrap();
5483 let err = match render_document(py, &document) {
5484 Ok(_) => panic!("expected YAML render failure"),
5485 Err(err) => err,
5486 };
5487 assert_eq!(err.kind, ErrorKind::Unrepresentable);
5488 assert!(err.message.contains(expected), "{name}: {}", err.message);
5489 }
5490 });
5491 }
5492
5493 #[test]
5494 fn renders_custom_tag_stream_and_complex_key_text() {
5495 Python::with_gil(|py| {
5496 let module = PyModule::from_code(
5497 py,
5498 pyo3::ffi::c_str!(
5499 "from string.templatelib import Template\ncomment_only_tail_stream=Template('---\\na: 1\\n--- # comment\\n...\\n')\ncustom_tag_sequence=Template('value: !custom [1, 2]\\n')\nflow_alias_map=Template('value: {left: &a 1, right: *a}\\n')\n"
5500 ),
5501 pyo3::ffi::c_str!("test_yaml_render_streams_and_keys.py"),
5502 pyo3::ffi::c_str!("test_yaml_render_streams_and_keys"),
5503 )
5504 .unwrap();
5505
5506 let comment_only_tail_stream = module.getattr("comment_only_tail_stream").unwrap();
5507 let comment_only_tail_stream =
5508 extract_template(py, &comment_only_tail_stream, "yaml_t/yaml_t_str").unwrap();
5509 let rendered =
5510 render_document(py, &parse_template(&comment_only_tail_stream).unwrap()).unwrap();
5511 assert_eq!(rendered.text, "---\na: 1\n---\nnull\n...");
5512 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5513 assert_eq!(documents.len(), 2);
5514 assert_integer_entry(&documents[0], "a", 1);
5515 assert!(documents[1].is_null());
5516
5517 let custom_tag_sequence = module.getattr("custom_tag_sequence").unwrap();
5518 let custom_tag_sequence =
5519 extract_template(py, &custom_tag_sequence, "yaml_t/yaml_t_str").unwrap();
5520 let rendered =
5521 render_document(py, &parse_template(&custom_tag_sequence).unwrap()).unwrap();
5522 assert_eq!(rendered.text, "value: !custom [ 1, 2 ]");
5523 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5524 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5525 assert_eq!(yaml_sequence_len(value), Some(2));
5526
5527 let flow_alias_map = module.getattr("flow_alias_map").unwrap();
5528 let flow_alias_map =
5529 extract_template(py, &flow_alias_map, "yaml_t/yaml_t_str").unwrap();
5530 let rendered = render_document(py, &parse_template(&flow_alias_map).unwrap()).unwrap();
5531 assert_eq!(rendered.text, "value: { left: &a 1, right: *a }");
5532 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5533 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5534 assert_integer_entry(value, "left", 1);
5535 assert_integer_entry(value, "right", 1);
5536 });
5537 }
5538
5539 #[test]
5540 fn renders_custom_tag_scalar_mapping_and_root_sequence_shapes() {
5541 Python::with_gil(|py| {
5542 let module = PyModule::from_code(
5543 py,
5544 pyo3::ffi::c_str!(
5545 "scalar_root=t'!custom 3\\n'\nmapping=t'value: !custom 3\\n'\nsequence=t'value: !custom [1, 2]\\n'\ncommented_root_sequence=t'--- # comment\\n!custom [1, 2]\\n'\n"
5546 ),
5547 pyo3::ffi::c_str!("test_yaml_custom_tag_shapes.py"),
5548 pyo3::ffi::c_str!("test_yaml_custom_tag_shapes"),
5549 )
5550 .unwrap();
5551
5552 let scalar_root = module.getattr("scalar_root").unwrap();
5553 let scalar_root = extract_template(py, &scalar_root, "yaml_t/yaml_t_str").unwrap();
5554 let rendered = render_document(py, &parse_template(&scalar_root).unwrap()).unwrap();
5555 assert_eq!(rendered.text, "!custom 3");
5556 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5557 assert_eq!(yaml_integer(&documents[0]), Some(3));
5558
5559 let mapping = module.getattr("mapping").unwrap();
5560 let mapping = extract_template(py, &mapping, "yaml_t/yaml_t_str").unwrap();
5561 let rendered = render_document(py, &parse_template(&mapping).unwrap()).unwrap();
5562 assert_eq!(rendered.text, "value: !custom 3");
5563 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5564 assert_integer_entry(&documents[0], "value", 3);
5565
5566 let sequence = module.getattr("sequence").unwrap();
5567 let sequence = extract_template(py, &sequence, "yaml_t/yaml_t_str").unwrap();
5568 let rendered = render_document(py, &parse_template(&sequence).unwrap()).unwrap();
5569 assert_eq!(rendered.text, "value: !custom [ 1, 2 ]");
5570 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5571 assert_eq!(
5572 yaml_sequence_len(yaml_mapping_entry(&documents[0], "value").expect("value key")),
5573 Some(2)
5574 );
5575
5576 let commented_root_sequence = module.getattr("commented_root_sequence").unwrap();
5577 let commented_root_sequence =
5578 extract_template(py, &commented_root_sequence, "yaml_t/yaml_t_str").unwrap();
5579 let rendered =
5580 render_document(py, &parse_template(&commented_root_sequence).unwrap()).unwrap();
5581 assert_eq!(rendered.text, "---\n!custom [ 1, 2 ]");
5582 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5583 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
5584 });
5585 }
5586
5587 #[test]
5588 fn renders_core_schema_scalars_and_top_level_sequence() {
5589 Python::with_gil(|py| {
5590 let module = PyModule::from_code(
5591 py,
5592 pyo3::ffi::c_str!(
5593 "from string.templatelib import Template\ncore_scalars=Template('value: true\\nnone: null\\nlegacy_bool: on\\nlegacy_yes: yes\\n')\ntop_level_sequence=Template('- 1\\n- true\\n- null\\n- on\\n')\n"
5594 ),
5595 pyo3::ffi::c_str!("test_yaml_core_scalars_and_sequence.py"),
5596 pyo3::ffi::c_str!("test_yaml_core_scalars_and_sequence"),
5597 )
5598 .unwrap();
5599
5600 let core_scalars = module.getattr("core_scalars").unwrap();
5601 let core_scalars = extract_template(py, &core_scalars, "yaml_t/yaml_t_str").unwrap();
5602 let rendered = render_document(py, &parse_template(&core_scalars).unwrap()).unwrap();
5603 assert_eq!(
5604 rendered.text,
5605 "value: true\nnone: null\nlegacy_bool: on\nlegacy_yes: yes"
5606 );
5607 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5608 assert_eq!(documents.len(), 1);
5609 assert_eq!(
5610 yaml_mapping_entry(&documents[0], "value").and_then(YamlOwned::as_bool),
5611 Some(true)
5612 );
5613 assert!(
5614 yaml_mapping_entry(&documents[0], "none")
5615 .expect("none key")
5616 .is_null()
5617 );
5618 assert_string_entry(&documents[0], "legacy_bool", "on");
5619 assert_string_entry(&documents[0], "legacy_yes", "yes");
5620
5621 let top_level_sequence = module.getattr("top_level_sequence").unwrap();
5622 let top_level_sequence =
5623 extract_template(py, &top_level_sequence, "yaml_t/yaml_t_str").unwrap();
5624 let rendered =
5625 render_document(py, &parse_template(&top_level_sequence).unwrap()).unwrap();
5626 assert_eq!(rendered.text, "- 1\n- true\n- null\n- on");
5627 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5628 assert_eq!(documents.len(), 1);
5629 let sequence = documents[0].as_vec().expect("expected top-level sequence");
5630 assert_eq!(sequence.len(), 4);
5631 assert_eq!(yaml_integer(&sequence[0]), Some(1));
5632 assert_eq!(sequence[1].as_bool(), Some(true));
5633 assert!(sequence[2].is_null());
5634 assert_eq!(yaml_scalar_text(&sequence[3]), Some("on"));
5635 });
5636 }
5637
5638 #[test]
5639 fn renders_end_to_end_supported_positions_text_and_data() {
5640 Python::with_gil(|py| {
5641 let module = PyModule::from_code(
5642 py,
5643 pyo3::ffi::c_str!(
5644 "user='Alice'\nkey='owner'\nanchor='item'\ntag='str'\ntemplate=t'''\n{key}: {user}\nlabel: \"prefix-{user}\"\nplain: item-{user}\nitems:\n - &{anchor} {user}\n - *{anchor}\ntagged: !{tag} {user}\nflow: [{user}, {{label: {user}}}]\n'''\n"
5645 ),
5646 pyo3::ffi::c_str!("test_yaml_end_to_end_positions.py"),
5647 pyo3::ffi::c_str!("test_yaml_end_to_end_positions"),
5648 )
5649 .unwrap();
5650
5651 let template = module.getattr("template").unwrap();
5652 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5653 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5654 assert_eq!(
5655 rendered.text,
5656 "\"owner\": \"Alice\"\nlabel: \"prefix-Alice\"\nplain: item-Alice\nitems:\n - &item \"Alice\"\n - *item\ntagged: !str \"Alice\"\nflow: [ \"Alice\", { label: \"Alice\" } ]"
5657 );
5658 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5659 assert_eq!(documents.len(), 1);
5660 assert_string_entry(&documents[0], "owner", "Alice");
5661 assert_string_entry(&documents[0], "label", "prefix-Alice");
5662 assert_string_entry(&documents[0], "plain", "item-Alice");
5663 assert_string_entry(&documents[0], "tagged", "Alice");
5664 let items = yaml_mapping_entry(&documents[0], "items").expect("items key");
5665 let items = items.as_vec().expect("items sequence");
5666 assert_eq!(items.len(), 2);
5667 assert_eq!(yaml_scalar_text(&items[0]), Some("Alice"));
5668 assert_eq!(yaml_scalar_text(&items[1]), Some("Alice"));
5669 let flow = yaml_mapping_entry(&documents[0], "flow").expect("flow key");
5670 let flow = flow.as_vec().expect("flow sequence");
5671 assert_eq!(yaml_scalar_text(&flow[0]), Some("Alice"));
5672 assert_string_entry(&flow[1], "label", "Alice");
5673 });
5674 }
5675
5676 #[test]
5677 fn renders_block_scalars_and_sequence_item_text_and_data() {
5678 Python::with_gil(|py| {
5679 let module = PyModule::from_code(
5680 py,
5681 pyo3::ffi::c_str!(
5682 "user='Alice'\ntemplate=t'''\nliteral: |\n hello {user}\n world\nfolded: >\n hello {user}\n world\nlines:\n - |\n item {user}\n'''\n"
5683 ),
5684 pyo3::ffi::c_str!("test_yaml_block_scalars_render.py"),
5685 pyo3::ffi::c_str!("test_yaml_block_scalars_render"),
5686 )
5687 .unwrap();
5688
5689 let template = module.getattr("template").unwrap();
5690 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5691 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5692 assert_eq!(
5693 rendered.text,
5694 "literal: |\n hello Alice\n world\nfolded: >\n hello Alice\n world\nlines:\n - |\n item Alice\n"
5695 );
5696 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5697 assert_eq!(documents.len(), 1);
5698 assert_string_entry(&documents[0], "literal", "hello Alice\nworld\n");
5699 assert_string_entry(&documents[0], "folded", "hello Alice world\n");
5700 let lines = yaml_mapping_entry(&documents[0], "lines").expect("lines key");
5701 let lines = lines.as_vec().expect("lines sequence");
5702 assert_eq!(yaml_scalar_text(&lines[0]), Some("item Alice\n"));
5703 });
5704 }
5705
5706 #[test]
5707 fn renders_comment_only_document_variants_and_mid_stream_shapes() {
5708 Python::with_gil(|py| {
5709 let module = PyModule::from_code(
5710 py,
5711 pyo3::ffi::c_str!(
5712 "comment_only=t'# comment\\n'\ncomment_only_explicit=t'--- # comment\\n'\ncomment_only_explicit_end=t'--- # comment\\n...\\n'\ncomment_only_mid_stream=t'---\\na: 1\\n--- # comment\\n...\\n---\\nb: 2\\n'\n"
5713 ),
5714 pyo3::ffi::c_str!("test_yaml_comment_variants.py"),
5715 pyo3::ffi::c_str!("test_yaml_comment_variants"),
5716 )
5717 .unwrap();
5718
5719 for (name, expected_text) in [
5720 ("comment_only", "null"),
5721 ("comment_only_explicit", "---\nnull"),
5722 ("comment_only_explicit_end", "---\nnull\n..."),
5723 ] {
5724 let template = module.getattr(name).unwrap();
5725 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5726 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5727 assert_eq!(rendered.text, expected_text);
5728 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5729 assert_eq!(documents.len(), 1);
5730 assert!(documents[0].is_null());
5731 }
5732
5733 let comment_only_mid_stream = module.getattr("comment_only_mid_stream").unwrap();
5734 let comment_only_mid_stream =
5735 extract_template(py, &comment_only_mid_stream, "yaml_t/yaml_t_str").unwrap();
5736 let rendered =
5737 render_document(py, &parse_template(&comment_only_mid_stream).unwrap()).unwrap();
5738 assert_eq!(rendered.text, "---\na: 1\n---\nnull\n...\n---\nb: 2");
5739 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5740 assert_eq!(documents.len(), 3);
5741 assert_integer_entry(&documents[0], "a", 1);
5742 assert!(documents[1].is_null());
5743 assert_integer_entry(&documents[2], "b", 2);
5744 });
5745 }
5746
5747 #[test]
5748 fn renders_tag_directives_and_handle_tag_roots() {
5749 Python::with_gil(|py| {
5750 let module = PyModule::from_code(
5751 py,
5752 pyo3::ffi::c_str!(
5753 "tag_directive_scalar=t'%TAG !e! tag:example.com,2020:\\n---\\nvalue: !e!foo 1\\n'\ntag_directive_root=t'%YAML 1.2\\n%TAG !e! tag:example.com,2020:\\n---\\n!e!root {{value: !e!leaf 1}}\\n'\ntag_directive_root_comment=t'%YAML 1.2\\n%TAG !e! tag:example.com,2020:\\n--- # comment\\n!e!root {{value: !e!leaf 1}}\\n'\nverbatim_root_mapping=t'--- !<tag:yaml.org,2002:map>\\na: 1\\n'\nverbatim_root_sequence=t'--- !<tag:yaml.org,2002:seq>\\n- 1\\n- 2\\n'\n"
5754 ),
5755 pyo3::ffi::c_str!("test_yaml_tag_directives.py"),
5756 pyo3::ffi::c_str!("test_yaml_tag_directives"),
5757 )
5758 .unwrap();
5759
5760 let tag_directive_scalar = module.getattr("tag_directive_scalar").unwrap();
5761 let tag_directive_scalar =
5762 extract_template(py, &tag_directive_scalar, "yaml_t/yaml_t_str").unwrap();
5763 let rendered =
5764 render_document(py, &parse_template(&tag_directive_scalar).unwrap()).unwrap();
5765 assert_eq!(
5766 rendered.text,
5767 "%TAG !e! tag:example.com,2020:\n---\nvalue: !e!foo 1"
5768 );
5769 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5770 assert_integer_entry(&documents[0], "value", 1);
5771
5772 let tag_directive_root = module.getattr("tag_directive_root").unwrap();
5773 let tag_directive_root =
5774 extract_template(py, &tag_directive_root, "yaml_t/yaml_t_str").unwrap();
5775 let rendered =
5776 render_document(py, &parse_template(&tag_directive_root).unwrap()).unwrap();
5777 assert_eq!(
5778 rendered.text,
5779 "%YAML 1.2\n%TAG !e! tag:example.com,2020:\n---\n!e!root { value: !e!leaf 1 }"
5780 );
5781 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5782 assert_integer_entry(&documents[0], "value", 1);
5783
5784 let tag_directive_root_comment = module.getattr("tag_directive_root_comment").unwrap();
5785 let tag_directive_root_comment =
5786 extract_template(py, &tag_directive_root_comment, "yaml_t/yaml_t_str").unwrap();
5787 let rendered =
5788 render_document(py, &parse_template(&tag_directive_root_comment).unwrap()).unwrap();
5789 assert_eq!(
5790 rendered.text,
5791 "%YAML 1.2\n%TAG !e! tag:example.com,2020:\n---\n!e!root { value: !e!leaf 1 }"
5792 );
5793 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5794 assert_integer_entry(&documents[0], "value", 1);
5795
5796 let verbatim_root_mapping = module.getattr("verbatim_root_mapping").unwrap();
5797 let verbatim_root_mapping =
5798 extract_template(py, &verbatim_root_mapping, "yaml_t/yaml_t_str").unwrap();
5799 let rendered =
5800 render_document(py, &parse_template(&verbatim_root_mapping).unwrap()).unwrap();
5801 assert_eq!(rendered.text, "---\n!<tag:yaml.org,2002:map>\na: 1");
5802 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5803 assert_integer_entry(&documents[0], "a", 1);
5804
5805 let verbatim_root_sequence = module.getattr("verbatim_root_sequence").unwrap();
5806 let verbatim_root_sequence =
5807 extract_template(py, &verbatim_root_sequence, "yaml_t/yaml_t_str").unwrap();
5808 let rendered =
5809 render_document(py, &parse_template(&verbatim_root_sequence).unwrap()).unwrap();
5810 assert_eq!(rendered.text, "---\n!<tag:yaml.org,2002:seq>\n- 1\n- 2");
5811 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5812 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
5813 });
5814 }
5815
5816 #[test]
5817 fn renders_explicit_core_tag_root_shapes() {
5818 Python::with_gil(|py| {
5819 let module = PyModule::from_code(
5820 py,
5821 pyo3::ffi::c_str!(
5822 "root_bool=t'--- !!bool true\\n'\nroot_str=t'--- !!str true\\n'\nroot_int=t'--- !!int 3\\n'\n"
5823 ),
5824 pyo3::ffi::c_str!("test_yaml_core_tag_roots.py"),
5825 pyo3::ffi::c_str!("test_yaml_core_tag_roots"),
5826 )
5827 .unwrap();
5828
5829 for (name, expected_text) in [
5830 ("root_bool", "---\n!!bool true"),
5831 ("root_str", "---\n!!str true"),
5832 ("root_int", "---\n!!int 3"),
5833 ] {
5834 let template = module.getattr(name).unwrap();
5835 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5836 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5837 assert_eq!(rendered.text, expected_text);
5838 }
5839
5840 let root_bool = module.getattr("root_bool").unwrap();
5841 let root_bool = extract_template(py, &root_bool, "yaml_t/yaml_t_str").unwrap();
5842 let rendered = render_document(py, &parse_template(&root_bool).unwrap()).unwrap();
5843 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5844 assert_eq!(documents[0].as_bool(), Some(true));
5845
5846 let root_str = module.getattr("root_str").unwrap();
5847 let root_str = extract_template(py, &root_str, "yaml_t/yaml_t_str").unwrap();
5848 let rendered = render_document(py, &parse_template(&root_str).unwrap()).unwrap();
5849 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5850 assert_eq!(yaml_scalar_text(&documents[0]), Some("true"));
5851
5852 let root_int = module.getattr("root_int").unwrap();
5853 let root_int = extract_template(py, &root_int, "yaml_t/yaml_t_str").unwrap();
5854 let rendered = render_document(py, &parse_template(&root_int).unwrap()).unwrap();
5855 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5856 assert_eq!(yaml_integer(&documents[0]), Some(3));
5857 });
5858 }
5859
5860 #[test]
5861 fn renders_explicit_core_tag_mapping_and_root_text_families() {
5862 Python::with_gil(|py| {
5863 let module = PyModule::from_code(
5864 py,
5865 pyo3::ffi::c_str!(
5866 "mapping=t'value_bool: !!bool true\\nvalue_str: !!str true\\nvalue_float: !!float 1\\nvalue_null: !!null null\\n'\nroot_int=t'--- !!int 3\\n'\nroot_str=t'--- !!str true\\n'\nroot_bool=t'--- !!bool true\\n'\n"
5867 ),
5868 pyo3::ffi::c_str!("test_yaml_explicit_core_tag_families.py"),
5869 pyo3::ffi::c_str!("test_yaml_explicit_core_tag_families"),
5870 )
5871 .unwrap();
5872
5873 let mapping = module.getattr("mapping").unwrap();
5874 let mapping = extract_template(py, &mapping, "yaml_t/yaml_t_str").unwrap();
5875 let rendered = render_document(py, &parse_template(&mapping).unwrap()).unwrap();
5876 assert_eq!(
5877 rendered.text,
5878 "value_bool: !!bool true\nvalue_str: !!str true\nvalue_float: !!float 1\nvalue_null: !!null null"
5879 );
5880 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5881 assert_eq!(
5882 yaml_mapping_entry(&documents[0], "value_bool").and_then(YamlOwned::as_bool),
5883 Some(true)
5884 );
5885 assert_eq!(
5886 yaml_scalar_text(
5887 yaml_mapping_entry(&documents[0], "value_str").expect("value_str")
5888 ),
5889 Some("true")
5890 );
5891 assert_eq!(
5892 yaml_mapping_entry(&documents[0], "value_float").and_then(yaml_float),
5893 Some(1.0)
5894 );
5895 assert!(
5896 yaml_mapping_entry(&documents[0], "value_null")
5897 .expect("value_null")
5898 .is_null()
5899 );
5900
5901 for (name, expected_text) in [
5902 ("root_int", "---\n!!int 3"),
5903 ("root_str", "---\n!!str true"),
5904 ("root_bool", "---\n!!bool true"),
5905 ] {
5906 let template = module.getattr(name).unwrap();
5907 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5908 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5909 assert_eq!(rendered.text, expected_text, "{name}");
5910 }
5911 });
5912 }
5913
5914 #[test]
5915 fn renders_flow_trailing_comma_explicit_key_and_indent_indicator_families() {
5916 Python::with_gil(|py| {
5917 let module = PyModule::from_code(
5918 py,
5919 pyo3::ffi::c_str!(
5920 "from string.templatelib import Template\nflow_sequence=t'[1, 2,]\\n'\nflow_mapping=Template('{a: 1,}\\n')\nexplicit_key_sequence_value=t'? a\\n: - 1\\n - 2\\n'\nindent_indicator=t'value: |1\\n a\\n b\\n'\n"
5921 ),
5922 pyo3::ffi::c_str!("test_yaml_flow_indent_families.py"),
5923 pyo3::ffi::c_str!("test_yaml_flow_indent_families"),
5924 )
5925 .unwrap();
5926
5927 let flow_sequence = module.getattr("flow_sequence").unwrap();
5928 let flow_sequence = extract_template(py, &flow_sequence, "yaml_t/yaml_t_str").unwrap();
5929 let rendered = render_document(py, &parse_template(&flow_sequence).unwrap()).unwrap();
5930 assert_eq!(rendered.text, "[ 1, 2 ]");
5931 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5932 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
5933
5934 let flow_mapping = module.getattr("flow_mapping").unwrap();
5935 let flow_mapping = extract_template(py, &flow_mapping, "yaml_t/yaml_t_str").unwrap();
5936 let rendered = render_document(py, &parse_template(&flow_mapping).unwrap()).unwrap();
5937 assert_eq!(rendered.text, "{ a: 1 }");
5938 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5939 assert_integer_entry(&documents[0], "a", 1);
5940
5941 let explicit_key_sequence_value =
5942 module.getattr("explicit_key_sequence_value").unwrap();
5943 let explicit_key_sequence_value =
5944 extract_template(py, &explicit_key_sequence_value, "yaml_t/yaml_t_str").unwrap();
5945 let rendered =
5946 render_document(py, &parse_template(&explicit_key_sequence_value).unwrap())
5947 .unwrap();
5948 assert_eq!(rendered.text, "? a\n:\n - 1\n - 2");
5949 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5950 let entry = documents[0].as_mapping().expect("mapping");
5951 assert_eq!(entry.len(), 1);
5952
5953 let indent_indicator = module.getattr("indent_indicator").unwrap();
5954 let indent_indicator =
5955 extract_template(py, &indent_indicator, "yaml_t/yaml_t_str").unwrap();
5956 let rendered =
5957 render_document(py, &parse_template(&indent_indicator).unwrap()).unwrap();
5958 assert_eq!(rendered.text, "value: |1\n a\n b\n");
5959 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5960 assert_string_entry(&documents[0], "value", "a\nb\n");
5961 });
5962 }
5963
5964 #[test]
5965 fn renders_flow_alias_and_merge_shapes() {
5966 Python::with_gil(|py| {
5967 let module = PyModule::from_code(
5968 py,
5969 pyo3::ffi::c_str!(
5970 "from string.templatelib import Template\nflow_alias_map=Template('value: {left: &a 1, right: *a}\\n')\nflow_alias_seq=Template('value: [&a 1, *a]\\n')\nflow_merge=Template('value: {<<: &base {a: 1}, b: 2}\\n')\n"
5971 ),
5972 pyo3::ffi::c_str!("test_yaml_flow_alias_merge.py"),
5973 pyo3::ffi::c_str!("test_yaml_flow_alias_merge"),
5974 )
5975 .unwrap();
5976
5977 let flow_alias_map = module.getattr("flow_alias_map").unwrap();
5978 let flow_alias_map =
5979 extract_template(py, &flow_alias_map, "yaml_t/yaml_t_str").unwrap();
5980 let rendered = render_document(py, &parse_template(&flow_alias_map).unwrap()).unwrap();
5981 assert_eq!(rendered.text, "value: { left: &a 1, right: *a }");
5982 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5983 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
5984 assert_integer_entry(value, "left", 1);
5985 assert_integer_entry(value, "right", 1);
5986
5987 let flow_alias_seq = module.getattr("flow_alias_seq").unwrap();
5988 let flow_alias_seq =
5989 extract_template(py, &flow_alias_seq, "yaml_t/yaml_t_str").unwrap();
5990 let rendered = render_document(py, &parse_template(&flow_alias_seq).unwrap()).unwrap();
5991 assert_eq!(rendered.text, "value: [ &a 1, *a ]");
5992 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5993 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
5994 let value = value.as_vec().expect("value seq");
5995 assert_eq!(yaml_integer(&value[0]), Some(1));
5996 assert_eq!(yaml_integer(&value[1]), Some(1));
5997
5998 let flow_merge = module.getattr("flow_merge").unwrap();
5999 let flow_merge = extract_template(py, &flow_merge, "yaml_t/yaml_t_str").unwrap();
6000 let rendered = render_document(py, &parse_template(&flow_merge).unwrap()).unwrap();
6001 assert_eq!(rendered.text, "value: { <<: &base { a: 1 }, b: 2 }");
6002 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6003 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6004 assert_integer_entry(value, "b", 2);
6005 assert!(
6006 yaml_mapping_entry(value, "a").is_some()
6007 || yaml_mapping_entry(value, "<<").is_some()
6008 );
6009 });
6010 }
6011
6012 #[test]
6013 fn renders_document_stream_and_root_decorator_shapes() {
6014 Python::with_gil(|py| {
6015 let module = PyModule::from_code(
6016 py,
6017 pyo3::ffi::c_str!(
6018 "from string.templatelib import Template\ncomment_only_explicit_end_document=Template('--- # comment\\n...\\n')\ncomment_only_explicit_end_stream=Template('--- # comment\\n...\\n---\\na: 1\\n')\ncomment_only_mid_stream=Template('---\\na: 1\\n--- # comment\\n...\\n---\\nb: 2\\n')\nexplicit_end_comment_stream=Template('---\\na: 1\\n... # end\\n---\\nb: 2\\n')\ndoc_start_comment=Template('--- # comment\\nvalue: 1\\n')\ndoc_start_tag_comment=Template('--- !!str true # comment\\n')\ntagged_block_root_mapping=Template('--- !!map\\na: 1\\n')\ntagged_block_root_sequence=Template('--- !!seq\\n- 1\\n- 2\\n')\nroot_anchor_sequence=Template('--- &root\\n - 1\\n - 2\\n')\nroot_anchor_custom_mapping=Template('--- &root !custom\\n a: 1\\n')\nroot_custom_anchor_sequence=Template('--- !custom &root\\n - 1\\n - 2\\n')\nflow_newline=Template('{a: 1, b: [2, 3]}\\n')\n"
6019 ),
6020 pyo3::ffi::c_str!("test_yaml_stream_root_shapes.py"),
6021 pyo3::ffi::c_str!("test_yaml_stream_root_shapes"),
6022 )
6023 .unwrap();
6024
6025 let comment_only_explicit_end_document = module
6026 .getattr("comment_only_explicit_end_document")
6027 .unwrap();
6028 let comment_only_explicit_end_document =
6029 extract_template(py, &comment_only_explicit_end_document, "yaml_t/yaml_t_str")
6030 .unwrap();
6031 let rendered = render_document(
6032 py,
6033 &parse_template(&comment_only_explicit_end_document).unwrap(),
6034 )
6035 .unwrap();
6036 assert_eq!(rendered.text, "---\nnull\n...");
6037 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6038 assert!(matches!(documents[0], YamlOwned::Value(_)));
6039
6040 let comment_only_explicit_end_stream =
6041 module.getattr("comment_only_explicit_end_stream").unwrap();
6042 let comment_only_explicit_end_stream =
6043 extract_template(py, &comment_only_explicit_end_stream, "yaml_t/yaml_t_str")
6044 .unwrap();
6045 let rendered = render_document(
6046 py,
6047 &parse_template(&comment_only_explicit_end_stream).unwrap(),
6048 )
6049 .unwrap();
6050 assert_eq!(rendered.text, "---\nnull\n...\n---\na: 1");
6051 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6052 assert!(matches!(documents[0], YamlOwned::Value(_)));
6053 assert_integer_entry(&documents[1], "a", 1);
6054
6055 let comment_only_mid_stream = module.getattr("comment_only_mid_stream").unwrap();
6056 let comment_only_mid_stream =
6057 extract_template(py, &comment_only_mid_stream, "yaml_t/yaml_t_str").unwrap();
6058 let rendered =
6059 render_document(py, &parse_template(&comment_only_mid_stream).unwrap()).unwrap();
6060 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6061 assert_integer_entry(&documents[0], "a", 1);
6062 assert!(matches!(documents[1], YamlOwned::Value(_)));
6063 assert_integer_entry(&documents[2], "b", 2);
6064
6065 let explicit_end_comment_stream =
6066 module.getattr("explicit_end_comment_stream").unwrap();
6067 let explicit_end_comment_stream =
6068 extract_template(py, &explicit_end_comment_stream, "yaml_t/yaml_t_str").unwrap();
6069 let rendered =
6070 render_document(py, &parse_template(&explicit_end_comment_stream).unwrap())
6071 .unwrap();
6072 assert_eq!(rendered.text, "---\na: 1\n...\n---\nb: 2");
6073 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6074 assert_integer_entry(&documents[0], "a", 1);
6075 assert_integer_entry(&documents[1], "b", 2);
6076
6077 let doc_start_comment = module.getattr("doc_start_comment").unwrap();
6078 let doc_start_comment =
6079 extract_template(py, &doc_start_comment, "yaml_t/yaml_t_str").unwrap();
6080 let rendered =
6081 render_document(py, &parse_template(&doc_start_comment).unwrap()).unwrap();
6082 assert_eq!(rendered.text, "---\nvalue: 1");
6083 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6084 assert_integer_entry(&documents[0], "value", 1);
6085
6086 let doc_start_tag_comment = module.getattr("doc_start_tag_comment").unwrap();
6087 let doc_start_tag_comment =
6088 extract_template(py, &doc_start_tag_comment, "yaml_t/yaml_t_str").unwrap();
6089 let rendered =
6090 render_document(py, &parse_template(&doc_start_tag_comment).unwrap()).unwrap();
6091 assert_eq!(rendered.text, "---\n!!str true");
6092 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6093 assert_eq!(yaml_scalar_text(&documents[0]), Some("true"));
6094
6095 let tagged_block_root_mapping = module.getattr("tagged_block_root_mapping").unwrap();
6096 let tagged_block_root_mapping =
6097 extract_template(py, &tagged_block_root_mapping, "yaml_t/yaml_t_str").unwrap();
6098 let rendered =
6099 render_document(py, &parse_template(&tagged_block_root_mapping).unwrap()).unwrap();
6100 assert_eq!(rendered.text, "---\n!!map\na: 1");
6101 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6102 assert_integer_entry(&documents[0], "a", 1);
6103
6104 let tagged_block_root_sequence = module.getattr("tagged_block_root_sequence").unwrap();
6105 let tagged_block_root_sequence =
6106 extract_template(py, &tagged_block_root_sequence, "yaml_t/yaml_t_str").unwrap();
6107 let rendered =
6108 render_document(py, &parse_template(&tagged_block_root_sequence).unwrap()).unwrap();
6109 assert_eq!(rendered.text, "---\n!!seq\n- 1\n- 2");
6110 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6111 assert_eq!(documents[0].as_vec().expect("root seq").len(), 2);
6112
6113 let root_anchor_sequence = module.getattr("root_anchor_sequence").unwrap();
6114 let root_anchor_sequence =
6115 extract_template(py, &root_anchor_sequence, "yaml_t/yaml_t_str").unwrap();
6116 let rendered =
6117 render_document(py, &parse_template(&root_anchor_sequence).unwrap()).unwrap();
6118 assert_eq!(rendered.text, "---\n&root\n- 1\n- 2");
6119 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6120 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
6121
6122 let root_anchor_custom_mapping = module.getattr("root_anchor_custom_mapping").unwrap();
6123 let root_anchor_custom_mapping =
6124 extract_template(py, &root_anchor_custom_mapping, "yaml_t/yaml_t_str").unwrap();
6125 let rendered =
6126 render_document(py, &parse_template(&root_anchor_custom_mapping).unwrap()).unwrap();
6127 assert_eq!(rendered.text, "---\n!custom &root\na: 1");
6128 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6129 assert_integer_entry(&documents[0], "a", 1);
6130
6131 let root_custom_anchor_sequence =
6132 module.getattr("root_custom_anchor_sequence").unwrap();
6133 let root_custom_anchor_sequence =
6134 extract_template(py, &root_custom_anchor_sequence, "yaml_t/yaml_t_str").unwrap();
6135 let rendered =
6136 render_document(py, &parse_template(&root_custom_anchor_sequence).unwrap())
6137 .unwrap();
6138 assert_eq!(rendered.text, "---\n!custom &root\n- 1\n- 2");
6139 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6140 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
6141
6142 let flow_newline = module.getattr("flow_newline").unwrap();
6143 let flow_newline = extract_template(py, &flow_newline, "yaml_t/yaml_t_str").unwrap();
6144 let rendered = render_document(py, &parse_template(&flow_newline).unwrap()).unwrap();
6145 assert_eq!(rendered.text, "{ a: 1, b: [ 2, 3 ] }");
6146 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6147 let mapping = yaml_mapping(&documents[0]).expect("expected root flow mapping");
6148 assert_eq!(mapping.len(), 2);
6149 });
6150 }
6151
6152 #[test]
6153 fn renders_merge_and_collection_shape_families() {
6154 Python::with_gil(|py| {
6155 let module = PyModule::from_code(
6156 py,
6157 pyo3::ffi::c_str!(
6158 "from string.templatelib import Template\nmerge=Template('base: &base\\n a: 1\\n b: 2\\nderived:\\n <<: *base\\n c: 3\\n')\nflow_nested_alias_merge=Template('value: [{<<: &base {a: 1}, b: 2}, *base]\\n')\nalias_seq_value=Template('a: &x [1, 2]\\nb: *x\\n')\nempty_flow_sequence=Template('value: []\\n')\nempty_flow_mapping=Template('value: {}\\n')\nflow_mapping_missing_value=Template('value: {a: }\\n')\nflow_seq_missing_value_before_end=Template('value: [1, 2, ]\\n')\nindentless_sequence_value=Template('a:\\n- 1\\n- 2\\n')\nsequence_of_mappings=Template('- a: 1\\n b: 2\\n- c: 3\\n')\nmapping_of_sequence_of_mappings=Template('items:\\n- a: 1\\n b: 2\\n- c: 3\\n')\nsequence_of_sequences=Template('- - 1\\n - 2\\n- - 3\\n')\n"
6159 ),
6160 pyo3::ffi::c_str!("test_yaml_merge_collection_shapes.py"),
6161 pyo3::ffi::c_str!("test_yaml_merge_collection_shapes"),
6162 )
6163 .unwrap();
6164
6165 let merge = module.getattr("merge").unwrap();
6166 let merge = extract_template(py, &merge, "yaml_t/yaml_t_str").unwrap();
6167 let rendered = render_document(py, &parse_template(&merge).unwrap()).unwrap();
6168 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6169 let derived = yaml_mapping_entry(&documents[0], "derived").expect("derived");
6170 assert_integer_entry(derived, "c", 3);
6171 assert!(
6172 yaml_mapping_entry(derived, "a").is_some()
6173 || yaml_mapping_entry(derived, "<<").is_some()
6174 );
6175 assert!(
6176 yaml_mapping_entry(derived, "b").is_some()
6177 || yaml_mapping_entry(derived, "<<").is_some()
6178 );
6179
6180 let flow_nested_alias_merge = module.getattr("flow_nested_alias_merge").unwrap();
6181 let flow_nested_alias_merge =
6182 extract_template(py, &flow_nested_alias_merge, "yaml_t/yaml_t_str").unwrap();
6183 let rendered =
6184 render_document(py, &parse_template(&flow_nested_alias_merge).unwrap()).unwrap();
6185 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6186 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6187 let value = value.as_vec().expect("value seq");
6188 assert_eq!(value.len(), 2);
6189
6190 let alias_seq_value = module.getattr("alias_seq_value").unwrap();
6191 let alias_seq_value =
6192 extract_template(py, &alias_seq_value, "yaml_t/yaml_t_str").unwrap();
6193 let rendered = render_document(py, &parse_template(&alias_seq_value).unwrap()).unwrap();
6194 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6195 assert_eq!(
6196 yaml_sequence_len(yaml_mapping_entry(&documents[0], "a").expect("a")),
6197 Some(2)
6198 );
6199 assert_eq!(
6200 yaml_sequence_len(yaml_mapping_entry(&documents[0], "b").expect("b")),
6201 Some(2)
6202 );
6203
6204 for (name, expected_len) in [
6205 ("empty_flow_sequence", 0usize),
6206 ("flow_seq_missing_value_before_end", 2usize),
6207 ] {
6208 let template = module.getattr(name).unwrap();
6209 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6210 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6211 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6212 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6213 assert_eq!(yaml_sequence_len(value), Some(expected_len), "{name}");
6214 }
6215
6216 let empty_flow_mapping = module.getattr("empty_flow_mapping").unwrap();
6217 let empty_flow_mapping =
6218 extract_template(py, &empty_flow_mapping, "yaml_t/yaml_t_str").unwrap();
6219 let rendered =
6220 render_document(py, &parse_template(&empty_flow_mapping).unwrap()).unwrap();
6221 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6222 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6223 assert_eq!(yaml_mapping(value).expect("mapping").len(), 0);
6224
6225 let flow_mapping_missing_value = module.getattr("flow_mapping_missing_value").unwrap();
6226 let flow_mapping_missing_value =
6227 extract_template(py, &flow_mapping_missing_value, "yaml_t/yaml_t_str").unwrap();
6228 let rendered =
6229 render_document(py, &parse_template(&flow_mapping_missing_value).unwrap()).unwrap();
6230 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6231 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6232 assert!(yaml_mapping_entry(value, "a").is_some());
6233
6234 let indentless_sequence_value = module.getattr("indentless_sequence_value").unwrap();
6235 let indentless_sequence_value =
6236 extract_template(py, &indentless_sequence_value, "yaml_t/yaml_t_str").unwrap();
6237 let rendered =
6238 render_document(py, &parse_template(&indentless_sequence_value).unwrap()).unwrap();
6239 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6240 assert_eq!(
6241 yaml_sequence_len(yaml_mapping_entry(&documents[0], "a").expect("a")),
6242 Some(2)
6243 );
6244
6245 let sequence_of_mappings = module.getattr("sequence_of_mappings").unwrap();
6246 let sequence_of_mappings =
6247 extract_template(py, &sequence_of_mappings, "yaml_t/yaml_t_str").unwrap();
6248 let rendered =
6249 render_document(py, &parse_template(&sequence_of_mappings).unwrap()).unwrap();
6250 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6251 assert_eq!(documents[0].as_vec().expect("top-level seq").len(), 2);
6252
6253 let mapping_of_sequence_of_mappings =
6254 module.getattr("mapping_of_sequence_of_mappings").unwrap();
6255 let mapping_of_sequence_of_mappings =
6256 extract_template(py, &mapping_of_sequence_of_mappings, "yaml_t/yaml_t_str")
6257 .unwrap();
6258 let rendered = render_document(
6259 py,
6260 &parse_template(&mapping_of_sequence_of_mappings).unwrap(),
6261 )
6262 .unwrap();
6263 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6264 assert_eq!(
6265 yaml_sequence_len(yaml_mapping_entry(&documents[0], "items").expect("items")),
6266 Some(2)
6267 );
6268
6269 let sequence_of_sequences = module.getattr("sequence_of_sequences").unwrap();
6270 let sequence_of_sequences =
6271 extract_template(py, &sequence_of_sequences, "yaml_t/yaml_t_str").unwrap();
6272 let rendered =
6273 render_document(py, &parse_template(&sequence_of_sequences).unwrap()).unwrap();
6274 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6275 let top = documents[0].as_vec().expect("top-level seq");
6276 assert_eq!(top.len(), 2);
6277 assert_eq!(yaml_sequence_len(&top[0]), Some(2));
6278 assert_eq!(yaml_sequence_len(&top[1]), Some(1));
6279 });
6280 }
6281
6282 #[test]
6283 fn renders_flow_scalar_key_and_comment_edge_families() {
6284 Python::with_gil(|py| {
6285 let module = PyModule::from_code(
6286 py,
6287 pyo3::ffi::c_str!(
6288 "from string.templatelib import Template\nflow_plain_scalar_with_space=Template('value: [1 2]\\n')\nmapping_empty_flow_values=Template('value: {a: [], b: {}}\\n')\nflow_mapping_empty_key_and_values=Template('{\"\": [], foo: {}}\\n')\nflow_null_key=Template('{null: 1, \"\": 2}\\n')\nblock_null_key=Template('? null\\n: 1\\n')\nquoted_null_key=Template('? \"\"\\n: 1\\n')\nplain_question_mark_scalar=Template('value: ?x\\n')\nplain_colon_scalar_flow=Template('value: [a:b, c:d]\\n')\nflow_mapping_plain_key_questions=Template('value: {?x: 1, ?y: 2}\\n')\nflow_hash_mapping_four=Template('value: {a: b#c, d: e#f, g: h#i, j: k#l}\\n')\nflow_hash_seq_seven=Template('value: [a#b, c#d, e#f, g#h, i#j, k#l, m#n]\\n')\ncomment_after_flow_plain_colon=Template('value: [a:b # c\\n]\\n')\nflow_plain_comment_after_colon_deeper=Template('value: [a:b:c:d # comment\\n]\\n')\n"
6289 ),
6290 pyo3::ffi::c_str!("test_yaml_flow_scalar_edge_families.py"),
6291 pyo3::ffi::c_str!("test_yaml_flow_scalar_edge_families"),
6292 )
6293 .unwrap();
6294
6295 let flow_plain_scalar_with_space =
6296 module.getattr("flow_plain_scalar_with_space").unwrap();
6297 let flow_plain_scalar_with_space =
6298 extract_template(py, &flow_plain_scalar_with_space, "yaml_t/yaml_t_str").unwrap();
6299 let rendered =
6300 render_document(py, &parse_template(&flow_plain_scalar_with_space).unwrap())
6301 .unwrap();
6302 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6303 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6304 let value = value.as_vec().expect("value seq");
6305 assert_eq!(yaml_scalar_text(&value[0]), Some("1 2"));
6306
6307 let mapping_empty_flow_values = module.getattr("mapping_empty_flow_values").unwrap();
6308 let mapping_empty_flow_values =
6309 extract_template(py, &mapping_empty_flow_values, "yaml_t/yaml_t_str").unwrap();
6310 let rendered =
6311 render_document(py, &parse_template(&mapping_empty_flow_values).unwrap()).unwrap();
6312 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6313 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6314 assert_eq!(
6315 yaml_sequence_len(yaml_mapping_entry(value, "a").expect("a")),
6316 Some(0)
6317 );
6318 assert_eq!(
6319 yaml_mapping(yaml_mapping_entry(value, "b").expect("b"))
6320 .expect("b mapping")
6321 .len(),
6322 0
6323 );
6324
6325 let flow_mapping_empty_key_and_values =
6326 module.getattr("flow_mapping_empty_key_and_values").unwrap();
6327 let flow_mapping_empty_key_and_values =
6328 extract_template(py, &flow_mapping_empty_key_and_values, "yaml_t/yaml_t_str")
6329 .unwrap();
6330 let rendered = render_document(
6331 py,
6332 &parse_template(&flow_mapping_empty_key_and_values).unwrap(),
6333 )
6334 .unwrap();
6335 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6336 assert_eq!(
6337 yaml_sequence_len(yaml_mapping_entry(&documents[0], "").expect("empty key")),
6338 Some(0)
6339 );
6340 assert_eq!(
6341 yaml_mapping(yaml_mapping_entry(&documents[0], "foo").expect("foo"))
6342 .expect("foo mapping")
6343 .len(),
6344 0
6345 );
6346
6347 let flow_null_key = module.getattr("flow_null_key").unwrap();
6348 let flow_null_key = extract_template(py, &flow_null_key, "yaml_t/yaml_t_str").unwrap();
6349 let rendered = render_document(py, &parse_template(&flow_null_key).unwrap()).unwrap();
6350 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6351 assert_eq!(documents[0].as_mapping().expect("mapping").len(), 2);
6352
6353 let block_null_key = module.getattr("block_null_key").unwrap();
6354 let block_null_key =
6355 extract_template(py, &block_null_key, "yaml_t/yaml_t_str").unwrap();
6356 let rendered = render_document(py, &parse_template(&block_null_key).unwrap()).unwrap();
6357 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6358 assert_eq!(documents[0].as_mapping().expect("mapping").len(), 1);
6359
6360 let quoted_null_key = module.getattr("quoted_null_key").unwrap();
6361 let quoted_null_key =
6362 extract_template(py, "ed_null_key, "yaml_t/yaml_t_str").unwrap();
6363 let rendered = render_document(py, &parse_template("ed_null_key).unwrap()).unwrap();
6364 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6365 assert_eq!(documents[0].as_mapping().expect("mapping").len(), 1);
6366
6367 let plain_question_mark_scalar = module.getattr("plain_question_mark_scalar").unwrap();
6368 let plain_question_mark_scalar =
6369 extract_template(py, &plain_question_mark_scalar, "yaml_t/yaml_t_str").unwrap();
6370 let rendered =
6371 render_document(py, &parse_template(&plain_question_mark_scalar).unwrap()).unwrap();
6372 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6373 assert_string_entry(&documents[0], "value", "?x");
6374
6375 let plain_colon_scalar_flow = module.getattr("plain_colon_scalar_flow").unwrap();
6376 let plain_colon_scalar_flow =
6377 extract_template(py, &plain_colon_scalar_flow, "yaml_t/yaml_t_str").unwrap();
6378 let rendered =
6379 render_document(py, &parse_template(&plain_colon_scalar_flow).unwrap()).unwrap();
6380 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6381 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6382 let value = value.as_vec().expect("value seq");
6383 assert_eq!(yaml_scalar_text(&value[0]), Some("a:b"));
6384 assert_eq!(yaml_scalar_text(&value[1]), Some("c:d"));
6385
6386 let flow_mapping_plain_key_questions =
6387 module.getattr("flow_mapping_plain_key_questions").unwrap();
6388 let flow_mapping_plain_key_questions =
6389 extract_template(py, &flow_mapping_plain_key_questions, "yaml_t/yaml_t_str")
6390 .unwrap();
6391 let rendered = render_document(
6392 py,
6393 &parse_template(&flow_mapping_plain_key_questions).unwrap(),
6394 )
6395 .unwrap();
6396 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6397 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6398 assert_eq!(yaml_mapping(value).expect("mapping").len(), 2);
6399
6400 let flow_hash_mapping_four = module.getattr("flow_hash_mapping_four").unwrap();
6401 let flow_hash_mapping_four =
6402 extract_template(py, &flow_hash_mapping_four, "yaml_t/yaml_t_str").unwrap();
6403 let rendered =
6404 render_document(py, &parse_template(&flow_hash_mapping_four).unwrap()).unwrap();
6405 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6406 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6407 assert_eq!(yaml_mapping(value).expect("mapping").len(), 4);
6408
6409 let flow_hash_seq_seven = module.getattr("flow_hash_seq_seven").unwrap();
6410 let flow_hash_seq_seven =
6411 extract_template(py, &flow_hash_seq_seven, "yaml_t/yaml_t_str").unwrap();
6412 let rendered =
6413 render_document(py, &parse_template(&flow_hash_seq_seven).unwrap()).unwrap();
6414 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6415 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6416 assert_eq!(yaml_sequence_len(value), Some(7));
6417
6418 let comment_after_flow_plain_colon =
6419 module.getattr("comment_after_flow_plain_colon").unwrap();
6420 let comment_after_flow_plain_colon =
6421 extract_template(py, &comment_after_flow_plain_colon, "yaml_t/yaml_t_str").unwrap();
6422 let rendered = render_document(
6423 py,
6424 &parse_template(&comment_after_flow_plain_colon).unwrap(),
6425 )
6426 .unwrap();
6427 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6428 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6429 let value = value.as_vec().expect("value seq");
6430 assert_eq!(yaml_scalar_text(&value[0]), Some("a:b"));
6431
6432 let flow_plain_comment_after_colon_deeper = module
6433 .getattr("flow_plain_comment_after_colon_deeper")
6434 .unwrap();
6435 let flow_plain_comment_after_colon_deeper = extract_template(
6436 py,
6437 &flow_plain_comment_after_colon_deeper,
6438 "yaml_t/yaml_t_str",
6439 )
6440 .unwrap();
6441 let rendered = render_document(
6442 py,
6443 &parse_template(&flow_plain_comment_after_colon_deeper).unwrap(),
6444 )
6445 .unwrap();
6446 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6447 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6448 let value = value.as_vec().expect("value seq");
6449 assert_eq!(yaml_scalar_text(&value[0]), Some("a:b:c:d"));
6450 });
6451 }
6452
6453 #[test]
6454 fn renders_flow_collection_comment_and_verbatim_tag_families() {
6455 Python::with_gil(|py| {
6456 let module = PyModule::from_code(
6457 py,
6458 pyo3::ffi::c_str!(
6459 "from string.templatelib import Template\nverbatim_tag=Template('value: !<tag:yaml.org,2002:str> hello\\n')\nflow_wrapped_sequence=Template('key: [a,\\n b]\\n')\nflow_wrapped_mapping=Template('key: {a: 1,\\n b: 2}\\n')\nflow_sequence_comment=Template('key: [a, # first\\n b]\\n')\nflow_mapping_comment=Template('key: {a: 1, # first\\n b: 2}\\n')\nalias_in_flow_mapping_value=Template('base: &a {x: 1}\\nvalue: {ref: *a}\\n')\nflow_null_and_alias=Template('base: &a {x: 1}\\nvalue: {null: *a}\\n')\n"
6460 ),
6461 pyo3::ffi::c_str!("test_yaml_flow_collection_comment_and_tag_families.py"),
6462 pyo3::ffi::c_str!("test_yaml_flow_collection_comment_and_tag_families"),
6463 )
6464 .unwrap();
6465
6466 let verbatim_tag = module.getattr("verbatim_tag").unwrap();
6467 let verbatim_tag = extract_template(py, &verbatim_tag, "yaml_t/yaml_t_str").unwrap();
6468 let rendered = render_document(py, &parse_template(&verbatim_tag).unwrap()).unwrap();
6469 assert_eq!(rendered.text, "value: !<tag:yaml.org,2002:str> hello");
6470 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6471 assert_string_entry(&documents[0], "value", "hello");
6472
6473 for (name, expected_text) in [
6474 ("flow_wrapped_sequence", "key: [ a, b ]"),
6475 ("flow_sequence_comment", "key: [ a, b ]"),
6476 ] {
6477 let template = module.getattr(name).unwrap();
6478 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6479 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6480 assert_eq!(rendered.text, expected_text, "{name}");
6481 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6482 let value = yaml_mapping_entry(&documents[0], "key").expect("sequence key");
6483 assert_eq!(yaml_sequence_len(value), Some(2), "{name}");
6484 }
6485
6486 for (name, expected_text) in [
6487 ("flow_wrapped_mapping", "key: { a: 1, b: 2 }"),
6488 ("flow_mapping_comment", "key: { a: 1, b: 2 }"),
6489 ] {
6490 let template = module.getattr(name).unwrap();
6491 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6492 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6493 assert_eq!(rendered.text, expected_text, "{name}");
6494 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6495 let value = yaml_mapping_entry(&documents[0], "key").expect("mapping key");
6496 assert_eq!(yaml_mapping(value).expect("mapping").len(), 2, "{name}");
6497 }
6498
6499 let alias_in_flow_mapping_value =
6500 module.getattr("alias_in_flow_mapping_value").unwrap();
6501 let alias_in_flow_mapping_value =
6502 extract_template(py, &alias_in_flow_mapping_value, "yaml_t/yaml_t_str").unwrap();
6503 let rendered =
6504 render_document(py, &parse_template(&alias_in_flow_mapping_value).unwrap())
6505 .unwrap();
6506 assert_eq!(rendered.text, "base: &a { x: 1 }\nvalue: { ref: *a }");
6507 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6508 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6509 let referenced = yaml_mapping_entry(value, "ref").expect("ref key");
6510 assert_integer_entry(referenced, "x", 1);
6511
6512 let flow_null_and_alias = module.getattr("flow_null_and_alias").unwrap();
6513 let flow_null_and_alias =
6514 extract_template(py, &flow_null_and_alias, "yaml_t/yaml_t_str").unwrap();
6515 let rendered =
6516 render_document(py, &parse_template(&flow_null_and_alias).unwrap()).unwrap();
6517 assert_eq!(rendered.text, "base: &a { x: 1 }\nvalue: { null: *a }");
6518 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6519 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6520 assert_eq!(yaml_mapping(value).expect("mapping").len(), 1);
6521 });
6522 }
6523
6524 #[test]
6525 fn renders_verbatim_root_scalar_text_and_data() {
6526 Python::with_gil(|py| {
6527 let module = PyModule::from_code(
6528 py,
6529 pyo3::ffi::c_str!("template=t'--- !<tag:yaml.org,2002:str> hello\\n'\n"),
6530 pyo3::ffi::c_str!("test_yaml_verbatim_root_scalar.py"),
6531 pyo3::ffi::c_str!("test_yaml_verbatim_root_scalar"),
6532 )
6533 .unwrap();
6534
6535 let template = module.getattr("template").unwrap();
6536 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6537 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6538 assert_eq!(rendered.text, "---\n!<tag:yaml.org,2002:str> hello");
6539 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6540 assert_eq!(yaml_scalar_text(&documents[0]), Some("hello"));
6541 });
6542 }
6543
6544 #[test]
6545 fn renders_verbatim_root_anchor_scalar_text_and_data() {
6546 Python::with_gil(|py| {
6547 let module = PyModule::from_code(
6548 py,
6549 pyo3::ffi::c_str!("template=t'--- !<tag:yaml.org,2002:str> &root hello\\n'\n"),
6550 pyo3::ffi::c_str!("test_yaml_verbatim_root_anchor_scalar.py"),
6551 pyo3::ffi::c_str!("test_yaml_verbatim_root_anchor_scalar"),
6552 )
6553 .unwrap();
6554
6555 let template = module.getattr("template").unwrap();
6556 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6557 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6558 assert_eq!(rendered.text, "---\n!<tag:yaml.org,2002:str> &root hello");
6559 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6560 assert_eq!(yaml_scalar_text(&documents[0]), Some("hello"));
6561 });
6562 }
6563
6564 #[test]
6565 fn renders_spec_chapter_2_examples_text_and_data() {
6566 Python::with_gil(|py| {
6567 let module = PyModule::from_code(
6568 py,
6569 pyo3::ffi::c_str!(
6570 "players=t'- Mark McGwire\\n- Sammy Sosa\\n- Ken Griffey\\n'\nclubs=t'american:\\n- Boston Red Sox\\n- Detroit Tigers\\n- New York Yankees\\nnational:\\n- New York Mets\\n- Chicago Cubs\\n- Atlanta Braves\\n'\nstats_seq=t'-\\n name: Mark McGwire\\n hr: 65\\n avg: 0.278\\n-\\n name: Sammy Sosa\\n hr: 63\\n avg: 0.288\\n'\nmap_of_maps=t'Mark McGwire: {{hr: 65, avg: 0.278}}\\nSammy Sosa: {{\\n hr: 63,\\n avg: 0.288,\\n}}\\n'\ntwo_docs=t'# Ranking of 1998 home runs\\n---\\n- Mark McGwire\\n- Sammy Sosa\\n- Ken Griffey\\n\\n# Team ranking\\n---\\n- Chicago Cubs\\n- St Louis Cardinals\\n'\nplay_feed=t'---\\ntime: 20:03:20\\nplayer: Sammy Sosa\\naction: strike (miss)\\n...\\n---\\ntime: 20:03:47\\nplayer: Sammy Sosa\\naction: grand slam\\n...\\n'\n"
6571 ),
6572 pyo3::ffi::c_str!("test_yaml_spec_chapter_2_examples.py"),
6573 pyo3::ffi::c_str!("test_yaml_spec_chapter_2_examples"),
6574 )
6575 .unwrap();
6576
6577 for (name, expected_text) in [
6578 ("players", "- Mark McGwire\n- Sammy Sosa\n- Ken Griffey"),
6579 (
6580 "clubs",
6581 "american:\n - Boston Red Sox\n - Detroit Tigers\n - New York Yankees\nnational:\n - New York Mets\n - Chicago Cubs\n - Atlanta Braves",
6582 ),
6583 (
6584 "stats_seq",
6585 "-\n name: Mark McGwire\n hr: 65\n avg: 0.278\n-\n name: Sammy Sosa\n hr: 63\n avg: 0.288",
6586 ),
6587 (
6588 "map_of_maps",
6589 "Mark McGwire: { hr: 65, avg: 0.278 }\nSammy Sosa: { hr: 63, avg: 0.288 }",
6590 ),
6591 (
6592 "two_docs",
6593 "---\n- Mark McGwire\n- Sammy Sosa\n- Ken Griffey\n---\n- Chicago Cubs\n- St Louis Cardinals",
6594 ),
6595 (
6596 "play_feed",
6597 "---\ntime: 20:03:20\nplayer: Sammy Sosa\naction: strike (miss)\n...\n---\ntime: 20:03:47\nplayer: Sammy Sosa\naction: grand slam\n...",
6598 ),
6599 ] {
6600 let template = module.getattr(name).unwrap();
6601 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6602 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6603 assert_eq!(rendered.text, expected_text, "{name}");
6604 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6605 match name {
6606 "players" => assert_eq!(yaml_sequence_len(&documents[0]), Some(3)),
6607 "clubs" => assert_eq!(yaml_mapping(&documents[0]).expect("clubs").len(), 2),
6608 "stats_seq" => assert_eq!(yaml_sequence_len(&documents[0]), Some(2)),
6609 "map_of_maps" => {
6610 assert_eq!(yaml_mapping(&documents[0]).expect("map_of_maps").len(), 2)
6611 }
6612 "two_docs" => assert_eq!(documents.len(), 2),
6613 "play_feed" => assert_eq!(documents.len(), 2),
6614 _ => unreachable!(),
6615 }
6616 }
6617 });
6618 }
6619
6620 #[test]
6621 fn test_parse_rendered_yaml_surfaces_parse_failures() {
6622 let err =
6623 parse_rendered_yaml("value: *missing\n").expect_err("expected YAML parse failure");
6624 assert_eq!(err.kind, ErrorKind::Parse);
6625 assert!(err.message.contains("Rendered YAML could not be reparsed"));
6626 }
6627}