1use tstring_syntax::{
2 BackendError, BackendResult, NormalizedDate, NormalizedDocument, NormalizedEntry,
3 NormalizedFloat, NormalizedKey, NormalizedLocalDateTime, NormalizedOffsetDateTime,
4 NormalizedStream, NormalizedTemporal, NormalizedTime, NormalizedValue, SourcePosition,
5 SourceSpan, StreamItem, TemplateInput,
6};
7
8#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
9pub enum TomlProfile {
10 V1_0,
11 V1_1,
12}
13
14impl TomlProfile {
15 #[must_use]
16 pub const fn as_str(self) -> &'static str {
17 match self {
18 Self::V1_0 => "1.0",
19 Self::V1_1 => "1.1",
20 }
21 }
22
23 #[must_use]
24 const fn allows_missing_seconds(self) -> bool {
25 matches!(self, Self::V1_1)
26 }
27
28 #[must_use]
29 const fn allows_inline_table_newlines(self) -> bool {
30 matches!(self, Self::V1_1)
31 }
32
33 #[must_use]
34 const fn allows_extended_basic_string_escapes(self) -> bool {
35 matches!(self, Self::V1_1)
36 }
37}
38
39impl Default for TomlProfile {
40 fn default() -> Self {
41 Self::V1_1
42 }
43}
44
45impl std::str::FromStr for TomlProfile {
46 type Err = String;
47
48 fn from_str(value: &str) -> Result<Self, Self::Err> {
49 match value {
50 "1.0" => Ok(Self::V1_0),
51 "1.1" => Ok(Self::V1_1),
52 other => Err(format!(
53 "Unsupported TOML profile {other:?}. Supported profiles: \"1.0\", \"1.1\"."
54 )),
55 }
56 }
57}
58
59#[derive(Clone, Debug)]
60pub struct TomlInterpolationNode {
61 pub span: SourceSpan,
62 pub interpolation_index: usize,
63 pub role: String,
64}
65
66#[derive(Clone, Debug)]
67pub struct TomlStringChunkNode {
68 pub span: SourceSpan,
69 pub value: String,
70}
71
72#[derive(Clone, Debug)]
73pub enum TomlStringPart {
74 Chunk(TomlStringChunkNode),
75 Interpolation(TomlInterpolationNode),
76}
77
78#[derive(Clone, Debug)]
79pub struct TomlStringNode {
80 pub span: SourceSpan,
81 pub style: String,
82 pub chunks: Vec<TomlStringPart>,
83}
84
85#[derive(Clone, Debug)]
86pub struct TomlLiteralNode {
87 pub span: SourceSpan,
88 pub source: String,
89 pub value: toml::Value,
90}
91
92#[derive(Clone, Debug)]
93pub enum TomlKeySegmentValue {
94 Bare(String),
95 String(TomlStringNode),
96 Interpolation(TomlInterpolationNode),
97}
98
99#[derive(Clone, Debug)]
100pub struct TomlKeySegmentNode {
101 pub span: SourceSpan,
102 pub value: TomlKeySegmentValue,
103 pub bare: bool,
104}
105
106#[derive(Clone, Debug)]
107pub struct TomlKeyPathNode {
108 pub span: SourceSpan,
109 pub segments: Vec<TomlKeySegmentNode>,
110}
111
112#[derive(Clone, Debug)]
113pub struct TomlAssignmentNode {
114 pub span: SourceSpan,
115 pub key_path: TomlKeyPathNode,
116 pub value: TomlValueNode,
117}
118
119#[derive(Clone, Debug)]
120pub struct TomlTableHeaderNode {
121 pub span: SourceSpan,
122 pub key_path: TomlKeyPathNode,
123}
124
125#[derive(Clone, Debug)]
126pub struct TomlArrayTableHeaderNode {
127 pub span: SourceSpan,
128 pub key_path: TomlKeyPathNode,
129}
130
131#[derive(Clone, Debug)]
132pub struct TomlArrayNode {
133 pub span: SourceSpan,
134 pub items: Vec<TomlValueNode>,
135}
136
137#[derive(Clone, Debug)]
138pub struct TomlInlineTableNode {
139 pub span: SourceSpan,
140 pub entries: Vec<TomlAssignmentNode>,
141}
142
143#[derive(Clone, Debug)]
144pub struct TomlDocumentNode {
145 pub span: SourceSpan,
146 pub statements: Vec<TomlStatementNode>,
147}
148
149#[derive(Clone, Debug)]
150pub enum TomlValueNode {
151 String(TomlStringNode),
152 Literal(TomlLiteralNode),
153 Interpolation(TomlInterpolationNode),
154 Array(TomlArrayNode),
155 InlineTable(TomlInlineTableNode),
156}
157
158#[derive(Clone, Debug)]
159pub enum TomlStatementNode {
160 Assignment(TomlAssignmentNode),
161 TableHeader(TomlTableHeaderNode),
162 ArrayTableHeader(TomlArrayTableHeaderNode),
163}
164
165pub struct TomlParser {
166 items: Vec<StreamItem>,
167 index: usize,
168 inline_table_depth: usize,
169 profile: TomlProfile,
170 literal_materialization: LiteralMaterialization,
171}
172
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174enum LiteralMaterialization {
175 SharedHelper,
176 Direct,
177}
178
179impl TomlParser {
180 #[must_use]
181 pub fn new(template: &TemplateInput, profile: TomlProfile) -> Self {
182 Self::new_with_materialization(template, profile, LiteralMaterialization::SharedHelper)
183 }
184
185 #[must_use]
186 fn new_with_materialization(
187 template: &TemplateInput,
188 profile: TomlProfile,
189 literal_materialization: LiteralMaterialization,
190 ) -> Self {
191 Self {
192 items: template.flatten(),
193 index: 0,
194 inline_table_depth: 0,
195 profile,
196 literal_materialization,
197 }
198 }
199
200 pub fn parse(&mut self) -> BackendResult<TomlDocumentNode> {
201 let start = self.mark();
202 let mut statements = Vec::new();
203 self.skip_document_junk();
204 while self.current_kind() != "eof" {
205 if self.starts_with("[[") {
206 statements.push(TomlStatementNode::ArrayTableHeader(
207 self.parse_array_table_header()?,
208 ));
209 } else if self.current_char() == Some('[') {
210 statements.push(TomlStatementNode::TableHeader(self.parse_table_header()?));
211 } else {
212 statements.push(TomlStatementNode::Assignment(self.parse_assignment()?));
213 }
214 self.skip_line_suffix()?;
215 self.skip_document_junk();
216 }
217 Ok(TomlDocumentNode {
218 span: self.span_from(start),
219 statements,
220 })
221 }
222
223 fn current(&self) -> &StreamItem {
224 &self.items[self.index]
225 }
226
227 fn current_kind(&self) -> &'static str {
228 self.current().kind()
229 }
230
231 fn current_char(&self) -> Option<char> {
232 self.current().char()
233 }
234
235 fn mark(&self) -> SourcePosition {
236 self.current().span().start.clone()
237 }
238
239 fn previous_end(&self) -> SourcePosition {
240 if self.index == 0 {
241 return self.current().span().start.clone();
242 }
243 self.items[self.index - 1].span().end.clone()
244 }
245
246 fn span_from(&self, start: SourcePosition) -> SourceSpan {
247 SourceSpan::between(start, self.previous_end())
248 }
249
250 fn error(&self, message: impl Into<String>) -> BackendError {
251 BackendError::parse_at("toml.parse", message, Some(self.current().span().clone()))
252 }
253
254 fn advance(&mut self) {
255 if self.current_kind() != "eof" {
256 self.index += 1;
257 }
258 }
259
260 fn starts_with(&self, text: &str) -> bool {
261 let mut probe = self.index;
262 for expected in text.chars() {
263 match &self.items[probe] {
264 StreamItem::Char { ch, .. } if *ch == expected => probe += 1,
265 _ => return false,
266 }
267 }
268 true
269 }
270
271 fn skip_horizontal_space(&mut self) {
272 while matches!(self.current_char(), Some(' ' | '\t')) {
273 self.advance();
274 }
275 }
276
277 fn skip_document_junk(&mut self) {
278 loop {
279 self.skip_horizontal_space();
280 match self.current_char() {
281 Some('#') => self.skip_comment(),
282 Some('\n') => self.advance(),
283 Some('\r') if self.peek_char(1) == Some('\n') => {
284 self.advance();
285 self.advance();
286 }
287 _ => return,
288 }
289 }
290 }
291
292 fn skip_comment(&mut self) {
293 while !matches!(self.current_char(), None | Some('\n')) {
294 if self
295 .current_char()
296 .is_some_and(|ch| is_disallowed_toml_control(ch, false))
297 {
298 break;
299 }
300 self.advance();
301 }
302 }
303
304 fn skip_line_suffix(&mut self) -> BackendResult<()> {
305 self.skip_horizontal_space();
306 if self.current_char() == Some('#') {
307 self.skip_comment();
308 }
309 if self.current_char() == Some('\n') {
310 self.advance();
311 return Ok(());
312 }
313 if self.current_char() == Some('\r') && self.peek_char(1) == Some('\n') {
314 self.advance();
315 self.advance();
316 return Ok(());
317 }
318 if self.current_kind() != "eof" {
319 return Err(self.error("Unexpected trailing TOML content on the line."));
320 }
321 Ok(())
322 }
323
324 fn consume_char(&mut self, expected: char) -> BackendResult<()> {
325 if self.current_char() != Some(expected) {
326 return Err(self.error(format!("Expected {expected:?} in TOML template.")));
327 }
328 self.advance();
329 Ok(())
330 }
331
332 fn parse_assignment(&mut self) -> BackendResult<TomlAssignmentNode> {
333 let start = self.mark();
334 let key_path = self.parse_key_path(false)?;
335 self.skip_horizontal_space();
336 self.consume_char('=')?;
337 let value = self.parse_value("line")?;
338 Ok(TomlAssignmentNode {
339 span: self.span_from(start),
340 key_path,
341 value,
342 })
343 }
344
345 fn parse_table_header(&mut self) -> BackendResult<TomlTableHeaderNode> {
346 let start = self.mark();
347 self.consume_char('[')?;
348 let key_path = self.parse_key_path(true)?;
349 self.consume_char(']')?;
350 Ok(TomlTableHeaderNode {
351 span: self.span_from(start),
352 key_path,
353 })
354 }
355
356 fn parse_array_table_header(&mut self) -> BackendResult<TomlArrayTableHeaderNode> {
357 let start = self.mark();
358 self.consume_char('[')?;
359 self.consume_char('[')?;
360 let key_path = self.parse_key_path(true)?;
361 self.consume_char(']')?;
362 self.consume_char(']')?;
363 Ok(TomlArrayTableHeaderNode {
364 span: self.span_from(start),
365 key_path,
366 })
367 }
368
369 fn parse_key_path(&mut self, header: bool) -> BackendResult<TomlKeyPathNode> {
370 let start = self.mark();
371 let mut segments = vec![self.parse_key_segment()?];
372 loop {
373 self.skip_horizontal_space();
374 if self.current_char() != Some('.') {
375 break;
376 }
377 self.advance();
378 self.skip_horizontal_space();
379 segments.push(self.parse_key_segment()?);
380 }
381 if header {
382 self.skip_horizontal_space();
383 }
384 Ok(TomlKeyPathNode {
385 span: self.span_from(start),
386 segments,
387 })
388 }
389
390 fn parse_key_segment(&mut self) -> BackendResult<TomlKeySegmentNode> {
391 self.skip_horizontal_space();
392 let start = self.mark();
393 if self.current_kind() == "interpolation" {
394 return Ok(TomlKeySegmentNode {
395 span: self.span_from(start),
396 value: TomlKeySegmentValue::Interpolation(self.consume_interpolation("key")?),
397 bare: false,
398 });
399 }
400 if self.starts_with("\"\"\"") {
401 return Err(self.error("TOML v1.0 quoted keys cannot be multiline strings."));
402 }
403 if self.starts_with("'''") {
404 return Err(self.error("TOML v1.0 quoted keys cannot be multiline strings."));
405 }
406 if self.current_char() == Some('"') {
407 return Ok(TomlKeySegmentNode {
408 span: self.span_from(start),
409 value: TomlKeySegmentValue::String(self.parse_string("basic")?),
410 bare: false,
411 });
412 }
413 if self.current_char() == Some('\'') {
414 return Ok(TomlKeySegmentNode {
415 span: self.span_from(start),
416 value: TomlKeySegmentValue::String(self.parse_string("literal")?),
417 bare: false,
418 });
419 }
420 let bare = self.collect_bare_key();
421 if bare.is_empty() {
422 return Err(self.error("Expected a TOML key segment."));
423 }
424 Ok(TomlKeySegmentNode {
425 span: self.span_from(start),
426 value: TomlKeySegmentValue::Bare(bare),
427 bare: true,
428 })
429 }
430
431 fn collect_bare_key(&mut self) -> String {
432 let mut value = String::new();
433 while matches!(
434 self.current_char(),
435 Some('A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-')
436 ) {
437 value.push(self.current_char().unwrap_or_default());
438 self.advance();
439 }
440 value
441 }
442
443 fn parse_value(&mut self, context: &str) -> BackendResult<TomlValueNode> {
444 self.skip_value_space(context);
445 if self.current_kind() == "interpolation" {
446 let interpolation = self.consume_interpolation("value")?;
447 if self.starts_value_terminator(context) {
448 return Ok(TomlValueNode::Interpolation(interpolation));
449 }
450 return Err(self.error("Whole-value TOML interpolations cannot have bare suffix text."));
451 }
452 if self.current_char() == Some('[') {
453 return Ok(TomlValueNode::Array(self.parse_array()?));
454 }
455 if self.current_char() == Some('{') {
456 return Ok(TomlValueNode::InlineTable(self.parse_inline_table()?));
457 }
458 if self.starts_with("\"\"\"") {
459 return Ok(TomlValueNode::String(self.parse_string("multiline_basic")?));
460 }
461 if self.starts_with("'''") {
462 return Ok(TomlValueNode::String(
463 self.parse_string("multiline_literal")?,
464 ));
465 }
466 if self.current_char() == Some('"') {
467 return Ok(TomlValueNode::String(self.parse_string("basic")?));
468 }
469 if self.current_char() == Some('\'') {
470 return Ok(TomlValueNode::String(self.parse_string("literal")?));
471 }
472 Ok(TomlValueNode::Literal(self.parse_literal(context)?))
473 }
474
475 fn skip_value_space(&mut self, context: &str) {
476 loop {
477 while matches!(self.current_char(), Some(' ' | '\t')) {
478 self.advance();
479 }
480 if self.current_char() == Some('#') {
481 if self.inline_table_depth > 0
482 && context != "array"
483 && !self.profile.allows_inline_table_newlines()
484 {
485 return;
486 }
487 self.skip_comment();
488 continue;
489 }
490 if self.current_char() == Some('\n') {
491 if self.inline_table_depth > 0
492 && context != "array"
493 && !self.profile.allows_inline_table_newlines()
494 {
495 return;
496 }
497 if context == "line" {
498 return;
499 }
500 self.advance();
501 continue;
502 }
503 if self.current_char() == Some('\r') && self.peek_char(1) == Some('\n') {
504 if context == "line"
505 || self.inline_table_depth > 0
506 && context != "array"
507 && !self.profile.allows_inline_table_newlines()
508 {
509 return;
510 }
511 self.advance();
512 self.advance();
513 continue;
514 }
515 if self.inline_table_depth > 0
516 && context != "array"
517 && !self.profile.allows_inline_table_newlines()
518 {
519 return;
520 }
521 return;
522 }
523 }
524
525 fn starts_value_terminator(&self, context: &str) -> bool {
526 let mut probe = self.index;
527 while matches!(self.items[probe].char(), Some(' ' | '\t' | '\r')) {
528 probe += 1;
529 }
530 let item = &self.items[probe];
531 if self.inline_table_depth > 0 {
532 return match context {
533 "array" => matches!(
534 item,
535 StreamItem::Eof { .. }
536 | StreamItem::Char {
537 ch: ',' | ']' | '#',
538 ..
539 }
540 ),
541 _ if self.profile.allows_inline_table_newlines() => matches!(
542 item,
543 StreamItem::Eof { .. } | StreamItem::Char { ch: ',' | '}', .. }
544 ),
545 _ => matches!(
546 item,
547 StreamItem::Eof { .. }
548 | StreamItem::Char {
549 ch: ',' | '}' | '#' | '\n' | '\r',
550 ..
551 }
552 ),
553 };
554 }
555 if context == "line" {
556 matches!(
557 item,
558 StreamItem::Eof { .. } | StreamItem::Char { ch: '#' | '\n', .. }
559 )
560 } else {
561 matches!(
562 item,
563 StreamItem::Eof { .. }
564 | StreamItem::Char {
565 ch: ',' | ']' | '}' | '#' | '\n',
566 ..
567 }
568 )
569 }
570 }
571
572 fn parse_array(&mut self) -> BackendResult<TomlArrayNode> {
573 let start = self.mark();
574 self.consume_char('[')?;
575 let mut items = Vec::new();
576 self.skip_value_space("array");
577 if self.current_char() == Some(']') {
578 self.advance();
579 return Ok(TomlArrayNode {
580 span: self.span_from(start),
581 items,
582 });
583 }
584 loop {
585 items.push(self.parse_value("array")?);
586 self.skip_value_space("array");
587 if self.current_char() == Some(']') {
588 self.advance();
589 break;
590 }
591 self.consume_char(',')?;
592 self.skip_value_space("array");
593 if self.current_char() == Some(']') {
594 self.advance();
595 break;
596 }
597 }
598 Ok(TomlArrayNode {
599 span: self.span_from(start),
600 items,
601 })
602 }
603
604 fn parse_inline_table(&mut self) -> BackendResult<TomlInlineTableNode> {
605 let start = self.mark();
606 self.consume_char('{')?;
607 self.inline_table_depth += 1;
608 let mut entries = Vec::new();
609 self.skip_value_space("inline");
610 if self.current_char() == Some('}') {
611 self.advance();
612 self.inline_table_depth -= 1;
613 return Ok(TomlInlineTableNode {
614 span: self.span_from(start),
615 entries,
616 });
617 }
618 loop {
619 let entry_start = self.mark();
620 let key_path = self.parse_key_path(false)?;
621 self.skip_horizontal_space();
622 self.consume_char('=')?;
623 let value = self.parse_value("inline")?;
624 entries.push(TomlAssignmentNode {
625 span: self.span_from(entry_start),
626 key_path,
627 value,
628 });
629 self.skip_value_space("inline");
630 if self.current_char() == Some('}') {
631 self.advance();
632 break;
633 }
634 self.consume_char(',')?;
635 self.skip_value_space("inline");
636 if self.current_char() == Some('}') {
637 if !self.profile.allows_inline_table_newlines() {
638 return Err(
639 self.error("Trailing commas are not permitted in TOML 1.0 inline tables.")
640 );
641 }
642 self.advance();
643 break;
644 }
645 }
646 self.inline_table_depth -= 1;
647 Ok(TomlInlineTableNode {
648 span: self.span_from(start),
649 entries,
650 })
651 }
652
653 fn parse_string(&mut self, style: &str) -> BackendResult<TomlStringNode> {
654 let start = self.mark();
655 match style {
656 "basic" => {
657 self.consume_char('"')?;
658 self.parse_basic_like_string(start, style, false)
659 }
660 "multiline_basic" => {
661 self.consume_char('"')?;
662 self.consume_char('"')?;
663 self.consume_char('"')?;
664 self.consume_multiline_opening_newline();
665 self.parse_basic_like_string(start, style, true)
666 }
667 "literal" => {
668 self.consume_char('\'')?;
669 self.parse_literal_like_string(start, style, false)
670 }
671 _ => {
672 self.consume_char('\'')?;
673 self.consume_char('\'')?;
674 self.consume_char('\'')?;
675 self.consume_multiline_opening_newline();
676 self.parse_literal_like_string(start, style, true)
677 }
678 }
679 }
680
681 fn consume_multiline_opening_newline(&mut self) {
682 if self.current_char() == Some('\r') {
683 self.advance();
684 }
685 if self.current_char() == Some('\n') {
686 self.advance();
687 }
688 }
689
690 fn parse_basic_like_string(
691 &mut self,
692 start: SourcePosition,
693 style: &str,
694 multiline: bool,
695 ) -> BackendResult<TomlStringNode> {
696 let mut chunks = Vec::new();
697 let mut buffer = String::new();
698 loop {
699 if multiline && self.starts_with("\"\"\"") {
700 let quote_run = self.count_consecutive_chars('"');
701 if quote_run == 4 || quote_run == 5 {
702 for _ in 0..(quote_run - 3) {
703 buffer.push('"');
704 self.advance();
705 }
706 continue;
707 }
708 self.flush_buffer(&mut buffer, &mut chunks);
709 self.consume_char('"')?;
710 self.consume_char('"')?;
711 self.consume_char('"')?;
712 break;
713 }
714 if !multiline && self.current_char() == Some('"') {
715 self.flush_buffer(&mut buffer, &mut chunks);
716 self.advance();
717 break;
718 }
719 if !multiline && matches!(self.current_char(), Some('\r' | '\n')) {
720 return Err(self.error("TOML single-line basic strings cannot contain newlines."));
721 }
722 if self.current_kind() == "eof" {
723 return Err(self.error("Unterminated TOML basic string."));
724 }
725 if self.current_kind() == "interpolation" {
726 self.flush_buffer(&mut buffer, &mut chunks);
727 chunks.push(TomlStringPart::Interpolation(
728 self.consume_interpolation("string_fragment")?,
729 ));
730 continue;
731 }
732 if multiline && self.current_char() == Some('\\') && self.starts_multiline_escape() {
733 self.consume_multiline_escape();
734 continue;
735 }
736 if self.current_char() == Some('\\') {
737 buffer.push(self.parse_basic_escape()?);
738 continue;
739 }
740 if multiline && self.current_char() == Some('\r') && self.peek_char(1) == Some('\n') {
741 buffer.push('\n');
742 self.advance();
743 self.advance();
744 continue;
745 }
746 if multiline && self.current_char() == Some('\n') {
747 buffer.push('\n');
748 self.advance();
749 continue;
750 }
751 if let Some(ch) = self.current_char() {
752 if is_disallowed_toml_control(ch, multiline) {
753 return Err(self.error("Invalid TOML character in basic string."));
754 }
755 buffer.push(ch);
756 self.advance();
757 continue;
758 }
759 return Err(self.error("Unterminated TOML basic string."));
760 }
761 Ok(TomlStringNode {
762 span: self.span_from(start),
763 style: style.to_owned(),
764 chunks,
765 })
766 }
767
768 fn starts_multiline_escape(&self) -> bool {
769 let mut probe = self.index + 1;
770 while self
771 .items
772 .get(probe)
773 .and_then(StreamItem::char)
774 .is_some_and(|ch| matches!(ch, ' ' | '\t'))
775 {
776 probe += 1;
777 }
778 if self
779 .items
780 .get(probe)
781 .and_then(StreamItem::char)
782 .is_some_and(|ch| ch == '\r')
783 {
784 probe += 1;
785 }
786 self.items
787 .get(probe)
788 .and_then(StreamItem::char)
789 .is_some_and(|ch| ch == '\n')
790 }
791
792 fn consume_multiline_escape(&mut self) {
793 self.advance();
794 while matches!(self.current_char(), Some(' ' | '\t')) {
795 self.advance();
796 }
797 if self.current_char() == Some('\r') {
798 self.advance();
799 }
800 if self.current_char() == Some('\n') {
801 self.advance();
802 }
803 while matches!(self.current_char(), Some(' ' | '\t' | '\n' | '\r')) {
804 self.advance();
805 }
806 }
807
808 fn parse_literal_like_string(
809 &mut self,
810 start: SourcePosition,
811 style: &str,
812 multiline: bool,
813 ) -> BackendResult<TomlStringNode> {
814 let mut chunks = Vec::new();
815 let mut buffer = String::new();
816 loop {
817 if multiline && self.starts_with("'''") {
818 let quote_run = self.count_consecutive_chars('\'');
819 if quote_run == 4 || quote_run == 5 {
820 for _ in 0..(quote_run - 3) {
821 buffer.push('\'');
822 self.advance();
823 }
824 continue;
825 }
826 self.flush_buffer(&mut buffer, &mut chunks);
827 self.consume_char('\'')?;
828 self.consume_char('\'')?;
829 self.consume_char('\'')?;
830 break;
831 }
832 if !multiline && self.current_char() == Some('\'') {
833 self.flush_buffer(&mut buffer, &mut chunks);
834 self.advance();
835 break;
836 }
837 if !multiline && matches!(self.current_char(), Some('\r' | '\n')) {
838 return Err(self.error("TOML single-line literal strings cannot contain newlines."));
839 }
840 if self.current_kind() == "eof" {
841 return Err(self.error("Unterminated TOML literal string."));
842 }
843 if self.current_kind() == "interpolation" {
844 self.flush_buffer(&mut buffer, &mut chunks);
845 chunks.push(TomlStringPart::Interpolation(
846 self.consume_interpolation("string_fragment")?,
847 ));
848 continue;
849 }
850 if multiline && self.current_char() == Some('\r') && self.peek_char(1) == Some('\n') {
851 buffer.push('\n');
852 self.advance();
853 self.advance();
854 continue;
855 }
856 if multiline && self.current_char() == Some('\n') {
857 buffer.push('\n');
858 self.advance();
859 continue;
860 }
861 if let Some(ch) = self.current_char() {
862 if is_disallowed_toml_control(ch, multiline) {
863 return Err(self.error("Invalid TOML character in literal string."));
864 }
865 buffer.push(ch);
866 self.advance();
867 continue;
868 }
869 return Err(self.error("Unterminated TOML literal string."));
870 }
871 Ok(TomlStringNode {
872 span: self.span_from(start),
873 style: style.to_owned(),
874 chunks,
875 })
876 }
877
878 fn parse_basic_escape(&mut self) -> BackendResult<char> {
879 self.consume_char('\\')?;
880 let ch = self
881 .current_char()
882 .ok_or_else(|| self.error("Incomplete TOML escape sequence."))?;
883 self.advance();
884 let mapped = match ch {
885 '"' => Some('"'),
886 '\\' => Some('\\'),
887 'b' => Some('\u{0008}'),
888 'f' => Some('\u{000c}'),
889 'n' => Some('\n'),
890 'r' => Some('\r'),
891 't' => Some('\t'),
892 _ => None,
893 };
894 if let Some(value) = mapped {
895 return Ok(value);
896 }
897 if ch == 'u' {
898 let digits = self.collect_exact_chars(4)?;
899 let codepoint = u32::from_str_radix(&digits, 16)
900 .map_err(|_| self.error("Invalid TOML escape sequence."))?;
901 return char::from_u32(codepoint)
902 .ok_or_else(|| self.error("Invalid TOML escape sequence."));
903 }
904 if ch == 'U' {
905 let digits = self.collect_exact_chars(8)?;
906 let codepoint = u32::from_str_radix(&digits, 16)
907 .map_err(|_| self.error("Invalid TOML escape sequence."))?;
908 return char::from_u32(codepoint)
909 .ok_or_else(|| self.error("Invalid TOML escape sequence."));
910 }
911 if self.profile.allows_extended_basic_string_escapes() && ch == 'e' {
912 return Ok('\u{001b}');
913 }
914 if self.profile.allows_extended_basic_string_escapes() && ch == 'x' {
915 let digits = self.collect_exact_chars(2)?;
916 let codepoint = u32::from_str_radix(&digits, 16)
917 .map_err(|_| self.error("Invalid TOML escape sequence."))?;
918 return char::from_u32(codepoint)
919 .ok_or_else(|| self.error("Invalid TOML escape sequence."));
920 }
921 Err(self.error("Invalid TOML escape sequence."))
922 }
923
924 fn collect_exact_chars(&mut self, count: usize) -> BackendResult<String> {
925 let mut chars = String::new();
926 for _ in 0..count {
927 let ch = self
928 .current_char()
929 .ok_or_else(|| self.error("Unexpected end of TOML escape sequence."))?;
930 chars.push(ch);
931 self.advance();
932 }
933 Ok(chars)
934 }
935
936 fn count_consecutive_chars(&self, ch: char) -> usize {
937 let mut probe = self.index;
938 let mut count = 0usize;
939 while self.items.get(probe).and_then(StreamItem::char) == Some(ch) {
940 count += 1;
941 probe += 1;
942 }
943 count
944 }
945
946 fn peek_char(&self, offset: usize) -> Option<char> {
947 self.items
948 .get(self.index + offset)
949 .and_then(StreamItem::char)
950 }
951
952 fn flush_buffer(&self, buffer: &mut String, chunks: &mut Vec<TomlStringPart>) {
953 if buffer.is_empty() {
954 return;
955 }
956 chunks.push(TomlStringPart::Chunk(TomlStringChunkNode {
957 span: SourceSpan::point(0, 0),
958 value: std::mem::take(buffer),
959 }));
960 }
961
962 fn parse_literal(&mut self, context: &str) -> BackendResult<TomlLiteralNode> {
963 let start = self.mark();
964 let mut source = String::new();
965 while self.current_kind() != "eof" {
966 if self.starts_value_terminator(context) {
967 break;
968 }
969 if self.current_kind() == "interpolation" {
970 return Err(
971 self.error("TOML bare literals cannot contain fragment interpolations.")
972 );
973 }
974 if let Some(ch) = self.current_char() {
975 source.push(ch);
976 self.advance();
977 } else {
978 break;
979 }
980 }
981 source = source.trim_end().to_owned();
982 if source.is_empty() {
983 return Err(self.error("Expected a TOML value."));
984 }
985 let value = match self.literal_materialization {
986 LiteralMaterialization::SharedHelper => materialize_value_source(self.profile, &source)
987 .map_err(|message| self.error(message))?,
988 LiteralMaterialization::Direct => {
989 materialize_value_source_direct(self.profile, &source)
990 .map_err(|message| self.error(message))?
991 }
992 };
993 Ok(TomlLiteralNode {
994 span: self.span_from(start),
995 source,
996 value,
997 })
998 }
999
1000 fn consume_interpolation(&mut self, role: &str) -> BackendResult<TomlInterpolationNode> {
1001 let (interpolation_index, span) = match self.current() {
1002 StreamItem::Interpolation {
1003 interpolation_index,
1004 span,
1005 ..
1006 } => (*interpolation_index, span.clone()),
1007 _ => return Err(self.error("Expected an interpolation.")),
1008 };
1009 self.advance();
1010 Ok(TomlInterpolationNode {
1011 span,
1012 interpolation_index,
1013 role: role.to_owned(),
1014 })
1015 }
1016}
1017
1018fn is_disallowed_toml_control(ch: char, multiline: bool) -> bool {
1019 if ch == '\t' {
1020 return false;
1021 }
1022 if multiline && ch == '\n' {
1023 return false;
1024 }
1025 matches!(
1026 ch,
1027 '\u{0000}'..='\u{0008}'
1028 | '\u{000b}'
1029 | '\u{000c}'
1030 | '\u{000e}'..='\u{001f}'
1031 | '\u{007f}'
1032 )
1033}
1034
1035fn is_v1_datetime_without_seconds(source: &str) -> bool {
1036 if !source.chars().all(|ch| !ch.is_whitespace()) {
1037 return false;
1038 }
1039 if let Some(time_part) = source.split('T').nth(1) {
1040 return is_time_without_seconds(time_part);
1041 }
1042 source.matches(':').count() == 1 && is_time_without_seconds(source)
1043}
1044
1045fn is_time_without_seconds(value: &str) -> bool {
1046 if value.len() < 5 {
1047 return false;
1048 }
1049 let bytes = value.as_bytes();
1050 if !(bytes[0].is_ascii_digit()
1051 && bytes[1].is_ascii_digit()
1052 && bytes[2] == b':'
1053 && bytes[3].is_ascii_digit()
1054 && bytes[4].is_ascii_digit())
1055 {
1056 return false;
1057 }
1058 !matches!(bytes.get(5), Some(b':'))
1059}
1060
1061fn materialize_value_source_direct(
1062 profile: TomlProfile,
1063 source_text: &str,
1064) -> Result<toml::Value, String> {
1065 if !profile.allows_missing_seconds() && is_v1_datetime_without_seconds(source_text) {
1066 return Err("Invalid TOML literal: missing seconds in time value.".to_owned());
1067 }
1068 let table: toml::Table = toml::from_str(&format!("value = {source_text}"))
1069 .map_err(|err| format!("Invalid TOML literal: {err}"))?;
1070 table
1071 .get("value")
1072 .cloned()
1073 .ok_or_else(|| "Expected a TOML value.".to_owned())
1074}
1075
1076pub fn materialize_value_source(
1077 profile: TomlProfile,
1078 source_text: &str,
1079) -> Result<toml::Value, String> {
1080 let template = TemplateInput::from_segments(vec![tstring_syntax::TemplateSegment::StaticText(
1084 format!("value = {source_text}"),
1085 )]);
1086 TomlParser::new_with_materialization(&template, profile, LiteralMaterialization::Direct)
1087 .parse()
1088 .map_err(|err| err.message.clone())?;
1089 materialize_value_source_direct(profile, source_text)
1090}
1091
1092pub fn parse_template_with_profile(
1093 template: &TemplateInput,
1094 profile: TomlProfile,
1095) -> BackendResult<TomlDocumentNode> {
1096 let items = template.flatten();
1097 for window in items.windows(2) {
1098 if window[0].char() == Some('\r') && window[1].char() != Some('\n') {
1099 return Err(BackendError::parse_at(
1100 "toml.parse",
1101 "Bare carriage returns are not valid in TOML input.",
1102 Some(window[0].span().clone()),
1103 ));
1104 }
1105 }
1106 TomlParser::new(template, profile).parse()
1107}
1108
1109pub fn parse_template(template: &TemplateInput) -> BackendResult<TomlDocumentNode> {
1110 parse_template_with_profile(template, TomlProfile::default())
1111}
1112
1113pub fn check_template_with_profile(
1114 template: &TemplateInput,
1115 profile: TomlProfile,
1116) -> BackendResult<()> {
1117 parse_template_with_profile(template, profile).map(|_| ())
1118}
1119
1120pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
1121 check_template_with_profile(template, TomlProfile::default())
1122}
1123
1124pub fn format_template_with_profile(
1125 template: &TemplateInput,
1126 profile: TomlProfile,
1127) -> BackendResult<String> {
1128 let document = parse_template_with_profile(template, profile)?;
1129 format_toml_document(template, &document)
1130}
1131
1132pub fn format_template(template: &TemplateInput) -> BackendResult<String> {
1133 format_template_with_profile(template, TomlProfile::default())
1134}
1135
1136pub fn normalize_document_with_profile(
1137 value: &toml::Value,
1138 _profile: TomlProfile,
1139) -> BackendResult<NormalizedStream> {
1140 Ok(NormalizedStream::new(vec![NormalizedDocument::Value(
1141 normalize_value(value)?,
1142 )]))
1143}
1144
1145pub fn normalize_document(value: &toml::Value) -> BackendResult<NormalizedStream> {
1146 normalize_document_with_profile(value, TomlProfile::default())
1147}
1148
1149pub fn normalize_value(value: &toml::Value) -> BackendResult<NormalizedValue> {
1150 match value {
1151 toml::Value::String(value) => Ok(NormalizedValue::String(value.clone())),
1152 toml::Value::Integer(value) => Ok(NormalizedValue::Integer((*value).into())),
1153 toml::Value::Float(value) => Ok(NormalizedValue::Float(normalize_float(*value))),
1154 toml::Value::Boolean(value) => Ok(NormalizedValue::Bool(*value)),
1155 toml::Value::Datetime(value) => Ok(NormalizedValue::Temporal(normalize_datetime(value)?)),
1156 toml::Value::Array(values) => values
1157 .iter()
1158 .map(normalize_value)
1159 .collect::<BackendResult<Vec<_>>>()
1160 .map(NormalizedValue::Sequence),
1161 toml::Value::Table(values) => values
1162 .iter()
1163 .map(|(key, value)| {
1164 Ok(NormalizedEntry {
1165 key: NormalizedKey::String(key.clone()),
1166 value: normalize_value(value)?,
1167 })
1168 })
1169 .collect::<BackendResult<Vec<_>>>()
1170 .map(NormalizedValue::Mapping),
1171 }
1172}
1173
1174fn format_toml_document(
1175 template: &TemplateInput,
1176 node: &TomlDocumentNode,
1177) -> BackendResult<String> {
1178 node.statements
1179 .iter()
1180 .map(|statement| format_toml_statement(template, statement))
1181 .collect::<BackendResult<Vec<_>>>()
1182 .map(|statements| statements.join("\n"))
1183}
1184
1185fn format_toml_statement(
1186 template: &TemplateInput,
1187 node: &TomlStatementNode,
1188) -> BackendResult<String> {
1189 match node {
1190 TomlStatementNode::Assignment(node) => Ok(format!(
1191 "{} = {}",
1192 format_key_path(template, &node.key_path)?,
1193 format_toml_value(template, &node.value)?
1194 )),
1195 TomlStatementNode::TableHeader(node) => {
1196 Ok(format!("[{}]", format_key_path(template, &node.key_path)?))
1197 }
1198 TomlStatementNode::ArrayTableHeader(node) => Ok(format!(
1199 "[[{}]]",
1200 format_key_path(template, &node.key_path)?
1201 )),
1202 }
1203}
1204
1205fn format_key_path(template: &TemplateInput, node: &TomlKeyPathNode) -> BackendResult<String> {
1206 node.segments
1207 .iter()
1208 .map(|segment| format_key_segment(template, segment))
1209 .collect::<BackendResult<Vec<_>>>()
1210 .map(|segments| segments.join("."))
1211}
1212
1213fn format_key_segment(
1214 template: &TemplateInput,
1215 node: &TomlKeySegmentNode,
1216) -> BackendResult<String> {
1217 match &node.value {
1218 TomlKeySegmentValue::Bare(value) => {
1219 if node.bare && is_bare_key(value) {
1220 Ok(value.clone())
1221 } else {
1222 Ok(render_basic_string(value))
1223 }
1224 }
1225 TomlKeySegmentValue::String(value) => format_toml_string(template, value),
1226 TomlKeySegmentValue::Interpolation(value) => {
1227 interpolation_raw_source(template, value.interpolation_index, &value.span, "TOML key")
1228 }
1229 }
1230}
1231
1232fn format_toml_value(template: &TemplateInput, node: &TomlValueNode) -> BackendResult<String> {
1233 match node {
1234 TomlValueNode::String(node) => format_toml_string(template, node),
1235 TomlValueNode::Literal(node) => Ok(node.source.clone()),
1236 TomlValueNode::Interpolation(node) => {
1237 interpolation_raw_source(template, node.interpolation_index, &node.span, "TOML value")
1238 }
1239 TomlValueNode::Array(node) => node
1240 .items
1241 .iter()
1242 .map(|item| format_toml_value(template, item))
1243 .collect::<BackendResult<Vec<_>>>()
1244 .map(|items| format!("[{}]", items.join(", "))),
1245 TomlValueNode::InlineTable(node) => node
1246 .entries
1247 .iter()
1248 .map(|entry| {
1249 Ok(format!(
1250 "{} = {}",
1251 format_key_path(template, &entry.key_path)?,
1252 format_toml_value(template, &entry.value)?
1253 ))
1254 })
1255 .collect::<BackendResult<Vec<_>>>()
1256 .map(|entries| {
1257 if entries.is_empty() {
1258 "{}".to_owned()
1259 } else {
1260 format!("{{ {} }}", entries.join(", "))
1261 }
1262 }),
1263 }
1264}
1265
1266fn format_toml_string(template: &TemplateInput, node: &TomlStringNode) -> BackendResult<String> {
1267 let mut rendered = String::new();
1268 for chunk in &node.chunks {
1269 match chunk {
1270 TomlStringPart::Chunk(chunk) => rendered.push_str(&chunk.value),
1271 TomlStringPart::Interpolation(node) => rendered.push_str(&interpolation_raw_source(
1272 template,
1273 node.interpolation_index,
1274 &node.span,
1275 "TOML string fragment",
1276 )?),
1277 }
1278 }
1279 Ok(render_basic_string(&rendered))
1280}
1281
1282fn interpolation_raw_source(
1283 template: &TemplateInput,
1284 interpolation_index: usize,
1285 span: &SourceSpan,
1286 context: &str,
1287) -> BackendResult<String> {
1288 template
1289 .interpolation_raw_source(interpolation_index)
1290 .map(str::to_owned)
1291 .ok_or_else(|| {
1292 let expression = template.interpolation(interpolation_index).map_or_else(
1293 || format!("slot {interpolation_index}"),
1294 |value| value.expression_label().to_owned(),
1295 );
1296 BackendError::semantic_at(
1297 "toml.format",
1298 format!(
1299 "Cannot format {context} interpolation {expression:?} without raw source text."
1300 ),
1301 Some(span.clone()),
1302 )
1303 })
1304}
1305
1306fn is_bare_key(value: &str) -> bool {
1307 value
1308 .chars()
1309 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
1310}
1311
1312fn render_basic_string(value: &str) -> String {
1313 let mut rendered = String::with_capacity(value.len() + 2);
1314 rendered.push('"');
1315 for ch in value.chars() {
1316 match ch {
1317 '\u{0008}' => rendered.push_str("\\b"),
1318 '\t' => rendered.push_str("\\t"),
1319 '\n' => rendered.push_str("\\n"),
1320 '\u{000c}' => rendered.push_str("\\f"),
1321 '\r' => rendered.push_str("\\r"),
1322 '"' => rendered.push_str("\\\""),
1323 '\\' => rendered.push_str("\\\\"),
1324 '\u{0000}'..='\u{001f}' | '\u{007f}' => {
1325 rendered.push_str(&format!("\\u{:04X}", ch as u32));
1326 }
1327 _ => rendered.push(ch),
1328 }
1329 }
1330 rendered.push('"');
1331 rendered
1332}
1333
1334fn normalize_float(value: f64) -> NormalizedFloat {
1335 if value.is_nan() {
1336 return NormalizedFloat::NaN;
1337 }
1338 if value.is_infinite() {
1339 return if value.is_sign_negative() {
1340 NormalizedFloat::NegInf
1341 } else {
1342 NormalizedFloat::PosInf
1343 };
1344 }
1345 NormalizedFloat::finite(value)
1346}
1347
1348fn normalize_datetime(value: &toml::value::Datetime) -> BackendResult<NormalizedTemporal> {
1349 match (&value.date, &value.time, &value.offset) {
1350 (Some(date), Some(time), Some(offset)) => Ok(NormalizedTemporal::OffsetDateTime(
1351 NormalizedOffsetDateTime {
1352 date: normalize_date(*date),
1353 time: normalize_time(*time),
1354 offset_minutes: match offset {
1355 toml::value::Offset::Z => 0,
1356 toml::value::Offset::Custom { minutes } => *minutes,
1357 },
1358 },
1359 )),
1360 (Some(date), Some(time), None) => {
1361 Ok(NormalizedTemporal::LocalDateTime(NormalizedLocalDateTime {
1362 date: normalize_date(*date),
1363 time: normalize_time(*time),
1364 }))
1365 }
1366 (Some(date), None, None) => Ok(NormalizedTemporal::LocalDate(normalize_date(*date))),
1367 (None, Some(time), None) => Ok(NormalizedTemporal::LocalTime(normalize_time(*time))),
1368 _ => Err(BackendError::semantic(format!(
1369 "Unsupported TOML datetime shape: {value}"
1370 ))),
1371 }
1372}
1373
1374fn normalize_date(value: toml::value::Date) -> NormalizedDate {
1375 NormalizedDate {
1376 year: i32::from(value.year),
1377 month: value.month,
1378 day: value.day,
1379 }
1380}
1381
1382fn normalize_time(value: toml::value::Time) -> NormalizedTime {
1383 NormalizedTime {
1384 hour: value.hour,
1385 minute: value.minute,
1386 second: value.second,
1387 nanosecond: value.nanosecond,
1388 }
1389}
1390
1391#[cfg(test)]
1392mod tests {
1393 use super::{parse_template, TomlKeySegmentValue, TomlStatementNode, TomlValueNode};
1394 use pyo3::prelude::*;
1395 use tstring_pyo3_bindings::{extract_template, toml::render_document};
1396 use tstring_syntax::{BackendError, BackendResult, ErrorKind};
1397
1398 fn parse_rendered_toml(text: &str) -> BackendResult<toml::Value> {
1399 toml::from_str(text).map_err(|err| {
1400 BackendError::parse(format!(
1401 "Rendered TOML could not be reparsed during test verification: {err}"
1402 ))
1403 })
1404 }
1405
1406 #[test]
1407 fn parses_toml_string_families() {
1408 Python::with_gil(|py| {
1409 let module = PyModule::from_code(
1410 py,
1411 pyo3::ffi::c_str!("template=t'basic = \"hi-{1}\"\\nliteral = \\'hi-{2}\\''\n"),
1412 pyo3::ffi::c_str!("test_toml.py"),
1413 pyo3::ffi::c_str!("test_toml"),
1414 )
1415 .unwrap();
1416 let template = module.getattr("template").unwrap();
1417 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1418 let document = parse_template(&template).unwrap();
1419 assert_eq!(document.statements.len(), 2);
1420 let TomlStatementNode::Assignment(first) = &document.statements[0] else {
1421 panic!("expected assignment");
1422 };
1423 let TomlValueNode::String(first_value) = &first.value else {
1424 panic!("expected string");
1425 };
1426 assert_eq!(first_value.style, "basic");
1427 });
1428 }
1429
1430 #[test]
1431 fn parses_headers_and_interpolated_key_segments() {
1432 Python::with_gil(|py| {
1433 let module = PyModule::from_code(
1434 py,
1435 pyo3::ffi::c_str!(
1436 "env='prod'\nname='api'\ntemplate=t'[servers.{env}]\\nservice = \"{name}\"\\n[[services]]\\nid = 1\\n'\n"
1437 ),
1438 pyo3::ffi::c_str!("test_toml_headers.py"),
1439 pyo3::ffi::c_str!("test_toml_headers"),
1440 )
1441 .unwrap();
1442 let template = module.getattr("template").unwrap();
1443 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1444 let document = parse_template(&template).unwrap();
1445
1446 assert_eq!(document.statements.len(), 4);
1447 let TomlStatementNode::TableHeader(header) = &document.statements[0] else {
1448 panic!("expected table header");
1449 };
1450 assert_eq!(header.key_path.segments.len(), 2);
1451 assert!(matches!(
1452 header.key_path.segments[1].value,
1453 TomlKeySegmentValue::Interpolation(_)
1454 ));
1455 assert!(matches!(
1456 document.statements[2],
1457 TomlStatementNode::ArrayTableHeader(_)
1458 ));
1459 });
1460 }
1461
1462 #[test]
1463 fn parses_quoted_keys_and_multiline_array_comments() {
1464 Python::with_gil(|py| {
1465 let module = PyModule::from_code(
1466 py,
1467 pyo3::ffi::c_str!(
1468 "from string.templatelib import Template\ntemplate=t'\"a.b\" = 1\\nsite.\"google.com\".value = 2\\nvalue = [\\n 1, # first\\n 2, # second\\n]\\n'\nempty_basic=Template('\"\" = 1\\n')\nempty_literal=Template(\"'' = 1\\n\")\nempty_segment=Template('a.\"\".b = 1\\n')\n"
1469 ),
1470 pyo3::ffi::c_str!("test_toml_quoted_keys.py"),
1471 pyo3::ffi::c_str!("test_toml_quoted_keys"),
1472 )
1473 .unwrap();
1474 let template = module.getattr("template").unwrap();
1475 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1476 let document = parse_template(&template).unwrap();
1477 let rendered = render_document(py, &document).unwrap();
1478 let table = rendered.data.as_table().expect("expected TOML table");
1479
1480 assert_eq!(table["a.b"].as_integer(), Some(1));
1481 assert_eq!(table["site"]["google.com"]["value"].as_integer(), Some(2));
1482 assert_eq!(
1483 table["value"]
1484 .as_array()
1485 .expect("array")
1486 .iter()
1487 .filter_map(toml::Value::as_integer)
1488 .collect::<Vec<_>>(),
1489 vec![1, 2]
1490 );
1491
1492 let empty_basic = module.getattr("empty_basic").unwrap();
1493 let empty_basic = extract_template(py, &empty_basic, "toml_t/toml_t_str").unwrap();
1494 let rendered = render_document(py, &parse_template(&empty_basic).unwrap()).unwrap();
1495 let table = rendered.data.as_table().expect("expected TOML table");
1496 assert_eq!(table[""].as_integer(), Some(1));
1497
1498 let empty_literal = module.getattr("empty_literal").unwrap();
1499 let empty_literal = extract_template(py, &empty_literal, "toml_t/toml_t_str").unwrap();
1500 let rendered = render_document(py, &parse_template(&empty_literal).unwrap()).unwrap();
1501 let table = rendered.data.as_table().expect("expected TOML table");
1502 assert_eq!(table[""].as_integer(), Some(1));
1503
1504 let empty_segment = module.getattr("empty_segment").unwrap();
1505 let empty_segment = extract_template(py, &empty_segment, "toml_t/toml_t_str").unwrap();
1506 let rendered = render_document(py, &parse_template(&empty_segment).unwrap()).unwrap();
1507 let table = rendered.data.as_table().expect("expected TOML table");
1508 assert_eq!(table["a"][""]["b"].as_integer(), Some(1));
1509 });
1510 }
1511
1512 #[test]
1513 fn renders_temporal_values_and_inline_tables() {
1514 Python::with_gil(|py| {
1515 let module = PyModule::from_code(
1516 py,
1517 pyo3::ffi::c_str!(
1518 "from datetime import datetime\nmoment=datetime(2025, 1, 2, 3, 4, 5)\nmeta={'count': 2, 'active': True}\ntemplate=t'when = {moment}\\nmeta = {meta}\\n'\n"
1519 ),
1520 pyo3::ffi::c_str!("test_toml_render.py"),
1521 pyo3::ffi::c_str!("test_toml_render"),
1522 )
1523 .unwrap();
1524 let template = module.getattr("template").unwrap();
1525 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1526 let document = parse_template(&template).unwrap();
1527 let rendered = render_document(py, &document).unwrap();
1528 let table = rendered.data.as_table().expect("expected TOML table");
1529
1530 assert!(rendered.text.contains("2025-01-02T03:04:05"));
1531 assert_eq!(table["meta"]["count"].as_integer(), Some(2));
1532 assert_eq!(table["meta"]["active"].as_bool(), Some(true));
1533 });
1534 }
1535
1536 #[test]
1537 fn rejects_null_like_interpolations() {
1538 Python::with_gil(|py| {
1539 let module = PyModule::from_code(
1540 py,
1541 pyo3::ffi::c_str!("missing=None\ntemplate=t'value = {missing}\\n'\n"),
1542 pyo3::ffi::c_str!("test_toml_error.py"),
1543 pyo3::ffi::c_str!("test_toml_error"),
1544 )
1545 .unwrap();
1546 let template = module.getattr("template").unwrap();
1547 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1548 let document = parse_template(&template).unwrap();
1549 let err = match render_document(py, &document) {
1550 Ok(_) => panic!("expected TOML render failure"),
1551 Err(err) => err,
1552 };
1553
1554 assert_eq!(err.kind, ErrorKind::Unrepresentable);
1555 assert!(err.message.contains("TOML has no null"));
1556 });
1557 }
1558
1559 #[test]
1560 fn trims_multiline_basic_line_end_backslashes() {
1561 Python::with_gil(|py| {
1562 let module = PyModule::from_code(
1563 py,
1564 pyo3::ffi::c_str!(
1565 "from string.templatelib import Template\ntrimmed=t'value = \"\"\"\\nalpha\\\\\\n beta\\n\"\"\"'\ncrlf=Template('value = \"\"\"\\r\\na\\\\\\r\\n b\\r\\n\"\"\"\\n')\n"
1566 ),
1567 pyo3::ffi::c_str!("test_toml_multiline.py"),
1568 pyo3::ffi::c_str!("test_toml_multiline"),
1569 )
1570 .unwrap();
1571 for (name, expected) in [("trimmed", "alphabeta\n"), ("crlf", "ab\n")] {
1572 let template = module.getattr(name).unwrap();
1573 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1574 let document = parse_template(&template).unwrap();
1575 let rendered = render_document(py, &document).unwrap();
1576 let table = rendered.data.as_table().expect("expected TOML table");
1577 assert_eq!(table["value"].as_str(), Some(expected));
1578 }
1579 });
1580 }
1581
1582 #[test]
1583 fn parses_multiline_strings_with_one_or_two_quotes_before_terminator() {
1584 Python::with_gil(|py| {
1585 let module = PyModule::from_code(
1586 py,
1587 pyo3::ffi::c_str!(
1588 "template=t'value = \"\"\"\"\"\"\"\\none = \"\"\"\"\"\"\"\"\\nliteral = \\'\\'\\'\\'\\'\\'\\'\\nliteral_two = \\'\\'\\'\\'\\'\\'\\'\\'\\n'\n"
1589 ),
1590 pyo3::ffi::c_str!("test_toml_quote_run.py"),
1591 pyo3::ffi::c_str!("test_toml_quote_run"),
1592 )
1593 .unwrap();
1594 let template = module.getattr("template").unwrap();
1595 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1596 let document = parse_template(&template).unwrap();
1597 let rendered = render_document(py, &document).unwrap();
1598 let table = rendered.data.as_table().expect("expected TOML table");
1599
1600 assert_eq!(table["value"].as_str(), Some("\""));
1601 assert_eq!(table["one"].as_str(), Some("\"\""));
1602 assert_eq!(table["literal"].as_str(), Some("'"));
1603 assert_eq!(table["literal_two"].as_str(), Some("''"));
1604 });
1605 }
1606
1607 #[test]
1608 fn parses_numeric_forms_and_local_datetimes() {
1609 Python::with_gil(|py| {
1610 let module = PyModule::from_code(
1611 py,
1612 pyo3::ffi::c_str!(
1613 "template=t'value = 0xDEADBEEF\\nhex_underscore = 0xDEAD_BEEF\\nbinary = 0b1101\\noctal = 0o755\\nunderscored = 1_000_000\\nfloat = +1.0\\nexp = -2e-2\\nlocal = 2024-01-02T03:04:05\\n'\n"
1614 ),
1615 pyo3::ffi::c_str!("test_toml_numeric_forms.py"),
1616 pyo3::ffi::c_str!("test_toml_numeric_forms"),
1617 )
1618 .unwrap();
1619 let template = module.getattr("template").unwrap();
1620 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1621 let document = parse_template(&template).unwrap();
1622 let rendered = render_document(py, &document).unwrap();
1623 let table = rendered.data.as_table().expect("expected TOML table");
1624
1625 assert_eq!(table["value"].as_integer(), Some(3_735_928_559));
1626 assert_eq!(table["hex_underscore"].as_integer(), Some(3_735_928_559));
1627 assert_eq!(table["binary"].as_integer(), Some(13));
1628 assert_eq!(table["octal"].as_integer(), Some(493));
1629 assert_eq!(table["underscored"].as_integer(), Some(1_000_000));
1630 assert_eq!(table["float"].as_float(), Some(1.0));
1631 assert_eq!(table["exp"].as_float(), Some(-0.02));
1632 assert_eq!(
1633 table["local"]
1634 .as_datetime()
1635 .map(std::string::ToString::to_string),
1636 Some("2024-01-02T03:04:05".to_owned())
1637 );
1638 });
1639 }
1640
1641 #[test]
1642 fn parses_empty_strings_and_quoted_empty_table_headers() {
1643 Python::with_gil(|py| {
1644 let module = PyModule::from_code(
1645 py,
1646 pyo3::ffi::c_str!(
1647 "from string.templatelib import Template\nbasic=t'value = \"\"\\n'\nliteral=Template(\"value = ''\\n\")\nheader=Template('[\"\"]\\nvalue = 1\\n')\nheader_subtable=Template('[\"\"]\\nvalue = 1\\n[\"\".inner]\\nname = \"x\"\\n')\nescaped_quote=Template('value = \"\"\"a\\\\\"b\"\"\"\\n')\n"
1648 ),
1649 pyo3::ffi::c_str!("test_toml_empty_strings.py"),
1650 pyo3::ffi::c_str!("test_toml_empty_strings"),
1651 )
1652 .unwrap();
1653
1654 for name in ["basic", "literal"] {
1655 let template = module.getattr(name).unwrap();
1656 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1657 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
1658 let table = rendered.data.as_table().expect("expected TOML table");
1659 assert_eq!(table["value"].as_str(), Some(""));
1660 }
1661
1662 let escaped_quote = module.getattr("escaped_quote").unwrap();
1663 let escaped_quote = extract_template(py, &escaped_quote, "toml_t/toml_t_str").unwrap();
1664 let rendered = render_document(py, &parse_template(&escaped_quote).unwrap()).unwrap();
1665 let table = rendered.data.as_table().expect("expected TOML table");
1666 assert_eq!(table["value"].as_str(), Some("a\"b"));
1667
1668 let header = module.getattr("header").unwrap();
1669 let header = extract_template(py, &header, "toml_t/toml_t_str").unwrap();
1670 let rendered = render_document(py, &parse_template(&header).unwrap()).unwrap();
1671 let table = rendered.data.as_table().expect("expected TOML table");
1672 assert_eq!(table[""]["value"].as_integer(), Some(1));
1673
1674 let header_subtable = module.getattr("header_subtable").unwrap();
1675 let header_subtable =
1676 extract_template(py, &header_subtable, "toml_t/toml_t_str").unwrap();
1677 let rendered = render_document(py, &parse_template(&header_subtable).unwrap()).unwrap();
1678 let table = rendered.data.as_table().expect("expected TOML table");
1679 assert_eq!(table[""]["inner"]["name"].as_str(), Some("x"));
1680 });
1681 }
1682
1683 #[test]
1684 fn parses_empty_collections_and_quoted_empty_dotted_tables() {
1685 Python::with_gil(|py| {
1686 let module = PyModule::from_code(
1687 py,
1688 pyo3::ffi::c_str!(
1689 "from string.templatelib import Template\nempty_array=Template('value = []\\n')\nempty_inline_table=Template('value = {}\\n')\nquoted_empty_dotted_table=Template('[a.\"\".b]\\nvalue = 1\\n')\nquoted_empty_subsegments=Template('[\"\".\"\".leaf]\\nvalue = 1\\n')\nquoted_empty_leaf_chain=Template('[\"\".\"\".\"leaf\"]\\nvalue = 1\\n')\nmixed_array_tables=Template('[[a]]\\nname = \"x\"\\n[[a]]\\nname = \"y\"\\n')\n"
1690 ),
1691 pyo3::ffi::c_str!("test_toml_empty_collections.py"),
1692 pyo3::ffi::c_str!("test_toml_empty_collections"),
1693 )
1694 .unwrap();
1695
1696 let empty_array = module.getattr("empty_array").unwrap();
1697 let empty_array = extract_template(py, &empty_array, "toml_t/toml_t_str").unwrap();
1698 let rendered = render_document(py, &parse_template(&empty_array).unwrap()).unwrap();
1699 let table = rendered.data.as_table().expect("expected TOML table");
1700 assert_eq!(table["value"].as_array().expect("array").len(), 0);
1701
1702 let empty_inline_table = module.getattr("empty_inline_table").unwrap();
1703 let empty_inline_table =
1704 extract_template(py, &empty_inline_table, "toml_t/toml_t_str").unwrap();
1705 let rendered =
1706 render_document(py, &parse_template(&empty_inline_table).unwrap()).unwrap();
1707 let table = rendered.data.as_table().expect("expected TOML table");
1708 assert_eq!(table["value"].as_table().expect("table").len(), 0);
1709
1710 let quoted_empty_dotted_table = module.getattr("quoted_empty_dotted_table").unwrap();
1711 let quoted_empty_dotted_table =
1712 extract_template(py, "ed_empty_dotted_table, "toml_t/toml_t_str").unwrap();
1713 let rendered =
1714 render_document(py, &parse_template("ed_empty_dotted_table).unwrap()).unwrap();
1715 let table = rendered.data.as_table().expect("expected TOML table");
1716 assert_eq!(table["a"][""]["b"]["value"].as_integer(), Some(1));
1717
1718 let quoted_empty_subsegments = module.getattr("quoted_empty_subsegments").unwrap();
1719 let quoted_empty_subsegments =
1720 extract_template(py, "ed_empty_subsegments, "toml_t/toml_t_str").unwrap();
1721 let rendered =
1722 render_document(py, &parse_template("ed_empty_subsegments).unwrap()).unwrap();
1723 let table = rendered.data.as_table().expect("expected TOML table");
1724 assert_eq!(table[""][""]["leaf"]["value"].as_integer(), Some(1));
1725
1726 let quoted_empty_leaf_chain = module.getattr("quoted_empty_leaf_chain").unwrap();
1727 let quoted_empty_leaf_chain =
1728 extract_template(py, "ed_empty_leaf_chain, "toml_t/toml_t_str").unwrap();
1729 let rendered =
1730 render_document(py, &parse_template("ed_empty_leaf_chain).unwrap()).unwrap();
1731 let table = rendered.data.as_table().expect("expected TOML table");
1732 assert_eq!(table[""][""]["leaf"]["value"].as_integer(), Some(1));
1733
1734 let mixed_array_tables = module.getattr("mixed_array_tables").unwrap();
1735 let mixed_array_tables =
1736 extract_template(py, &mixed_array_tables, "toml_t/toml_t_str").unwrap();
1737 let rendered =
1738 render_document(py, &parse_template(&mixed_array_tables).unwrap()).unwrap();
1739 let table = rendered.data.as_table().expect("expected TOML table");
1740 assert_eq!(table["a"].as_array().expect("array").len(), 2);
1741 });
1742 }
1743
1744 #[test]
1745 fn parses_additional_numeric_and_datetime_forms() {
1746 Python::with_gil(|py| {
1747 let module = PyModule::from_code(
1748 py,
1749 pyo3::ffi::c_str!(
1750 "template=t'plus_int = +1\\nplus_zero = +0\\nplus_zero_float = +0.0\\nzero_float_exp = 0e0\\nplus_zero_float_exp = +0e0\\nplus_zero_fraction_exp = +0.0e0\\nexp_underscore = 1e1_0\\nfrac_underscore = 1_2.3_4\\nlocal_space = 2024-01-02 03:04:05\\nlocal_lower_t = 2024-01-02t03:04:05\\nlocal_date = 2024-01-02\\nlocal_time_fraction = 03:04:05.123456\\narray_of_dates = [2024-01-02, 2024-01-03]\\narray_of_dates_trailing = [2024-01-02, 2024-01-03,]\\nmixed_date_time_array = [2024-01-02, 03:04:05]\\narray_of_local_times = [03:04:05, 03:04:06.123456]\\nnested_array_mixed_dates = [[2024-01-02], [2024-01-03]]\\noffset_array = [1979-05-27T07:32:00Z, 1979-05-27T00:32:00-07:00]\\noffset_array_positive = [1979-05-27T07:32:00+07:00]\\ndatetime_array_trailing = [1979-05-27T07:32:00Z, 1979-05-27T00:32:00-07:00,]\\noffset_fraction_dt = 1979-05-27T07:32:00.999999-07:00\\noffset_fraction_space = 1979-05-27 07:32:00.999999-07:00\\narray_offset_fraction = [1979-05-27T07:32:00.999999-07:00, 1979-05-27T07:32:00Z]\\nfraction_lower_z = 2024-01-02T03:04:05.123456z\\narray_fraction_lower_z = [2024-01-02T03:04:05.123456z]\\nutc_fraction_lower_array = [2024-01-02T03:04:05.123456z, 2024-01-02T03:04:06z]\\nutc_fraction_lower_array_trailing = [2024-01-02T03:04:05.123456z, 2024-01-02T03:04:06z,]\\nlowercase_offset_array_trailing = [2024-01-02T03:04:05z, 2024-01-02T03:04:06z,]\\nlower_hex = 0xdeadbeef\\nutc_z = 2024-01-02T03:04:05Z\\nutc_lower_z = 2024-01-02T03:04:05z\\nutc_fraction = 2024-01-02T03:04:05.123456Z\\nutc_fraction_array = [2024-01-02T03:04:05.123456Z, 2024-01-02T03:04:06Z]\\nupper_exp = 1E2\\nsigned_int_array = [+1, +0, -1]\\nspecial_float_array = [+inf, -inf, nan]\\nspecial_float_nested_arrays = [[+inf], [-inf], [nan]]\\nspecial_float_deeper_arrays = [[[+inf]], [[-inf]], [[nan]]]\\nupper_exp_nested_mixed = [[1E2, 0E0], [-1E-2]]\\nspecial_float_inline_table = {{ pos = +inf, neg = -inf, nan = nan }}\\nspecial_float_mixed_nested = [[+inf, -inf], [nan]]\\nnested_datetime_arrays = [[1979-05-27 07:32:00+07:00], [1979-05-27T00:32:00-07:00]]\\nupper_exp_nested_array = [[1E2], [+0.0E0], [-1E-2]]\\npositive_negative_offsets = [1979-05-27T07:32:00+07:00, 1979-05-27T00:32:00-07:00]\\npositive_offset_scalar_space = 1979-05-27 07:32:00+07:00\\npositive_offset_array_space = [1979-05-27 07:32:00+07:00, 1979-05-27T00:32:00-07:00]\\n'\n"
1751 ),
1752 pyo3::ffi::c_str!("test_toml_more_numeric_forms.py"),
1753 pyo3::ffi::c_str!("test_toml_more_numeric_forms"),
1754 )
1755 .unwrap();
1756 let template = module.getattr("template").unwrap();
1757 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1758 let document = parse_template(&template).unwrap();
1759 let rendered = render_document(py, &document).unwrap();
1760 let table = rendered.data.as_table().expect("expected TOML table");
1761
1762 assert_eq!(table["plus_int"].as_integer(), Some(1));
1763 assert_eq!(table["plus_zero"].as_integer(), Some(0));
1764 assert_eq!(table["plus_zero_float"].as_float(), Some(0.0));
1765 assert_eq!(table["zero_float_exp"].as_float(), Some(0.0));
1766 assert_eq!(table["plus_zero_float_exp"].as_float(), Some(0.0));
1767 assert_eq!(table["plus_zero_fraction_exp"].as_float(), Some(0.0));
1768 assert_eq!(table["exp_underscore"].as_float(), Some(1e10));
1769 assert_eq!(table["frac_underscore"].as_float(), Some(12.34));
1770 assert_eq!(
1771 table["local_space"]
1772 .as_datetime()
1773 .map(std::string::ToString::to_string),
1774 Some("2024-01-02T03:04:05".to_owned())
1775 );
1776 assert_eq!(
1777 table["local_lower_t"]
1778 .as_datetime()
1779 .map(std::string::ToString::to_string),
1780 Some("2024-01-02T03:04:05".to_owned())
1781 );
1782 assert_eq!(
1783 table["local_date"]
1784 .as_datetime()
1785 .map(std::string::ToString::to_string),
1786 Some("2024-01-02".to_owned())
1787 );
1788 assert_eq!(
1789 table["array_of_dates_trailing"]
1790 .as_array()
1791 .expect("array")
1792 .len(),
1793 2
1794 );
1795 assert_eq!(
1796 table["mixed_date_time_array"]
1797 .as_array()
1798 .expect("array")
1799 .len(),
1800 2
1801 );
1802 assert_eq!(
1803 table["local_time_fraction"]
1804 .as_datetime()
1805 .map(std::string::ToString::to_string),
1806 Some("03:04:05.123456".to_owned())
1807 );
1808 assert_eq!(table["array_of_dates"].as_array().expect("array").len(), 2);
1809 assert_eq!(
1810 table["array_of_local_times"]
1811 .as_array()
1812 .expect("array")
1813 .len(),
1814 2
1815 );
1816 assert_eq!(
1817 table["nested_array_mixed_dates"]
1818 .as_array()
1819 .expect("array")
1820 .len(),
1821 2
1822 );
1823 assert_eq!(table["offset_array"].as_array().expect("array").len(), 2);
1824 assert_eq!(
1825 table["offset_array_positive"]
1826 .as_array()
1827 .expect("array")
1828 .len(),
1829 1
1830 );
1831 assert_eq!(
1832 table["datetime_array_trailing"]
1833 .as_array()
1834 .expect("array")
1835 .len(),
1836 2
1837 );
1838 assert_eq!(
1839 table["lowercase_offset_array_trailing"]
1840 .as_array()
1841 .expect("array")
1842 .len(),
1843 2
1844 );
1845 assert_eq!(
1846 table["offset_fraction_dt"]
1847 .as_datetime()
1848 .map(std::string::ToString::to_string),
1849 Some("1979-05-27T07:32:00.999999-07:00".to_owned())
1850 );
1851 assert_eq!(
1852 table["offset_fraction_space"]
1853 .as_datetime()
1854 .map(std::string::ToString::to_string),
1855 Some("1979-05-27T07:32:00.999999-07:00".to_owned())
1856 );
1857 assert_eq!(
1858 table["array_offset_fraction"]
1859 .as_array()
1860 .expect("array")
1861 .len(),
1862 2
1863 );
1864 assert_eq!(
1865 table["fraction_lower_z"]
1866 .as_datetime()
1867 .map(std::string::ToString::to_string),
1868 Some("2024-01-02T03:04:05.123456Z".to_owned())
1869 );
1870 assert_eq!(
1871 table["array_fraction_lower_z"]
1872 .as_array()
1873 .expect("array")
1874 .len(),
1875 1
1876 );
1877 assert_eq!(
1878 table["utc_fraction_lower_array"]
1879 .as_array()
1880 .expect("array")
1881 .len(),
1882 2
1883 );
1884 assert_eq!(
1885 table["utc_fraction_lower_array_trailing"]
1886 .as_array()
1887 .expect("array")
1888 .len(),
1889 2
1890 );
1891 assert_eq!(table["lower_hex"].as_integer(), Some(0xdead_beef));
1892 assert_eq!(table["upper_exp"].as_float(), Some(100.0));
1893 assert_eq!(
1894 table["signed_int_array"]
1895 .as_array()
1896 .expect("array")
1897 .iter()
1898 .filter_map(toml::Value::as_integer)
1899 .collect::<Vec<_>>(),
1900 vec![1, 0, -1]
1901 );
1902 let special_floats = table["special_float_array"].as_array().expect("array");
1903 assert!(special_floats[0].as_float().expect("float").is_infinite());
1904 assert!(special_floats[1]
1905 .as_float()
1906 .expect("float")
1907 .is_sign_negative());
1908 assert!(special_floats[2].as_float().expect("float").is_nan());
1909 assert_eq!(
1910 table["special_float_nested_arrays"]
1911 .as_array()
1912 .expect("array")
1913 .len(),
1914 3
1915 );
1916 let special_float_deeper_arrays = table["special_float_deeper_arrays"]
1917 .as_array()
1918 .expect("array");
1919 assert!(special_float_deeper_arrays[0][0][0]
1920 .as_float()
1921 .expect("float")
1922 .is_infinite());
1923 assert!(special_float_deeper_arrays[1][0][0]
1924 .as_float()
1925 .expect("float")
1926 .is_sign_negative());
1927 assert!(special_float_deeper_arrays[2][0][0]
1928 .as_float()
1929 .expect("float")
1930 .is_nan());
1931 assert_eq!(
1932 table["upper_exp_nested_mixed"]
1933 .as_array()
1934 .expect("array")
1935 .len(),
1936 2
1937 );
1938 assert!(table["special_float_inline_table"]["pos"]
1939 .as_float()
1940 .expect("float")
1941 .is_infinite());
1942 assert!(table["special_float_inline_table"]["nan"]
1943 .as_float()
1944 .expect("float")
1945 .is_nan());
1946 assert_eq!(
1947 table["special_float_mixed_nested"]
1948 .as_array()
1949 .expect("array")
1950 .len(),
1951 2
1952 );
1953 assert_eq!(
1954 table["nested_datetime_arrays"]
1955 .as_array()
1956 .expect("array")
1957 .len(),
1958 2
1959 );
1960 assert_eq!(
1961 table["upper_exp_nested_array"]
1962 .as_array()
1963 .expect("array")
1964 .len(),
1965 3
1966 );
1967 assert_eq!(
1968 table["positive_negative_offsets"]
1969 .as_array()
1970 .expect("array")
1971 .len(),
1972 2
1973 );
1974 assert_eq!(
1975 table["positive_offset_scalar_space"]
1976 .as_datetime()
1977 .map(std::string::ToString::to_string),
1978 Some("1979-05-27T07:32:00+07:00".to_owned())
1979 );
1980 assert_eq!(
1981 table["positive_offset_array_space"]
1982 .as_array()
1983 .expect("array")
1984 .len(),
1985 2
1986 );
1987 assert_eq!(
1988 table["utc_z"]
1989 .as_datetime()
1990 .map(std::string::ToString::to_string),
1991 Some("2024-01-02T03:04:05Z".to_owned())
1992 );
1993 assert_eq!(
1994 table["utc_lower_z"]
1995 .as_datetime()
1996 .map(std::string::ToString::to_string),
1997 Some("2024-01-02T03:04:05Z".to_owned())
1998 );
1999 assert_eq!(
2000 table["utc_fraction"]
2001 .as_datetime()
2002 .map(std::string::ToString::to_string),
2003 Some("2024-01-02T03:04:05.123456Z".to_owned())
2004 );
2005 assert_eq!(
2006 table["utc_fraction_array"].as_array().expect("array").len(),
2007 2
2008 );
2009 });
2010 }
2011
2012 #[test]
2013 fn rejects_newlines_in_single_line_strings() {
2014 Python::with_gil(|py| {
2015 let module = PyModule::from_code(
2016 py,
2017 pyo3::ffi::c_str!("template=t'value = \"a\\nb\"'\n"),
2018 pyo3::ffi::c_str!("test_toml_newline_error.py"),
2019 pyo3::ffi::c_str!("test_toml_newline_error"),
2020 )
2021 .unwrap();
2022 let template = module.getattr("template").unwrap();
2023 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2024 let err = match parse_template(&template) {
2025 Ok(_) => panic!("expected TOML parse failure"),
2026 Err(err) => err,
2027 };
2028 assert_eq!(err.kind, ErrorKind::Parse);
2029 assert!(err
2030 .message
2031 .contains("single-line basic strings cannot contain newlines"));
2032 });
2033 }
2034
2035 #[test]
2036 fn renders_toml_special_floats() {
2037 Python::with_gil(|py| {
2038 let module = PyModule::from_code(
2039 py,
2040 pyo3::ffi::c_str!(
2041 "pos=float('inf')\nneg=float('-inf')\nvalue=float('nan')\ntemplate=t'pos = {pos}\\nplus_inf = +inf\\nneg = {neg}\\nvalue = {value}\\nplus_nan = +nan\\nminus_nan = -nan\\n'\n"
2042 ),
2043 pyo3::ffi::c_str!("test_toml_special_floats.py"),
2044 pyo3::ffi::c_str!("test_toml_special_floats"),
2045 )
2046 .unwrap();
2047 let template = module.getattr("template").unwrap();
2048 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2049 let document = parse_template(&template).unwrap();
2050 let rendered = render_document(py, &document).unwrap();
2051 let table = rendered.data.as_table().expect("expected TOML table");
2052
2053 assert!(rendered.text.contains("pos = inf"));
2054 assert!(rendered.text.contains("plus_inf = +inf"));
2055 assert!(rendered.text.contains("neg = -inf"));
2056 assert!(rendered.text.contains("value = nan"));
2057 assert!(rendered.text.contains("plus_nan = +nan"));
2058 assert!(rendered.text.contains("minus_nan = -nan"));
2059 assert!(table["pos"].as_float().expect("float").is_infinite());
2060 assert!(table["plus_inf"].as_float().expect("float").is_infinite());
2061 assert!(table["neg"].as_float().expect("float").is_sign_negative());
2062 assert!(table["value"].as_float().expect("float").is_nan());
2063 assert!(table["plus_nan"].as_float().expect("float").is_nan());
2064 assert!(table["minus_nan"].as_float().expect("float").is_nan());
2065 });
2066 }
2067
2068 #[test]
2069 fn parses_arrays_with_trailing_commas() {
2070 Python::with_gil(|py| {
2071 let module = PyModule::from_code(
2072 py,
2073 pyo3::ffi::c_str!(
2074 "from string.templatelib import Template\ntemplate=Template('value = [1, 2,]\\nnested = [[ ], [1, 2,],]\\nempty_inline_tables = [{}, {}]\\nnested_empty_inline_arrays = { inner = [[], [1]] }\\n')\n"
2075 ),
2076 pyo3::ffi::c_str!("test_toml_trailing_comma.py"),
2077 pyo3::ffi::c_str!("test_toml_trailing_comma"),
2078 )
2079 .unwrap();
2080 let template = module.getattr("template").unwrap();
2081 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2082 let document = parse_template(&template).unwrap();
2083 let rendered = render_document(py, &document).unwrap();
2084 let table = rendered.data.as_table().expect("expected TOML table");
2085
2086 assert_eq!(
2087 table["value"]
2088 .as_array()
2089 .expect("array")
2090 .iter()
2091 .filter_map(toml::Value::as_integer)
2092 .collect::<Vec<_>>(),
2093 vec![1, 2]
2094 );
2095 assert_eq!(table["nested"].as_array().expect("array").len(), 2);
2096 assert_eq!(
2097 table["empty_inline_tables"]
2098 .as_array()
2099 .expect("array")
2100 .len(),
2101 2
2102 );
2103 assert_eq!(
2104 table["nested_empty_inline_arrays"]["inner"]
2105 .as_array()
2106 .expect("array")
2107 .len(),
2108 2
2109 );
2110 });
2111 }
2112
2113 #[test]
2114 fn renders_nested_collections_and_array_tables() {
2115 Python::with_gil(|py| {
2116 let module = PyModule::from_code(
2117 py,
2118 pyo3::ffi::c_str!(
2119 "from string.templatelib import Template\ntemplate=Template('matrix = [[1, 2], [3, 4]]\\nmeta = { inner = { value = 1 } }\\nnested_inline_arrays = { items = [[1, 2], [3, 4]] }\\ndeep_nested_inline = { inner = { deep = { value = 1 } } }\\ninline_table_array = [{ a = 1 }, { a = 2 }]\\ninline_table_array_nested = [[{ a = 1 }], [{ a = 2 }]]\\n[a]\\nvalue = 1\\n[[a.b]]\\nname = \"x\"\\n[[services]]\\nname = \"api\"\\n[[services]]\\nname = \"worker\"\\n')\n"
2120 ),
2121 pyo3::ffi::c_str!("test_toml_nested_collections.py"),
2122 pyo3::ffi::c_str!("test_toml_nested_collections"),
2123 )
2124 .unwrap();
2125 let template = module.getattr("template").unwrap();
2126 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2127 let document = parse_template(&template).unwrap();
2128 let rendered = render_document(py, &document).unwrap();
2129 let table = rendered.data.as_table().expect("expected TOML table");
2130
2131 assert_eq!(table["matrix"].as_array().expect("array").len(), 2);
2132 assert_eq!(table["meta"]["inner"]["value"].as_integer(), Some(1));
2133 assert_eq!(
2134 table["nested_inline_arrays"]["items"]
2135 .as_array()
2136 .expect("array")
2137 .len(),
2138 2
2139 );
2140 assert_eq!(
2141 table["deep_nested_inline"]["inner"]["deep"]["value"].as_integer(),
2142 Some(1)
2143 );
2144 assert_eq!(
2145 table["inline_table_array"].as_array().expect("array").len(),
2146 2
2147 );
2148 assert_eq!(table["inline_table_array"][0]["a"].as_integer(), Some(1));
2149 assert_eq!(table["inline_table_array"][1]["a"].as_integer(), Some(2));
2150 assert_eq!(
2151 table["inline_table_array_nested"]
2152 .as_array()
2153 .expect("array")
2154 .len(),
2155 2
2156 );
2157 assert_eq!(
2158 table["inline_table_array_nested"][0]
2159 .as_array()
2160 .expect("array")[0]["a"]
2161 .as_integer(),
2162 Some(1)
2163 );
2164 assert_eq!(
2165 table["inline_table_array_nested"][1]
2166 .as_array()
2167 .expect("array")[0]["a"]
2168 .as_integer(),
2169 Some(2)
2170 );
2171 assert_eq!(table["a"]["value"].as_integer(), Some(1));
2172 assert_eq!(table["a"]["b"].as_array().expect("array").len(), 1);
2173 assert_eq!(table["a"]["b"][0]["name"].as_str(), Some("x"));
2174 assert_eq!(table["services"].as_array().expect("array").len(), 2);
2175 });
2176 }
2177
2178 #[test]
2179 fn parses_headers_comments_and_crlf_literal_strings() {
2180 Python::with_gil(|py| {
2181 let module = PyModule::from_code(
2182 py,
2183 pyo3::ffi::c_str!(
2184 "from string.templatelib import Template\nquoted_header=t'[\"a.b\"]\\nvalue = 1\\n'\ndotted_header=t'[site.\"google.com\"]\\nvalue = 1\\n'\nquoted_segments=t'[\"a\".\"b\"]\\nvalue = 1\\n'\nquoted_header_then_dotted=Template('[\"a.b\"]\\nvalue = 1\\n\\n[\"a.b\".c]\\nname = \"x\"\\n')\ninline_comment=Template('value = { a = 1 } # comment\\n')\ncommented_array=t'value = [\\n 1,\\n # comment\\n 2,\\n]\\n'\nliteral_crlf=Template(\"value = '''a\\r\\nb'''\\n\")\narray_then_table=t'[[items]]\\nname = \"a\"\\n\\n[tool]\\nvalue = 1\\n'\n"
2185 ),
2186 pyo3::ffi::c_str!("test_toml_additional_surface.py"),
2187 pyo3::ffi::c_str!("test_toml_additional_surface"),
2188 )
2189 .unwrap();
2190
2191 let quoted_header = module.getattr("quoted_header").unwrap();
2192 let quoted_header = extract_template(py, "ed_header, "toml_t/toml_t_str").unwrap();
2193 let rendered = render_document(py, &parse_template("ed_header).unwrap()).unwrap();
2194 let table = rendered.data.as_table().expect("expected TOML table");
2195 assert_eq!(table["a.b"]["value"].as_integer(), Some(1));
2196
2197 let dotted_header = module.getattr("dotted_header").unwrap();
2198 let dotted_header = extract_template(py, &dotted_header, "toml_t/toml_t_str").unwrap();
2199 let rendered = render_document(py, &parse_template(&dotted_header).unwrap()).unwrap();
2200 let table = rendered.data.as_table().expect("expected TOML table");
2201 assert_eq!(table["site"]["google.com"]["value"].as_integer(), Some(1));
2202
2203 let quoted_segments = module.getattr("quoted_segments").unwrap();
2204 let quoted_segments =
2205 extract_template(py, "ed_segments, "toml_t/toml_t_str").unwrap();
2206 let rendered = render_document(py, &parse_template("ed_segments).unwrap()).unwrap();
2207 let table = rendered.data.as_table().expect("expected TOML table");
2208 assert_eq!(table["a"]["b"]["value"].as_integer(), Some(1));
2209
2210 let quoted_header_then_dotted = module.getattr("quoted_header_then_dotted").unwrap();
2211 let quoted_header_then_dotted =
2212 extract_template(py, "ed_header_then_dotted, "toml_t/toml_t_str").unwrap();
2213 let rendered =
2214 render_document(py, &parse_template("ed_header_then_dotted).unwrap()).unwrap();
2215 let table = rendered.data.as_table().expect("expected TOML table");
2216 assert_eq!(table["a.b"]["value"].as_integer(), Some(1));
2217 assert_eq!(table["a.b"]["c"]["name"].as_str(), Some("x"));
2218
2219 let inline_comment = module.getattr("inline_comment").unwrap();
2220 let inline_comment =
2221 extract_template(py, &inline_comment, "toml_t/toml_t_str").unwrap();
2222 let rendered = render_document(py, &parse_template(&inline_comment).unwrap()).unwrap();
2223 let table = rendered.data.as_table().expect("expected TOML table");
2224 assert_eq!(table["value"]["a"].as_integer(), Some(1));
2225
2226 let commented_array = module.getattr("commented_array").unwrap();
2227 let commented_array =
2228 extract_template(py, &commented_array, "toml_t/toml_t_str").unwrap();
2229 let rendered = render_document(py, &parse_template(&commented_array).unwrap()).unwrap();
2230 let table = rendered.data.as_table().expect("expected TOML table");
2231 assert_eq!(
2232 table["value"]
2233 .as_array()
2234 .expect("array")
2235 .iter()
2236 .filter_map(toml::Value::as_integer)
2237 .collect::<Vec<_>>(),
2238 vec![1, 2]
2239 );
2240
2241 let literal_crlf = module.getattr("literal_crlf").unwrap();
2242 let literal_crlf = extract_template(py, &literal_crlf, "toml_t/toml_t_str").unwrap();
2243 let rendered = render_document(py, &parse_template(&literal_crlf).unwrap()).unwrap();
2244 let table = rendered.data.as_table().expect("expected TOML table");
2245 assert_eq!(table["value"].as_str(), Some("a\nb"));
2246
2247 let array_then_table = module.getattr("array_then_table").unwrap();
2248 let array_then_table =
2249 extract_template(py, &array_then_table, "toml_t/toml_t_str").unwrap();
2250 let rendered =
2251 render_document(py, &parse_template(&array_then_table).unwrap()).unwrap();
2252 let table = rendered.data.as_table().expect("expected TOML table");
2253 assert_eq!(table["items"].as_array().expect("array").len(), 1);
2254 assert_eq!(table["tool"]["value"].as_integer(), Some(1));
2255 });
2256 }
2257
2258 #[test]
2259 fn rejects_multiline_inline_tables() {
2260 Python::with_gil(|py| {
2261 let module = PyModule::from_code(
2262 py,
2263 pyo3::ffi::c_str!(
2264 "from string.templatelib import Template\ntemplate=Template('value = { a = 1,\\n b = 2 }\\n')\n"
2265 ),
2266 pyo3::ffi::c_str!("test_toml_inline_table_newline.py"),
2267 pyo3::ffi::c_str!("test_toml_inline_table_newline"),
2268 )
2269 .unwrap();
2270 let template = module.getattr("template").unwrap();
2271 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2272 let err = match parse_template(&template) {
2273 Ok(_) => panic!("expected TOML parse failure"),
2274 Err(err) => err,
2275 };
2276 assert_eq!(err.kind, ErrorKind::Parse);
2277 assert!(err.message.contains("Expected a TOML key segment"));
2278 });
2279 }
2280
2281 #[test]
2282 fn rejects_invalid_table_redefinitions_and_newlines_after_dots() {
2283 Python::with_gil(|py| {
2284 let module = PyModule::from_code(
2285 py,
2286 pyo3::ffi::c_str!(
2287 "from string.templatelib import Template\ntable_redefine=Template('[a]\\nvalue = 1\\n[a]\\nname = \"x\"\\n')\narray_redefine=Template('[[a]]\\nname = \"x\"\\n[a]\\nvalue = 1\\n')\nnewline_after_dot=Template('a.\\nb = 1\\n')\nextra_array_comma=Template('value = [1,,2]\\n')\ninline_double_comma=Template('value = { a = 1,, b = 2 }\\n')\narray_leading_comma=Template('value = [,1]\\n')\ninline_trailing_comma=Template('value = { a = 1, }\\n')\ninvalid_decimal_underscore=Template('value = 1__2\\n')\ninvalid_hex_underscore=Template('value = 0x_DEAD\\n')\nheader_trailing_dot=Template('[a.]\\nvalue = 1\\n')\ninvalid_octal_underscore=Template('value = 0o_7\\n')\ninvalid_fraction_underscore=Template('value = 1_.0\\n')\ninvalid_plus_zero_float_underscore=Template('value = +0_.0\\n')\ninvalid_fraction_double_underscore=Template('value = 1_2.3__4\\n')\ninvalid_exp_double_underscore=Template('value = 1e1__0\\n')\ninvalid_double_plus=Template('value = ++1\\n')\ninvalid_double_plus_inline_table=Template('value = { pos = ++1 }\\n')\ninvalid_double_plus_nested=Template('value = { inner = { deeper = ++1 } }\\n')\ninvalid_double_plus_nested_inline_table=Template('value = { inner = { pos = ++1 } }\\n')\ninvalid_double_plus_array_nested=Template('value = [[++1]]\\n')\ninvalid_double_plus_array_mixed=Template('value = [1, ++1]\\n')\ninvalid_double_plus_after_scalar=Template('value = [1, 2, ++1]\\n')\nleading_zero=Template('value = 00\\n')\nleading_zero_plus=Template('value = +01\\n')\nleading_zero_float=Template('value = 01.2\\n')\nbinary_leading_underscore=Template('value = 0b_1\\n')\nsigned_binary=Template('value = +0b1\\n')\ntime_with_offset=Template('value = 03:04:05+09:00\\n')\nplus_inf_underscore=Template('value = +inf_\\n')\nplus_nan_underscore=Template('value = +nan_\\n')\ntime_lower_z=Template('value = 03:04:05z\\n')\ninvalid_exp_leading_underscore=Template('value = 1e_1\\n')\ninvalid_exp_trailing_underscore=Template('value = 1e1_\\n')\ndouble_sign_exp=Template('value = 1e--1\\n')\ndouble_sign_float=Template('value = --1.0\\n')\ndouble_dot_dotted_key=Template('a..b = 1\\n')\nhex_float_like=Template('value = 0x1.2\\n')\nsigned_octal=Template('value = -0o7\\n')\ninline_table_missing_comma=Template('value = { a = 1 b = 2 }\\n')\n"
2288 ),
2289 pyo3::ffi::c_str!("test_toml_invalid_tables.py"),
2290 pyo3::ffi::c_str!("test_toml_invalid_tables"),
2291 )
2292 .unwrap();
2293
2294 for name in ["table_redefine", "array_redefine"] {
2295 let template = module.getattr(name).unwrap();
2296 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2297 let document = parse_template(&template).unwrap();
2298 let rendered = render_document(py, &document).unwrap();
2299 let err =
2300 parse_rendered_toml(&rendered.text).expect_err("expected TOML parse failure");
2301 assert_eq!(err.kind, ErrorKind::Parse);
2302 assert!(
2303 err.message.contains("duplicate key"),
2304 "{name}: {}",
2305 err.message
2306 );
2307 }
2308
2309 let template = module.getattr("newline_after_dot").unwrap();
2310 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2311 let err = match parse_template(&template) {
2312 Ok(_) => panic!("expected TOML parse failure for newline_after_dot"),
2313 Err(err) => err,
2314 };
2315 assert_eq!(err.kind, ErrorKind::Parse);
2316 assert!(err.message.contains("Expected a TOML key segment"));
2317
2318 for (name, expected) in [
2319 ("extra_array_comma", "Expected a TOML value"),
2320 ("inline_double_comma", "Expected a TOML key segment"),
2321 ("array_leading_comma", "Expected a TOML value"),
2322 ("inline_trailing_comma", "Expected a TOML key segment"),
2323 ("invalid_decimal_underscore", "Invalid TOML literal"),
2324 ("invalid_hex_underscore", "Invalid TOML literal"),
2325 ("header_trailing_dot", "Expected a TOML key segment"),
2326 ("invalid_octal_underscore", "Invalid TOML literal"),
2327 ("invalid_fraction_underscore", "Invalid TOML literal"),
2328 ("invalid_plus_zero_float_underscore", "Invalid TOML literal"),
2329 ("invalid_fraction_double_underscore", "Invalid TOML literal"),
2330 ("invalid_exp_double_underscore", "Invalid TOML literal"),
2331 ("invalid_double_plus", "Invalid TOML literal"),
2332 ("invalid_double_plus_inline_table", "Invalid TOML literal"),
2333 ("invalid_double_plus_nested", "Invalid TOML literal"),
2334 (
2335 "invalid_double_plus_nested_inline_table",
2336 "Invalid TOML literal",
2337 ),
2338 ("invalid_double_plus_array_nested", "Invalid TOML literal"),
2339 ("invalid_double_plus_array_mixed", "Invalid TOML literal"),
2340 ("invalid_double_plus_after_scalar", "Invalid TOML literal"),
2341 ("leading_zero", "Invalid TOML literal"),
2342 ("leading_zero_plus", "Invalid TOML literal"),
2343 ("leading_zero_float", "Invalid TOML literal"),
2344 ("binary_leading_underscore", "Invalid TOML literal"),
2345 ("signed_binary", "Invalid TOML literal"),
2346 ("time_with_offset", "Invalid TOML literal"),
2347 ("plus_inf_underscore", "Invalid TOML literal"),
2348 ("plus_nan_underscore", "Invalid TOML literal"),
2349 ("time_lower_z", "Invalid TOML literal"),
2350 ("invalid_exp_leading_underscore", "Invalid TOML literal"),
2351 ("invalid_exp_trailing_underscore", "Invalid TOML literal"),
2352 ("double_sign_exp", "Invalid TOML literal"),
2353 ("double_sign_float", "Invalid TOML literal"),
2354 ("double_dot_dotted_key", "Expected a TOML key segment"),
2355 ("hex_float_like", "Invalid TOML literal"),
2356 ("signed_octal", "Invalid TOML literal"),
2357 ("inline_table_missing_comma", "Invalid TOML literal"),
2358 ] {
2359 let template = module.getattr(name).unwrap();
2360 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2361 let err = parse_template(&template).expect_err("expected TOML parse failure");
2362 assert_eq!(err.kind, ErrorKind::Parse);
2363 assert!(err.message.contains(expected), "{name}: {}", err.message);
2364 }
2365 });
2366 }
2367
2368 #[test]
2369 fn rejects_value_contracts() {
2370 Python::with_gil(|py| {
2371 let module = PyModule::from_code(
2372 py,
2373 pyo3::ffi::c_str!(
2374 "from datetime import UTC, time\nfrom string.templatelib import Template\nclass BadStringValue:\n def __str__(self):\n raise ValueError('cannot stringify')\nbad_key=3\nbad_time=time(1, 2, 3, tzinfo=UTC)\nbad_fragment=BadStringValue()\nkey_template=t'{bad_key} = 1'\nnull_template=t'name = {None}'\ntime_template=t'when = {bad_time}'\nfragment_template=t'title = \"hi-{bad_fragment}\"'\nduplicate_table=Template('[a]\\nvalue = 1\\n[a]\\nname = \"x\"\\n')\n"
2375 ),
2376 pyo3::ffi::c_str!("test_toml_value_contracts.py"),
2377 pyo3::ffi::c_str!("test_toml_value_contracts"),
2378 )
2379 .unwrap();
2380
2381 for (name, expected) in [
2382 ("key_template", "TOML keys must be str"),
2383 ("null_template", "TOML has no null value"),
2384 ("time_template", "timezone"),
2385 ("fragment_template", "string fragment"),
2386 ] {
2387 let template = module.getattr(name).unwrap();
2388 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2389 let document = parse_template(&template).unwrap();
2390 let err = match render_document(py, &document) {
2391 Ok(_) => panic!("expected TOML render failure"),
2392 Err(err) => err,
2393 };
2394 assert_eq!(err.kind, ErrorKind::Unrepresentable);
2395 assert!(err.message.contains(expected), "{name}: {}", err.message);
2396 }
2397
2398 let duplicate_table = module.getattr("duplicate_table").unwrap();
2399 let duplicate_table =
2400 extract_template(py, &duplicate_table, "toml_t/toml_t_str").unwrap();
2401 let document = parse_template(&duplicate_table).unwrap();
2402 let rendered = render_document(py, &document).unwrap();
2403 let err = parse_rendered_toml(&rendered.text)
2404 .expect_err("expected TOML duplicate-key parse failure");
2405 assert_eq!(err.kind, ErrorKind::Parse);
2406 assert!(err.message.contains("duplicate key"));
2407 });
2408 }
2409
2410 #[test]
2411 fn rejects_invalid_numeric_literal_families() {
2412 Python::with_gil(|py| {
2413 let module = PyModule::from_code(
2414 py,
2415 pyo3::ffi::c_str!(
2416 "from string.templatelib import Template\nleading_zero=Template('value = 00\\n')\npositive_leading_zero=Template('value = +01\\n')\ndouble_underscore=Template('value = 1__2\\n')\nhex_underscore=Template('value = 0x_DEAD\\n')\ndouble_plus=Template('value = ++1\\n')\n"
2417 ),
2418 pyo3::ffi::c_str!("test_toml_invalid_literals.py"),
2419 pyo3::ffi::c_str!("test_toml_invalid_literals"),
2420 )
2421 .unwrap();
2422
2423 for name in [
2424 "leading_zero",
2425 "positive_leading_zero",
2426 "double_underscore",
2427 "hex_underscore",
2428 "double_plus",
2429 ] {
2430 let template = module.getattr(name).unwrap();
2431 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2432 let err = parse_template(&template).expect_err("expected TOML parse failure");
2433 assert_eq!(err.kind, ErrorKind::Parse);
2434 assert!(
2435 err.message.contains("Invalid TOML literal"),
2436 "{name}: {}",
2437 err.message
2438 );
2439 }
2440 });
2441 }
2442
2443 #[test]
2444 fn rejects_additional_invalid_literal_families() {
2445 Python::with_gil(|py| {
2446 let module = PyModule::from_code(
2447 py,
2448 pyo3::ffi::c_str!(
2449 "from string.templatelib import Template\ninvalid_exp_mixed_sign=Template('value = 1e_+1\\n')\ninvalid_float_then_exp=Template('value = 1.e1\\n')\ninvalid_inline_pos=Template('value = { pos = ++1 }\\n')\ninvalid_nested_inline_pos=Template('value = { inner = { pos = ++1 } }\\n')\ninvalid_triple_nested_plus=Template('value = [[[++1]]]\\n')\n"
2450 ),
2451 pyo3::ffi::c_str!("test_toml_additional_invalid_literals.py"),
2452 pyo3::ffi::c_str!("test_toml_additional_invalid_literals"),
2453 )
2454 .unwrap();
2455
2456 for name in [
2457 "invalid_exp_mixed_sign",
2458 "invalid_float_then_exp",
2459 "invalid_inline_pos",
2460 "invalid_nested_inline_pos",
2461 "invalid_triple_nested_plus",
2462 ] {
2463 let template = module.getattr(name).unwrap();
2464 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2465 let err = parse_template(&template).expect_err("expected TOML parse failure");
2466 assert_eq!(err.kind, ErrorKind::Parse);
2467 assert!(
2468 err.message.contains("Invalid TOML literal"),
2469 "{name}: {}",
2470 err.message
2471 );
2472 }
2473 });
2474 }
2475
2476 #[test]
2477 fn rejects_bare_literal_fragment_and_suffix_families() {
2478 Python::with_gil(|py| {
2479 let module = PyModule::from_code(
2480 py,
2481 pyo3::ffi::c_str!(
2482 "count=1\nfragment=t'value = 2{count}\\n'\nsuffix=t'value = {count}ms\\n'\n"
2483 ),
2484 pyo3::ffi::c_str!("test_toml_literal_fragments.py"),
2485 pyo3::ffi::c_str!("test_toml_literal_fragments"),
2486 )
2487 .unwrap();
2488
2489 for (name, expected) in [
2490 (
2491 "fragment",
2492 "TOML bare literals cannot contain fragment interpolations.",
2493 ),
2494 (
2495 "suffix",
2496 "Whole-value TOML interpolations cannot have bare suffix text.",
2497 ),
2498 ] {
2499 let template = module.getattr(name).unwrap();
2500 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2501 let err = parse_template(&template).expect_err("expected TOML parse failure");
2502 assert_eq!(err.kind, ErrorKind::Parse);
2503 assert!(err.message.contains(expected), "{name}: {}", err.message);
2504 }
2505 });
2506 }
2507
2508 #[test]
2509 fn renders_header_progressions_comments_and_crlf_text() {
2510 Python::with_gil(|py| {
2511 let module = PyModule::from_code(
2512 py,
2513 pyo3::ffi::c_str!(
2514 "from string.templatelib import Template\nquoted_header_then_dotted=Template('[\"a.b\"]\\nvalue = 1\\n\\n[\"a.b\".c]\\nname = \"x\"\\n')\ncommented_array=t'value = [\\n 1,\\n # comment\\n 2,\\n]\\n'\nliteral_crlf=Template(\"value = '''a\\r\\nb'''\\n\")\narray_then_table=t'[[items]]\\nname = \"a\"\\n\\n[tool]\\nvalue = 1\\n'\n"
2515 ),
2516 pyo3::ffi::c_str!("test_toml_render_progressions.py"),
2517 pyo3::ffi::c_str!("test_toml_render_progressions"),
2518 )
2519 .unwrap();
2520
2521 let quoted_header_then_dotted = module.getattr("quoted_header_then_dotted").unwrap();
2522 let quoted_header_then_dotted =
2523 extract_template(py, "ed_header_then_dotted, "toml_t/toml_t_str").unwrap();
2524 let rendered =
2525 render_document(py, &parse_template("ed_header_then_dotted).unwrap()).unwrap();
2526 assert_eq!(
2527 rendered.text,
2528 "[\"a.b\"]\nvalue = 1\n[\"a.b\".c]\nname = \"x\""
2529 );
2530 let table = rendered.data.as_table().expect("expected TOML table");
2531 assert_eq!(table["a.b"]["c"]["name"].as_str(), Some("x"));
2532
2533 let commented_array = module.getattr("commented_array").unwrap();
2534 let commented_array =
2535 extract_template(py, &commented_array, "toml_t/toml_t_str").unwrap();
2536 let rendered = render_document(py, &parse_template(&commented_array).unwrap()).unwrap();
2537 assert_eq!(rendered.text, "value = [1, 2]");
2538 assert_eq!(
2539 rendered.data["value"]
2540 .as_array()
2541 .expect("array")
2542 .iter()
2543 .filter_map(toml::Value::as_integer)
2544 .collect::<Vec<_>>(),
2545 vec![1, 2]
2546 );
2547
2548 let literal_crlf = module.getattr("literal_crlf").unwrap();
2549 let literal_crlf = extract_template(py, &literal_crlf, "toml_t/toml_t_str").unwrap();
2550 let rendered = render_document(py, &parse_template(&literal_crlf).unwrap()).unwrap();
2551 assert_eq!(rendered.text, "value = \"a\\nb\"");
2552 assert_eq!(rendered.data["value"].as_str(), Some("a\nb"));
2553
2554 let array_then_table = module.getattr("array_then_table").unwrap();
2555 let array_then_table =
2556 extract_template(py, &array_then_table, "toml_t/toml_t_str").unwrap();
2557 let rendered =
2558 render_document(py, &parse_template(&array_then_table).unwrap()).unwrap();
2559 assert_eq!(rendered.text, "[[items]]\nname = \"a\"\n[tool]\nvalue = 1");
2560 let table = rendered.data.as_table().expect("expected TOML table");
2561 assert_eq!(table["items"].as_array().expect("array").len(), 1);
2562 assert_eq!(table["tool"]["value"].as_integer(), Some(1));
2563 });
2564 }
2565
2566 #[test]
2567 fn renders_temporal_values_and_special_float_arrays() {
2568 Python::with_gil(|py| {
2569 let module = PyModule::from_code(
2570 py,
2571 pyo3::ffi::c_str!(
2572 "from datetime import date, datetime, time, timedelta, timezone\nlocal_date=date(2024, 1, 2)\nlocal_time=time(3, 4, 5, 678901)\noffset_time=datetime(1979, 5, 27, 7, 32, 0, 999999, tzinfo=timezone(timedelta(hours=-7)))\ndoc=t'local_date = {local_date}\\nlocal_time = {local_time}\\noffset_times = [{offset_time}]\\nspecial = [+inf, -inf, nan]\\n'\n"
2573 ),
2574 pyo3::ffi::c_str!("test_toml_temporal_arrays.py"),
2575 pyo3::ffi::c_str!("test_toml_temporal_arrays"),
2576 )
2577 .unwrap();
2578
2579 let doc = module.getattr("doc").unwrap();
2580 let doc = extract_template(py, &doc, "toml_t/toml_t_str").unwrap();
2581 let rendered = render_document(py, &parse_template(&doc).unwrap()).unwrap();
2582 assert_eq!(
2583 rendered.text,
2584 "local_date = 2024-01-02\nlocal_time = 03:04:05.678901\noffset_times = [1979-05-27T07:32:00.999999-07:00]\nspecial = [+inf, -inf, nan]"
2585 );
2586 assert_eq!(
2587 rendered.data["local_date"]
2588 .as_datetime()
2589 .map(ToString::to_string),
2590 Some("2024-01-02".to_string())
2591 );
2592 assert_eq!(
2593 rendered.data["local_time"]
2594 .as_datetime()
2595 .map(ToString::to_string),
2596 Some("03:04:05.678901".to_string())
2597 );
2598 assert_eq!(
2599 rendered.data["offset_times"][0]
2600 .as_datetime()
2601 .map(ToString::to_string),
2602 Some("1979-05-27T07:32:00.999999-07:00".to_string())
2603 );
2604 assert_eq!(
2605 rendered.data["special"]
2606 .as_array()
2607 .expect("special array")
2608 .iter()
2609 .map(ToString::to_string)
2610 .collect::<Vec<_>>(),
2611 vec!["inf".to_string(), "-inf".to_string(), "nan".to_string()]
2612 );
2613 });
2614 }
2615
2616 #[test]
2617 fn renders_end_to_end_supported_positions_text_and_data() {
2618 Python::with_gil(|py| {
2619 let module = PyModule::from_code(
2620 py,
2621 pyo3::ffi::c_str!(
2622 "from datetime import UTC, datetime\nkey='leaf'\nleft='prefix'\nright='suffix'\ncreated=datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC)\ntemplate=t'''\ntitle = \"item-{left}\"\n[root.{key}]\nname = {right}\nlabel = \"{left}-{right}\"\ncreated = {created}\nrows = [{left}, {right}]\nmeta = {{ enabled = true, target = {right} }}\n'''\n"
2623 ),
2624 pyo3::ffi::c_str!("test_toml_end_to_end_positions.py"),
2625 pyo3::ffi::c_str!("test_toml_end_to_end_positions"),
2626 )
2627 .unwrap();
2628
2629 let template = module.getattr("template").unwrap();
2630 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2631 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2632 assert_eq!(
2633 rendered.text,
2634 "title = \"item-prefix\"\n[root.\"leaf\"]\nname = \"suffix\"\nlabel = \"prefix-suffix\"\ncreated = 2024-01-02T03:04:05+00:00\nrows = [\"prefix\", \"suffix\"]\nmeta = { enabled = true, target = \"suffix\" }"
2635 );
2636 let table = rendered.data.as_table().expect("expected TOML table");
2637 assert_eq!(table["title"].as_str(), Some("item-prefix"));
2638 assert_eq!(table["root"]["leaf"]["name"].as_str(), Some("suffix"));
2639 assert_eq!(
2640 table["root"]["leaf"]["label"].as_str(),
2641 Some("prefix-suffix")
2642 );
2643 assert_eq!(
2644 table["root"]["leaf"]["created"]
2645 .as_datetime()
2646 .map(ToString::to_string),
2647 Some("2024-01-02T03:04:05+00:00".to_string())
2648 );
2649 assert_eq!(
2650 table["root"]["leaf"]["rows"]
2651 .as_array()
2652 .expect("rows")
2653 .iter()
2654 .filter_map(toml::Value::as_str)
2655 .collect::<Vec<_>>(),
2656 vec!["prefix", "suffix"]
2657 );
2658 assert_eq!(
2659 table["root"]["leaf"]["meta"]["target"].as_str(),
2660 Some("suffix")
2661 );
2662 });
2663 }
2664
2665 #[test]
2666 fn renders_string_families_exact_text_and_data() {
2667 Python::with_gil(|py| {
2668 let module = PyModule::from_code(
2669 py,
2670 pyo3::ffi::c_str!(
2671 "value='name'\nbasic=t'basic = \"hi-{value}\"'\nliteral=t\"literal = 'hi-{value}'\"\nmulti_basic=t'multi_basic = \"\"\"hi-{value}\"\"\"'\nmulti_literal=t\"\"\"multi_literal = '''hi-{value}'''\"\"\"\n"
2672 ),
2673 pyo3::ffi::c_str!("test_toml_string_families_render.py"),
2674 pyo3::ffi::c_str!("test_toml_string_families_render"),
2675 )
2676 .unwrap();
2677
2678 for (name, expected_key) in [
2679 ("basic", "basic"),
2680 ("literal", "literal"),
2681 ("multi_basic", "multi_basic"),
2682 ("multi_literal", "multi_literal"),
2683 ] {
2684 let template = module.getattr(name).unwrap();
2685 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2686 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2687 assert_eq!(rendered.data[expected_key].as_str(), Some("hi-name"));
2688 assert!(
2689 rendered.text.contains("hi-name"),
2690 "{name}: {}",
2691 rendered.text
2692 );
2693 }
2694 });
2695 }
2696
2697 #[test]
2698 fn renders_date_and_time_round_trip_shapes() {
2699 Python::with_gil(|py| {
2700 let module = PyModule::from_code(
2701 py,
2702 pyo3::ffi::c_str!(
2703 "from datetime import date, time\nday=date(2024, 1, 2)\nmoment=time(4, 5, 6)\ntemplate=t'day = {day}\\nmoment = {moment}'\n"
2704 ),
2705 pyo3::ffi::c_str!("test_toml_date_time_round_trip.py"),
2706 pyo3::ffi::c_str!("test_toml_date_time_round_trip"),
2707 )
2708 .unwrap();
2709
2710 let template = module.getattr("template").unwrap();
2711 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2712 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2713 assert_eq!(rendered.text, "day = 2024-01-02\nmoment = 04:05:06");
2714 assert_eq!(
2715 rendered.data["day"].as_datetime().map(ToString::to_string),
2716 Some("2024-01-02".to_string())
2717 );
2718 assert_eq!(
2719 rendered.data["moment"]
2720 .as_datetime()
2721 .map(ToString::to_string),
2722 Some("04:05:06".to_string())
2723 );
2724 });
2725 }
2726
2727 #[test]
2728 fn renders_array_tables_and_comment_preserving_shapes() {
2729 Python::with_gil(|py| {
2730 let module = PyModule::from_code(
2731 py,
2732 pyo3::ffi::c_str!(
2733 "name='api'\nworker='worker'\ntemplate=t'''\n# comment before content\n[[services]]\nname = {name} # inline comment\n\n[[services]]\nname = {worker}\n'''\n"
2734 ),
2735 pyo3::ffi::c_str!("test_toml_array_tables_comments.py"),
2736 pyo3::ffi::c_str!("test_toml_array_tables_comments"),
2737 )
2738 .unwrap();
2739
2740 let template = module.getattr("template").unwrap();
2741 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2742 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2743 assert_eq!(
2744 rendered.text,
2745 "[[services]]\nname = \"api\"\n[[services]]\nname = \"worker\""
2746 );
2747 let services = rendered.data["services"]
2748 .as_array()
2749 .expect("services array");
2750 assert_eq!(services.len(), 2);
2751 assert_eq!(services[0]["name"].as_str(), Some("api"));
2752 assert_eq!(services[1]["name"].as_str(), Some("worker"));
2753 });
2754 }
2755
2756 #[test]
2757 fn renders_array_of_tables_spec_example_text_and_data() {
2758 Python::with_gil(|py| {
2759 let module = PyModule::from_code(
2760 py,
2761 pyo3::ffi::c_str!(
2762 "template=t'[[products]]\\nname = \"Hammer\"\\nsku = 738594937\\n\\n[[products]]\\nname = \"Nail\"\\nsku = 284758393\\ncolor = \"gray\"\\n'\n"
2763 ),
2764 pyo3::ffi::c_str!("test_toml_array_of_tables_spec_example.py"),
2765 pyo3::ffi::c_str!("test_toml_array_of_tables_spec_example"),
2766 )
2767 .unwrap();
2768
2769 let template = module.getattr("template").unwrap();
2770 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2771 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2772 assert_eq!(
2773 rendered.text,
2774 "[[products]]\nname = \"Hammer\"\nsku = 738594937\n[[products]]\nname = \"Nail\"\nsku = 284758393\ncolor = \"gray\""
2775 );
2776 let products = rendered.data["products"]
2777 .as_array()
2778 .expect("products array");
2779 assert_eq!(products.len(), 2);
2780 assert_eq!(products[0]["name"].as_str(), Some("Hammer"));
2781 assert_eq!(products[0]["sku"].as_integer(), Some(738594937));
2782 assert_eq!(products[1]["name"].as_str(), Some("Nail"));
2783 assert_eq!(products[1]["sku"].as_integer(), Some(284758393));
2784 assert_eq!(products[1]["color"].as_str(), Some("gray"));
2785 });
2786 }
2787
2788 #[test]
2789 fn renders_nested_array_of_tables_spec_hierarchy_text_and_data() {
2790 Python::with_gil(|py| {
2791 let module = PyModule::from_code(
2792 py,
2793 pyo3::ffi::c_str!(
2794 "template=t'[[fruit]]\\nname = \"apple\"\\n\\n[fruit.physical]\\ncolor = \"red\"\\nshape = \"round\"\\n\\n[[fruit.variety]]\\nname = \"red delicious\"\\n\\n[[fruit.variety]]\\nname = \"granny smith\"\\n\\n[[fruit]]\\nname = \"banana\"\\n\\n[[fruit.variety]]\\nname = \"plantain\"\\n'\n"
2795 ),
2796 pyo3::ffi::c_str!("test_toml_nested_array_tables_spec_example.py"),
2797 pyo3::ffi::c_str!("test_toml_nested_array_tables_spec_example"),
2798 )
2799 .unwrap();
2800
2801 let template = module.getattr("template").unwrap();
2802 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2803 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2804 assert_eq!(
2805 rendered.text,
2806 "[[fruit]]\nname = \"apple\"\n[fruit.physical]\ncolor = \"red\"\nshape = \"round\"\n[[fruit.variety]]\nname = \"red delicious\"\n[[fruit.variety]]\nname = \"granny smith\"\n[[fruit]]\nname = \"banana\"\n[[fruit.variety]]\nname = \"plantain\""
2807 );
2808 let fruit = rendered.data["fruit"].as_array().expect("fruit array");
2809 assert_eq!(fruit.len(), 2);
2810 assert_eq!(fruit[0]["name"].as_str(), Some("apple"));
2811 assert_eq!(fruit[0]["physical"]["color"].as_str(), Some("red"));
2812 assert_eq!(fruit[0]["physical"]["shape"].as_str(), Some("round"));
2813 let varieties = fruit[0]["variety"].as_array().expect("apple varieties");
2814 assert_eq!(varieties.len(), 2);
2815 assert_eq!(varieties[0]["name"].as_str(), Some("red delicious"));
2816 assert_eq!(varieties[1]["name"].as_str(), Some("granny smith"));
2817 assert_eq!(fruit[1]["name"].as_str(), Some("banana"));
2818 let varieties = fruit[1]["variety"].as_array().expect("banana varieties");
2819 assert_eq!(varieties.len(), 1);
2820 assert_eq!(varieties[0]["name"].as_str(), Some("plantain"));
2821 });
2822 }
2823
2824 #[test]
2825 fn renders_main_spec_example_text_and_data() {
2826 Python::with_gil(|py| {
2827 let module = PyModule::from_code(
2828 py,
2829 pyo3::ffi::c_str!(
2830 "template=t'title = \"TOML Example\"\\n\\n[owner]\\nname = \"Tom Preston-Werner\"\\ndob = 1979-05-27T07:32:00-08:00\\n\\n[database]\\nenabled = true\\nports = [ 8000, 8001, 8002 ]\\ndata = [ [\"delta\", \"phi\"], [3.14] ]\\ntemp_targets = {{ cpu = 79.5, case = 72.0 }}\\n\\n[servers]\\n\\n[servers.alpha]\\nip = \"10.0.0.1\"\\nrole = \"frontend\"\\n\\n[servers.beta]\\nip = \"10.0.0.2\"\\nrole = \"backend\"\\n'\n"
2831 ),
2832 pyo3::ffi::c_str!("test_toml_main_spec_example.py"),
2833 pyo3::ffi::c_str!("test_toml_main_spec_example"),
2834 )
2835 .unwrap();
2836
2837 let template = module.getattr("template").unwrap();
2838 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2839 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2840 assert_eq!(
2841 rendered.text,
2842 "title = \"TOML Example\"\n[owner]\nname = \"Tom Preston-Werner\"\ndob = 1979-05-27T07:32:00-08:00\n[database]\nenabled = true\nports = [8000, 8001, 8002]\ndata = [[\"delta\", \"phi\"], [3.14]]\ntemp_targets = { cpu = 79.5, case = 72.0 }\n[servers]\n[servers.alpha]\nip = \"10.0.0.1\"\nrole = \"frontend\"\n[servers.beta]\nip = \"10.0.0.2\"\nrole = \"backend\""
2843 );
2844 assert_eq!(rendered.data["title"].as_str(), Some("TOML Example"));
2845 assert_eq!(
2846 rendered.data["owner"]["name"].as_str(),
2847 Some("Tom Preston-Werner")
2848 );
2849 assert_eq!(rendered.data["database"]["enabled"].as_bool(), Some(true));
2850 assert_eq!(
2851 rendered.data["database"]["ports"]
2852 .as_array()
2853 .expect("ports array")
2854 .len(),
2855 3
2856 );
2857 assert_eq!(
2858 rendered.data["servers"]["alpha"]["ip"].as_str(),
2859 Some("10.0.0.1")
2860 );
2861 assert_eq!(
2862 rendered.data["servers"]["beta"]["role"].as_str(),
2863 Some("backend")
2864 );
2865 });
2866 }
2867
2868 #[test]
2869 fn renders_empty_headers_and_empty_path_segments() {
2870 Python::with_gil(|py| {
2871 let module = PyModule::from_code(
2872 py,
2873 pyo3::ffi::c_str!(
2874 "empty_header=t'[\"\"]\\nvalue = 1\\n'\nempty_segment=t'a.\"\".b = 1\\n'\nempty_subsegments=t'[\"\".\"\".leaf]\\nvalue = 1\\n'\n"
2875 ),
2876 pyo3::ffi::c_str!("test_toml_empty_path_shapes.py"),
2877 pyo3::ffi::c_str!("test_toml_empty_path_shapes"),
2878 )
2879 .unwrap();
2880
2881 let empty_header = module.getattr("empty_header").unwrap();
2882 let empty_header = extract_template(py, &empty_header, "toml_t/toml_t_str").unwrap();
2883 let rendered = render_document(py, &parse_template(&empty_header).unwrap()).unwrap();
2884 assert_eq!(rendered.text, "[\"\"]\nvalue = 1");
2885 assert_eq!(rendered.data[""]["value"].as_integer(), Some(1));
2886
2887 let empty_segment = module.getattr("empty_segment").unwrap();
2888 let empty_segment = extract_template(py, &empty_segment, "toml_t/toml_t_str").unwrap();
2889 let rendered = render_document(py, &parse_template(&empty_segment).unwrap()).unwrap();
2890 assert_eq!(rendered.text, "a.\"\".b = 1");
2891 assert_eq!(rendered.data["a"][""]["b"].as_integer(), Some(1));
2892
2893 let empty_subsegments = module.getattr("empty_subsegments").unwrap();
2894 let empty_subsegments =
2895 extract_template(py, &empty_subsegments, "toml_t/toml_t_str").unwrap();
2896 let rendered =
2897 render_document(py, &parse_template(&empty_subsegments).unwrap()).unwrap();
2898 assert_eq!(rendered.text, "[\"\".\"\".leaf]\nvalue = 1");
2899 assert_eq!(rendered.data[""][""]["leaf"]["value"].as_integer(), Some(1));
2900 });
2901 }
2902
2903 #[test]
2904 fn renders_special_float_nested_shapes() {
2905 Python::with_gil(|py| {
2906 let module = PyModule::from_code(
2907 py,
2908 pyo3::ffi::c_str!(
2909 "template=t'special_float_inline_table = {{ pos = +inf, neg = -inf, nan = nan }}\\nspecial_float_mixed_nested = [[+inf, -inf], [nan]]\\n'\n"
2910 ),
2911 pyo3::ffi::c_str!("test_toml_special_float_nested.py"),
2912 pyo3::ffi::c_str!("test_toml_special_float_nested"),
2913 )
2914 .unwrap();
2915
2916 let template = module.getattr("template").unwrap();
2917 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2918 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2919 assert_eq!(
2920 rendered.text,
2921 "special_float_inline_table = { pos = +inf, neg = -inf, nan = nan }\nspecial_float_mixed_nested = [[+inf, -inf], [nan]]"
2922 );
2923 assert!(rendered.data["special_float_inline_table"]["pos"]
2924 .as_float()
2925 .expect("pos float")
2926 .is_infinite());
2927 assert!(rendered.data["special_float_inline_table"]["neg"]
2928 .as_float()
2929 .expect("neg float")
2930 .is_sign_negative());
2931 assert!(rendered.data["special_float_inline_table"]["nan"]
2932 .as_float()
2933 .expect("nan float")
2934 .is_nan());
2935 assert!(rendered.data["special_float_mixed_nested"][0][0]
2936 .as_float()
2937 .expect("nested pos")
2938 .is_infinite());
2939 assert!(rendered.data["special_float_mixed_nested"][0][1]
2940 .as_float()
2941 .expect("nested neg")
2942 .is_sign_negative());
2943 assert!(rendered.data["special_float_mixed_nested"][1][0]
2944 .as_float()
2945 .expect("nested nan")
2946 .is_nan());
2947 });
2948 }
2949
2950 #[test]
2951 fn renders_numeric_and_datetime_literal_shapes() {
2952 Python::with_gil(|py| {
2953 let module = PyModule::from_code(
2954 py,
2955 pyo3::ffi::c_str!(
2956 "from string.templatelib import Template\ntemplate=Template('plus_int = +1\\nplus_zero = +0\\nplus_zero_float = +0.0\\nlocal_date = 2024-01-02\\nlocal_time_fraction = 03:04:05.123456\\noffset_fraction_dt = 1979-05-27T07:32:00.999999-07:00\\nutc_fraction_lower_array = [2024-01-02T03:04:05.123456z, 2024-01-02T03:04:06z]\\nsigned_int_array = [+1, +0, -1]\\n')\n"
2957 ),
2958 pyo3::ffi::c_str!("test_toml_numeric_datetime_literal_shapes.py"),
2959 pyo3::ffi::c_str!("test_toml_numeric_datetime_literal_shapes"),
2960 )
2961 .unwrap();
2962
2963 let template = module.getattr("template").unwrap();
2964 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2965 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2966 assert_eq!(
2967 rendered.text,
2968 "plus_int = +1\nplus_zero = +0\nplus_zero_float = +0.0\nlocal_date = 2024-01-02\nlocal_time_fraction = 03:04:05.123456\noffset_fraction_dt = 1979-05-27T07:32:00.999999-07:00\nutc_fraction_lower_array = [2024-01-02T03:04:05.123456z, 2024-01-02T03:04:06z]\nsigned_int_array = [+1, +0, -1]"
2969 );
2970 assert_eq!(rendered.data["plus_int"].as_integer(), Some(1));
2971 assert_eq!(rendered.data["plus_zero"].as_integer(), Some(0));
2972 assert_eq!(rendered.data["plus_zero_float"].as_float(), Some(0.0));
2973 assert_eq!(
2974 rendered.data["local_date"]
2975 .as_datetime()
2976 .and_then(|value| value.date.as_ref())
2977 .map(ToString::to_string),
2978 Some("2024-01-02".to_string())
2979 );
2980 assert_eq!(
2981 rendered.data["local_time_fraction"]
2982 .as_datetime()
2983 .and_then(|value| value.time.as_ref())
2984 .map(ToString::to_string),
2985 Some("03:04:05.123456".to_string())
2986 );
2987 assert_eq!(
2988 rendered.data["offset_fraction_dt"]
2989 .as_datetime()
2990 .map(ToString::to_string),
2991 Some("1979-05-27T07:32:00.999999-07:00".to_string())
2992 );
2993 assert_eq!(
2994 rendered.data["utc_fraction_lower_array"]
2995 .as_array()
2996 .expect("utc array")
2997 .len(),
2998 2
2999 );
3000 assert_eq!(
3001 rendered.data["signed_int_array"]
3002 .as_array()
3003 .expect("signed array")
3004 .iter()
3005 .filter_map(toml::Value::as_integer)
3006 .collect::<Vec<_>>(),
3007 vec![1, 0, -1]
3008 );
3009 });
3010 }
3011
3012 #[test]
3013 fn test_parse_rendered_toml_surfaces_parse_failures() {
3014 let err = parse_rendered_toml("[a]\nvalue = 1\n[a]\nname = \"x\"\n")
3015 .expect_err("expected TOML parse failure");
3016 assert_eq!(err.kind, ErrorKind::Parse);
3017 assert!(err.message.contains("Rendered TOML could not be reparsed"));
3018 assert!(err.message.contains("duplicate key"));
3019 }
3020}