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