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