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 check_template_with_profile(
2052 template: &TemplateInput,
2053 profile: YamlProfile,
2054) -> BackendResult<()> {
2055 parse_template_with_profile(template, profile).map(|_| ())
2056}
2057
2058pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
2059 check_template_with_profile(template, YamlProfile::default())
2060}
2061
2062pub fn format_template_with_profile(
2063 template: &TemplateInput,
2064 profile: YamlProfile,
2065) -> BackendResult<String> {
2066 let stream = parse_template_with_profile(template, profile)?;
2067 format_yaml_stream(template, &stream)
2068}
2069
2070pub fn format_template(template: &TemplateInput) -> BackendResult<String> {
2071 format_template_with_profile(template, YamlProfile::default())
2072}
2073
2074pub fn normalize_documents_with_profile(
2075 documents: &[YamlOwned],
2076 _profile: YamlProfile,
2077) -> BackendResult<NormalizedStream> {
2078 documents
2081 .iter()
2082 .map(normalize_document)
2083 .collect::<BackendResult<Vec<_>>>()
2084 .map(NormalizedStream::new)
2085}
2086
2087pub fn normalize_documents(documents: &[YamlOwned]) -> BackendResult<NormalizedStream> {
2088 normalize_documents_with_profile(documents, YamlProfile::default())
2089}
2090
2091pub fn align_normalized_stream_with_ast(
2092 stream: &YamlStreamNode,
2093 normalized: &mut NormalizedStream,
2094) {
2095 for (document_node, document) in stream.documents.iter().zip(normalized.documents.iter_mut()) {
2096 align_document_with_ast(document_node, document);
2097 }
2098}
2099
2100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2101enum CollectionRenderContext {
2102 BlockAllowed,
2103 FlowRequired,
2104}
2105
2106#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2107enum RenderedLayout {
2108 Inline,
2109 Block,
2110 Flow,
2111}
2112
2113struct RenderedYamlValue {
2114 text: String,
2115 layout: RenderedLayout,
2116 empty_collection: bool,
2117}
2118
2119impl RenderedYamlValue {
2120 fn inline(text: String) -> Self {
2121 Self {
2122 text,
2123 layout: RenderedLayout::Inline,
2124 empty_collection: false,
2125 }
2126 }
2127
2128 fn block(text: String) -> Self {
2129 Self {
2130 text,
2131 layout: RenderedLayout::Block,
2132 empty_collection: false,
2133 }
2134 }
2135
2136 fn flow(text: String, empty_collection: bool) -> Self {
2137 Self {
2138 text,
2139 layout: RenderedLayout::Flow,
2140 empty_collection,
2141 }
2142 }
2143}
2144
2145fn format_yaml_stream(template: &TemplateInput, stream: &YamlStreamNode) -> BackendResult<String> {
2146 let mut parts = Vec::with_capacity(stream.documents.len());
2147 for document in &stream.documents {
2148 let mut lines = Vec::new();
2149 lines.extend(document.directives.iter().cloned());
2150 if document.explicit_start || !document.directives.is_empty() || stream.documents.len() > 1
2151 {
2152 lines.push("---".to_owned());
2153 }
2154 lines.push(
2155 format_yaml_value(
2156 template,
2157 &document.value,
2158 0,
2159 CollectionRenderContext::BlockAllowed,
2160 )?
2161 .text,
2162 );
2163 if document.explicit_end {
2164 lines.push("...".to_owned());
2165 }
2166 parts.push(lines.join("\n"));
2167 }
2168 Ok(parts.join("\n"))
2169}
2170
2171fn format_yaml_value(
2172 template: &TemplateInput,
2173 node: &YamlValueNode,
2174 indent: usize,
2175 context: CollectionRenderContext,
2176) -> BackendResult<RenderedYamlValue> {
2177 match node {
2178 YamlValueNode::Decorated(node) => {
2179 let mut prefix = String::new();
2180 if let Some(tag) = &node.tag {
2181 prefix.push('!');
2182 prefix.push_str(&assemble_yaml_chunks(template, &tag.chunks, true)?);
2183 }
2184 if let Some(anchor) = &node.anchor {
2185 if !prefix.is_empty() {
2186 prefix.push(' ');
2187 }
2188 prefix.push('&');
2189 prefix.push_str(&assemble_yaml_chunks(template, &anchor.chunks, true)?);
2190 }
2191 let rendered = format_yaml_value(template, node.value.as_ref(), indent, context)?;
2192 if prefix.is_empty() {
2193 Ok(rendered)
2194 } else {
2195 Ok(apply_rendered_prefix(prefix, rendered))
2196 }
2197 }
2198 YamlValueNode::Mapping(node) => format_yaml_mapping(template, node, indent, context),
2199 YamlValueNode::Sequence(node) => format_yaml_sequence(template, node, indent, context),
2200 YamlValueNode::Interpolation(node) => Ok(RenderedYamlValue::inline(
2201 interpolation_raw_source(template, node.interpolation_index, &node.span, "YAML value")?,
2202 )),
2203 YamlValueNode::Scalar(YamlScalarNode::Alias(node)) => Ok(RenderedYamlValue::inline(
2204 format!("*{}", assemble_yaml_chunks(template, &node.chunks, true)?),
2205 )),
2206 YamlValueNode::Scalar(YamlScalarNode::Block(node)) => Ok(RenderedYamlValue::inline(
2207 format_yaml_block_scalar(template, node, indent)?,
2208 )),
2209 YamlValueNode::Scalar(YamlScalarNode::DoubleQuoted(node)) => Ok(RenderedYamlValue::inline(
2210 serde_json::to_string(&assemble_yaml_chunks(template, &node.chunks, false)?).unwrap(),
2211 )),
2212 YamlValueNode::Scalar(YamlScalarNode::SingleQuoted(node)) => {
2213 Ok(RenderedYamlValue::inline(format!(
2214 "'{}'",
2215 assemble_yaml_chunks(template, &node.chunks, false)?.replace('\'', "''")
2216 )))
2217 }
2218 YamlValueNode::Scalar(YamlScalarNode::Plain(node)) => Ok(RenderedYamlValue::inline(
2219 format_yaml_plain_scalar(template, node)?,
2220 )),
2221 }
2222}
2223
2224fn format_yaml_mapping(
2225 template: &TemplateInput,
2226 node: &YamlMappingNode,
2227 indent: usize,
2228 context: CollectionRenderContext,
2229) -> BackendResult<RenderedYamlValue> {
2230 if node.flow || context == CollectionRenderContext::FlowRequired {
2231 let mut entries = Vec::with_capacity(node.entries.len());
2232 for entry in &node.entries {
2233 let rendered_key = match &entry.key.value {
2234 YamlKeyValue::Complex(key) => {
2235 format_yaml_value(template, key, indent, CollectionRenderContext::FlowRequired)?
2236 .text
2237 }
2238 _ => format_yaml_key(template, &entry.key)?,
2239 };
2240 let rendered_key = normalize_flow_key_text(rendered_key);
2241 let rendered_value = format_yaml_value(
2242 template,
2243 &entry.value,
2244 indent,
2245 CollectionRenderContext::FlowRequired,
2246 )?;
2247 entries.push(format!("{rendered_key}: {}", rendered_value.text));
2248 }
2249 return Ok(RenderedYamlValue::flow(
2250 format!("{{ {} }}", entries.join(", ")),
2251 node.entries.is_empty(),
2252 ));
2253 }
2254
2255 let mut rendered = String::new();
2256 for entry in &node.entries {
2257 if let YamlKeyValue::Complex(key) = &entry.key.value {
2258 let rendered_key = format_yaml_value(
2259 template,
2260 key,
2261 indent + 2,
2262 CollectionRenderContext::FlowRequired,
2263 )?;
2264 let rendered_value = format_yaml_value(
2265 template,
2266 &entry.value,
2267 indent + 2,
2268 CollectionRenderContext::BlockAllowed,
2269 )?;
2270 push_yaml_section(
2271 &mut rendered,
2272 format!("{}? {}", " ".repeat(indent), rendered_key.text),
2273 );
2274 push_rendered_value_with_prefix(
2275 &mut rendered,
2276 format!("{}:", " ".repeat(indent)),
2277 rendered_value,
2278 );
2279 continue;
2280 }
2281 let key = format_yaml_key(template, &entry.key)?;
2282 let rendered_value = format_yaml_value(
2283 template,
2284 &entry.value,
2285 indent + 2,
2286 CollectionRenderContext::BlockAllowed,
2287 )?;
2288 push_rendered_value_with_prefix(
2289 &mut rendered,
2290 format!("{}{}:", " ".repeat(indent), key),
2291 rendered_value,
2292 );
2293 }
2294 Ok(RenderedYamlValue::block(rendered))
2295}
2296
2297fn format_yaml_sequence(
2298 template: &TemplateInput,
2299 node: &YamlSequenceNode,
2300 indent: usize,
2301 context: CollectionRenderContext,
2302) -> BackendResult<RenderedYamlValue> {
2303 if node.flow || context == CollectionRenderContext::FlowRequired {
2304 let items = node
2305 .items
2306 .iter()
2307 .map(|item| {
2308 format_yaml_value(
2309 template,
2310 item,
2311 indent,
2312 CollectionRenderContext::FlowRequired,
2313 )
2314 .map(|item| item.text)
2315 })
2316 .collect::<BackendResult<Vec<_>>>()?;
2317 return Ok(RenderedYamlValue::flow(
2318 format!("[ {} ]", items.join(", ")),
2319 node.items.is_empty(),
2320 ));
2321 }
2322
2323 let mut rendered = String::new();
2324 for item in &node.items {
2325 let rendered_item = format_yaml_value(
2326 template,
2327 item,
2328 indent + 2,
2329 CollectionRenderContext::BlockAllowed,
2330 )?;
2331 push_rendered_value_with_prefix(
2332 &mut rendered,
2333 format!("{}-", " ".repeat(indent)),
2334 rendered_item,
2335 );
2336 }
2337 Ok(RenderedYamlValue::block(rendered))
2338}
2339
2340fn format_yaml_key(template: &TemplateInput, node: &YamlKeyNode) -> BackendResult<String> {
2341 match &node.value {
2342 YamlKeyValue::Interpolation(value) => {
2343 interpolation_raw_source(template, value.interpolation_index, &value.span, "YAML key")
2344 }
2345 YamlKeyValue::Scalar(YamlScalarNode::DoubleQuoted(value)) => Ok(serde_json::to_string(
2346 &assemble_yaml_chunks(template, &value.chunks, false)?,
2347 )
2348 .unwrap()),
2349 YamlKeyValue::Scalar(YamlScalarNode::SingleQuoted(value)) => Ok(format!(
2350 "'{}'",
2351 assemble_yaml_chunks(template, &value.chunks, false)?.replace('\'', "''")
2352 )),
2353 YamlKeyValue::Scalar(YamlScalarNode::Plain(value)) => {
2354 format_yaml_plain_scalar(template, value)
2355 }
2356 YamlKeyValue::Scalar(YamlScalarNode::Alias(node)) => Ok(format!(
2357 "*{}",
2358 assemble_yaml_chunks(template, &node.chunks, true)?
2359 )),
2360 YamlKeyValue::Scalar(YamlScalarNode::Block(node)) => {
2361 format_yaml_block_scalar(template, node, 0)
2362 }
2363 YamlKeyValue::Complex(value) => {
2364 format_yaml_value(template, value, 0, CollectionRenderContext::FlowRequired)
2365 .map(|value| value.text)
2366 }
2367 }
2368}
2369
2370fn format_yaml_plain_scalar(
2371 template: &TemplateInput,
2372 node: &YamlPlainScalarNode,
2373) -> BackendResult<String> {
2374 let text = assemble_yaml_chunks(template, &node.chunks, false)?
2375 .trim()
2376 .to_owned();
2377 if text.is_empty() {
2378 return Ok("null".to_owned());
2379 }
2380 if text.contains('\n') {
2381 return Ok(serde_json::to_string(&text).unwrap());
2382 }
2383 Ok(text)
2384}
2385
2386fn format_yaml_block_scalar(
2387 template: &TemplateInput,
2388 node: &YamlBlockScalarNode,
2389 indent: usize,
2390) -> BackendResult<String> {
2391 let mut header = node.style.clone();
2392 if let Some(chomping) = &node.chomping {
2393 header.push_str(chomping);
2394 }
2395 if let Some(indent_indicator) = node.indent_indicator {
2396 header.push_str(&indent_indicator.to_string());
2397 }
2398 let content = assemble_yaml_chunks(template, &node.chunks, false)?;
2399 if content.is_empty() {
2400 let mut rendered = header;
2401 rendered.push('\n');
2402 return Ok(rendered);
2403 }
2404
2405 let indentation_width = node
2406 .indent_indicator
2407 .map_or(indent, |indicator| indent.saturating_sub(2) + indicator);
2408 let indentation = " ".repeat(indentation_width);
2409 let trailing_breaks = content.chars().rev().take_while(|&ch| ch == '\n').count();
2410 let body_content = content.trim_end_matches('\n');
2411 let body = if body_content.is_empty() {
2412 String::new()
2413 } else {
2414 body_content
2415 .split('\n')
2416 .map(|line| format!("{indentation}{line}"))
2417 .collect::<Vec<_>>()
2418 .join("\n")
2419 };
2420 let trailing = match node.chomping.as_deref() {
2421 Some("-") => 0,
2422 Some("+") => trailing_breaks.max(1),
2423 _ => usize::from(!content.is_empty()),
2424 };
2425
2426 let mut rendered = header;
2427 rendered.push('\n');
2428 rendered.push_str(&body);
2429 for _ in 0..trailing {
2430 rendered.push('\n');
2431 }
2432 Ok(rendered)
2433}
2434
2435fn assemble_yaml_chunks(
2436 template: &TemplateInput,
2437 chunks: &[YamlChunk],
2438 _metadata: bool,
2439) -> BackendResult<String> {
2440 let mut text = String::new();
2441 for chunk in chunks {
2442 match chunk {
2443 YamlChunk::Text(chunk) => text.push_str(&chunk.value),
2444 YamlChunk::Interpolation(chunk) => text.push_str(&interpolation_raw_source(
2445 template,
2446 chunk.interpolation_index,
2447 &chunk.span,
2448 "YAML fragment",
2449 )?),
2450 }
2451 }
2452 Ok(text)
2453}
2454
2455fn apply_rendered_prefix(prefix: String, rendered: RenderedYamlValue) -> RenderedYamlValue {
2456 if rendered.layout == RenderedLayout::Block {
2457 return RenderedYamlValue {
2458 text: format!("{prefix}\n{}", rendered.text),
2459 layout: RenderedLayout::Block,
2460 empty_collection: rendered.empty_collection,
2461 };
2462 }
2463 RenderedYamlValue {
2464 text: format!("{prefix} {}", rendered.text),
2465 layout: rendered.layout,
2466 empty_collection: rendered.empty_collection,
2467 }
2468}
2469
2470fn push_rendered_value_with_prefix(
2471 output: &mut String,
2472 prefix: String,
2473 rendered: RenderedYamlValue,
2474) {
2475 if rendered.layout == RenderedLayout::Block && !rendered.empty_collection {
2476 if let Some((first, rest)) = rendered.text.split_once('\n')
2477 && !first.starts_with(' ')
2478 {
2479 push_yaml_section(output, format!("{prefix} {first}"));
2480 if !rest.is_empty() {
2481 push_yaml_section(output, rest.to_owned());
2482 }
2483 return;
2484 }
2485 push_yaml_section(output, prefix);
2486 push_yaml_section(output, rendered.text);
2487 return;
2488 }
2489
2490 push_yaml_section(output, format!("{prefix} {}", rendered.text));
2491}
2492
2493fn push_yaml_section(output: &mut String, section: String) {
2494 if output.is_empty() {
2495 output.push_str(§ion);
2496 return;
2497 }
2498 if !output.ends_with('\n') {
2499 output.push('\n');
2500 }
2501 output.push_str(§ion);
2502}
2503
2504fn normalize_flow_key_text(rendered_key: String) -> String {
2505 if rendered_key.starts_with('?') && !rendered_key.starts_with("? ") {
2506 return format!("? {}", &rendered_key[1..]);
2507 }
2508 rendered_key
2509}
2510
2511fn interpolation_raw_source(
2512 template: &TemplateInput,
2513 interpolation_index: usize,
2514 span: &SourceSpan,
2515 context: &str,
2516) -> BackendResult<String> {
2517 template
2518 .interpolation_raw_source(interpolation_index)
2519 .map(str::to_owned)
2520 .ok_or_else(|| {
2521 let expression = template.interpolation(interpolation_index).map_or_else(
2522 || format!("slot {interpolation_index}"),
2523 |value| value.expression_label().to_owned(),
2524 );
2525 BackendError::semantic_at(
2526 "yaml.format",
2527 format!(
2528 "Cannot format {context} interpolation {expression:?} without raw source text."
2529 ),
2530 Some(span.clone()),
2531 )
2532 })
2533}
2534
2535fn normalize_document(document: &YamlOwned) -> BackendResult<NormalizedDocument> {
2536 if matches!(document, YamlOwned::BadValue) {
2537 return Ok(NormalizedDocument::Empty);
2538 }
2539 Ok(NormalizedDocument::Value(normalize_value(document)?))
2540}
2541
2542fn align_document_with_ast(node: &YamlDocumentNode, document: &mut NormalizedDocument) {
2543 if let NormalizedDocument::Value(value) = document {
2544 align_value_with_ast(&node.value, value);
2545 }
2546}
2547
2548fn normalize_value(value: &YamlOwned) -> BackendResult<NormalizedValue> {
2549 match value {
2550 YamlOwned::Value(value) => normalize_scalar(value),
2551 YamlOwned::Representation(value, style, tag) => {
2552 normalize_representation_value(value, *style, tag.as_ref())
2553 }
2554 YamlOwned::Sequence(values) => values
2555 .iter()
2556 .map(normalize_value)
2557 .collect::<BackendResult<Vec<_>>>()
2558 .map(NormalizedValue::Sequence),
2559 YamlOwned::Mapping(values) => normalize_mapping(values),
2560 YamlOwned::Tagged(tag, value) => {
2561 normalize_tagged_value(tag.handle.as_str(), &tag.suffix, value)
2562 }
2563 YamlOwned::Alias(_) => Err(BackendError::semantic(
2564 "Rendered YAML still contains an unresolved alias after validation.",
2565 )),
2566 YamlOwned::BadValue => Ok(NormalizedValue::Null),
2567 }
2568}
2569
2570fn normalize_representation_value(
2571 value: &str,
2572 style: ScalarStyle,
2573 tag: Option<&Tag>,
2574) -> BackendResult<NormalizedValue> {
2575 let parsed = ScalarOwned::parse_from_cow_and_metadata(
2576 Cow::Borrowed(value),
2577 style,
2578 tag.map(Cow::Borrowed).as_ref(),
2579 )
2580 .ok_or_else(|| {
2581 BackendError::semantic(format!(
2582 "Rendered YAML representation {value:?} could not be normalized with the active tag/schema."
2583 ))
2584 })?;
2585 normalize_scalar(&parsed)
2586}
2587
2588fn normalize_scalar(value: &saphyr::ScalarOwned) -> BackendResult<NormalizedValue> {
2589 match value {
2590 saphyr::ScalarOwned::Null => Ok(NormalizedValue::Null),
2591 saphyr::ScalarOwned::Boolean(value) => Ok(NormalizedValue::Bool(*value)),
2592 saphyr::ScalarOwned::Integer(value) => Ok(NormalizedValue::Integer((*value).into())),
2593 saphyr::ScalarOwned::FloatingPoint(value) => {
2594 let value = value.into_inner();
2595 if !value.is_finite() {
2596 return Err(BackendError::semantic(
2597 "Rendered YAML contained a non-finite float, which this backend does not normalize.",
2598 ));
2599 }
2600 Ok(NormalizedValue::Float(NormalizedFloat::finite(value)))
2601 }
2602 saphyr::ScalarOwned::String(value) => Ok(NormalizedValue::String(value.clone())),
2603 }
2604}
2605
2606fn normalize_mapping(values: &saphyr::MappingOwned) -> BackendResult<NormalizedValue> {
2607 let mut entries = Vec::new();
2608 for (key, value) in values {
2609 if is_merge_key(key) {
2610 apply_merge_entries(value, &mut entries)?;
2611 continue;
2612 }
2613 insert_mapping_entry(
2614 &mut entries,
2615 normalize_key(key)?,
2616 normalize_value(value)?,
2617 true,
2618 );
2619 }
2620 Ok(NormalizedValue::Mapping(entries))
2621}
2622
2623fn align_value_with_ast(node: &YamlValueNode, normalized: &mut NormalizedValue) {
2624 match node {
2625 YamlValueNode::Decorated(decorated) => {
2626 if decorated.tag.as_ref().is_some_and(is_set_tag_literal)
2627 && let NormalizedValue::Mapping(entries) = normalized
2628 {
2629 let keys = entries.iter().map(|entry| entry.key.clone()).collect();
2630 *normalized = NormalizedValue::Set(keys);
2631 return;
2632 }
2633 align_value_with_ast(&decorated.value, normalized);
2634 }
2635 YamlValueNode::Mapping(mapping) => {
2636 if let NormalizedValue::Mapping(entries) = normalized {
2637 for (entry_node, entry) in mapping.entries.iter().zip(entries.iter_mut()) {
2638 align_key_with_ast(mapping.flow, &entry_node.key, &mut entry.key);
2639 align_value_with_ast(&entry_node.value, &mut entry.value);
2640 }
2641 }
2642 }
2643 YamlValueNode::Sequence(sequence) => {
2644 if let NormalizedValue::Sequence(values) = normalized {
2645 for (item_node, value) in sequence.items.iter().zip(values.iter_mut()) {
2646 align_value_with_ast(item_node, value);
2647 }
2648 }
2649 }
2650 YamlValueNode::Scalar(_) | YamlValueNode::Interpolation(_) => {}
2651 }
2652}
2653
2654fn align_key_with_ast(flow_mapping: bool, key: &YamlKeyNode, normalized: &mut NormalizedKey) {
2655 match &key.value {
2656 YamlKeyValue::Scalar(YamlScalarNode::Plain(node))
2657 if flow_mapping && plain_scalar_starts_with_question(node) =>
2658 {
2659 if let NormalizedKey::String(value) = normalized
2660 && let Some(stripped) = value.strip_prefix('?')
2661 {
2662 *value = stripped.to_owned();
2663 }
2664 }
2665 YamlKeyValue::Complex(value) => align_key_value_with_ast(value, normalized),
2666 YamlKeyValue::Scalar(_) | YamlKeyValue::Interpolation(_) => {}
2667 }
2668}
2669
2670fn align_key_value_with_ast(node: &YamlValueNode, normalized: &mut NormalizedKey) {
2671 match node {
2672 YamlValueNode::Decorated(decorated) => {
2673 align_key_value_with_ast(&decorated.value, normalized)
2674 }
2675 YamlValueNode::Sequence(sequence) => {
2676 if let NormalizedKey::Sequence(keys) = normalized {
2677 for (item_node, key) in sequence.items.iter().zip(keys.iter_mut()) {
2678 align_key_value_with_ast(item_node, key);
2679 }
2680 }
2681 }
2682 YamlValueNode::Mapping(mapping) => {
2683 if let NormalizedKey::Mapping(entries) = normalized {
2684 for (entry_node, entry) in mapping.entries.iter().zip(entries.iter_mut()) {
2685 align_key_with_ast(mapping.flow, &entry_node.key, &mut entry.key);
2686 align_key_value_with_ast(&entry_node.value, &mut entry.value);
2687 }
2688 }
2689 }
2690 YamlValueNode::Scalar(_) | YamlValueNode::Interpolation(_) => {}
2691 }
2692}
2693
2694fn plain_scalar_starts_with_question(node: &YamlPlainScalarNode) -> bool {
2695 let Some(YamlChunk::Text(text)) = node.chunks.first() else {
2696 return false;
2697 };
2698 text.value.starts_with('?')
2699}
2700
2701fn is_set_tag_literal(tag: &YamlTagNode) -> bool {
2702 let mut literal = String::new();
2703 for chunk in &tag.chunks {
2704 let YamlChunk::Text(text) = chunk else {
2705 return false;
2706 };
2707 literal.push_str(&text.value);
2708 }
2709 matches!(
2710 literal.as_str(),
2711 "!set" | "!!set" | "<tag:yaml.org,2002:set>" | "tag:yaml.org,2002:set"
2712 )
2713}
2714
2715fn normalize_key(value: &YamlOwned) -> BackendResult<NormalizedKey> {
2716 match value {
2717 YamlOwned::Value(value) => match value {
2718 saphyr::ScalarOwned::Null => Ok(NormalizedKey::Null),
2719 saphyr::ScalarOwned::Boolean(value) => Ok(NormalizedKey::Bool(*value)),
2720 saphyr::ScalarOwned::Integer(value) => Ok(NormalizedKey::Integer((*value).into())),
2721 saphyr::ScalarOwned::FloatingPoint(value) => {
2722 let value = value.into_inner();
2723 if !value.is_finite() {
2724 return Err(BackendError::semantic(
2725 "Rendered YAML contained a non-finite float mapping key, which this backend does not normalize.",
2726 ));
2727 }
2728 Ok(NormalizedKey::Float(NormalizedFloat::finite(value)))
2729 }
2730 saphyr::ScalarOwned::String(value) => Ok(NormalizedKey::String(value.clone())),
2731 },
2732 YamlOwned::Representation(value, style, tag) => {
2733 normalize_representation_key(value, *style, tag.as_ref())
2734 }
2735 YamlOwned::Sequence(values) => values
2736 .iter()
2737 .map(normalize_key)
2738 .collect::<BackendResult<Vec<_>>>()
2739 .map(NormalizedKey::Sequence),
2740 YamlOwned::Mapping(values) => values
2741 .iter()
2742 .map(|(key, value)| {
2743 Ok(NormalizedKeyEntry {
2744 key: normalize_key(key)?,
2745 value: normalize_key(value)?,
2746 })
2747 })
2748 .collect::<BackendResult<Vec<_>>>()
2749 .map(NormalizedKey::Mapping),
2750 YamlOwned::Tagged(tag, value) => {
2751 normalize_tagged_key(tag.handle.as_str(), &tag.suffix, value)
2752 }
2753 YamlOwned::Alias(_) => Err(BackendError::semantic(
2754 "Rendered YAML still contains an unresolved alias after validation.",
2755 )),
2756 YamlOwned::BadValue => Ok(NormalizedKey::Null),
2757 }
2758}
2759
2760fn normalize_representation_key(
2761 value: &str,
2762 style: ScalarStyle,
2763 tag: Option<&Tag>,
2764) -> BackendResult<NormalizedKey> {
2765 let parsed = ScalarOwned::parse_from_cow_and_metadata(
2766 Cow::Borrowed(value),
2767 style,
2768 tag.map(Cow::Borrowed).as_ref(),
2769 )
2770 .ok_or_else(|| {
2771 BackendError::semantic(format!(
2772 "Rendered YAML key representation {value:?} could not be normalized with the active tag/schema."
2773 ))
2774 })?;
2775 match parsed {
2776 ScalarOwned::Null => Ok(NormalizedKey::Null),
2777 ScalarOwned::Boolean(value) => Ok(NormalizedKey::Bool(value)),
2778 ScalarOwned::Integer(value) => Ok(NormalizedKey::Integer(value.into())),
2779 ScalarOwned::FloatingPoint(value) => {
2780 let value = value.into_inner();
2781 if !value.is_finite() {
2782 return Err(BackendError::semantic(
2783 "Rendered YAML contained a non-finite float mapping key, which this backend does not normalize.",
2784 ));
2785 }
2786 Ok(NormalizedKey::Float(NormalizedFloat::finite(value)))
2787 }
2788 ScalarOwned::String(value) => Ok(NormalizedKey::String(value)),
2789 }
2790}
2791
2792fn normalize_tagged_value(
2793 handle: &str,
2794 suffix: &str,
2795 value: &YamlOwned,
2796) -> BackendResult<NormalizedValue> {
2797 if is_yaml_core_tag(handle, suffix, "set") {
2798 let YamlOwned::Mapping(values) = value else {
2799 return Err(BackendError::semantic(
2800 "YAML !!set nodes must normalize from a mapping value.",
2801 ));
2802 };
2803 return values
2804 .iter()
2805 .map(|(key, _)| normalize_key(key))
2806 .collect::<BackendResult<Vec<_>>>()
2807 .map(NormalizedValue::Set);
2808 }
2809 if let Some(normalized) = normalize_core_tagged_scalar_value(handle, suffix, value)? {
2810 return Ok(normalized);
2811 }
2812 normalize_value(value)
2813}
2814
2815fn normalize_tagged_key(
2816 handle: &str,
2817 suffix: &str,
2818 value: &YamlOwned,
2819) -> BackendResult<NormalizedKey> {
2820 if is_yaml_core_tag(handle, suffix, "set") {
2821 return Err(BackendError::semantic(
2822 "YAML set values cannot be used as mapping keys in normalized output.",
2823 ));
2824 }
2825 if let Some(normalized) = normalize_core_tagged_scalar_key(handle, suffix, value)? {
2826 return Ok(normalized);
2827 }
2828 normalize_key(value)
2829}
2830
2831fn normalize_core_tagged_scalar_value(
2832 handle: &str,
2833 suffix: &str,
2834 value: &YamlOwned,
2835) -> BackendResult<Option<NormalizedValue>> {
2836 if !matches!(suffix, "null" | "bool" | "int" | "float" | "str")
2837 || !matches!(handle, "" | "!!" | "tag:yaml.org,2002:")
2838 {
2839 return Ok(None);
2840 }
2841 if is_empty_tagged_scalar(value) {
2842 return Ok(match suffix {
2843 "null" => Some(NormalizedValue::Null),
2844 "str" => Some(NormalizedValue::String(String::new())),
2845 _ => None,
2846 });
2847 }
2848 let Some(text) = scalar_text_for_core_tagged_value(value) else {
2849 return Ok(None);
2850 };
2851 normalize_representation_value(
2852 text.as_ref(),
2853 ScalarStyle::Plain,
2854 Some(&Tag {
2855 handle: canonical_core_tag_handle(handle).to_owned(),
2856 suffix: suffix.to_owned(),
2857 }),
2858 )
2859 .map(Some)
2860}
2861
2862fn normalize_core_tagged_scalar_key(
2863 handle: &str,
2864 suffix: &str,
2865 value: &YamlOwned,
2866) -> BackendResult<Option<NormalizedKey>> {
2867 if !matches!(suffix, "null" | "bool" | "int" | "float" | "str")
2868 || !matches!(handle, "" | "!!" | "tag:yaml.org,2002:")
2869 {
2870 return Ok(None);
2871 }
2872 if is_empty_tagged_scalar(value) {
2873 return Ok(match suffix {
2874 "null" => Some(NormalizedKey::Null),
2875 "str" => Some(NormalizedKey::String(String::new())),
2876 _ => None,
2877 });
2878 }
2879 let Some(text) = scalar_text_for_core_tagged_value(value) else {
2880 return Ok(None);
2881 };
2882 normalize_representation_key(
2883 text.as_ref(),
2884 ScalarStyle::Plain,
2885 Some(&Tag {
2886 handle: canonical_core_tag_handle(handle).to_owned(),
2887 suffix: suffix.to_owned(),
2888 }),
2889 )
2890 .map(Some)
2891}
2892
2893fn canonical_core_tag_handle(handle: &str) -> &str {
2894 match handle {
2895 "" | "!!" | "tag:yaml.org,2002:" => "tag:yaml.org,2002:",
2896 other => other,
2897 }
2898}
2899
2900fn scalar_text_for_tagged_value(value: &YamlOwned) -> Option<Cow<'_, str>> {
2901 match value {
2902 YamlOwned::Representation(value, _, _) => Some(Cow::Borrowed(value.as_str())),
2903 YamlOwned::Value(ScalarOwned::Null) => Some(Cow::Borrowed("null")),
2904 YamlOwned::Value(ScalarOwned::Boolean(value)) => {
2905 Some(Cow::Borrowed(if *value { "true" } else { "false" }))
2906 }
2907 YamlOwned::Value(ScalarOwned::Integer(value)) => Some(Cow::Owned(value.to_string())),
2908 YamlOwned::Value(ScalarOwned::FloatingPoint(value)) => {
2909 Some(Cow::Owned(value.into_inner().to_string()))
2910 }
2911 YamlOwned::Value(ScalarOwned::String(value)) => Some(Cow::Borrowed(value.as_str())),
2912 YamlOwned::Tagged(_, value) => scalar_text_for_tagged_value(value),
2913 YamlOwned::Sequence(_)
2914 | YamlOwned::Mapping(_)
2915 | YamlOwned::Alias(_)
2916 | YamlOwned::BadValue => None,
2917 }
2918}
2919
2920fn scalar_text_for_core_tagged_value(value: &YamlOwned) -> Option<Cow<'_, str>> {
2921 match value {
2922 YamlOwned::Value(ScalarOwned::Null) => Some(Cow::Borrowed("")),
2923 YamlOwned::Tagged(_, value) => scalar_text_for_core_tagged_value(value),
2924 other => scalar_text_for_tagged_value(other),
2925 }
2926}
2927
2928fn is_empty_tagged_scalar(value: &YamlOwned) -> bool {
2929 match value {
2930 YamlOwned::Value(ScalarOwned::Null) => true,
2931 YamlOwned::Representation(value, _, _) => value.is_empty(),
2932 YamlOwned::Tagged(_, value) => is_empty_tagged_scalar(value),
2933 _ => false,
2934 }
2935}
2936
2937fn is_yaml_core_tag(handle: &str, suffix: &str, expected_suffix: &str) -> bool {
2938 suffix == expected_suffix && matches!(handle, "" | "!!" | "tag:yaml.org,2002:")
2939}
2940
2941fn is_merge_key(value: &YamlOwned) -> bool {
2942 match value {
2943 YamlOwned::Value(saphyr::ScalarOwned::String(value)) => value == "<<",
2944 YamlOwned::Representation(value, _, _) => value == "<<",
2945 YamlOwned::Tagged(_, value) => is_merge_key(value),
2946 _ => false,
2947 }
2948}
2949
2950fn apply_merge_entries(value: &YamlOwned, entries: &mut Vec<NormalizedEntry>) -> BackendResult<()> {
2951 match value {
2952 YamlOwned::Mapping(values) => merge_mapping_entries(values, entries),
2953 YamlOwned::Sequence(values) => {
2954 for value in values {
2955 let YamlOwned::Mapping(values) = value else {
2956 return Err(BackendError::semantic(
2957 "YAML merge sequences must contain only mappings.",
2958 ));
2959 };
2960 merge_mapping_entries(values, entries)?;
2961 }
2962 Ok(())
2963 }
2964 YamlOwned::Tagged(_, value) => apply_merge_entries(value, entries),
2965 _ => Err(BackendError::semantic(
2966 "YAML merge values must be a mapping or sequence of mappings.",
2967 )),
2968 }
2969}
2970
2971fn merge_mapping_entries(
2972 values: &saphyr::MappingOwned,
2973 entries: &mut Vec<NormalizedEntry>,
2974) -> BackendResult<()> {
2975 for (key, value) in values {
2976 insert_mapping_entry(entries, normalize_key(key)?, normalize_value(value)?, false);
2977 }
2978 Ok(())
2979}
2980
2981fn insert_mapping_entry(
2982 entries: &mut Vec<NormalizedEntry>,
2983 key: NormalizedKey,
2984 value: NormalizedValue,
2985 override_existing: bool,
2986) {
2987 if let Some(existing) = entries.iter_mut().find(|entry| entry.key == key) {
2988 if override_existing {
2989 existing.value = value;
2990 }
2991 return;
2992 }
2993 entries.push(NormalizedEntry { key, value });
2994}
2995
2996fn wrap_decorators(
2997 decorated: Option<(Option<YamlTagNode>, Option<YamlAnchorNode>)>,
2998 value: YamlValueNode,
2999) -> BackendResult<YamlValueNode> {
3000 if let Some((tag, anchor)) = decorated {
3001 let decorator_span = tag
3002 .as_ref()
3003 .map(|tag| tag.span.clone())
3004 .or_else(|| anchor.as_ref().map(|anchor| anchor.span.clone()))
3005 .unwrap_or_else(|| value_span(&value).clone());
3006 let span = decorator_span.merge(value_span(&value));
3007
3008 match value {
3009 YamlValueNode::Decorated(inner) => {
3010 if anchor.is_some() && inner.anchor.is_some() {
3011 return Err(BackendError::parse_at(
3012 "yaml.parse",
3013 "YAML nodes cannot define more than one anchor.",
3014 Some(span),
3015 ));
3016 }
3017 if tag.is_some() && inner.tag.is_some() {
3018 return Err(BackendError::parse_at(
3019 "yaml.parse",
3020 "YAML nodes cannot define more than one tag.",
3021 Some(span),
3022 ));
3023 }
3024
3025 Ok(YamlValueNode::Decorated(YamlDecoratedNode {
3026 span,
3027 value: inner.value,
3028 tag: tag.or(inner.tag),
3029 anchor: anchor.or(inner.anchor),
3030 }))
3031 }
3032 YamlValueNode::Scalar(YamlScalarNode::Alias(_))
3033 if tag.is_some() || anchor.is_some() =>
3034 {
3035 Err(BackendError::parse_at(
3036 "yaml.parse",
3037 "YAML aliases cannot define tags or anchors.",
3038 Some(span),
3039 ))
3040 }
3041 value => Ok(YamlValueNode::Decorated(YamlDecoratedNode {
3042 span,
3043 value: Box::new(value),
3044 tag,
3045 anchor,
3046 })),
3047 }
3048 } else {
3049 Ok(value)
3050 }
3051}
3052
3053fn null_plain_scalar() -> YamlPlainScalarNode {
3054 YamlPlainScalarNode {
3055 span: SourceSpan::point(0, 0),
3056 chunks: vec![YamlChunk::Text(YamlTextChunkNode {
3057 span: SourceSpan::point(0, 0),
3058 value: "null".to_owned(),
3059 })],
3060 }
3061}
3062
3063fn empty_plain_scalar() -> YamlPlainScalarNode {
3064 YamlPlainScalarNode {
3065 span: SourceSpan::point(0, 0),
3066 chunks: Vec::new(),
3067 }
3068}
3069
3070fn flow_implicit_plain_key_needs_separator(key: &YamlKeyNode) -> bool {
3071 matches!(
3072 &key.value,
3073 YamlKeyValue::Scalar(YamlScalarNode::Plain(node))
3074 if node.chunks.iter().any(|chunk| match chunk {
3075 YamlChunk::Text(text) => text.value.chars().any(char::is_whitespace),
3076 YamlChunk::Interpolation(_) => true,
3077 })
3078 )
3079}
3080
3081fn split_stream(template: &TemplateInput) -> BackendResult<Vec<YamlDocumentFragment>> {
3082 let items = template.flatten();
3083 let mut fragments = Vec::new();
3084 let mut directives = Vec::new();
3085 let mut current_start = None;
3086 let mut explicit_start = false;
3087 let mut explicit_end = false;
3088 let mut line_start = 0;
3089
3090 while line_start < items.len() {
3091 let (line_end, next_line) = line_bounds(&items, line_start);
3092 let line = collect_line(&items[line_start..line_end]);
3093 let trimmed = line.trim_end_matches(['\r', '\n']);
3094 let document_start_payload = document_start_payload_start(&items, line_start, line_end);
3095 let document_end = is_document_end_marker(trimmed);
3096 let malformed_document_end = trimmed.starts_with("...") && !document_end;
3097
3098 if malformed_document_end {
3099 let span = items
3100 .get(line_start)
3101 .map(|item| item.span().clone())
3102 .unwrap_or_else(|| SourceSpan::point(0, 0));
3103 return Err(BackendError::parse_at(
3104 "yaml.parse",
3105 "Unexpected trailing YAML content.",
3106 Some(span),
3107 ));
3108 }
3109
3110 if current_start.is_none() {
3111 if trimmed.is_empty() || trimmed.starts_with('#') {
3112 line_start = next_line;
3113 continue;
3114 }
3115 if trimmed.starts_with('%') {
3116 if !is_valid_directive(trimmed) {
3117 let span = items
3118 .get(line_start)
3119 .map(|item| item.span().clone())
3120 .unwrap_or_else(|| SourceSpan::point(0, 0));
3121 return Err(BackendError::parse_at(
3122 "yaml.parse",
3123 "Unexpected trailing YAML content.",
3124 Some(span),
3125 ));
3126 }
3127 if duplicates_existing_directive(&directives, trimmed) {
3128 let span = items
3129 .get(line_start)
3130 .map(|item| item.span().clone())
3131 .unwrap_or_else(|| SourceSpan::point(0, 0));
3132 return Err(BackendError::parse_at(
3133 "yaml.parse",
3134 "Unexpected trailing YAML content.",
3135 Some(span),
3136 ));
3137 }
3138 directives.push(trimmed.to_owned());
3139 line_start = next_line;
3140 continue;
3141 }
3142 if trimmed == "---" {
3143 current_start = Some(next_line);
3144 explicit_start = true;
3145 line_start = next_line;
3146 continue;
3147 }
3148 if let Some(payload_start) = document_start_payload {
3149 if explicit_start_payload_looks_like_block_mapping(trimmed) {
3150 let span = items
3151 .get(line_start)
3152 .map(|item| item.span().clone())
3153 .unwrap_or_else(|| SourceSpan::point(0, 0));
3154 return Err(BackendError::parse_at(
3155 "yaml.parse",
3156 "Unexpected trailing YAML content.",
3157 Some(span),
3158 ));
3159 }
3160 current_start = Some(payload_start.unwrap_or(next_line));
3161 explicit_start = true;
3162 line_start = next_line;
3163 continue;
3164 }
3165 if document_end {
3166 line_start = next_line;
3167 continue;
3168 }
3169 current_start = Some(line_start);
3170 } else if trimmed == "---" {
3171 fragments.push(build_fragment(
3172 &items,
3173 current_start.unwrap_or(line_start),
3174 line_start,
3175 std::mem::take(&mut directives),
3176 explicit_start,
3177 explicit_end,
3178 ));
3179 current_start = Some(next_line);
3180 explicit_start = true;
3181 explicit_end = false;
3182 line_start = next_line;
3183 continue;
3184 } else if let Some(payload_start) = document_start_payload {
3185 fragments.push(build_fragment(
3186 &items,
3187 current_start.unwrap_or(line_start),
3188 line_start,
3189 std::mem::take(&mut directives),
3190 explicit_start,
3191 explicit_end,
3192 ));
3193 current_start = Some(payload_start.unwrap_or(next_line));
3194 explicit_start = true;
3195 explicit_end = false;
3196 line_start = next_line;
3197 continue;
3198 } else if document_end {
3199 fragments.push(build_fragment(
3200 &items,
3201 current_start.unwrap_or(line_start),
3202 line_start,
3203 std::mem::take(&mut directives),
3204 explicit_start,
3205 true,
3206 ));
3207 current_start = None;
3208 explicit_start = false;
3209 explicit_end = false;
3210 line_start = next_line;
3211 continue;
3212 }
3213
3214 line_start = next_line;
3215 }
3216
3217 if let Some(start) = current_start {
3218 fragments.push(build_fragment(
3219 &items,
3220 start,
3221 items.len().saturating_sub(1),
3222 std::mem::take(&mut directives),
3223 explicit_start,
3224 explicit_end,
3225 ));
3226 }
3227
3228 if current_start.is_none() && !directives.is_empty() {
3229 let span = items
3230 .iter()
3231 .rev()
3232 .find(|item| item.kind() != "eof")
3233 .map(|item| item.span().clone())
3234 .unwrap_or_else(|| SourceSpan::point(0, 0));
3235 return Err(BackendError::parse_at(
3236 "yaml.parse",
3237 "Unexpected trailing YAML content.",
3238 Some(span),
3239 ));
3240 }
3241
3242 if fragments.is_empty() {
3243 fragments.push(YamlDocumentFragment {
3244 directives: Vec::new(),
3245 explicit_start: false,
3246 explicit_end: false,
3247 items: vec![StreamItem::Eof {
3248 span: SourceSpan::point(0, 0),
3249 }],
3250 });
3251 }
3252
3253 Ok(fragments)
3254}
3255
3256fn build_fragment(
3257 items: &[StreamItem],
3258 start: usize,
3259 end: usize,
3260 directives: Vec<String>,
3261 explicit_start: bool,
3262 explicit_end: bool,
3263) -> YamlDocumentFragment {
3264 let mut fragment_items = items[start..end].to_vec();
3265 let eof_span = fragment_items
3266 .last()
3267 .map_or_else(|| SourceSpan::point(0, 0), |item| item.span().clone());
3268 fragment_items.push(StreamItem::Eof { span: eof_span });
3269 YamlDocumentFragment {
3270 directives,
3271 explicit_start,
3272 explicit_end,
3273 items: fragment_items,
3274 }
3275}
3276
3277fn line_bounds(items: &[StreamItem], start: usize) -> (usize, usize) {
3278 let mut probe = start;
3279 while probe < items.len() {
3280 if items[probe].char() == Some('\n') {
3281 return (probe, probe + 1);
3282 }
3283 if items[probe].kind() == "eof" {
3284 return (probe, probe + 1);
3285 }
3286 probe += 1;
3287 }
3288 (items.len(), items.len())
3289}
3290
3291fn document_start_payload_start(
3292 items: &[StreamItem],
3293 line_start: usize,
3294 line_end: usize,
3295) -> Option<Option<usize>> {
3296 let mut probe = line_start;
3297 for expected in ['-', '-', '-'] {
3298 if items.get(probe).and_then(StreamItem::char) != Some(expected) {
3299 return None;
3300 }
3301 probe += 1;
3302 }
3303 if probe >= line_end {
3304 return Some(None);
3305 }
3306 if !matches!(
3307 items.get(probe).and_then(StreamItem::char),
3308 Some(' ' | '\t')
3309 ) {
3310 return None;
3311 }
3312 while probe < line_end
3313 && matches!(
3314 items.get(probe).and_then(StreamItem::char),
3315 Some(' ' | '\t')
3316 )
3317 {
3318 probe += 1;
3319 }
3320 if probe >= line_end || items.get(probe).and_then(StreamItem::char) == Some('#') {
3321 return Some(None);
3322 }
3323 Some(Some(probe))
3324}
3325
3326fn is_document_end_marker(trimmed: &str) -> bool {
3327 if !trimmed.starts_with("...") {
3328 return false;
3329 }
3330 let Some(remainder) = trimmed.get(3..) else {
3331 return true;
3332 };
3333 let remainder = remainder.trim_start_matches([' ', '\t']);
3334 remainder.is_empty() || remainder.starts_with('#')
3335}
3336
3337fn explicit_start_payload_looks_like_block_mapping(trimmed: &str) -> bool {
3338 let Some(rest) = trimmed.strip_prefix("---") else {
3339 return false;
3340 };
3341 let rest = rest.trim_start_matches([' ', '\t']);
3342 if !(rest.starts_with('&') || rest.starts_with('!')) {
3343 return false;
3344 }
3345 if rest.starts_with('[') || rest.starts_with('{') {
3346 return false;
3347 }
3348 let Some((before_colon, after_colon)) = rest.split_once(':') else {
3349 return false;
3350 };
3351 !before_colon.contains('[')
3352 && !before_colon.contains('{')
3353 && !before_colon.ends_with(':')
3354 && matches!(after_colon.chars().next(), Some(' ' | '\t') | None)
3355}
3356
3357fn is_valid_directive(trimmed: &str) -> bool {
3358 let directive = trimmed
3359 .split_once('#')
3360 .map(|(directive, _)| directive)
3361 .unwrap_or(trimmed)
3362 .trim_end();
3363 let mut parts = directive.split_whitespace();
3364 match parts.next() {
3365 Some("%YAML") => matches!(
3366 (parts.next(), parts.next(), parts.next()),
3367 (Some(_version), None, None)
3368 ),
3369 Some("%TAG") => matches!(
3370 (parts.next(), parts.next(), parts.next(), parts.next()),
3371 (Some(_handle), Some(_prefix), None, None)
3372 ),
3373 Some(_) => true,
3374 None => false,
3375 }
3376}
3377
3378fn duplicates_existing_directive(existing: &[String], candidate: &str) -> bool {
3379 let Some((name, handle)) = directive_identity(candidate) else {
3380 return false;
3381 };
3382 existing.iter().any(|directive| {
3383 directive_identity(directive).is_some_and(|(existing_name, existing_handle)| {
3384 existing_name == name && existing_handle == handle
3385 })
3386 })
3387}
3388
3389fn directive_identity(trimmed: &str) -> Option<(&str, Option<&str>)> {
3390 let directive = trimmed
3391 .split_once('#')
3392 .map(|(directive, _)| directive)
3393 .unwrap_or(trimmed)
3394 .trim_end();
3395 let mut parts = directive.split_whitespace();
3396 match parts.next() {
3397 Some("%YAML") => Some(("%YAML", None)),
3398 Some("%TAG") => parts.next().map(|handle| ("%TAG", Some(handle))),
3399 _ => None,
3400 }
3401}
3402
3403fn collect_line(items: &[StreamItem]) -> String {
3404 let mut line = String::new();
3405 for item in items {
3406 if let Some(ch) = item.char() {
3407 line.push(ch);
3408 } else {
3409 line.push('\u{fffc}');
3410 }
3411 }
3412 line
3413}
3414
3415fn value_span(value: &YamlValueNode) -> &SourceSpan {
3416 match value {
3417 YamlValueNode::Scalar(YamlScalarNode::Plain(node)) => &node.span,
3418 YamlValueNode::Scalar(YamlScalarNode::DoubleQuoted(node)) => &node.span,
3419 YamlValueNode::Scalar(YamlScalarNode::SingleQuoted(node)) => &node.span,
3420 YamlValueNode::Scalar(YamlScalarNode::Block(node)) => &node.span,
3421 YamlValueNode::Scalar(YamlScalarNode::Alias(node)) => &node.span,
3422 YamlValueNode::Interpolation(node) => &node.span,
3423 YamlValueNode::Mapping(node) => &node.span,
3424 YamlValueNode::Sequence(node) => &node.span,
3425 YamlValueNode::Decorated(node) => &node.span,
3426 }
3427}
3428
3429#[cfg(test)]
3430mod tests {
3431 use super::{YamlValueNode, parse_template};
3432 use pyo3::prelude::*;
3433 use saphyr::{LoadableYamlNode, ScalarOwned, YamlOwned};
3434 use tstring_pyo3_bindings::{extract_template, yaml::render_document};
3435 use tstring_syntax::{BackendError, BackendResult, ErrorKind};
3436
3437 fn parse_rendered_yaml(text: &str) -> BackendResult<Vec<YamlOwned>> {
3438 YamlOwned::load_from_str(text).map_err(|err| {
3439 BackendError::parse(format!(
3440 "Rendered YAML could not be reparsed during test verification: {err}"
3441 ))
3442 })
3443 }
3444
3445 fn yaml_scalar_text(value: &YamlOwned) -> Option<&str> {
3446 match value {
3447 YamlOwned::Value(value) => value.as_str(),
3448 YamlOwned::Representation(value, _, _) => Some(value.as_str()),
3449 YamlOwned::Tagged(_, value) => yaml_scalar_text(value),
3450 _ => None,
3451 }
3452 }
3453
3454 fn yaml_integer(value: &YamlOwned) -> Option<i64> {
3455 match value {
3456 YamlOwned::Value(value) => value.as_integer(),
3457 YamlOwned::Tagged(_, value) => yaml_integer(value),
3458 _ => None,
3459 }
3460 }
3461
3462 fn yaml_float(value: &YamlOwned) -> Option<f64> {
3463 match value {
3464 YamlOwned::Value(ScalarOwned::FloatingPoint(value)) => Some(value.into_inner()),
3465 YamlOwned::Tagged(_, value) => yaml_float(value),
3466 _ => None,
3467 }
3468 }
3469
3470 fn yaml_sequence_len(value: &YamlOwned) -> Option<usize> {
3471 match value {
3472 YamlOwned::Sequence(value) => Some(value.len()),
3473 YamlOwned::Tagged(_, value) => yaml_sequence_len(value),
3474 _ => None,
3475 }
3476 }
3477
3478 fn yaml_mapping(value: &YamlOwned) -> Option<&saphyr::MappingOwned> {
3479 match value {
3480 YamlOwned::Mapping(value) => Some(value),
3481 YamlOwned::Tagged(_, value) => yaml_mapping(value),
3482 _ => None,
3483 }
3484 }
3485
3486 fn assert_string_entry(document: &YamlOwned, key: &str, expected: &str) {
3487 let mapping = yaml_mapping(document).expect("expected YAML mapping");
3488 let value = mapping
3489 .iter()
3490 .find_map(|(entry_key, entry_value)| {
3491 (yaml_scalar_text(entry_key) == Some(key)).then_some(entry_value)
3492 })
3493 .expect("expected YAML mapping entry");
3494 assert_eq!(yaml_scalar_text(value), Some(expected));
3495 }
3496
3497 fn assert_integer_entry(document: &YamlOwned, key: &str, expected: i64) {
3498 let mapping = yaml_mapping(document).expect("expected YAML mapping");
3499 let value = mapping
3500 .iter()
3501 .find_map(|(entry_key, entry_value)| {
3502 (yaml_scalar_text(entry_key) == Some(key)).then_some(entry_value)
3503 })
3504 .expect("expected YAML mapping entry");
3505 assert_eq!(yaml_integer(value), Some(expected));
3506 }
3507
3508 fn yaml_mapping_entry<'a>(document: &'a YamlOwned, key: &str) -> Option<&'a YamlOwned> {
3509 yaml_mapping(document).and_then(|mapping| {
3510 mapping.iter().find_map(|(entry_key, entry_value)| {
3511 (yaml_scalar_text(entry_key) == Some(key)).then_some(entry_value)
3512 })
3513 })
3514 }
3515
3516 #[test]
3517 fn parses_yaml_flow_and_scalar_nodes() {
3518 Python::with_gil(|py| {
3519 let module = PyModule::from_code(
3520 py,
3521 pyo3::ffi::c_str!(
3522 "user='Alice'\ntemplate=t'name: \"hi-{user}\"\\nitems: [1, {user}]'\n"
3523 ),
3524 pyo3::ffi::c_str!("test_yaml.py"),
3525 pyo3::ffi::c_str!("test_yaml"),
3526 )
3527 .unwrap();
3528 let template = module.getattr("template").unwrap();
3529 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3530 let stream = parse_template(&template).unwrap();
3531 let YamlValueNode::Mapping(mapping) = &stream.documents[0].value else {
3532 panic!("expected mapping");
3533 };
3534 assert_eq!(mapping.entries.len(), 2);
3535 });
3536 }
3537
3538 #[test]
3539 fn parses_tags_and_anchors_on_scalars() {
3540 Python::with_gil(|py| {
3541 let module = PyModule::from_code(
3542 py,
3543 pyo3::ffi::c_str!(
3544 "tag='str'\nanchor='user'\ntemplate=t'value: !{tag} &{anchor} \"hi\"\\n'\n"
3545 ),
3546 pyo3::ffi::c_str!("test_yaml_decorators.py"),
3547 pyo3::ffi::c_str!("test_yaml_decorators"),
3548 )
3549 .unwrap();
3550 let template = module.getattr("template").unwrap();
3551 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3552 let stream = parse_template(&template).unwrap();
3553 let YamlValueNode::Mapping(mapping) = &stream.documents[0].value else {
3554 panic!("expected mapping");
3555 };
3556 let YamlValueNode::Decorated(node) = &mapping.entries[0].value else {
3557 panic!("expected decorated scalar value");
3558 };
3559 assert!(node.tag.is_some());
3560 assert!(node.anchor.is_some());
3561 });
3562 }
3563
3564 #[test]
3565 fn renders_nested_yaml_values_and_validates() {
3566 Python::with_gil(|py| {
3567 let module = PyModule::from_code(
3568 py,
3569 pyo3::ffi::c_str!(
3570 "name='Ada'\nmeta={'active': True, 'count': 2}\ntemplate=t'name: {name}\\nmeta: {meta}\\n'\n"
3571 ),
3572 pyo3::ffi::c_str!("test_yaml_render.py"),
3573 pyo3::ffi::c_str!("test_yaml_render"),
3574 )
3575 .unwrap();
3576 let template = module.getattr("template").unwrap();
3577 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3578 let stream = parse_template(&template).unwrap();
3579 let rendered = render_document(py, &stream).unwrap();
3580 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3581
3582 assert!(rendered.text.contains("name: \"Ada\""));
3583 assert_string_entry(&documents[0], "name", "Ada");
3584 });
3585 }
3586
3587 #[test]
3588 fn rejects_metadata_with_whitespace() {
3589 Python::with_gil(|py| {
3590 let module = PyModule::from_code(
3591 py,
3592 pyo3::ffi::c_str!("anchor='bad anchor'\ntemplate=t'value: &{anchor} \"hi\"\\n'\n"),
3593 pyo3::ffi::c_str!("test_yaml_error.py"),
3594 pyo3::ffi::c_str!("test_yaml_error"),
3595 )
3596 .unwrap();
3597 let template = module.getattr("template").unwrap();
3598 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3599 let stream = parse_template(&template).unwrap();
3600 let err = match render_document(py, &stream) {
3601 Ok(_) => panic!("expected YAML render failure"),
3602 Err(err) => err,
3603 };
3604
3605 assert_eq!(err.kind, ErrorKind::Unrepresentable);
3606 assert!(err.message.contains("YAML metadata"));
3607 });
3608 }
3609
3610 #[test]
3611 fn rejects_flow_mappings_missing_commas_during_parse() {
3612 Python::with_gil(|py| {
3613 let module = PyModule::from_code(
3614 py,
3615 pyo3::ffi::c_str!(
3616 "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"
3617 ),
3618 pyo3::ffi::c_str!("test_yaml_flow_error.py"),
3619 pyo3::ffi::c_str!("test_yaml_flow_error"),
3620 )
3621 .unwrap();
3622 for name in [
3623 "mapping",
3624 "missing_colon",
3625 "leading_comma_mapping",
3626 "sequence",
3627 "trailing_sequence",
3628 "empty_sequence",
3629 "empty_mapping",
3630 ] {
3631 let template = module.getattr(name).unwrap();
3632 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3633 let err = parse_template(&template).expect_err("expected YAML parse failure");
3634 assert_eq!(err.kind, ErrorKind::Parse);
3635 assert!(err.message.contains("Expected"));
3636 }
3637 });
3638 }
3639
3640 #[test]
3641 fn rejects_tabs_as_mapping_separation_whitespace() {
3642 Python::with_gil(|py| {
3643 let module = PyModule::from_code(
3644 py,
3645 pyo3::ffi::c_str!(
3646 "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"
3647 ),
3648 pyo3::ffi::c_str!("test_yaml_tab_error.py"),
3649 pyo3::ffi::c_str!("test_yaml_tab_error"),
3650 )
3651 .unwrap();
3652 for name in ["mapping", "plain", "indent"] {
3653 let template = module.getattr(name).unwrap();
3654 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3655 let err = parse_template(&template).expect_err("expected YAML parse failure");
3656 assert_eq!(err.kind, ErrorKind::Parse);
3657 assert!(
3658 err.message.contains("Tabs are not allowed"),
3659 "{name}: {}",
3660 err.message
3661 );
3662 }
3663 });
3664 }
3665
3666 #[test]
3667 fn splits_explicit_document_streams() {
3668 Python::with_gil(|py| {
3669 let module = PyModule::from_code(
3670 py,
3671 pyo3::ffi::c_str!("template=t'---\\nname: Alice\\n---\\nname: Bob\\n'\n"),
3672 pyo3::ffi::c_str!("test_yaml_stream.py"),
3673 pyo3::ffi::c_str!("test_yaml_stream"),
3674 )
3675 .unwrap();
3676 let template = module.getattr("template").unwrap();
3677 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3678 let stream = parse_template(&template).unwrap();
3679 assert_eq!(stream.documents.len(), 2);
3680 });
3681 }
3682
3683 #[test]
3684 fn parses_and_renders_flow_complex_keys_with_interpolation() {
3685 Python::with_gil(|py| {
3686 let module = PyModule::from_code(
3687 py,
3688 pyo3::ffi::c_str!(
3689 "left='Alice'\nright='Bob'\ntemplate=t'{{ {{name: [{left}, {right}]}}: 1, [{left}, {right}]: 2 }}'\n"
3690 ),
3691 pyo3::ffi::c_str!("test_yaml_flow_complex_keys.py"),
3692 pyo3::ffi::c_str!("test_yaml_flow_complex_keys"),
3693 )
3694 .unwrap();
3695 let template = module.getattr("template").unwrap();
3696 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3697 let stream = parse_template(&template).unwrap();
3698 let YamlValueNode::Mapping(mapping) = &stream.documents[0].value else {
3699 panic!("expected mapping");
3700 };
3701 assert!(matches!(
3702 mapping.entries[0].key.value,
3703 super::YamlKeyValue::Complex(_)
3704 ));
3705 assert!(matches!(
3706 mapping.entries[1].key.value,
3707 super::YamlKeyValue::Complex(_)
3708 ));
3709
3710 let rendered = render_document(py, &stream).unwrap();
3711 assert_eq!(
3712 rendered.text,
3713 "{ { name: [ \"Alice\", \"Bob\" ] }: 1, [ \"Alice\", \"Bob\" ]: 2 }"
3714 );
3715 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3716 assert_eq!(
3717 documents[0]
3718 .as_mapping()
3719 .expect("expected YAML mapping")
3720 .len(),
3721 2
3722 );
3723 });
3724 }
3725
3726 #[test]
3727 fn parses_explicit_mapping_keys_with_nested_collections() {
3728 Python::with_gil(|py| {
3729 let module = PyModule::from_code(
3730 py,
3731 pyo3::ffi::c_str!(
3732 "left='Alice'\nright='Bob'\ntemplate=t'? {{name: [{left}, {right}]}}\\n: 1\\n'\n"
3733 ),
3734 pyo3::ffi::c_str!("test_yaml_explicit_complex_key.py"),
3735 pyo3::ffi::c_str!("test_yaml_explicit_complex_key"),
3736 )
3737 .unwrap();
3738 let template = module.getattr("template").unwrap();
3739 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3740 let stream = parse_template(&template).unwrap();
3741 let YamlValueNode::Mapping(mapping) = &stream.documents[0].value else {
3742 panic!("expected mapping");
3743 };
3744 assert!(matches!(
3745 mapping.entries[0].key.value,
3746 super::YamlKeyValue::Complex(_)
3747 ));
3748 let rendered = render_document(py, &stream).unwrap();
3749 assert!(rendered.text.contains("? { name: [ \"Alice\", \"Bob\" ] }"));
3750 });
3751 }
3752
3753 #[test]
3754 fn renders_explicit_complex_keys_text_and_validated_shape() {
3755 Python::with_gil(|py| {
3756 let module = PyModule::from_code(
3757 py,
3758 pyo3::ffi::c_str!(
3759 "left='Alice'\nright='Bob'\ncomplex_key=t'? {{name: [{left}, {right}]}}\\n: 1\\n'\nempty_key=t'?\\n: 1\\n'\n"
3760 ),
3761 pyo3::ffi::c_str!("test_yaml_explicit_key_render.py"),
3762 pyo3::ffi::c_str!("test_yaml_explicit_key_render"),
3763 )
3764 .unwrap();
3765
3766 let complex_key = module.getattr("complex_key").unwrap();
3767 let complex_key = extract_template(py, &complex_key, "yaml_t/yaml_t_str").unwrap();
3768 let rendered = render_document(py, &parse_template(&complex_key).unwrap()).unwrap();
3769 assert!(
3770 rendered
3771 .text
3772 .contains("? { name: [ \"Alice\", \"Bob\" ] }\n: 1")
3773 );
3774 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3775 assert_eq!(
3776 documents[0]
3777 .as_mapping()
3778 .expect("expected YAML mapping")
3779 .len(),
3780 1
3781 );
3782
3783 let empty_key = module.getattr("empty_key").unwrap();
3784 let empty_key = extract_template(py, &empty_key, "yaml_t/yaml_t_str").unwrap();
3785 let rendered = render_document(py, &parse_template(&empty_key).unwrap()).unwrap();
3786 assert_eq!(rendered.text, "? null\n: 1");
3787 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3788 assert_eq!(
3789 documents[0]
3790 .as_mapping()
3791 .expect("expected YAML mapping")
3792 .len(),
3793 1
3794 );
3795 });
3796 }
3797
3798 #[test]
3799 fn parses_yaml_quoted_scalar_escapes() {
3800 Python::with_gil(|py| {
3801 let module = PyModule::from_code(
3802 py,
3803 pyo3::ffi::c_str!(
3804 "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"
3805 ),
3806 pyo3::ffi::c_str!("test_yaml_quoted_scalars.py"),
3807 pyo3::ffi::c_str!("test_yaml_quoted_scalars"),
3808 )
3809 .unwrap();
3810 let template = module.getattr("template").unwrap();
3811 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3812 let stream = parse_template(&template).unwrap();
3813 let rendered = render_document(py, &stream).unwrap();
3814 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3815 assert_string_entry(&documents[0], "value", "line\nnext α A");
3816 assert_string_entry(&documents[0], "quote", "it's ok");
3817
3818 for (name, expected) in [
3819 ("unicode", "𝄞"),
3820 ("crlf_join", "ab"),
3821 ("nel", "\u{0085}"),
3822 ("nbsp", "\u{00a0}"),
3823 ("empty_single", ""),
3824 ("empty_double", ""),
3825 ("single_blank", "a\nb\nc"),
3826 ] {
3827 let template = module.getattr(name).unwrap();
3828 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3829 let stream = parse_template(&template).unwrap();
3830 let rendered = render_document(py, &stream).unwrap();
3831 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3832 assert_string_entry(&documents[0], "value", expected);
3833 }
3834 });
3835 }
3836
3837 #[test]
3838 fn renders_quoted_scalar_escape_and_folding_families() {
3839 Python::with_gil(|py| {
3840 let module = PyModule::from_code(
3841 py,
3842 pyo3::ffi::c_str!(
3843 "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"
3844 ),
3845 pyo3::ffi::c_str!("test_yaml_quoted_scalar_families.py"),
3846 pyo3::ffi::c_str!("test_yaml_quoted_scalar_families"),
3847 )
3848 .unwrap();
3849
3850 let base = module.getattr("base").unwrap();
3851 let base = extract_template(py, &base, "yaml_t/yaml_t_str").unwrap();
3852 let rendered = render_document(py, &parse_template(&base).unwrap()).unwrap();
3853 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3854 assert_eq!(
3855 rendered.text,
3856 "value: \"line\\nnext α A\"\nquote: 'it''s ok'"
3857 );
3858 assert_string_entry(&documents[0], "value", "line\nnext α A");
3859 assert_string_entry(&documents[0], "quote", "it's ok");
3860
3861 for (name, expected_text, expected) in [
3862 ("multiline_double", "value: \"a b\"", "a b"),
3863 ("multiline_double_blank", "value: \"a\\nb\"", "a\nb"),
3864 (
3865 "multiline_double_more_blank",
3866 "value: \"a\\n\\nb\"",
3867 "a\n\nb",
3868 ),
3869 ("unicode", "value: \"𝄞\"", "𝄞"),
3870 ("crlf_join", "value: \"ab\"", "ab"),
3871 ("nel", "value: \"\u{0085}\"", "\u{0085}"),
3872 ("nbsp", "value: \"\u{00a0}\"", "\u{00a0}"),
3873 ("space", "value: \" \"", " "),
3874 ("slash", "value: \"/\"", "/"),
3875 ("tab", "value: \"\\t\"", "\t"),
3876 ("empty_single", "value: ''", ""),
3877 ("empty_double", "value: \"\"", ""),
3878 ("single_blank", "value: 'a\n\n b\n\n c'", "a\nb\nc"),
3879 ] {
3880 let template = module.getattr(name).unwrap();
3881 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3882 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
3883 assert_eq!(rendered.text, expected_text, "{name}");
3884 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3885 assert_string_entry(&documents[0], "value", expected);
3886 }
3887 });
3888 }
3889
3890 #[test]
3891 fn renders_spec_quoted_scalar_examples_round_trip() {
3892 Python::with_gil(|py| {
3893 let module = PyModule::from_code(
3894 py,
3895 pyo3::ffi::c_str!(
3896 "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"
3897 ),
3898 pyo3::ffi::c_str!("test_yaml_spec_quoted_examples.py"),
3899 pyo3::ffi::c_str!("test_yaml_spec_quoted_examples"),
3900 )
3901 .unwrap();
3902
3903 for (name, key, expected_text, expected_value) in [
3904 (
3905 "unicode",
3906 "unicode",
3907 "unicode: \"Sosa did fine.☺\"",
3908 "Sosa did fine.\u{263a}",
3909 ),
3910 (
3911 "control",
3912 "control",
3913 "control: \"\\b1998\\t1999\\t2000\\n\"",
3914 "\u{0008}1998\t1999\t2000\n",
3915 ),
3916 (
3917 "single",
3918 "single",
3919 "single: '\"Howdy!\" he cried.'",
3920 "\"Howdy!\" he cried.",
3921 ),
3922 (
3923 "quoted",
3924 "quoted",
3925 "quoted: ' # Not a ''comment''.'",
3926 " # Not a 'comment'.",
3927 ),
3928 ("tie", "tie", "tie: '|\\-*-/|'", "|\\-*-/|"),
3929 ] {
3930 let template = module.getattr(name).unwrap();
3931 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3932 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
3933 assert_eq!(rendered.text, expected_text, "{name}");
3934 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3935 assert_string_entry(&documents[0], key, expected_value);
3936 }
3937 });
3938 }
3939
3940 #[test]
3941 fn folds_multiline_double_quoted_scalars() {
3942 Python::with_gil(|py| {
3943 let module = PyModule::from_code(
3944 py,
3945 pyo3::ffi::c_str!(
3946 "single=t'value: \"a\\n b\"\\n'\nblank=t'value: \"a\\n\\n b\"\\n'\n"
3947 ),
3948 pyo3::ffi::c_str!("test_yaml_multiline_double_quoted.py"),
3949 pyo3::ffi::c_str!("test_yaml_multiline_double_quoted"),
3950 )
3951 .unwrap();
3952
3953 for (name, expected) in [("single", "a b"), ("blank", "a\nb")] {
3954 let template = module.getattr(name).unwrap();
3955 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3956 let stream = parse_template(&template).unwrap();
3957 let rendered = render_document(py, &stream).unwrap();
3958 let documents = parse_rendered_yaml(&rendered.text).unwrap();
3959 assert_string_entry(&documents[0], "value", expected);
3960 }
3961 });
3962 }
3963
3964 #[test]
3965 fn renders_block_chomping_and_indent_indicator_families() {
3966 Python::with_gil(|py| {
3967 let module = PyModule::from_code(
3968 py,
3969 pyo3::ffi::c_str!(
3970 "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"
3971 ),
3972 pyo3::ffi::c_str!("test_yaml_block_chomping_families.py"),
3973 pyo3::ffi::c_str!("test_yaml_block_chomping_families"),
3974 )
3975 .unwrap();
3976
3977 for (name, expected_text, key, expected_value) in [
3978 ("literal_strip", "value: |-\n a\n b", "value", "a\nb"),
3979 ("literal_keep", "value: |+\n a\n b\n", "value", "a\nb\n"),
3980 (
3981 "literal_keep_leading_blank",
3982 "value: |+\n \n a\n",
3983 "value",
3984 "\na\n",
3985 ),
3986 ("folded_strip", "value: >-\n a\n b", "value", "a b"),
3987 ("folded_keep", "value: >+\n a\n b\n", "value", "a b\n"),
3988 (
3989 "folded_more",
3990 "value: >\n a\n b\n c\n",
3991 "value",
3992 "a\n b\nc\n",
3993 ),
3994 (
3995 "indent_indicator",
3996 "value: |2\n a\n b\n",
3997 "value",
3998 "a\nb\n",
3999 ),
4000 (
4001 "literal_blank_keep",
4002 "value: |+\n a\n \n b\n",
4003 "value",
4004 "a\n\nb\n",
4005 ),
4006 (
4007 "folded_blank_keep",
4008 "value: >+\n a\n \n b\n",
4009 "value",
4010 "a\nb\n",
4011 ),
4012 ] {
4013 let template = module.getattr(name).unwrap();
4014 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4015 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
4016 assert_eq!(rendered.text, expected_text, "{name}");
4017 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4018 assert_string_entry(&documents[0], key, expected_value);
4019 }
4020 });
4021 }
4022
4023 #[test]
4024 fn folds_multiline_plain_scalars() {
4025 Python::with_gil(|py| {
4026 let module = PyModule::from_code(
4027 py,
4028 pyo3::ffi::c_str!("template=t'value: a\\n b\\n\\n c\\n'\n"),
4029 pyo3::ffi::c_str!("test_yaml_multiline_plain.py"),
4030 pyo3::ffi::c_str!("test_yaml_multiline_plain"),
4031 )
4032 .unwrap();
4033 let template = module.getattr("template").unwrap();
4034 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4035 let stream = parse_template(&template).unwrap();
4036 let rendered = render_document(py, &stream).unwrap();
4037 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4038 assert_string_entry(&documents[0], "value", "a b\nc");
4039 assert!(rendered.text.contains("\"a b\\nc\""));
4040 });
4041 }
4042
4043 #[test]
4044 fn accepts_top_level_flow_collections_with_trailing_newlines() {
4045 Python::with_gil(|py| {
4046 let module = PyModule::from_code(
4047 py,
4048 pyo3::ffi::c_str!(
4049 "from string.templatelib import Template\ntemplate=Template('{a: 1, b: [2, 3]}\\n')\n"
4050 ),
4051 pyo3::ffi::c_str!("test_yaml_flow_trailing_newline.py"),
4052 pyo3::ffi::c_str!("test_yaml_flow_trailing_newline"),
4053 )
4054 .unwrap();
4055 let template = module.getattr("template").unwrap();
4056 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4057 let stream = parse_template(&template).unwrap();
4058 let rendered = render_document(py, &stream).unwrap();
4059 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4060 assert_eq!(
4061 documents[0]
4062 .as_mapping()
4063 .expect("expected YAML mapping")
4064 .len(),
4065 2
4066 );
4067 });
4068 }
4069
4070 #[test]
4071 fn accepts_line_wrapped_flow_collections() {
4072 Python::with_gil(|py| {
4073 let module = PyModule::from_code(
4074 py,
4075 pyo3::ffi::c_str!(
4076 "from string.templatelib import Template\nsequence=Template('key: [a,\\n b]\\n')\nmapping=Template('key: {a: 1,\\n b: 2}\\n')\n"
4077 ),
4078 pyo3::ffi::c_str!("test_yaml_wrapped_flow.py"),
4079 pyo3::ffi::c_str!("test_yaml_wrapped_flow"),
4080 )
4081 .unwrap();
4082
4083 for name in ["sequence", "mapping"] {
4084 let template = module.getattr(name).unwrap();
4085 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4086 let stream = parse_template(&template).unwrap();
4087 let rendered = render_document(py, &stream).unwrap();
4088 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4089 assert_eq!(
4090 documents[0]
4091 .as_mapping()
4092 .expect("expected YAML mapping")
4093 .len(),
4094 1
4095 );
4096 }
4097 });
4098 }
4099
4100 #[test]
4101 fn accepts_flow_collections_with_comments() {
4102 Python::with_gil(|py| {
4103 let module = PyModule::from_code(
4104 py,
4105 pyo3::ffi::c_str!(
4106 "from string.templatelib import Template\nsequence=Template('key: [a, # first\\n b]\\n')\nmapping=Template('key: {a: 1, # first\\n b: 2}\\n')\n"
4107 ),
4108 pyo3::ffi::c_str!("test_yaml_flow_comments.py"),
4109 pyo3::ffi::c_str!("test_yaml_flow_comments"),
4110 )
4111 .unwrap();
4112
4113 for name in ["sequence", "mapping"] {
4114 let template = module.getattr(name).unwrap();
4115 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4116 let stream = parse_template(&template).unwrap();
4117 let rendered = render_document(py, &stream).unwrap();
4118 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4119 assert_eq!(
4120 documents[0]
4121 .as_mapping()
4122 .expect("expected YAML mapping")
4123 .len(),
4124 1
4125 );
4126 }
4127 });
4128 }
4129
4130 #[test]
4131 fn treats_empty_documents_as_null() {
4132 Python::with_gil(|py| {
4133 let module = PyModule::from_code(
4134 py,
4135 pyo3::ffi::c_str!(
4136 "from string.templatelib import Template\nempty=Template('---\\n...\\n')\nstream=Template('---\\n\\n---\\na: 1\\n')\n"
4137 ),
4138 pyo3::ffi::c_str!("test_yaml_empty_docs.py"),
4139 pyo3::ffi::c_str!("test_yaml_empty_docs"),
4140 )
4141 .unwrap();
4142
4143 let empty = module.getattr("empty").unwrap();
4144 let empty = extract_template(py, &empty, "yaml_t/yaml_t_str").unwrap();
4145 let rendered = render_document(py, &parse_template(&empty).unwrap()).unwrap();
4146 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4147 assert!(documents.as_slice()[0].is_null());
4148
4149 let stream = module.getattr("stream").unwrap();
4150 let stream = extract_template(py, &stream, "yaml_t/yaml_t_str").unwrap();
4151 let rendered = render_document(py, &parse_template(&stream).unwrap()).unwrap();
4152 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4153 assert!(documents[0].is_null());
4154 assert!(documents[1].as_mapping().is_some());
4155 });
4156 }
4157
4158 #[test]
4159 fn supports_indentless_sequence_values_and_empty_explicit_keys() {
4160 Python::with_gil(|py| {
4161 let module = PyModule::from_code(
4162 py,
4163 pyo3::ffi::c_str!(
4164 "from string.templatelib import Template\nindentless=Template('a:\\n- 1\\n- 2\\n')\nempty_key=Template('?\\n: 1\\n')\n"
4165 ),
4166 pyo3::ffi::c_str!("test_yaml_indentless_and_empty_key.py"),
4167 pyo3::ffi::c_str!("test_yaml_indentless_and_empty_key"),
4168 )
4169 .unwrap();
4170
4171 let indentless = module.getattr("indentless").unwrap();
4172 let indentless = extract_template(py, &indentless, "yaml_t/yaml_t_str").unwrap();
4173 let rendered = render_document(py, &parse_template(&indentless).unwrap()).unwrap();
4174 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4175 let mapping = documents[0].as_mapping().expect("expected YAML mapping");
4176 let value = mapping
4177 .iter()
4178 .find_map(|(key, value)| (key.as_str() == Some("a")).then_some(value))
4179 .expect("expected key a");
4180 assert_eq!(value.as_vec().expect("expected YAML sequence").len(), 2);
4181
4182 let empty_key = module.getattr("empty_key").unwrap();
4183 let empty_key = extract_template(py, &empty_key, "yaml_t/yaml_t_str").unwrap();
4184 let rendered = render_document(py, &parse_template(&empty_key).unwrap()).unwrap();
4185 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4186 let mapping = documents[0].as_mapping().expect("expected YAML mapping");
4187 let value = mapping
4188 .iter()
4189 .find_map(|(key, value)| key.is_null().then_some(value))
4190 .expect("expected null key");
4191 assert_eq!(value.as_integer(), Some(1));
4192 });
4193 }
4194
4195 #[test]
4196 fn supports_compact_mappings_in_sequences_and_plain_hash_chars() {
4197 Python::with_gil(|py| {
4198 let module = PyModule::from_code(
4199 py,
4200 pyo3::ffi::c_str!(
4201 "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"
4202 ),
4203 pyo3::ffi::c_str!("test_yaml_compact_sequence_maps.py"),
4204 pyo3::ffi::c_str!("test_yaml_compact_sequence_maps"),
4205 )
4206 .unwrap();
4207
4208 for name in ["seq", "mapped", "seqs", "hash_value"] {
4209 let template = module.getattr(name).unwrap();
4210 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4211 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
4212 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4213 assert_eq!(documents.len(), 1);
4214 }
4215 });
4216 }
4217
4218 #[test]
4219 fn preserves_explicit_document_end_markers_in_streams() {
4220 Python::with_gil(|py| {
4221 let module = PyModule::from_code(
4222 py,
4223 pyo3::ffi::c_str!(
4224 "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"
4225 ),
4226 pyo3::ffi::c_str!("test_yaml_explicit_end.py"),
4227 pyo3::ffi::c_str!("test_yaml_explicit_end"),
4228 )
4229 .unwrap();
4230 let template = module.getattr("template").unwrap();
4231 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4232 let stream = parse_template(&template).unwrap();
4233 let rendered = render_document(py, &stream).unwrap();
4234
4235 assert!(rendered.text.contains("...\n---"));
4236
4237 let commented = module.getattr("commented").unwrap();
4238 let commented = extract_template(py, &commented, "yaml_t/yaml_t_str").unwrap();
4239 let rendered = render_document(py, &parse_template(&commented).unwrap()).unwrap();
4240 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4241 assert_eq!(documents.len(), 2);
4242 });
4243 }
4244
4245 #[test]
4246 fn parses_verbatim_tags() {
4247 Python::with_gil(|py| {
4248 let module = PyModule::from_code(
4249 py,
4250 pyo3::ffi::c_str!(
4251 "from string.templatelib import Template\ntemplate=Template('value: !<tag:yaml.org,2002:str> hello\\n')\n"
4252 ),
4253 pyo3::ffi::c_str!("test_yaml_verbatim_tag.py"),
4254 pyo3::ffi::c_str!("test_yaml_verbatim_tag"),
4255 )
4256 .unwrap();
4257 let template = module.getattr("template").unwrap();
4258 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4259 let stream = parse_template(&template).unwrap();
4260 let rendered = render_document(py, &stream).unwrap();
4261 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4262
4263 assert!(rendered.text.contains("!<tag:yaml.org,2002:str>"));
4264 assert_string_entry(&documents[0], "value", "hello");
4265 });
4266 }
4267
4268 #[test]
4269 fn validates_user_defined_tags_via_saphyr() {
4270 Python::with_gil(|py| {
4271 let module = PyModule::from_code(
4272 py,
4273 pyo3::ffi::c_str!(
4274 "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"
4275 ),
4276 pyo3::ffi::c_str!("test_yaml_custom_tags.py"),
4277 pyo3::ffi::c_str!("test_yaml_custom_tags"),
4278 )
4279 .unwrap();
4280
4281 let scalar = module.getattr("scalar").unwrap();
4282 let scalar = extract_template(py, &scalar, "yaml_t/yaml_t_str").unwrap();
4283 let rendered = render_document(py, &parse_template(&scalar).unwrap()).unwrap();
4284 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4285 assert_eq!(rendered.text.trim_end(), "!custom 3");
4286 assert!(rendered.text.contains("!custom 3"));
4287 assert_eq!(yaml_integer(&documents[0]), Some(3));
4288
4289 let custom_tag_scalar = module.getattr("custom_tag_scalar").unwrap();
4290 let custom_tag_scalar =
4291 extract_template(py, &custom_tag_scalar, "yaml_t/yaml_t_str").unwrap();
4292 let rendered =
4293 render_document(py, &parse_template(&custom_tag_scalar).unwrap()).unwrap();
4294 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4295 assert_eq!(rendered.text.trim_end(), "value: !custom 3");
4296 assert!(rendered.text.contains("value: !custom 3"));
4297 assert_integer_entry(&documents[0], "value", 3);
4298
4299 let custom_tag_sequence = module.getattr("custom_tag_sequence").unwrap();
4300 let custom_tag_sequence =
4301 extract_template(py, &custom_tag_sequence, "yaml_t/yaml_t_str").unwrap();
4302 let rendered =
4303 render_document(py, &parse_template(&custom_tag_sequence).unwrap()).unwrap();
4304 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4305 assert!(rendered.text.contains("value: !custom [ 1, 2 ]"));
4306 let value = documents[0]
4307 .as_mapping()
4308 .expect("expected YAML mapping")
4309 .iter()
4310 .find_map(|(key, value)| (yaml_scalar_text(key) == Some("value")).then_some(value))
4311 .expect("expected value key");
4312 assert_eq!(yaml_sequence_len(value), Some(2));
4313
4314 let commented_root = module.getattr("commented_root").unwrap();
4315 let commented_root =
4316 extract_template(py, &commented_root, "yaml_t/yaml_t_str").unwrap();
4317 let rendered = render_document(py, &parse_template(&commented_root).unwrap()).unwrap();
4318 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4319 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
4320 });
4321 }
4322
4323 #[test]
4324 fn rejects_aliases_that_cross_document_boundaries() {
4325 Python::with_gil(|py| {
4326 let module = PyModule::from_code(
4327 py,
4328 pyo3::ffi::c_str!(
4329 "from string.templatelib import Template\nstream=Template('--- &a\\n- 1\\n- 2\\n---\\n*a\\n')\n"
4330 ),
4331 pyo3::ffi::c_str!("test_yaml_cross_doc_alias.py"),
4332 pyo3::ffi::c_str!("test_yaml_cross_doc_alias"),
4333 )
4334 .unwrap();
4335
4336 let stream = module.getattr("stream").unwrap();
4337 let stream = extract_template(py, &stream, "yaml_t/yaml_t_str").unwrap();
4338 let rendered = render_document(py, &parse_template(&stream).unwrap()).unwrap();
4339 let err = parse_rendered_yaml(&rendered.text).expect_err("expected YAML parse failure");
4340 assert_eq!(err.kind, ErrorKind::Parse);
4341 assert!(err.message.contains("unknown anchor"));
4342 });
4343 }
4344
4345 #[test]
4346 fn preserves_tag_directives_and_handle_tags() {
4347 Python::with_gil(|py| {
4348 let module = PyModule::from_code(
4349 py,
4350 pyo3::ffi::c_str!(
4351 "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"
4352 ),
4353 pyo3::ffi::c_str!("test_yaml_tag_directives.py"),
4354 pyo3::ffi::c_str!("test_yaml_tag_directives"),
4355 )
4356 .unwrap();
4357
4358 let scalar = module.getattr("scalar").unwrap();
4359 let scalar = extract_template(py, &scalar, "yaml_t/yaml_t_str").unwrap();
4360 let stream = parse_template(&scalar).unwrap();
4361 let rendered = render_document(py, &stream).unwrap();
4362 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4363 assert_eq!(
4364 rendered.text,
4365 "%TAG !e! tag:example.com,2020:\n---\nvalue: !e!foo 1"
4366 );
4367 assert_integer_entry(&documents[0], "value", 1);
4368
4369 let doc_start_comment = module.getattr("doc_start_comment").unwrap();
4370 let doc_start_comment =
4371 extract_template(py, &doc_start_comment, "yaml_t/yaml_t_str").unwrap();
4372 let stream = parse_template(&doc_start_comment).unwrap();
4373 let rendered = render_document(py, &stream).unwrap();
4374 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4375 assert_eq!(rendered.text, "---\nvalue: 1");
4376 assert_integer_entry(&documents[0], "value", 1);
4377
4378 let doc_start_tag_comment = module.getattr("doc_start_tag_comment").unwrap();
4379 let doc_start_tag_comment =
4380 extract_template(py, &doc_start_tag_comment, "yaml_t/yaml_t_str").unwrap();
4381 let stream = parse_template(&doc_start_tag_comment).unwrap();
4382 let rendered = render_document(py, &stream).unwrap();
4383 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4384 assert_eq!(rendered.text, "---\n!!str true");
4385 assert_eq!(yaml_scalar_text(&documents[0]), Some("true"));
4386
4387 let root = module.getattr("root").unwrap();
4388 let root = extract_template(py, &root, "yaml_t/yaml_t_str").unwrap();
4389 let stream = parse_template(&root).unwrap();
4390 let rendered = render_document(py, &stream).unwrap();
4391 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4392 assert_eq!(
4393 rendered.text,
4394 "%YAML 1.2\n%TAG !e! tag:example.com,2020:\n---\n!e!root { value: !e!leaf 1 }"
4395 );
4396 assert_integer_entry(&documents[0], "value", 1);
4397
4398 let block_map = module.getattr("block_map").unwrap();
4399 let block_map = extract_template(py, &block_map, "yaml_t/yaml_t_str").unwrap();
4400 let stream = parse_template(&block_map).unwrap();
4401 let rendered = render_document(py, &stream).unwrap();
4402 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4403 assert_eq!(rendered.text, "---\n!!map\na: 1");
4404 assert_integer_entry(&documents[0], "a", 1);
4405
4406 let block_seq = module.getattr("block_seq").unwrap();
4407 let block_seq = extract_template(py, &block_seq, "yaml_t/yaml_t_str").unwrap();
4408 let stream = parse_template(&block_seq).unwrap();
4409 let rendered = render_document(py, &stream).unwrap();
4410 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4411 assert_eq!(rendered.text, "---\n!!seq\n- 1\n- 2");
4412 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
4413
4414 let anchor_map = module.getattr("anchor_map").unwrap();
4415 let anchor_map = extract_template(py, &anchor_map, "yaml_t/yaml_t_str").unwrap();
4416 let stream = parse_template(&anchor_map).unwrap();
4417 let rendered = render_document(py, &stream).unwrap();
4418 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4419 assert_eq!(rendered.text, "---\n&root\na: 1");
4420 assert_integer_entry(&documents[0], "a", 1);
4421
4422 let anchor_seq = module.getattr("anchor_seq").unwrap();
4423 let anchor_seq = extract_template(py, &anchor_seq, "yaml_t/yaml_t_str").unwrap();
4424 let stream = parse_template(&anchor_seq).unwrap();
4425 let rendered = render_document(py, &stream).unwrap();
4426 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4427 assert_eq!(rendered.text, "---\n!custom &root\n- 1\n- 2");
4428 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
4429 });
4430 }
4431
4432 #[test]
4433 fn preserves_explicit_core_schema_tags() {
4434 Python::with_gil(|py| {
4435 let module = PyModule::from_code(
4436 py,
4437 pyo3::ffi::c_str!(
4438 "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"
4439 ),
4440 pyo3::ffi::c_str!("test_yaml_core_tags.py"),
4441 pyo3::ffi::c_str!("test_yaml_core_tags"),
4442 )
4443 .unwrap();
4444
4445 let mapping = module.getattr("mapping").unwrap();
4446 let mapping = extract_template(py, &mapping, "yaml_t/yaml_t_str").unwrap();
4447 let rendered = render_document(py, &parse_template(&mapping).unwrap()).unwrap();
4448 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4449 let mapping = yaml_mapping(&documents[0]).expect("expected YAML mapping");
4450 let float_value = mapping
4451 .iter()
4452 .find_map(|(key, value)| {
4453 (yaml_scalar_text(key) == Some("value_float")).then_some(value)
4454 })
4455 .expect("expected value_float key");
4456 assert_eq!(yaml_float(float_value), Some(1.0));
4457 assert_string_entry(&documents[0], "value_str", "true");
4458
4459 let root_int = module.getattr("root_int").unwrap();
4460 let root_int = extract_template(py, &root_int, "yaml_t/yaml_t_str").unwrap();
4461 let rendered = render_document(py, &parse_template(&root_int).unwrap()).unwrap();
4462 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4463 assert_eq!(yaml_integer(&documents[0]), Some(3));
4464
4465 let root_str = module.getattr("root_str").unwrap();
4466 let root_str = extract_template(py, &root_str, "yaml_t/yaml_t_str").unwrap();
4467 let rendered = render_document(py, &parse_template(&root_str).unwrap()).unwrap();
4468 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4469 assert_eq!(yaml_scalar_text(&documents[0]), Some("true"));
4470
4471 let root_bool = module.getattr("root_bool").unwrap();
4472 let root_bool = extract_template(py, &root_bool, "yaml_t/yaml_t_str").unwrap();
4473 let rendered = render_document(py, &parse_template(&root_bool).unwrap()).unwrap();
4474 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4475 assert!(documents[0].is_boolean());
4476 });
4477 }
4478
4479 #[test]
4480 fn supports_flow_trailing_commas_sequence_values_and_indent_indicators() {
4481 Python::with_gil(|py| {
4482 let module = PyModule::from_code(
4483 py,
4484 pyo3::ffi::c_str!(
4485 "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"
4486 ),
4487 pyo3::ffi::c_str!("test_yaml_edge_cases.py"),
4488 pyo3::ffi::c_str!("test_yaml_edge_cases"),
4489 )
4490 .unwrap();
4491
4492 let flow_seq = module.getattr("flow_seq").unwrap();
4493 let flow_seq = extract_template(py, &flow_seq, "yaml_t/yaml_t_str").unwrap();
4494 let rendered = render_document(py, &parse_template(&flow_seq).unwrap()).unwrap();
4495 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4496 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
4497
4498 let comment_only = module.getattr("comment_only").unwrap();
4499 let comment_only = extract_template(py, &comment_only, "yaml_t/yaml_t_str").unwrap();
4500 let rendered = render_document(py, &parse_template(&comment_only).unwrap()).unwrap();
4501 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4502 assert!(documents[0].is_null());
4503
4504 let comment_only_explicit = module.getattr("comment_only_explicit").unwrap();
4505 let comment_only_explicit =
4506 extract_template(py, &comment_only_explicit, "yaml_t/yaml_t_str").unwrap();
4507 let rendered =
4508 render_document(py, &parse_template(&comment_only_explicit).unwrap()).unwrap();
4509 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4510 assert!(documents[0].is_null());
4511
4512 let comment_only_explicit_end = module.getattr("comment_only_explicit_end").unwrap();
4513 let comment_only_explicit_end =
4514 extract_template(py, &comment_only_explicit_end, "yaml_t/yaml_t_str").unwrap();
4515 let rendered =
4516 render_document(py, &parse_template(&comment_only_explicit_end).unwrap()).unwrap();
4517 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4518 assert!(documents[0].is_null());
4519
4520 let comment_only_explicit_end_stream =
4521 module.getattr("comment_only_explicit_end_stream").unwrap();
4522 let comment_only_explicit_end_stream =
4523 extract_template(py, &comment_only_explicit_end_stream, "yaml_t/yaml_t_str")
4524 .unwrap();
4525 let rendered = render_document(
4526 py,
4527 &parse_template(&comment_only_explicit_end_stream).unwrap(),
4528 )
4529 .unwrap();
4530 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4531 assert_eq!(documents.len(), 2);
4532 assert!(documents[0].is_null());
4533 assert_integer_entry(&documents[1], "a", 1);
4534
4535 let comment_only_mid_stream = module.getattr("comment_only_mid_stream").unwrap();
4536 let comment_only_mid_stream =
4537 extract_template(py, &comment_only_mid_stream, "yaml_t/yaml_t_str").unwrap();
4538 let rendered =
4539 render_document(py, &parse_template(&comment_only_mid_stream).unwrap()).unwrap();
4540 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4541 assert_eq!(documents.len(), 3);
4542 assert_integer_entry(&documents[0], "a", 1);
4543 assert!(documents[1].is_null());
4544 assert_integer_entry(&documents[2], "b", 2);
4545
4546 let comment_only_tail_stream = module.getattr("comment_only_tail_stream").unwrap();
4547 let comment_only_tail_stream =
4548 extract_template(py, &comment_only_tail_stream, "yaml_t/yaml_t_str").unwrap();
4549 let rendered =
4550 render_document(py, &parse_template(&comment_only_tail_stream).unwrap()).unwrap();
4551 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4552 assert_eq!(documents.len(), 2);
4553 assert_integer_entry(&documents[0], "a", 1);
4554 assert!(documents[1].is_null());
4555
4556 let flow_map = module.getattr("flow_map").unwrap();
4557 let flow_map = extract_template(py, &flow_map, "yaml_t/yaml_t_str").unwrap();
4558 let rendered = render_document(py, &parse_template(&flow_map).unwrap()).unwrap();
4559 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4560 assert_integer_entry(&documents[0], "a", 1);
4561
4562 let empty_flow_seq = module.getattr("empty_flow_seq").unwrap();
4563 let empty_flow_seq =
4564 extract_template(py, &empty_flow_seq, "yaml_t/yaml_t_str").unwrap();
4565 let rendered = render_document(py, &parse_template(&empty_flow_seq).unwrap()).unwrap();
4566 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4567 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4568 assert_eq!(yaml_sequence_len(value), Some(0));
4569
4570 let empty_flow_map = module.getattr("empty_flow_map").unwrap();
4571 let empty_flow_map =
4572 extract_template(py, &empty_flow_map, "yaml_t/yaml_t_str").unwrap();
4573 let rendered = render_document(py, &parse_template(&empty_flow_map).unwrap()).unwrap();
4574 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4575 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4576 assert!(yaml_mapping(value).is_some());
4577
4578 let flow_scalar_mix = module.getattr("flow_scalar_mix").unwrap();
4579 let flow_scalar_mix =
4580 extract_template(py, &flow_scalar_mix, "yaml_t/yaml_t_str").unwrap();
4581 let rendered = render_document(py, &parse_template(&flow_scalar_mix).unwrap()).unwrap();
4582 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4583 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4584 match value {
4585 YamlOwned::Sequence(sequence) => {
4586 assert_eq!(sequence.len(), 3);
4587 assert_eq!(yaml_scalar_text(&sequence[0]), Some(""));
4588 assert_eq!(yaml_scalar_text(&sequence[1]), Some(""));
4589 assert_eq!(yaml_scalar_text(&sequence[2]), Some("plain"));
4590 }
4591 _ => panic!("expected YAML sequence"),
4592 }
4593
4594 let flow_plain_scalar = module.getattr("flow_plain_scalar").unwrap();
4595 let flow_plain_scalar =
4596 extract_template(py, &flow_plain_scalar, "yaml_t/yaml_t_str").unwrap();
4597 let rendered =
4598 render_document(py, &parse_template(&flow_plain_scalar).unwrap()).unwrap();
4599 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4600 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4601 match value {
4602 YamlOwned::Sequence(sequence) => {
4603 assert_eq!(sequence.len(), 1);
4604 assert_eq!(yaml_scalar_text(&sequence[0]), Some("1 2"));
4605 }
4606 _ => panic!("expected YAML sequence"),
4607 }
4608
4609 let flow_hash_plain_mapping_value =
4610 module.getattr("flow_hash_plain_mapping_value").unwrap();
4611 let flow_hash_plain_mapping_value =
4612 extract_template(py, &flow_hash_plain_mapping_value, "yaml_t/yaml_t_str").unwrap();
4613 let rendered =
4614 render_document(py, &parse_template(&flow_hash_plain_mapping_value).unwrap())
4615 .unwrap();
4616 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4617 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4618 assert_string_entry(value, "a", "b#c");
4619
4620 let flow_hash_plain_mapping_values =
4621 module.getattr("flow_hash_plain_mapping_values").unwrap();
4622 let flow_hash_plain_mapping_values =
4623 extract_template(py, &flow_hash_plain_mapping_values, "yaml_t/yaml_t_str").unwrap();
4624 let rendered = render_document(
4625 py,
4626 &parse_template(&flow_hash_plain_mapping_values).unwrap(),
4627 )
4628 .unwrap();
4629 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4630 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4631 assert_string_entry(value, "a", "b#c");
4632 assert_string_entry(value, "d", "e#f");
4633
4634 let flow_hash_plain_scalars = module.getattr("flow_hash_plain_scalars").unwrap();
4635 let flow_hash_plain_scalars =
4636 extract_template(py, &flow_hash_plain_scalars, "yaml_t/yaml_t_str").unwrap();
4637 let rendered =
4638 render_document(py, &parse_template(&flow_hash_plain_scalars).unwrap()).unwrap();
4639 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4640 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4641 match value {
4642 YamlOwned::Sequence(sequence) => {
4643 assert_eq!(sequence.len(), 2);
4644 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b"));
4645 assert_eq!(yaml_scalar_text(&sequence[1]), Some("c#d"));
4646 }
4647 _ => panic!("expected YAML sequence"),
4648 }
4649
4650 let flow_hash_value_sequence = module.getattr("flow_hash_value_sequence").unwrap();
4651 let flow_hash_value_sequence =
4652 extract_template(py, &flow_hash_value_sequence, "yaml_t/yaml_t_str").unwrap();
4653 let rendered =
4654 render_document(py, &parse_template(&flow_hash_value_sequence).unwrap()).unwrap();
4655 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4656 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4657 match value {
4658 YamlOwned::Sequence(sequence) => {
4659 assert_eq!(sequence.len(), 3);
4660 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b"));
4661 assert_eq!(yaml_scalar_text(&sequence[1]), Some("c#d"));
4662 assert_eq!(yaml_scalar_text(&sequence[2]), Some("e#f"));
4663 }
4664 _ => panic!("expected YAML sequence"),
4665 }
4666
4667 let flow_hash_long_sequence = module.getattr("flow_hash_long_sequence").unwrap();
4668 let flow_hash_long_sequence =
4669 extract_template(py, &flow_hash_long_sequence, "yaml_t/yaml_t_str").unwrap();
4670 let rendered =
4671 render_document(py, &parse_template(&flow_hash_long_sequence).unwrap()).unwrap();
4672 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4673 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4674 match value {
4675 YamlOwned::Sequence(sequence) => {
4676 assert_eq!(sequence.len(), 4);
4677 assert_eq!(yaml_scalar_text(&sequence[3]), Some("g#h"));
4678 }
4679 _ => panic!("expected YAML sequence"),
4680 }
4681
4682 let flow_hash_five_sequence = module.getattr("flow_hash_five_sequence").unwrap();
4683 let flow_hash_five_sequence =
4684 extract_template(py, &flow_hash_five_sequence, "yaml_t/yaml_t_str").unwrap();
4685 let rendered =
4686 render_document(py, &parse_template(&flow_hash_five_sequence).unwrap()).unwrap();
4687 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4688 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4689 match value {
4690 YamlOwned::Sequence(sequence) => {
4691 assert_eq!(sequence.len(), 5);
4692 assert_eq!(yaml_scalar_text(&sequence[4]), Some("i#j"));
4693 }
4694 _ => panic!("expected YAML sequence"),
4695 }
4696
4697 let flow_mapping_hash_key = module.getattr("flow_mapping_hash_key").unwrap();
4698 let flow_mapping_hash_key =
4699 extract_template(py, &flow_mapping_hash_key, "yaml_t/yaml_t_str").unwrap();
4700 let rendered =
4701 render_document(py, &parse_template(&flow_mapping_hash_key).unwrap()).unwrap();
4702 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4703 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4704 assert_integer_entry(value, "a#b", 1);
4705
4706 let flow_sequence_comments_value =
4707 module.getattr("flow_sequence_comments_value").unwrap();
4708 let flow_sequence_comments_value =
4709 extract_template(py, &flow_sequence_comments_value, "yaml_t/yaml_t_str").unwrap();
4710 let rendered =
4711 render_document(py, &parse_template(&flow_sequence_comments_value).unwrap())
4712 .unwrap();
4713 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4714 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4715 match value {
4716 YamlOwned::Sequence(sequence) => {
4717 assert_eq!(sequence.len(), 2);
4718 assert_eq!(yaml_integer(&sequence[0]), Some(1));
4719 assert_eq!(yaml_integer(&sequence[1]), Some(2));
4720 }
4721 _ => panic!("expected YAML sequence"),
4722 }
4723
4724 let flow_mapping_comments_value =
4725 module.getattr("flow_mapping_comments_value").unwrap();
4726 let flow_mapping_comments_value =
4727 extract_template(py, &flow_mapping_comments_value, "yaml_t/yaml_t_str").unwrap();
4728 let rendered =
4729 render_document(py, &parse_template(&flow_mapping_comments_value).unwrap())
4730 .unwrap();
4731 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4732 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4733 assert_integer_entry(value, "a", 1);
4734 assert_integer_entry(value, "b", 2);
4735
4736 let comment_after_value = module.getattr("comment_after_value").unwrap();
4737 let comment_after_value =
4738 extract_template(py, &comment_after_value, "yaml_t/yaml_t_str").unwrap();
4739 let rendered =
4740 render_document(py, &parse_template(&comment_after_value).unwrap()).unwrap();
4741 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4742 assert_string_entry(&documents[0], "value", "a");
4743
4744 let plain_colon_hash = module.getattr("plain_colon_hash").unwrap();
4745 let plain_colon_hash =
4746 extract_template(py, &plain_colon_hash, "yaml_t/yaml_t_str").unwrap();
4747 let rendered =
4748 render_document(py, &parse_template(&plain_colon_hash).unwrap()).unwrap();
4749 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4750 assert_string_entry(&documents[0], "value", "a:b#c");
4751
4752 let plain_colon_hash_deeper = module.getattr("plain_colon_hash_deeper").unwrap();
4753 let plain_colon_hash_deeper =
4754 extract_template(py, &plain_colon_hash_deeper, "yaml_t/yaml_t_str").unwrap();
4755 let rendered =
4756 render_document(py, &parse_template(&plain_colon_hash_deeper).unwrap()).unwrap();
4757 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4758 assert_string_entry(&documents[0], "value", "a:b:c#d");
4759
4760 let plain_hash_chain = module.getattr("plain_hash_chain").unwrap();
4761 let plain_hash_chain =
4762 extract_template(py, &plain_hash_chain, "yaml_t/yaml_t_str").unwrap();
4763 let rendered =
4764 render_document(py, &parse_template(&plain_hash_chain).unwrap()).unwrap();
4765 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4766 assert_string_entry(&documents[0], "value", "a#b#c");
4767
4768 let plain_hash_chain_deeper = module.getattr("plain_hash_chain_deeper").unwrap();
4769 let plain_hash_chain_deeper =
4770 extract_template(py, &plain_hash_chain_deeper, "yaml_t/yaml_t_str").unwrap();
4771 let rendered =
4772 render_document(py, &parse_template(&plain_hash_chain_deeper).unwrap()).unwrap();
4773 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4774 assert_string_entry(&documents[0], "value", "a#b#c#d");
4775
4776 let plain_hash_chain_deeper_comment =
4777 module.getattr("plain_hash_chain_deeper_comment").unwrap();
4778 let plain_hash_chain_deeper_comment =
4779 extract_template(py, &plain_hash_chain_deeper_comment, "yaml_t/yaml_t_str")
4780 .unwrap();
4781 let rendered = render_document(
4782 py,
4783 &parse_template(&plain_hash_chain_deeper_comment).unwrap(),
4784 )
4785 .unwrap();
4786 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4787 assert_string_entry(&documents[0], "value", "a#b#c#d");
4788
4789 let flow_hash_mapping_long = module.getattr("flow_hash_mapping_long").unwrap();
4790 let flow_hash_mapping_long =
4791 extract_template(py, &flow_hash_mapping_long, "yaml_t/yaml_t_str").unwrap();
4792 let rendered =
4793 render_document(py, &parse_template(&flow_hash_mapping_long).unwrap()).unwrap();
4794 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4795 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4796 assert_string_entry(value, "a", "b#c");
4797 assert_string_entry(value, "d", "e#f");
4798 assert_string_entry(value, "g", "h#i");
4799
4800 let flow_hash_mapping_four = module.getattr("flow_hash_mapping_four").unwrap();
4801 let flow_hash_mapping_four =
4802 extract_template(py, &flow_hash_mapping_four, "yaml_t/yaml_t_str").unwrap();
4803 let rendered =
4804 render_document(py, &parse_template(&flow_hash_mapping_four).unwrap()).unwrap();
4805 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4806 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4807 assert_string_entry(value, "a", "b#c");
4808 assert_string_entry(value, "d", "e#f");
4809 assert_string_entry(value, "g", "h#i");
4810 assert_string_entry(value, "j", "k#l");
4811
4812 let flow_hash_mapping_five = module.getattr("flow_hash_mapping_five").unwrap();
4813 let flow_hash_mapping_five =
4814 extract_template(py, &flow_hash_mapping_five, "yaml_t/yaml_t_str").unwrap();
4815 let rendered =
4816 render_document(py, &parse_template(&flow_hash_mapping_five).unwrap()).unwrap();
4817 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4818 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4819 assert_string_entry(value, "m", "n#o");
4820
4821 let flow_hash_mapping_six = module.getattr("flow_hash_mapping_six").unwrap();
4822 let flow_hash_mapping_six =
4823 extract_template(py, &flow_hash_mapping_six, "yaml_t/yaml_t_str").unwrap();
4824 let rendered =
4825 render_document(py, &parse_template(&flow_hash_mapping_six).unwrap()).unwrap();
4826 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4827 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4828 assert_string_entry(value, "p", "q#r");
4829
4830 let comment_after_plain_colon = module.getattr("comment_after_plain_colon").unwrap();
4831 let comment_after_plain_colon =
4832 extract_template(py, &comment_after_plain_colon, "yaml_t/yaml_t_str").unwrap();
4833 let rendered =
4834 render_document(py, &parse_template(&comment_after_plain_colon).unwrap()).unwrap();
4835 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4836 assert_string_entry(&documents[0], "value", "a:b");
4837
4838 let comment_after_flow_plain_colon =
4839 module.getattr("comment_after_flow_plain_colon").unwrap();
4840 let comment_after_flow_plain_colon =
4841 extract_template(py, &comment_after_flow_plain_colon, "yaml_t/yaml_t_str").unwrap();
4842 let rendered = render_document(
4843 py,
4844 &parse_template(&comment_after_flow_plain_colon).unwrap(),
4845 )
4846 .unwrap();
4847 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4848 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4849 match value {
4850 YamlOwned::Sequence(sequence) => {
4851 assert_eq!(sequence.len(), 1);
4852 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a:b"));
4853 }
4854 _ => panic!("expected YAML sequence"),
4855 }
4856
4857 let flow_plain_hash_chain = module.getattr("flow_plain_hash_chain").unwrap();
4858 let flow_plain_hash_chain =
4859 extract_template(py, &flow_plain_hash_chain, "yaml_t/yaml_t_str").unwrap();
4860 let rendered =
4861 render_document(py, &parse_template(&flow_plain_hash_chain).unwrap()).unwrap();
4862 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4863 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4864 match value {
4865 YamlOwned::Sequence(sequence) => {
4866 assert_eq!(sequence.len(), 2);
4867 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b#c"));
4868 assert_eq!(yaml_scalar_text(&sequence[1]), Some("d#e#f"));
4869 }
4870 _ => panic!("expected YAML sequence"),
4871 }
4872
4873 let flow_plain_hash_chain_single_deeper = module
4874 .getattr("flow_plain_hash_chain_single_deeper")
4875 .unwrap();
4876 let flow_plain_hash_chain_single_deeper = extract_template(
4877 py,
4878 &flow_plain_hash_chain_single_deeper,
4879 "yaml_t/yaml_t_str",
4880 )
4881 .unwrap();
4882 let rendered = render_document(
4883 py,
4884 &parse_template(&flow_plain_hash_chain_single_deeper).unwrap(),
4885 )
4886 .unwrap();
4887 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4888 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4889 match value {
4890 YamlOwned::Sequence(sequence) => {
4891 assert_eq!(sequence.len(), 1);
4892 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b#c#d"));
4893 }
4894 _ => panic!("expected YAML sequence"),
4895 }
4896
4897 let flow_plain_hash_chain_single_deeper_comment = module
4898 .getattr("flow_plain_hash_chain_single_deeper_comment")
4899 .unwrap();
4900 let flow_plain_hash_chain_single_deeper_comment = extract_template(
4901 py,
4902 &flow_plain_hash_chain_single_deeper_comment,
4903 "yaml_t/yaml_t_str",
4904 )
4905 .unwrap();
4906 let rendered = render_document(
4907 py,
4908 &parse_template(&flow_plain_hash_chain_single_deeper_comment).unwrap(),
4909 )
4910 .unwrap();
4911 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4912 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4913 match value {
4914 YamlOwned::Sequence(sequence) => {
4915 assert_eq!(sequence.len(), 1);
4916 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b#c#d"));
4917 }
4918 _ => panic!("expected YAML sequence"),
4919 }
4920
4921 let flow_hash_seq_six = module.getattr("flow_hash_seq_six").unwrap();
4922 let flow_hash_seq_six =
4923 extract_template(py, &flow_hash_seq_six, "yaml_t/yaml_t_str").unwrap();
4924 let rendered =
4925 render_document(py, &parse_template(&flow_hash_seq_six).unwrap()).unwrap();
4926 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4927 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4928 match value {
4929 YamlOwned::Sequence(sequence) => {
4930 assert_eq!(sequence.len(), 6);
4931 assert_eq!(yaml_scalar_text(&sequence[5]), Some("k#l"));
4932 }
4933 _ => panic!("expected YAML sequence"),
4934 }
4935
4936 let flow_hash_seq_seven = module.getattr("flow_hash_seq_seven").unwrap();
4937 let flow_hash_seq_seven =
4938 extract_template(py, &flow_hash_seq_seven, "yaml_t/yaml_t_str").unwrap();
4939 let rendered =
4940 render_document(py, &parse_template(&flow_hash_seq_seven).unwrap()).unwrap();
4941 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4942 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4943 match value {
4944 YamlOwned::Sequence(sequence) => {
4945 assert_eq!(sequence.len(), 7);
4946 assert_eq!(yaml_scalar_text(&sequence[6]), Some("m#n"));
4947 }
4948 _ => panic!("expected YAML sequence"),
4949 }
4950
4951 let flow_plain_hash_chain_four = module.getattr("flow_plain_hash_chain_four").unwrap();
4952 let flow_plain_hash_chain_four =
4953 extract_template(py, &flow_plain_hash_chain_four, "yaml_t/yaml_t_str").unwrap();
4954 let rendered =
4955 render_document(py, &parse_template(&flow_plain_hash_chain_four).unwrap()).unwrap();
4956 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4957 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4958 match value {
4959 YamlOwned::Sequence(sequence) => {
4960 assert_eq!(sequence.len(), 4);
4961 assert_eq!(yaml_scalar_text(&sequence[3]), Some("j#k#l"));
4962 }
4963 _ => panic!("expected YAML sequence"),
4964 }
4965
4966 let block_plain_comment_after_colon_deeper = module
4967 .getattr("block_plain_comment_after_colon_deeper")
4968 .unwrap();
4969 let block_plain_comment_after_colon_deeper = extract_template(
4970 py,
4971 &block_plain_comment_after_colon_deeper,
4972 "yaml_t/yaml_t_str",
4973 )
4974 .unwrap();
4975 let rendered = render_document(
4976 py,
4977 &parse_template(&block_plain_comment_after_colon_deeper).unwrap(),
4978 )
4979 .unwrap();
4980 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4981 assert_string_entry(&documents[0], "value", "a:b:c:d");
4982
4983 let flow_plain_comment_after_colon_deeper = module
4984 .getattr("flow_plain_comment_after_colon_deeper")
4985 .unwrap();
4986 let flow_plain_comment_after_colon_deeper = extract_template(
4987 py,
4988 &flow_plain_comment_after_colon_deeper,
4989 "yaml_t/yaml_t_str",
4990 )
4991 .unwrap();
4992 let rendered = render_document(
4993 py,
4994 &parse_template(&flow_plain_comment_after_colon_deeper).unwrap(),
4995 )
4996 .unwrap();
4997 let documents = parse_rendered_yaml(&rendered.text).unwrap();
4998 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4999 match value {
5000 YamlOwned::Sequence(sequence) => {
5001 assert_eq!(sequence.len(), 1);
5002 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a:b:c:d"));
5003 }
5004 _ => panic!("expected YAML sequence"),
5005 }
5006
5007 let flow_mapping_plain_key_question =
5008 module.getattr("flow_mapping_plain_key_question").unwrap();
5009 let flow_mapping_plain_key_question =
5010 extract_template(py, &flow_mapping_plain_key_question, "yaml_t/yaml_t_str")
5011 .unwrap();
5012 let rendered = render_document(
5013 py,
5014 &parse_template(&flow_mapping_plain_key_question).unwrap(),
5015 )
5016 .unwrap();
5017 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5018 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5019 let mapping = yaml_mapping(value).expect("expected YAML mapping");
5020 assert_eq!(mapping.len(), 1);
5021 let (_, entry_value) = mapping.iter().next().expect("expected YAML mapping entry");
5022 assert_eq!(yaml_integer(entry_value), Some(1));
5023
5024 let flow_mapping_plain_key_questions =
5025 module.getattr("flow_mapping_plain_key_questions").unwrap();
5026 let flow_mapping_plain_key_questions =
5027 extract_template(py, &flow_mapping_plain_key_questions, "yaml_t/yaml_t_str")
5028 .unwrap();
5029 let rendered = render_document(
5030 py,
5031 &parse_template(&flow_mapping_plain_key_questions).unwrap(),
5032 )
5033 .unwrap();
5034 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5035 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5036 let mapping = yaml_mapping(value).expect("expected YAML mapping");
5037 assert_eq!(mapping.len(), 2);
5038
5039 let mapping_empty_flow_values = module.getattr("mapping_empty_flow_values").unwrap();
5040 let mapping_empty_flow_values =
5041 extract_template(py, &mapping_empty_flow_values, "yaml_t/yaml_t_str").unwrap();
5042 let rendered =
5043 render_document(py, &parse_template(&mapping_empty_flow_values).unwrap()).unwrap();
5044 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5045 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5046 let mapping = yaml_mapping(value).expect("expected YAML mapping");
5047 let left = mapping
5048 .iter()
5049 .find_map(|(key, value)| (yaml_scalar_text(key) == Some("a")).then_some(value))
5050 .expect("expected key a");
5051 let right = mapping
5052 .iter()
5053 .find_map(|(key, value)| (yaml_scalar_text(key) == Some("b")).then_some(value))
5054 .expect("expected key b");
5055 assert_eq!(yaml_sequence_len(left), Some(0));
5056 assert!(yaml_mapping(right).is_some());
5057
5058 let flow_mapping_empty_key = module.getattr("flow_mapping_empty_key").unwrap();
5059 let flow_mapping_empty_key =
5060 extract_template(py, &flow_mapping_empty_key, "yaml_t/yaml_t_str").unwrap();
5061 let rendered =
5062 render_document(py, &parse_template(&flow_mapping_empty_key).unwrap()).unwrap();
5063 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5064 assert_integer_entry(&documents[0], "", 1);
5065
5066 let flow_mapping_empty_key_and_values =
5067 module.getattr("flow_mapping_empty_key_and_values").unwrap();
5068 let flow_mapping_empty_key_and_values =
5069 extract_template(py, &flow_mapping_empty_key_and_values, "yaml_t/yaml_t_str")
5070 .unwrap();
5071 let rendered = render_document(
5072 py,
5073 &parse_template(&flow_mapping_empty_key_and_values).unwrap(),
5074 )
5075 .unwrap();
5076 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5077 assert!(yaml_mapping_entry(&documents[0], "").is_some());
5078 let foo = yaml_mapping_entry(&documents[0], "foo").expect("expected foo");
5079 assert!(yaml_mapping(foo).is_some());
5080
5081 let flow_mapping_nested_empty = module.getattr("flow_mapping_nested_empty").unwrap();
5082 let flow_mapping_nested_empty =
5083 extract_template(py, &flow_mapping_nested_empty, "yaml_t/yaml_t_str").unwrap();
5084 let rendered =
5085 render_document(py, &parse_template(&flow_mapping_nested_empty).unwrap()).unwrap();
5086 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5087 let a = yaml_mapping_entry(&documents[0], "a").expect("expected key a");
5088 let b = yaml_mapping_entry(&documents[0], "b").expect("expected key b");
5089 assert!(yaml_mapping(a).is_some());
5090 assert_eq!(yaml_sequence_len(b), Some(0));
5091
5092 let flow_null_key = module.getattr("flow_null_key").unwrap();
5093 let flow_null_key = extract_template(py, &flow_null_key, "yaml_t/yaml_t_str").unwrap();
5094 let rendered = render_document(py, &parse_template(&flow_null_key).unwrap()).unwrap();
5095 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5096 let mapping = yaml_mapping(&documents[0]).expect("expected YAML mapping");
5097 assert!(
5098 mapping
5099 .iter()
5100 .any(|(key, value)| key.is_null() && yaml_integer(value) == Some(1))
5101 );
5102 assert!(mapping.iter().any(|(key, value)| {
5103 yaml_scalar_text(key) == Some("") && yaml_integer(value) == Some(2)
5104 }));
5105
5106 let block_null_key = module.getattr("block_null_key").unwrap();
5107 let block_null_key =
5108 extract_template(py, &block_null_key, "yaml_t/yaml_t_str").unwrap();
5109 let rendered = render_document(py, &parse_template(&block_null_key).unwrap()).unwrap();
5110 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5111 let mapping = yaml_mapping(&documents[0]).expect("expected YAML mapping");
5112 assert!(
5113 mapping
5114 .iter()
5115 .any(|(key, value)| key.is_null() && yaml_integer(value) == Some(1))
5116 );
5117
5118 let quoted_null_key = module.getattr("quoted_null_key").unwrap();
5119 let quoted_null_key =
5120 extract_template(py, "ed_null_key, "yaml_t/yaml_t_str").unwrap();
5121 let rendered = render_document(py, &parse_template("ed_null_key).unwrap()).unwrap();
5122 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5123 assert_integer_entry(&documents[0], "", 1);
5124
5125 let flow_sequence_nested_empty = module.getattr("flow_sequence_nested_empty").unwrap();
5126 let flow_sequence_nested_empty =
5127 extract_template(py, &flow_sequence_nested_empty, "yaml_t/yaml_t_str").unwrap();
5128 let rendered =
5129 render_document(py, &parse_template(&flow_sequence_nested_empty).unwrap()).unwrap();
5130 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5131 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
5132
5133 let plain_scalar_colon_no_space =
5134 module.getattr("plain_scalar_colon_no_space").unwrap();
5135 let plain_scalar_colon_no_space =
5136 extract_template(py, &plain_scalar_colon_no_space, "yaml_t/yaml_t_str").unwrap();
5137 let rendered =
5138 render_document(py, &parse_template(&plain_scalar_colon_no_space).unwrap())
5139 .unwrap();
5140 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5141 assert_string_entry(&documents[0], "value", "a:b");
5142
5143 let plain_question_mark_scalar = module.getattr("plain_question_mark_scalar").unwrap();
5144 let plain_question_mark_scalar =
5145 extract_template(py, &plain_question_mark_scalar, "yaml_t/yaml_t_str").unwrap();
5146 let rendered =
5147 render_document(py, &parse_template(&plain_question_mark_scalar).unwrap()).unwrap();
5148 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5149 assert_string_entry(&documents[0], "value", "?x");
5150
5151 let plain_colon_scalar_flow = module.getattr("plain_colon_scalar_flow").unwrap();
5152 let plain_colon_scalar_flow =
5153 extract_template(py, &plain_colon_scalar_flow, "yaml_t/yaml_t_str").unwrap();
5154 let rendered =
5155 render_document(py, &parse_template(&plain_colon_scalar_flow).unwrap()).unwrap();
5156 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5157 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5158 assert_eq!(yaml_sequence_len(value), Some(2));
5159
5160 let flow_mapping_colon_plain_key =
5161 module.getattr("flow_mapping_colon_plain_key").unwrap();
5162 let flow_mapping_colon_plain_key =
5163 extract_template(py, &flow_mapping_colon_plain_key, "yaml_t/yaml_t_str").unwrap();
5164 let rendered =
5165 render_document(py, &parse_template(&flow_mapping_colon_plain_key).unwrap())
5166 .unwrap();
5167 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5168 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5169 assert_string_entry(value, "a:b", "c");
5170
5171 let flow_mapping_colon_and_hash =
5172 module.getattr("flow_mapping_colon_and_hash").unwrap();
5173 let flow_mapping_colon_and_hash =
5174 extract_template(py, &flow_mapping_colon_and_hash, "yaml_t/yaml_t_str").unwrap();
5175 let rendered =
5176 render_document(py, &parse_template(&flow_mapping_colon_and_hash).unwrap())
5177 .unwrap();
5178 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5179 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5180 assert_string_entry(value, "a:b", "c#d");
5181
5182 let block_plain_colon_no_space = module.getattr("block_plain_colon_no_space").unwrap();
5183 let block_plain_colon_no_space =
5184 extract_template(py, &block_plain_colon_no_space, "yaml_t/yaml_t_str").unwrap();
5185 let rendered =
5186 render_document(py, &parse_template(&block_plain_colon_no_space).unwrap()).unwrap();
5187 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5188 assert_string_entry(&documents[0], "value", "a:b:c");
5189
5190 let flow_plain_colon_hash_deeper =
5191 module.getattr("flow_plain_colon_hash_deeper").unwrap();
5192 let flow_plain_colon_hash_deeper =
5193 extract_template(py, &flow_plain_colon_hash_deeper, "yaml_t/yaml_t_str").unwrap();
5194 let rendered =
5195 render_document(py, &parse_template(&flow_plain_colon_hash_deeper).unwrap())
5196 .unwrap();
5197 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5198 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5199 match value {
5200 YamlOwned::Sequence(sequence) => {
5201 assert_eq!(sequence.len(), 1);
5202 assert_eq!(yaml_scalar_text(&sequence[0]), Some("a:b:c#d"));
5203 }
5204 _ => panic!("expected YAML sequence"),
5205 }
5206
5207 let alias_in_flow_mapping_value =
5208 module.getattr("alias_in_flow_mapping_value").unwrap();
5209 let alias_in_flow_mapping_value =
5210 extract_template(py, &alias_in_flow_mapping_value, "yaml_t/yaml_t_str").unwrap();
5211 let rendered =
5212 render_document(py, &parse_template(&alias_in_flow_mapping_value).unwrap())
5213 .unwrap();
5214 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5215 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5216 let reference = yaml_mapping_entry(value, "ref").expect("expected ref");
5217 assert_integer_entry(reference, "x", 1);
5218
5219 let flow_null_and_alias = module.getattr("flow_null_and_alias").unwrap();
5220 let flow_null_and_alias =
5221 extract_template(py, &flow_null_and_alias, "yaml_t/yaml_t_str").unwrap();
5222 let rendered =
5223 render_document(py, &parse_template(&flow_null_and_alias).unwrap()).unwrap();
5224 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5225 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5226 let null_value = yaml_mapping(value)
5227 .and_then(|entries| {
5228 entries
5229 .iter()
5230 .find_map(|(key, value)| key.is_null().then_some(value))
5231 })
5232 .expect("expected null key");
5233 assert_integer_entry(null_value, "x", 1);
5234
5235 let alias_seq_value = module.getattr("alias_seq_value").unwrap();
5236 let alias_seq_value =
5237 extract_template(py, &alias_seq_value, "yaml_t/yaml_t_str").unwrap();
5238 let rendered = render_document(py, &parse_template(&alias_seq_value).unwrap()).unwrap();
5239 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5240 assert_eq!(
5241 yaml_sequence_len(yaml_mapping_entry(&documents[0], "a").expect("expected key a")),
5242 Some(2)
5243 );
5244 assert_eq!(
5245 yaml_sequence_len(yaml_mapping_entry(&documents[0], "b").expect("expected key b")),
5246 Some(2)
5247 );
5248
5249 let flow_mapping_missing_value = module.getattr("flow_mapping_missing_value").unwrap();
5250 let flow_mapping_missing_value =
5251 extract_template(py, &flow_mapping_missing_value, "yaml_t/yaml_t_str").unwrap();
5252 let rendered =
5253 render_document(py, &parse_template(&flow_mapping_missing_value).unwrap()).unwrap();
5254 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5255 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5256 let a = yaml_mapping_entry(value, "a").expect("expected key a");
5257 assert!(a.is_null());
5258
5259 let flow_seq_missing_value_before_end =
5260 module.getattr("flow_seq_missing_value_before_end").unwrap();
5261 let flow_seq_missing_value_before_end =
5262 extract_template(py, &flow_seq_missing_value_before_end, "yaml_t/yaml_t_str")
5263 .unwrap();
5264 let rendered = render_document(
5265 py,
5266 &parse_template(&flow_seq_missing_value_before_end).unwrap(),
5267 )
5268 .unwrap();
5269 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5270 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5271 assert_eq!(yaml_sequence_len(value), Some(2));
5272
5273 let flow_alias_map = module.getattr("flow_alias_map").unwrap();
5274 let flow_alias_map =
5275 extract_template(py, &flow_alias_map, "yaml_t/yaml_t_str").unwrap();
5276 let rendered = render_document(py, &parse_template(&flow_alias_map).unwrap()).unwrap();
5277 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5278 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5279 assert_integer_entry(value, "left", 1);
5280 assert_integer_entry(value, "right", 1);
5281
5282 let flow_alias_seq = module.getattr("flow_alias_seq").unwrap();
5283 let flow_alias_seq =
5284 extract_template(py, &flow_alias_seq, "yaml_t/yaml_t_str").unwrap();
5285 let rendered = render_document(py, &parse_template(&flow_alias_seq).unwrap()).unwrap();
5286 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5287 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5288 assert_eq!(yaml_sequence_len(value), Some(2));
5289
5290 let flow_merge = module.getattr("flow_merge").unwrap();
5291 let flow_merge = extract_template(py, &flow_merge, "yaml_t/yaml_t_str").unwrap();
5292 let rendered = render_document(py, &parse_template(&flow_merge).unwrap()).unwrap();
5293 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5294 assert!(rendered.text.contains("<<: &base"));
5295 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5296 assert_integer_entry(value, "b", 2);
5297
5298 let template = module.getattr("nested_flow_alias_merge").unwrap();
5299 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5300 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5301 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5302 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5303 let sequence = match value {
5304 YamlOwned::Sequence(sequence) => sequence,
5305 _ => panic!("expected YAML sequence"),
5306 };
5307 assert_eq!(sequence.len(), 2);
5308 assert_integer_entry(&sequence[0], "b", 2);
5309 assert_integer_entry(&sequence[1], "a", 1);
5310
5311 let explicit_seq = module.getattr("explicit_seq").unwrap();
5312 let explicit_seq = extract_template(py, &explicit_seq, "yaml_t/yaml_t_str").unwrap();
5313 let rendered = render_document(py, &parse_template(&explicit_seq).unwrap()).unwrap();
5314 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5315 let mapping = documents[0].as_mapping().expect("expected YAML mapping");
5316 let value = mapping
5317 .iter()
5318 .find_map(|(key, value)| (yaml_scalar_text(key) == Some("a")).then_some(value))
5319 .expect("expected key a");
5320 assert_eq!(yaml_sequence_len(value), Some(2));
5321
5322 let indented_block = module.getattr("indented_block").unwrap();
5323 let indented_block =
5324 extract_template(py, &indented_block, "yaml_t/yaml_t_str").unwrap();
5325 let rendered = render_document(py, &parse_template(&indented_block).unwrap()).unwrap();
5326 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5327 assert_string_entry(&documents[0], "value", "a\nb\n");
5328 assert_eq!(rendered.text, "value: |1\n a\n b\n");
5329 });
5330 }
5331
5332 #[test]
5333 fn rejects_parse_render_and_validation_contracts() {
5334 Python::with_gil(|py| {
5335 let module = PyModule::from_code(
5336 py,
5337 pyo3::ffi::c_str!(
5338 "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"
5339 ),
5340 pyo3::ffi::c_str!("test_yaml_error_contracts.py"),
5341 pyo3::ffi::c_str!("test_yaml_error_contracts"),
5342 )
5343 .unwrap();
5344
5345 for (name, expected) in [
5346 ("parse_open", "Expected"),
5347 ("parse_tab", "Tabs are not allowed"),
5348 ("parse_nested_tab", "Tabs are not allowed"),
5349 ("parse_trailing", "Unexpected trailing YAML content"),
5350 ("parse_empty_flow", "Expected a YAML value"),
5351 ("parse_trailing_entry", "Expected"),
5352 ("parse_empty_mapping", "Expected ':' in YAML template"),
5353 ("parse_missing_colon", "Expected ':' in YAML template"),
5354 ("parse_extra_comma", "Expected ':' in YAML template"),
5355 ] {
5356 let template = module.getattr(name).unwrap();
5357 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5358 let err = parse_template(&template).expect_err("expected YAML parse failure");
5359 assert_eq!(err.kind, ErrorKind::Parse);
5360 assert!(err.message.contains(expected), "{name}: {}", err.message);
5361 }
5362
5363 let unknown_anchor = module.getattr("unknown_anchor").unwrap();
5364 let unknown_anchor =
5365 extract_template(py, &unknown_anchor, "yaml_t/yaml_t_str").unwrap();
5366 let rendered = render_document(py, &parse_template(&unknown_anchor).unwrap()).unwrap();
5367 let err = parse_rendered_yaml(&rendered.text)
5368 .expect_err("expected YAML unknown-anchor parse failure");
5369 assert_eq!(err.kind, ErrorKind::Parse);
5370 assert!(err.message.contains("unknown anchor"));
5371
5372 let cross_doc_anchor = module.getattr("cross_doc_anchor").unwrap();
5373 let cross_doc_anchor =
5374 extract_template(py, &cross_doc_anchor, "yaml_t/yaml_t_str").unwrap();
5375 let rendered =
5376 render_document(py, &parse_template(&cross_doc_anchor).unwrap()).unwrap();
5377 let err = parse_rendered_yaml(&rendered.text)
5378 .expect_err("expected YAML cross-document anchor parse failure");
5379 assert_eq!(err.kind, ErrorKind::Parse);
5380 assert!(err.message.contains("unknown anchor"));
5381
5382 for (name, expected) in [
5383 ("fragment_template", "fragment"),
5384 ("metadata_template", "metadata"),
5385 ("float_template", "non-finite float"),
5386 ] {
5387 let template = module.getattr(name).unwrap();
5388 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5389 let document = parse_template(&template).unwrap();
5390 let err = match render_document(py, &document) {
5391 Ok(_) => panic!("expected YAML render failure"),
5392 Err(err) => err,
5393 };
5394 assert_eq!(err.kind, ErrorKind::Unrepresentable);
5395 assert!(err.message.contains(expected), "{name}: {}", err.message);
5396 }
5397 });
5398 }
5399
5400 #[test]
5401 fn renders_custom_tag_stream_and_complex_key_text() {
5402 Python::with_gil(|py| {
5403 let module = PyModule::from_code(
5404 py,
5405 pyo3::ffi::c_str!(
5406 "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"
5407 ),
5408 pyo3::ffi::c_str!("test_yaml_render_streams_and_keys.py"),
5409 pyo3::ffi::c_str!("test_yaml_render_streams_and_keys"),
5410 )
5411 .unwrap();
5412
5413 let comment_only_tail_stream = module.getattr("comment_only_tail_stream").unwrap();
5414 let comment_only_tail_stream =
5415 extract_template(py, &comment_only_tail_stream, "yaml_t/yaml_t_str").unwrap();
5416 let rendered =
5417 render_document(py, &parse_template(&comment_only_tail_stream).unwrap()).unwrap();
5418 assert_eq!(rendered.text, "---\na: 1\n---\nnull\n...");
5419 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5420 assert_eq!(documents.len(), 2);
5421 assert_integer_entry(&documents[0], "a", 1);
5422 assert!(documents[1].is_null());
5423
5424 let custom_tag_sequence = module.getattr("custom_tag_sequence").unwrap();
5425 let custom_tag_sequence =
5426 extract_template(py, &custom_tag_sequence, "yaml_t/yaml_t_str").unwrap();
5427 let rendered =
5428 render_document(py, &parse_template(&custom_tag_sequence).unwrap()).unwrap();
5429 assert_eq!(rendered.text, "value: !custom [ 1, 2 ]");
5430 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5431 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5432 assert_eq!(yaml_sequence_len(value), Some(2));
5433
5434 let flow_alias_map = module.getattr("flow_alias_map").unwrap();
5435 let flow_alias_map =
5436 extract_template(py, &flow_alias_map, "yaml_t/yaml_t_str").unwrap();
5437 let rendered = render_document(py, &parse_template(&flow_alias_map).unwrap()).unwrap();
5438 assert_eq!(rendered.text, "value: { left: &a 1, right: *a }");
5439 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5440 let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5441 assert_integer_entry(value, "left", 1);
5442 assert_integer_entry(value, "right", 1);
5443 });
5444 }
5445
5446 #[test]
5447 fn renders_custom_tag_scalar_mapping_and_root_sequence_shapes() {
5448 Python::with_gil(|py| {
5449 let module = PyModule::from_code(
5450 py,
5451 pyo3::ffi::c_str!(
5452 "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"
5453 ),
5454 pyo3::ffi::c_str!("test_yaml_custom_tag_shapes.py"),
5455 pyo3::ffi::c_str!("test_yaml_custom_tag_shapes"),
5456 )
5457 .unwrap();
5458
5459 let scalar_root = module.getattr("scalar_root").unwrap();
5460 let scalar_root = extract_template(py, &scalar_root, "yaml_t/yaml_t_str").unwrap();
5461 let rendered = render_document(py, &parse_template(&scalar_root).unwrap()).unwrap();
5462 assert_eq!(rendered.text, "!custom 3");
5463 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5464 assert_eq!(yaml_integer(&documents[0]), Some(3));
5465
5466 let mapping = module.getattr("mapping").unwrap();
5467 let mapping = extract_template(py, &mapping, "yaml_t/yaml_t_str").unwrap();
5468 let rendered = render_document(py, &parse_template(&mapping).unwrap()).unwrap();
5469 assert_eq!(rendered.text, "value: !custom 3");
5470 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5471 assert_integer_entry(&documents[0], "value", 3);
5472
5473 let sequence = module.getattr("sequence").unwrap();
5474 let sequence = extract_template(py, &sequence, "yaml_t/yaml_t_str").unwrap();
5475 let rendered = render_document(py, &parse_template(&sequence).unwrap()).unwrap();
5476 assert_eq!(rendered.text, "value: !custom [ 1, 2 ]");
5477 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5478 assert_eq!(
5479 yaml_sequence_len(yaml_mapping_entry(&documents[0], "value").expect("value key")),
5480 Some(2)
5481 );
5482
5483 let commented_root_sequence = module.getattr("commented_root_sequence").unwrap();
5484 let commented_root_sequence =
5485 extract_template(py, &commented_root_sequence, "yaml_t/yaml_t_str").unwrap();
5486 let rendered =
5487 render_document(py, &parse_template(&commented_root_sequence).unwrap()).unwrap();
5488 assert_eq!(rendered.text, "---\n!custom [ 1, 2 ]");
5489 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5490 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
5491 });
5492 }
5493
5494 #[test]
5495 fn renders_core_schema_scalars_and_top_level_sequence() {
5496 Python::with_gil(|py| {
5497 let module = PyModule::from_code(
5498 py,
5499 pyo3::ffi::c_str!(
5500 "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"
5501 ),
5502 pyo3::ffi::c_str!("test_yaml_core_scalars_and_sequence.py"),
5503 pyo3::ffi::c_str!("test_yaml_core_scalars_and_sequence"),
5504 )
5505 .unwrap();
5506
5507 let core_scalars = module.getattr("core_scalars").unwrap();
5508 let core_scalars = extract_template(py, &core_scalars, "yaml_t/yaml_t_str").unwrap();
5509 let rendered = render_document(py, &parse_template(&core_scalars).unwrap()).unwrap();
5510 assert_eq!(
5511 rendered.text,
5512 "value: true\nnone: null\nlegacy_bool: on\nlegacy_yes: yes"
5513 );
5514 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5515 assert_eq!(documents.len(), 1);
5516 assert_eq!(
5517 yaml_mapping_entry(&documents[0], "value").and_then(YamlOwned::as_bool),
5518 Some(true)
5519 );
5520 assert!(
5521 yaml_mapping_entry(&documents[0], "none")
5522 .expect("none key")
5523 .is_null()
5524 );
5525 assert_string_entry(&documents[0], "legacy_bool", "on");
5526 assert_string_entry(&documents[0], "legacy_yes", "yes");
5527
5528 let top_level_sequence = module.getattr("top_level_sequence").unwrap();
5529 let top_level_sequence =
5530 extract_template(py, &top_level_sequence, "yaml_t/yaml_t_str").unwrap();
5531 let rendered =
5532 render_document(py, &parse_template(&top_level_sequence).unwrap()).unwrap();
5533 assert_eq!(rendered.text, "- 1\n- true\n- null\n- on");
5534 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5535 assert_eq!(documents.len(), 1);
5536 let sequence = documents[0].as_vec().expect("expected top-level sequence");
5537 assert_eq!(sequence.len(), 4);
5538 assert_eq!(yaml_integer(&sequence[0]), Some(1));
5539 assert_eq!(sequence[1].as_bool(), Some(true));
5540 assert!(sequence[2].is_null());
5541 assert_eq!(yaml_scalar_text(&sequence[3]), Some("on"));
5542 });
5543 }
5544
5545 #[test]
5546 fn renders_end_to_end_supported_positions_text_and_data() {
5547 Python::with_gil(|py| {
5548 let module = PyModule::from_code(
5549 py,
5550 pyo3::ffi::c_str!(
5551 "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"
5552 ),
5553 pyo3::ffi::c_str!("test_yaml_end_to_end_positions.py"),
5554 pyo3::ffi::c_str!("test_yaml_end_to_end_positions"),
5555 )
5556 .unwrap();
5557
5558 let template = module.getattr("template").unwrap();
5559 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5560 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5561 assert_eq!(
5562 rendered.text,
5563 "\"owner\": \"Alice\"\nlabel: \"prefix-Alice\"\nplain: item-Alice\nitems:\n - &item \"Alice\"\n - *item\ntagged: !str \"Alice\"\nflow: [ \"Alice\", { label: \"Alice\" } ]"
5564 );
5565 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5566 assert_eq!(documents.len(), 1);
5567 assert_string_entry(&documents[0], "owner", "Alice");
5568 assert_string_entry(&documents[0], "label", "prefix-Alice");
5569 assert_string_entry(&documents[0], "plain", "item-Alice");
5570 assert_string_entry(&documents[0], "tagged", "Alice");
5571 let items = yaml_mapping_entry(&documents[0], "items").expect("items key");
5572 let items = items.as_vec().expect("items sequence");
5573 assert_eq!(items.len(), 2);
5574 assert_eq!(yaml_scalar_text(&items[0]), Some("Alice"));
5575 assert_eq!(yaml_scalar_text(&items[1]), Some("Alice"));
5576 let flow = yaml_mapping_entry(&documents[0], "flow").expect("flow key");
5577 let flow = flow.as_vec().expect("flow sequence");
5578 assert_eq!(yaml_scalar_text(&flow[0]), Some("Alice"));
5579 assert_string_entry(&flow[1], "label", "Alice");
5580 });
5581 }
5582
5583 #[test]
5584 fn renders_block_scalars_and_sequence_item_text_and_data() {
5585 Python::with_gil(|py| {
5586 let module = PyModule::from_code(
5587 py,
5588 pyo3::ffi::c_str!(
5589 "user='Alice'\ntemplate=t'''\nliteral: |\n hello {user}\n world\nfolded: >\n hello {user}\n world\nlines:\n - |\n item {user}\n'''\n"
5590 ),
5591 pyo3::ffi::c_str!("test_yaml_block_scalars_render.py"),
5592 pyo3::ffi::c_str!("test_yaml_block_scalars_render"),
5593 )
5594 .unwrap();
5595
5596 let template = module.getattr("template").unwrap();
5597 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5598 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5599 assert_eq!(
5600 rendered.text,
5601 "literal: |\n hello Alice\n world\nfolded: >\n hello Alice\n world\nlines:\n - |\n item Alice\n"
5602 );
5603 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5604 assert_eq!(documents.len(), 1);
5605 assert_string_entry(&documents[0], "literal", "hello Alice\nworld\n");
5606 assert_string_entry(&documents[0], "folded", "hello Alice world\n");
5607 let lines = yaml_mapping_entry(&documents[0], "lines").expect("lines key");
5608 let lines = lines.as_vec().expect("lines sequence");
5609 assert_eq!(yaml_scalar_text(&lines[0]), Some("item Alice\n"));
5610 });
5611 }
5612
5613 #[test]
5614 fn renders_comment_only_document_variants_and_mid_stream_shapes() {
5615 Python::with_gil(|py| {
5616 let module = PyModule::from_code(
5617 py,
5618 pyo3::ffi::c_str!(
5619 "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"
5620 ),
5621 pyo3::ffi::c_str!("test_yaml_comment_variants.py"),
5622 pyo3::ffi::c_str!("test_yaml_comment_variants"),
5623 )
5624 .unwrap();
5625
5626 for (name, expected_text) in [
5627 ("comment_only", "null"),
5628 ("comment_only_explicit", "---\nnull"),
5629 ("comment_only_explicit_end", "---\nnull\n..."),
5630 ] {
5631 let template = module.getattr(name).unwrap();
5632 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5633 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5634 assert_eq!(rendered.text, expected_text);
5635 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5636 assert_eq!(documents.len(), 1);
5637 assert!(documents[0].is_null());
5638 }
5639
5640 let comment_only_mid_stream = module.getattr("comment_only_mid_stream").unwrap();
5641 let comment_only_mid_stream =
5642 extract_template(py, &comment_only_mid_stream, "yaml_t/yaml_t_str").unwrap();
5643 let rendered =
5644 render_document(py, &parse_template(&comment_only_mid_stream).unwrap()).unwrap();
5645 assert_eq!(rendered.text, "---\na: 1\n---\nnull\n...\n---\nb: 2");
5646 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5647 assert_eq!(documents.len(), 3);
5648 assert_integer_entry(&documents[0], "a", 1);
5649 assert!(documents[1].is_null());
5650 assert_integer_entry(&documents[2], "b", 2);
5651 });
5652 }
5653
5654 #[test]
5655 fn renders_tag_directives_and_handle_tag_roots() {
5656 Python::with_gil(|py| {
5657 let module = PyModule::from_code(
5658 py,
5659 pyo3::ffi::c_str!(
5660 "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"
5661 ),
5662 pyo3::ffi::c_str!("test_yaml_tag_directives.py"),
5663 pyo3::ffi::c_str!("test_yaml_tag_directives"),
5664 )
5665 .unwrap();
5666
5667 let tag_directive_scalar = module.getattr("tag_directive_scalar").unwrap();
5668 let tag_directive_scalar =
5669 extract_template(py, &tag_directive_scalar, "yaml_t/yaml_t_str").unwrap();
5670 let rendered =
5671 render_document(py, &parse_template(&tag_directive_scalar).unwrap()).unwrap();
5672 assert_eq!(
5673 rendered.text,
5674 "%TAG !e! tag:example.com,2020:\n---\nvalue: !e!foo 1"
5675 );
5676 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5677 assert_integer_entry(&documents[0], "value", 1);
5678
5679 let tag_directive_root = module.getattr("tag_directive_root").unwrap();
5680 let tag_directive_root =
5681 extract_template(py, &tag_directive_root, "yaml_t/yaml_t_str").unwrap();
5682 let rendered =
5683 render_document(py, &parse_template(&tag_directive_root).unwrap()).unwrap();
5684 assert_eq!(
5685 rendered.text,
5686 "%YAML 1.2\n%TAG !e! tag:example.com,2020:\n---\n!e!root { value: !e!leaf 1 }"
5687 );
5688 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5689 assert_integer_entry(&documents[0], "value", 1);
5690
5691 let tag_directive_root_comment = module.getattr("tag_directive_root_comment").unwrap();
5692 let tag_directive_root_comment =
5693 extract_template(py, &tag_directive_root_comment, "yaml_t/yaml_t_str").unwrap();
5694 let rendered =
5695 render_document(py, &parse_template(&tag_directive_root_comment).unwrap()).unwrap();
5696 assert_eq!(
5697 rendered.text,
5698 "%YAML 1.2\n%TAG !e! tag:example.com,2020:\n---\n!e!root { value: !e!leaf 1 }"
5699 );
5700 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5701 assert_integer_entry(&documents[0], "value", 1);
5702
5703 let verbatim_root_mapping = module.getattr("verbatim_root_mapping").unwrap();
5704 let verbatim_root_mapping =
5705 extract_template(py, &verbatim_root_mapping, "yaml_t/yaml_t_str").unwrap();
5706 let rendered =
5707 render_document(py, &parse_template(&verbatim_root_mapping).unwrap()).unwrap();
5708 assert_eq!(rendered.text, "---\n!<tag:yaml.org,2002:map>\na: 1");
5709 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5710 assert_integer_entry(&documents[0], "a", 1);
5711
5712 let verbatim_root_sequence = module.getattr("verbatim_root_sequence").unwrap();
5713 let verbatim_root_sequence =
5714 extract_template(py, &verbatim_root_sequence, "yaml_t/yaml_t_str").unwrap();
5715 let rendered =
5716 render_document(py, &parse_template(&verbatim_root_sequence).unwrap()).unwrap();
5717 assert_eq!(rendered.text, "---\n!<tag:yaml.org,2002:seq>\n- 1\n- 2");
5718 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5719 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
5720 });
5721 }
5722
5723 #[test]
5724 fn renders_explicit_core_tag_root_shapes() {
5725 Python::with_gil(|py| {
5726 let module = PyModule::from_code(
5727 py,
5728 pyo3::ffi::c_str!(
5729 "root_bool=t'--- !!bool true\\n'\nroot_str=t'--- !!str true\\n'\nroot_int=t'--- !!int 3\\n'\n"
5730 ),
5731 pyo3::ffi::c_str!("test_yaml_core_tag_roots.py"),
5732 pyo3::ffi::c_str!("test_yaml_core_tag_roots"),
5733 )
5734 .unwrap();
5735
5736 for (name, expected_text) in [
5737 ("root_bool", "---\n!!bool true"),
5738 ("root_str", "---\n!!str true"),
5739 ("root_int", "---\n!!int 3"),
5740 ] {
5741 let template = module.getattr(name).unwrap();
5742 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5743 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5744 assert_eq!(rendered.text, expected_text);
5745 }
5746
5747 let root_bool = module.getattr("root_bool").unwrap();
5748 let root_bool = extract_template(py, &root_bool, "yaml_t/yaml_t_str").unwrap();
5749 let rendered = render_document(py, &parse_template(&root_bool).unwrap()).unwrap();
5750 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5751 assert_eq!(documents[0].as_bool(), Some(true));
5752
5753 let root_str = module.getattr("root_str").unwrap();
5754 let root_str = extract_template(py, &root_str, "yaml_t/yaml_t_str").unwrap();
5755 let rendered = render_document(py, &parse_template(&root_str).unwrap()).unwrap();
5756 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5757 assert_eq!(yaml_scalar_text(&documents[0]), Some("true"));
5758
5759 let root_int = module.getattr("root_int").unwrap();
5760 let root_int = extract_template(py, &root_int, "yaml_t/yaml_t_str").unwrap();
5761 let rendered = render_document(py, &parse_template(&root_int).unwrap()).unwrap();
5762 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5763 assert_eq!(yaml_integer(&documents[0]), Some(3));
5764 });
5765 }
5766
5767 #[test]
5768 fn renders_explicit_core_tag_mapping_and_root_text_families() {
5769 Python::with_gil(|py| {
5770 let module = PyModule::from_code(
5771 py,
5772 pyo3::ffi::c_str!(
5773 "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"
5774 ),
5775 pyo3::ffi::c_str!("test_yaml_explicit_core_tag_families.py"),
5776 pyo3::ffi::c_str!("test_yaml_explicit_core_tag_families"),
5777 )
5778 .unwrap();
5779
5780 let mapping = module.getattr("mapping").unwrap();
5781 let mapping = extract_template(py, &mapping, "yaml_t/yaml_t_str").unwrap();
5782 let rendered = render_document(py, &parse_template(&mapping).unwrap()).unwrap();
5783 assert_eq!(
5784 rendered.text,
5785 "value_bool: !!bool true\nvalue_str: !!str true\nvalue_float: !!float 1\nvalue_null: !!null null"
5786 );
5787 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5788 assert_eq!(
5789 yaml_mapping_entry(&documents[0], "value_bool").and_then(YamlOwned::as_bool),
5790 Some(true)
5791 );
5792 assert_eq!(
5793 yaml_scalar_text(
5794 yaml_mapping_entry(&documents[0], "value_str").expect("value_str")
5795 ),
5796 Some("true")
5797 );
5798 assert_eq!(
5799 yaml_mapping_entry(&documents[0], "value_float").and_then(yaml_float),
5800 Some(1.0)
5801 );
5802 assert!(
5803 yaml_mapping_entry(&documents[0], "value_null")
5804 .expect("value_null")
5805 .is_null()
5806 );
5807
5808 for (name, expected_text) in [
5809 ("root_int", "---\n!!int 3"),
5810 ("root_str", "---\n!!str true"),
5811 ("root_bool", "---\n!!bool true"),
5812 ] {
5813 let template = module.getattr(name).unwrap();
5814 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5815 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5816 assert_eq!(rendered.text, expected_text, "{name}");
5817 }
5818 });
5819 }
5820
5821 #[test]
5822 fn renders_flow_trailing_comma_explicit_key_and_indent_indicator_families() {
5823 Python::with_gil(|py| {
5824 let module = PyModule::from_code(
5825 py,
5826 pyo3::ffi::c_str!(
5827 "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"
5828 ),
5829 pyo3::ffi::c_str!("test_yaml_flow_indent_families.py"),
5830 pyo3::ffi::c_str!("test_yaml_flow_indent_families"),
5831 )
5832 .unwrap();
5833
5834 let flow_sequence = module.getattr("flow_sequence").unwrap();
5835 let flow_sequence = extract_template(py, &flow_sequence, "yaml_t/yaml_t_str").unwrap();
5836 let rendered = render_document(py, &parse_template(&flow_sequence).unwrap()).unwrap();
5837 assert_eq!(rendered.text, "[ 1, 2 ]");
5838 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5839 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
5840
5841 let flow_mapping = module.getattr("flow_mapping").unwrap();
5842 let flow_mapping = extract_template(py, &flow_mapping, "yaml_t/yaml_t_str").unwrap();
5843 let rendered = render_document(py, &parse_template(&flow_mapping).unwrap()).unwrap();
5844 assert_eq!(rendered.text, "{ a: 1 }");
5845 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5846 assert_integer_entry(&documents[0], "a", 1);
5847
5848 let explicit_key_sequence_value =
5849 module.getattr("explicit_key_sequence_value").unwrap();
5850 let explicit_key_sequence_value =
5851 extract_template(py, &explicit_key_sequence_value, "yaml_t/yaml_t_str").unwrap();
5852 let rendered =
5853 render_document(py, &parse_template(&explicit_key_sequence_value).unwrap())
5854 .unwrap();
5855 assert_eq!(rendered.text, "? a\n:\n - 1\n - 2");
5856 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5857 let entry = documents[0].as_mapping().expect("mapping");
5858 assert_eq!(entry.len(), 1);
5859
5860 let indent_indicator = module.getattr("indent_indicator").unwrap();
5861 let indent_indicator =
5862 extract_template(py, &indent_indicator, "yaml_t/yaml_t_str").unwrap();
5863 let rendered =
5864 render_document(py, &parse_template(&indent_indicator).unwrap()).unwrap();
5865 assert_eq!(rendered.text, "value: |1\n a\n b\n");
5866 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5867 assert_string_entry(&documents[0], "value", "a\nb\n");
5868 });
5869 }
5870
5871 #[test]
5872 fn renders_flow_alias_and_merge_shapes() {
5873 Python::with_gil(|py| {
5874 let module = PyModule::from_code(
5875 py,
5876 pyo3::ffi::c_str!(
5877 "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"
5878 ),
5879 pyo3::ffi::c_str!("test_yaml_flow_alias_merge.py"),
5880 pyo3::ffi::c_str!("test_yaml_flow_alias_merge"),
5881 )
5882 .unwrap();
5883
5884 let flow_alias_map = module.getattr("flow_alias_map").unwrap();
5885 let flow_alias_map =
5886 extract_template(py, &flow_alias_map, "yaml_t/yaml_t_str").unwrap();
5887 let rendered = render_document(py, &parse_template(&flow_alias_map).unwrap()).unwrap();
5888 assert_eq!(rendered.text, "value: { left: &a 1, right: *a }");
5889 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5890 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
5891 assert_integer_entry(value, "left", 1);
5892 assert_integer_entry(value, "right", 1);
5893
5894 let flow_alias_seq = module.getattr("flow_alias_seq").unwrap();
5895 let flow_alias_seq =
5896 extract_template(py, &flow_alias_seq, "yaml_t/yaml_t_str").unwrap();
5897 let rendered = render_document(py, &parse_template(&flow_alias_seq).unwrap()).unwrap();
5898 assert_eq!(rendered.text, "value: [ &a 1, *a ]");
5899 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5900 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
5901 let value = value.as_vec().expect("value seq");
5902 assert_eq!(yaml_integer(&value[0]), Some(1));
5903 assert_eq!(yaml_integer(&value[1]), Some(1));
5904
5905 let flow_merge = module.getattr("flow_merge").unwrap();
5906 let flow_merge = extract_template(py, &flow_merge, "yaml_t/yaml_t_str").unwrap();
5907 let rendered = render_document(py, &parse_template(&flow_merge).unwrap()).unwrap();
5908 assert_eq!(rendered.text, "value: { <<: &base { a: 1 }, b: 2 }");
5909 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5910 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
5911 assert_integer_entry(value, "b", 2);
5912 assert!(
5913 yaml_mapping_entry(value, "a").is_some()
5914 || yaml_mapping_entry(value, "<<").is_some()
5915 );
5916 });
5917 }
5918
5919 #[test]
5920 fn renders_document_stream_and_root_decorator_shapes() {
5921 Python::with_gil(|py| {
5922 let module = PyModule::from_code(
5923 py,
5924 pyo3::ffi::c_str!(
5925 "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"
5926 ),
5927 pyo3::ffi::c_str!("test_yaml_stream_root_shapes.py"),
5928 pyo3::ffi::c_str!("test_yaml_stream_root_shapes"),
5929 )
5930 .unwrap();
5931
5932 let comment_only_explicit_end_document = module
5933 .getattr("comment_only_explicit_end_document")
5934 .unwrap();
5935 let comment_only_explicit_end_document =
5936 extract_template(py, &comment_only_explicit_end_document, "yaml_t/yaml_t_str")
5937 .unwrap();
5938 let rendered = render_document(
5939 py,
5940 &parse_template(&comment_only_explicit_end_document).unwrap(),
5941 )
5942 .unwrap();
5943 assert_eq!(rendered.text, "---\nnull\n...");
5944 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5945 assert!(matches!(documents[0], YamlOwned::Value(_)));
5946
5947 let comment_only_explicit_end_stream =
5948 module.getattr("comment_only_explicit_end_stream").unwrap();
5949 let comment_only_explicit_end_stream =
5950 extract_template(py, &comment_only_explicit_end_stream, "yaml_t/yaml_t_str")
5951 .unwrap();
5952 let rendered = render_document(
5953 py,
5954 &parse_template(&comment_only_explicit_end_stream).unwrap(),
5955 )
5956 .unwrap();
5957 assert_eq!(rendered.text, "---\nnull\n...\n---\na: 1");
5958 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5959 assert!(matches!(documents[0], YamlOwned::Value(_)));
5960 assert_integer_entry(&documents[1], "a", 1);
5961
5962 let comment_only_mid_stream = module.getattr("comment_only_mid_stream").unwrap();
5963 let comment_only_mid_stream =
5964 extract_template(py, &comment_only_mid_stream, "yaml_t/yaml_t_str").unwrap();
5965 let rendered =
5966 render_document(py, &parse_template(&comment_only_mid_stream).unwrap()).unwrap();
5967 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5968 assert_integer_entry(&documents[0], "a", 1);
5969 assert!(matches!(documents[1], YamlOwned::Value(_)));
5970 assert_integer_entry(&documents[2], "b", 2);
5971
5972 let explicit_end_comment_stream =
5973 module.getattr("explicit_end_comment_stream").unwrap();
5974 let explicit_end_comment_stream =
5975 extract_template(py, &explicit_end_comment_stream, "yaml_t/yaml_t_str").unwrap();
5976 let rendered =
5977 render_document(py, &parse_template(&explicit_end_comment_stream).unwrap())
5978 .unwrap();
5979 assert_eq!(rendered.text, "---\na: 1\n...\n---\nb: 2");
5980 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5981 assert_integer_entry(&documents[0], "a", 1);
5982 assert_integer_entry(&documents[1], "b", 2);
5983
5984 let doc_start_comment = module.getattr("doc_start_comment").unwrap();
5985 let doc_start_comment =
5986 extract_template(py, &doc_start_comment, "yaml_t/yaml_t_str").unwrap();
5987 let rendered =
5988 render_document(py, &parse_template(&doc_start_comment).unwrap()).unwrap();
5989 assert_eq!(rendered.text, "---\nvalue: 1");
5990 let documents = parse_rendered_yaml(&rendered.text).unwrap();
5991 assert_integer_entry(&documents[0], "value", 1);
5992
5993 let doc_start_tag_comment = module.getattr("doc_start_tag_comment").unwrap();
5994 let doc_start_tag_comment =
5995 extract_template(py, &doc_start_tag_comment, "yaml_t/yaml_t_str").unwrap();
5996 let rendered =
5997 render_document(py, &parse_template(&doc_start_tag_comment).unwrap()).unwrap();
5998 assert_eq!(rendered.text, "---\n!!str true");
5999 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6000 assert_eq!(yaml_scalar_text(&documents[0]), Some("true"));
6001
6002 let tagged_block_root_mapping = module.getattr("tagged_block_root_mapping").unwrap();
6003 let tagged_block_root_mapping =
6004 extract_template(py, &tagged_block_root_mapping, "yaml_t/yaml_t_str").unwrap();
6005 let rendered =
6006 render_document(py, &parse_template(&tagged_block_root_mapping).unwrap()).unwrap();
6007 assert_eq!(rendered.text, "---\n!!map\na: 1");
6008 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6009 assert_integer_entry(&documents[0], "a", 1);
6010
6011 let tagged_block_root_sequence = module.getattr("tagged_block_root_sequence").unwrap();
6012 let tagged_block_root_sequence =
6013 extract_template(py, &tagged_block_root_sequence, "yaml_t/yaml_t_str").unwrap();
6014 let rendered =
6015 render_document(py, &parse_template(&tagged_block_root_sequence).unwrap()).unwrap();
6016 assert_eq!(rendered.text, "---\n!!seq\n- 1\n- 2");
6017 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6018 assert_eq!(documents[0].as_vec().expect("root seq").len(), 2);
6019
6020 let root_anchor_sequence = module.getattr("root_anchor_sequence").unwrap();
6021 let root_anchor_sequence =
6022 extract_template(py, &root_anchor_sequence, "yaml_t/yaml_t_str").unwrap();
6023 let rendered =
6024 render_document(py, &parse_template(&root_anchor_sequence).unwrap()).unwrap();
6025 assert_eq!(rendered.text, "---\n&root\n- 1\n- 2");
6026 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6027 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
6028
6029 let root_anchor_custom_mapping = module.getattr("root_anchor_custom_mapping").unwrap();
6030 let root_anchor_custom_mapping =
6031 extract_template(py, &root_anchor_custom_mapping, "yaml_t/yaml_t_str").unwrap();
6032 let rendered =
6033 render_document(py, &parse_template(&root_anchor_custom_mapping).unwrap()).unwrap();
6034 assert_eq!(rendered.text, "---\n!custom &root\na: 1");
6035 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6036 assert_integer_entry(&documents[0], "a", 1);
6037
6038 let root_custom_anchor_sequence =
6039 module.getattr("root_custom_anchor_sequence").unwrap();
6040 let root_custom_anchor_sequence =
6041 extract_template(py, &root_custom_anchor_sequence, "yaml_t/yaml_t_str").unwrap();
6042 let rendered =
6043 render_document(py, &parse_template(&root_custom_anchor_sequence).unwrap())
6044 .unwrap();
6045 assert_eq!(rendered.text, "---\n!custom &root\n- 1\n- 2");
6046 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6047 assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
6048
6049 let flow_newline = module.getattr("flow_newline").unwrap();
6050 let flow_newline = extract_template(py, &flow_newline, "yaml_t/yaml_t_str").unwrap();
6051 let rendered = render_document(py, &parse_template(&flow_newline).unwrap()).unwrap();
6052 assert_eq!(rendered.text, "{ a: 1, b: [ 2, 3 ] }");
6053 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6054 let mapping = yaml_mapping(&documents[0]).expect("expected root flow mapping");
6055 assert_eq!(mapping.len(), 2);
6056 });
6057 }
6058
6059 #[test]
6060 fn renders_merge_and_collection_shape_families() {
6061 Python::with_gil(|py| {
6062 let module = PyModule::from_code(
6063 py,
6064 pyo3::ffi::c_str!(
6065 "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"
6066 ),
6067 pyo3::ffi::c_str!("test_yaml_merge_collection_shapes.py"),
6068 pyo3::ffi::c_str!("test_yaml_merge_collection_shapes"),
6069 )
6070 .unwrap();
6071
6072 let merge = module.getattr("merge").unwrap();
6073 let merge = extract_template(py, &merge, "yaml_t/yaml_t_str").unwrap();
6074 let rendered = render_document(py, &parse_template(&merge).unwrap()).unwrap();
6075 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6076 let derived = yaml_mapping_entry(&documents[0], "derived").expect("derived");
6077 assert_integer_entry(derived, "c", 3);
6078 assert!(
6079 yaml_mapping_entry(derived, "a").is_some()
6080 || yaml_mapping_entry(derived, "<<").is_some()
6081 );
6082 assert!(
6083 yaml_mapping_entry(derived, "b").is_some()
6084 || yaml_mapping_entry(derived, "<<").is_some()
6085 );
6086
6087 let flow_nested_alias_merge = module.getattr("flow_nested_alias_merge").unwrap();
6088 let flow_nested_alias_merge =
6089 extract_template(py, &flow_nested_alias_merge, "yaml_t/yaml_t_str").unwrap();
6090 let rendered =
6091 render_document(py, &parse_template(&flow_nested_alias_merge).unwrap()).unwrap();
6092 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6093 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6094 let value = value.as_vec().expect("value seq");
6095 assert_eq!(value.len(), 2);
6096
6097 let alias_seq_value = module.getattr("alias_seq_value").unwrap();
6098 let alias_seq_value =
6099 extract_template(py, &alias_seq_value, "yaml_t/yaml_t_str").unwrap();
6100 let rendered = render_document(py, &parse_template(&alias_seq_value).unwrap()).unwrap();
6101 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6102 assert_eq!(
6103 yaml_sequence_len(yaml_mapping_entry(&documents[0], "a").expect("a")),
6104 Some(2)
6105 );
6106 assert_eq!(
6107 yaml_sequence_len(yaml_mapping_entry(&documents[0], "b").expect("b")),
6108 Some(2)
6109 );
6110
6111 for (name, expected_len) in [
6112 ("empty_flow_sequence", 0usize),
6113 ("flow_seq_missing_value_before_end", 2usize),
6114 ] {
6115 let template = module.getattr(name).unwrap();
6116 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6117 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6118 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6119 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6120 assert_eq!(yaml_sequence_len(value), Some(expected_len), "{name}");
6121 }
6122
6123 let empty_flow_mapping = module.getattr("empty_flow_mapping").unwrap();
6124 let empty_flow_mapping =
6125 extract_template(py, &empty_flow_mapping, "yaml_t/yaml_t_str").unwrap();
6126 let rendered =
6127 render_document(py, &parse_template(&empty_flow_mapping).unwrap()).unwrap();
6128 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6129 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6130 assert_eq!(yaml_mapping(value).expect("mapping").len(), 0);
6131
6132 let flow_mapping_missing_value = module.getattr("flow_mapping_missing_value").unwrap();
6133 let flow_mapping_missing_value =
6134 extract_template(py, &flow_mapping_missing_value, "yaml_t/yaml_t_str").unwrap();
6135 let rendered =
6136 render_document(py, &parse_template(&flow_mapping_missing_value).unwrap()).unwrap();
6137 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6138 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6139 assert!(yaml_mapping_entry(value, "a").is_some());
6140
6141 let indentless_sequence_value = module.getattr("indentless_sequence_value").unwrap();
6142 let indentless_sequence_value =
6143 extract_template(py, &indentless_sequence_value, "yaml_t/yaml_t_str").unwrap();
6144 let rendered =
6145 render_document(py, &parse_template(&indentless_sequence_value).unwrap()).unwrap();
6146 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6147 assert_eq!(
6148 yaml_sequence_len(yaml_mapping_entry(&documents[0], "a").expect("a")),
6149 Some(2)
6150 );
6151
6152 let sequence_of_mappings = module.getattr("sequence_of_mappings").unwrap();
6153 let sequence_of_mappings =
6154 extract_template(py, &sequence_of_mappings, "yaml_t/yaml_t_str").unwrap();
6155 let rendered =
6156 render_document(py, &parse_template(&sequence_of_mappings).unwrap()).unwrap();
6157 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6158 assert_eq!(documents[0].as_vec().expect("top-level seq").len(), 2);
6159
6160 let mapping_of_sequence_of_mappings =
6161 module.getattr("mapping_of_sequence_of_mappings").unwrap();
6162 let mapping_of_sequence_of_mappings =
6163 extract_template(py, &mapping_of_sequence_of_mappings, "yaml_t/yaml_t_str")
6164 .unwrap();
6165 let rendered = render_document(
6166 py,
6167 &parse_template(&mapping_of_sequence_of_mappings).unwrap(),
6168 )
6169 .unwrap();
6170 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6171 assert_eq!(
6172 yaml_sequence_len(yaml_mapping_entry(&documents[0], "items").expect("items")),
6173 Some(2)
6174 );
6175
6176 let sequence_of_sequences = module.getattr("sequence_of_sequences").unwrap();
6177 let sequence_of_sequences =
6178 extract_template(py, &sequence_of_sequences, "yaml_t/yaml_t_str").unwrap();
6179 let rendered =
6180 render_document(py, &parse_template(&sequence_of_sequences).unwrap()).unwrap();
6181 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6182 let top = documents[0].as_vec().expect("top-level seq");
6183 assert_eq!(top.len(), 2);
6184 assert_eq!(yaml_sequence_len(&top[0]), Some(2));
6185 assert_eq!(yaml_sequence_len(&top[1]), Some(1));
6186 });
6187 }
6188
6189 #[test]
6190 fn renders_flow_scalar_key_and_comment_edge_families() {
6191 Python::with_gil(|py| {
6192 let module = PyModule::from_code(
6193 py,
6194 pyo3::ffi::c_str!(
6195 "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"
6196 ),
6197 pyo3::ffi::c_str!("test_yaml_flow_scalar_edge_families.py"),
6198 pyo3::ffi::c_str!("test_yaml_flow_scalar_edge_families"),
6199 )
6200 .unwrap();
6201
6202 let flow_plain_scalar_with_space =
6203 module.getattr("flow_plain_scalar_with_space").unwrap();
6204 let flow_plain_scalar_with_space =
6205 extract_template(py, &flow_plain_scalar_with_space, "yaml_t/yaml_t_str").unwrap();
6206 let rendered =
6207 render_document(py, &parse_template(&flow_plain_scalar_with_space).unwrap())
6208 .unwrap();
6209 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6210 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6211 let value = value.as_vec().expect("value seq");
6212 assert_eq!(yaml_scalar_text(&value[0]), Some("1 2"));
6213
6214 let mapping_empty_flow_values = module.getattr("mapping_empty_flow_values").unwrap();
6215 let mapping_empty_flow_values =
6216 extract_template(py, &mapping_empty_flow_values, "yaml_t/yaml_t_str").unwrap();
6217 let rendered =
6218 render_document(py, &parse_template(&mapping_empty_flow_values).unwrap()).unwrap();
6219 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6220 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6221 assert_eq!(
6222 yaml_sequence_len(yaml_mapping_entry(value, "a").expect("a")),
6223 Some(0)
6224 );
6225 assert_eq!(
6226 yaml_mapping(yaml_mapping_entry(value, "b").expect("b"))
6227 .expect("b mapping")
6228 .len(),
6229 0
6230 );
6231
6232 let flow_mapping_empty_key_and_values =
6233 module.getattr("flow_mapping_empty_key_and_values").unwrap();
6234 let flow_mapping_empty_key_and_values =
6235 extract_template(py, &flow_mapping_empty_key_and_values, "yaml_t/yaml_t_str")
6236 .unwrap();
6237 let rendered = render_document(
6238 py,
6239 &parse_template(&flow_mapping_empty_key_and_values).unwrap(),
6240 )
6241 .unwrap();
6242 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6243 assert_eq!(
6244 yaml_sequence_len(yaml_mapping_entry(&documents[0], "").expect("empty key")),
6245 Some(0)
6246 );
6247 assert_eq!(
6248 yaml_mapping(yaml_mapping_entry(&documents[0], "foo").expect("foo"))
6249 .expect("foo mapping")
6250 .len(),
6251 0
6252 );
6253
6254 let flow_null_key = module.getattr("flow_null_key").unwrap();
6255 let flow_null_key = extract_template(py, &flow_null_key, "yaml_t/yaml_t_str").unwrap();
6256 let rendered = render_document(py, &parse_template(&flow_null_key).unwrap()).unwrap();
6257 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6258 assert_eq!(documents[0].as_mapping().expect("mapping").len(), 2);
6259
6260 let block_null_key = module.getattr("block_null_key").unwrap();
6261 let block_null_key =
6262 extract_template(py, &block_null_key, "yaml_t/yaml_t_str").unwrap();
6263 let rendered = render_document(py, &parse_template(&block_null_key).unwrap()).unwrap();
6264 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6265 assert_eq!(documents[0].as_mapping().expect("mapping").len(), 1);
6266
6267 let quoted_null_key = module.getattr("quoted_null_key").unwrap();
6268 let quoted_null_key =
6269 extract_template(py, "ed_null_key, "yaml_t/yaml_t_str").unwrap();
6270 let rendered = render_document(py, &parse_template("ed_null_key).unwrap()).unwrap();
6271 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6272 assert_eq!(documents[0].as_mapping().expect("mapping").len(), 1);
6273
6274 let plain_question_mark_scalar = module.getattr("plain_question_mark_scalar").unwrap();
6275 let plain_question_mark_scalar =
6276 extract_template(py, &plain_question_mark_scalar, "yaml_t/yaml_t_str").unwrap();
6277 let rendered =
6278 render_document(py, &parse_template(&plain_question_mark_scalar).unwrap()).unwrap();
6279 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6280 assert_string_entry(&documents[0], "value", "?x");
6281
6282 let plain_colon_scalar_flow = module.getattr("plain_colon_scalar_flow").unwrap();
6283 let plain_colon_scalar_flow =
6284 extract_template(py, &plain_colon_scalar_flow, "yaml_t/yaml_t_str").unwrap();
6285 let rendered =
6286 render_document(py, &parse_template(&plain_colon_scalar_flow).unwrap()).unwrap();
6287 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6288 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6289 let value = value.as_vec().expect("value seq");
6290 assert_eq!(yaml_scalar_text(&value[0]), Some("a:b"));
6291 assert_eq!(yaml_scalar_text(&value[1]), Some("c:d"));
6292
6293 let flow_mapping_plain_key_questions =
6294 module.getattr("flow_mapping_plain_key_questions").unwrap();
6295 let flow_mapping_plain_key_questions =
6296 extract_template(py, &flow_mapping_plain_key_questions, "yaml_t/yaml_t_str")
6297 .unwrap();
6298 let rendered = render_document(
6299 py,
6300 &parse_template(&flow_mapping_plain_key_questions).unwrap(),
6301 )
6302 .unwrap();
6303 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6304 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6305 assert_eq!(yaml_mapping(value).expect("mapping").len(), 2);
6306
6307 let flow_hash_mapping_four = module.getattr("flow_hash_mapping_four").unwrap();
6308 let flow_hash_mapping_four =
6309 extract_template(py, &flow_hash_mapping_four, "yaml_t/yaml_t_str").unwrap();
6310 let rendered =
6311 render_document(py, &parse_template(&flow_hash_mapping_four).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!(yaml_mapping(value).expect("mapping").len(), 4);
6315
6316 let flow_hash_seq_seven = module.getattr("flow_hash_seq_seven").unwrap();
6317 let flow_hash_seq_seven =
6318 extract_template(py, &flow_hash_seq_seven, "yaml_t/yaml_t_str").unwrap();
6319 let rendered =
6320 render_document(py, &parse_template(&flow_hash_seq_seven).unwrap()).unwrap();
6321 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6322 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6323 assert_eq!(yaml_sequence_len(value), Some(7));
6324
6325 let comment_after_flow_plain_colon =
6326 module.getattr("comment_after_flow_plain_colon").unwrap();
6327 let comment_after_flow_plain_colon =
6328 extract_template(py, &comment_after_flow_plain_colon, "yaml_t/yaml_t_str").unwrap();
6329 let rendered = render_document(
6330 py,
6331 &parse_template(&comment_after_flow_plain_colon).unwrap(),
6332 )
6333 .unwrap();
6334 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6335 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6336 let value = value.as_vec().expect("value seq");
6337 assert_eq!(yaml_scalar_text(&value[0]), Some("a:b"));
6338
6339 let flow_plain_comment_after_colon_deeper = module
6340 .getattr("flow_plain_comment_after_colon_deeper")
6341 .unwrap();
6342 let flow_plain_comment_after_colon_deeper = extract_template(
6343 py,
6344 &flow_plain_comment_after_colon_deeper,
6345 "yaml_t/yaml_t_str",
6346 )
6347 .unwrap();
6348 let rendered = render_document(
6349 py,
6350 &parse_template(&flow_plain_comment_after_colon_deeper).unwrap(),
6351 )
6352 .unwrap();
6353 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6354 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6355 let value = value.as_vec().expect("value seq");
6356 assert_eq!(yaml_scalar_text(&value[0]), Some("a:b:c:d"));
6357 });
6358 }
6359
6360 #[test]
6361 fn renders_flow_collection_comment_and_verbatim_tag_families() {
6362 Python::with_gil(|py| {
6363 let module = PyModule::from_code(
6364 py,
6365 pyo3::ffi::c_str!(
6366 "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"
6367 ),
6368 pyo3::ffi::c_str!("test_yaml_flow_collection_comment_and_tag_families.py"),
6369 pyo3::ffi::c_str!("test_yaml_flow_collection_comment_and_tag_families"),
6370 )
6371 .unwrap();
6372
6373 let verbatim_tag = module.getattr("verbatim_tag").unwrap();
6374 let verbatim_tag = extract_template(py, &verbatim_tag, "yaml_t/yaml_t_str").unwrap();
6375 let rendered = render_document(py, &parse_template(&verbatim_tag).unwrap()).unwrap();
6376 assert_eq!(rendered.text, "value: !<tag:yaml.org,2002:str> hello");
6377 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6378 assert_string_entry(&documents[0], "value", "hello");
6379
6380 for (name, expected_text) in [
6381 ("flow_wrapped_sequence", "key: [ a, b ]"),
6382 ("flow_sequence_comment", "key: [ a, b ]"),
6383 ] {
6384 let template = module.getattr(name).unwrap();
6385 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6386 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6387 assert_eq!(rendered.text, expected_text, "{name}");
6388 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6389 let value = yaml_mapping_entry(&documents[0], "key").expect("sequence key");
6390 assert_eq!(yaml_sequence_len(value), Some(2), "{name}");
6391 }
6392
6393 for (name, expected_text) in [
6394 ("flow_wrapped_mapping", "key: { a: 1, b: 2 }"),
6395 ("flow_mapping_comment", "key: { a: 1, b: 2 }"),
6396 ] {
6397 let template = module.getattr(name).unwrap();
6398 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6399 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6400 assert_eq!(rendered.text, expected_text, "{name}");
6401 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6402 let value = yaml_mapping_entry(&documents[0], "key").expect("mapping key");
6403 assert_eq!(yaml_mapping(value).expect("mapping").len(), 2, "{name}");
6404 }
6405
6406 let alias_in_flow_mapping_value =
6407 module.getattr("alias_in_flow_mapping_value").unwrap();
6408 let alias_in_flow_mapping_value =
6409 extract_template(py, &alias_in_flow_mapping_value, "yaml_t/yaml_t_str").unwrap();
6410 let rendered =
6411 render_document(py, &parse_template(&alias_in_flow_mapping_value).unwrap())
6412 .unwrap();
6413 assert_eq!(rendered.text, "base: &a { x: 1 }\nvalue: { ref: *a }");
6414 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6415 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6416 let referenced = yaml_mapping_entry(value, "ref").expect("ref key");
6417 assert_integer_entry(referenced, "x", 1);
6418
6419 let flow_null_and_alias = module.getattr("flow_null_and_alias").unwrap();
6420 let flow_null_and_alias =
6421 extract_template(py, &flow_null_and_alias, "yaml_t/yaml_t_str").unwrap();
6422 let rendered =
6423 render_document(py, &parse_template(&flow_null_and_alias).unwrap()).unwrap();
6424 assert_eq!(rendered.text, "base: &a { x: 1 }\nvalue: { null: *a }");
6425 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6426 let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6427 assert_eq!(yaml_mapping(value).expect("mapping").len(), 1);
6428 });
6429 }
6430
6431 #[test]
6432 fn renders_verbatim_root_scalar_text_and_data() {
6433 Python::with_gil(|py| {
6434 let module = PyModule::from_code(
6435 py,
6436 pyo3::ffi::c_str!("template=t'--- !<tag:yaml.org,2002:str> hello\\n'\n"),
6437 pyo3::ffi::c_str!("test_yaml_verbatim_root_scalar.py"),
6438 pyo3::ffi::c_str!("test_yaml_verbatim_root_scalar"),
6439 )
6440 .unwrap();
6441
6442 let template = module.getattr("template").unwrap();
6443 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6444 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6445 assert_eq!(rendered.text, "---\n!<tag:yaml.org,2002:str> hello");
6446 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6447 assert_eq!(yaml_scalar_text(&documents[0]), Some("hello"));
6448 });
6449 }
6450
6451 #[test]
6452 fn renders_verbatim_root_anchor_scalar_text_and_data() {
6453 Python::with_gil(|py| {
6454 let module = PyModule::from_code(
6455 py,
6456 pyo3::ffi::c_str!("template=t'--- !<tag:yaml.org,2002:str> &root hello\\n'\n"),
6457 pyo3::ffi::c_str!("test_yaml_verbatim_root_anchor_scalar.py"),
6458 pyo3::ffi::c_str!("test_yaml_verbatim_root_anchor_scalar"),
6459 )
6460 .unwrap();
6461
6462 let template = module.getattr("template").unwrap();
6463 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6464 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6465 assert_eq!(rendered.text, "---\n!<tag:yaml.org,2002:str> &root hello");
6466 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6467 assert_eq!(yaml_scalar_text(&documents[0]), Some("hello"));
6468 });
6469 }
6470
6471 #[test]
6472 fn renders_spec_chapter_2_examples_text_and_data() {
6473 Python::with_gil(|py| {
6474 let module = PyModule::from_code(
6475 py,
6476 pyo3::ffi::c_str!(
6477 "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"
6478 ),
6479 pyo3::ffi::c_str!("test_yaml_spec_chapter_2_examples.py"),
6480 pyo3::ffi::c_str!("test_yaml_spec_chapter_2_examples"),
6481 )
6482 .unwrap();
6483
6484 for (name, expected_text) in [
6485 ("players", "- Mark McGwire\n- Sammy Sosa\n- Ken Griffey"),
6486 (
6487 "clubs",
6488 "american:\n - Boston Red Sox\n - Detroit Tigers\n - New York Yankees\nnational:\n - New York Mets\n - Chicago Cubs\n - Atlanta Braves",
6489 ),
6490 (
6491 "stats_seq",
6492 "-\n name: Mark McGwire\n hr: 65\n avg: 0.278\n-\n name: Sammy Sosa\n hr: 63\n avg: 0.288",
6493 ),
6494 (
6495 "map_of_maps",
6496 "Mark McGwire: { hr: 65, avg: 0.278 }\nSammy Sosa: { hr: 63, avg: 0.288 }",
6497 ),
6498 (
6499 "two_docs",
6500 "---\n- Mark McGwire\n- Sammy Sosa\n- Ken Griffey\n---\n- Chicago Cubs\n- St Louis Cardinals",
6501 ),
6502 (
6503 "play_feed",
6504 "---\ntime: 20:03:20\nplayer: Sammy Sosa\naction: strike (miss)\n...\n---\ntime: 20:03:47\nplayer: Sammy Sosa\naction: grand slam\n...",
6505 ),
6506 ] {
6507 let template = module.getattr(name).unwrap();
6508 let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6509 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6510 assert_eq!(rendered.text, expected_text, "{name}");
6511 let documents = parse_rendered_yaml(&rendered.text).unwrap();
6512 match name {
6513 "players" => assert_eq!(yaml_sequence_len(&documents[0]), Some(3)),
6514 "clubs" => assert_eq!(yaml_mapping(&documents[0]).expect("clubs").len(), 2),
6515 "stats_seq" => assert_eq!(yaml_sequence_len(&documents[0]), Some(2)),
6516 "map_of_maps" => {
6517 assert_eq!(yaml_mapping(&documents[0]).expect("map_of_maps").len(), 2)
6518 }
6519 "two_docs" => assert_eq!(documents.len(), 2),
6520 "play_feed" => assert_eq!(documents.len(), 2),
6521 _ => unreachable!(),
6522 }
6523 }
6524 });
6525 }
6526
6527 #[test]
6528 fn test_parse_rendered_yaml_surfaces_parse_failures() {
6529 let err =
6530 parse_rendered_yaml("value: *missing\n").expect_err("expected YAML parse failure");
6531 assert_eq!(err.kind, ErrorKind::Parse);
6532 assert!(err.message.contains("Rendered YAML could not be reparsed"));
6533 }
6534}