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 parse_validated_template_with_profile(
1114 template: &TemplateInput,
1115 profile: TomlProfile,
1116) -> BackendResult<TomlDocumentNode> {
1117 parse_template_with_profile(template, profile)
1121}
1122
1123pub fn parse_validated_template(template: &TemplateInput) -> BackendResult<TomlDocumentNode> {
1124 parse_validated_template_with_profile(template, TomlProfile::default())
1125}
1126
1127pub fn validate_template_with_profile(
1128 template: &TemplateInput,
1129 profile: TomlProfile,
1130) -> BackendResult<()> {
1131 parse_validated_template_with_profile(template, profile).map(|_| ())
1132}
1133
1134pub fn validate_template(template: &TemplateInput) -> BackendResult<()> {
1135 validate_template_with_profile(template, TomlProfile::default())
1136}
1137
1138pub fn check_template_with_profile(
1139 template: &TemplateInput,
1140 profile: TomlProfile,
1141) -> BackendResult<()> {
1142 validate_template_with_profile(template, profile)
1143}
1144
1145pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
1146 check_template_with_profile(template, TomlProfile::default())
1147}
1148
1149pub fn format_template_with_profile(
1150 template: &TemplateInput,
1151 profile: TomlProfile,
1152) -> BackendResult<String> {
1153 let document = parse_validated_template_with_profile(template, profile)?;
1154 format_toml_document(template, &document)
1155}
1156
1157pub fn format_template(template: &TemplateInput) -> BackendResult<String> {
1158 format_template_with_profile(template, TomlProfile::default())
1159}
1160
1161pub fn normalize_document_with_profile(
1162 value: &toml::Value,
1163 _profile: TomlProfile,
1164) -> BackendResult<NormalizedStream> {
1165 Ok(NormalizedStream::new(vec![NormalizedDocument::Value(
1166 normalize_value(value)?,
1167 )]))
1168}
1169
1170pub fn normalize_document(value: &toml::Value) -> BackendResult<NormalizedStream> {
1171 normalize_document_with_profile(value, TomlProfile::default())
1172}
1173
1174pub fn normalize_value(value: &toml::Value) -> BackendResult<NormalizedValue> {
1175 match value {
1176 toml::Value::String(value) => Ok(NormalizedValue::String(value.clone())),
1177 toml::Value::Integer(value) => Ok(NormalizedValue::Integer((*value).into())),
1178 toml::Value::Float(value) => Ok(NormalizedValue::Float(normalize_float(*value))),
1179 toml::Value::Boolean(value) => Ok(NormalizedValue::Bool(*value)),
1180 toml::Value::Datetime(value) => Ok(NormalizedValue::Temporal(normalize_datetime(value)?)),
1181 toml::Value::Array(values) => values
1182 .iter()
1183 .map(normalize_value)
1184 .collect::<BackendResult<Vec<_>>>()
1185 .map(NormalizedValue::Sequence),
1186 toml::Value::Table(values) => values
1187 .iter()
1188 .map(|(key, value)| {
1189 Ok(NormalizedEntry {
1190 key: NormalizedKey::String(key.clone()),
1191 value: normalize_value(value)?,
1192 })
1193 })
1194 .collect::<BackendResult<Vec<_>>>()
1195 .map(NormalizedValue::Mapping),
1196 }
1197}
1198
1199fn format_toml_document(
1200 template: &TemplateInput,
1201 node: &TomlDocumentNode,
1202) -> BackendResult<String> {
1203 node.statements
1204 .iter()
1205 .map(|statement| format_toml_statement(template, statement))
1206 .collect::<BackendResult<Vec<_>>>()
1207 .map(|statements| statements.join("\n"))
1208}
1209
1210fn format_toml_statement(
1211 template: &TemplateInput,
1212 node: &TomlStatementNode,
1213) -> BackendResult<String> {
1214 match node {
1215 TomlStatementNode::Assignment(node) => Ok(format!(
1216 "{} = {}",
1217 format_key_path(template, &node.key_path)?,
1218 format_toml_value(template, &node.value)?
1219 )),
1220 TomlStatementNode::TableHeader(node) => {
1221 Ok(format!("[{}]", format_key_path(template, &node.key_path)?))
1222 }
1223 TomlStatementNode::ArrayTableHeader(node) => Ok(format!(
1224 "[[{}]]",
1225 format_key_path(template, &node.key_path)?
1226 )),
1227 }
1228}
1229
1230fn format_key_path(template: &TemplateInput, node: &TomlKeyPathNode) -> BackendResult<String> {
1231 node.segments
1232 .iter()
1233 .map(|segment| format_key_segment(template, segment))
1234 .collect::<BackendResult<Vec<_>>>()
1235 .map(|segments| segments.join("."))
1236}
1237
1238fn format_key_segment(
1239 template: &TemplateInput,
1240 node: &TomlKeySegmentNode,
1241) -> BackendResult<String> {
1242 match &node.value {
1243 TomlKeySegmentValue::Bare(value) => {
1244 if node.bare && is_bare_key(value) {
1245 Ok(value.clone())
1246 } else {
1247 Ok(render_basic_string(value))
1248 }
1249 }
1250 TomlKeySegmentValue::String(value) => format_toml_string(template, value),
1251 TomlKeySegmentValue::Interpolation(value) => {
1252 interpolation_raw_source(template, value.interpolation_index, &value.span, "TOML key")
1253 }
1254 }
1255}
1256
1257fn format_toml_value(template: &TemplateInput, node: &TomlValueNode) -> BackendResult<String> {
1258 match node {
1259 TomlValueNode::String(node) => format_toml_string(template, node),
1260 TomlValueNode::Literal(node) => Ok(node.source.clone()),
1261 TomlValueNode::Interpolation(node) => {
1262 interpolation_raw_source(template, node.interpolation_index, &node.span, "TOML value")
1263 }
1264 TomlValueNode::Array(node) => node
1265 .items
1266 .iter()
1267 .map(|item| format_toml_value(template, item))
1268 .collect::<BackendResult<Vec<_>>>()
1269 .map(|items| format!("[{}]", items.join(", "))),
1270 TomlValueNode::InlineTable(node) => node
1271 .entries
1272 .iter()
1273 .map(|entry| {
1274 Ok(format!(
1275 "{} = {}",
1276 format_key_path(template, &entry.key_path)?,
1277 format_toml_value(template, &entry.value)?
1278 ))
1279 })
1280 .collect::<BackendResult<Vec<_>>>()
1281 .map(|entries| {
1282 if entries.is_empty() {
1283 "{}".to_owned()
1284 } else {
1285 format!("{{ {} }}", entries.join(", "))
1286 }
1287 }),
1288 }
1289}
1290
1291fn format_toml_string(template: &TemplateInput, node: &TomlStringNode) -> BackendResult<String> {
1292 let mut rendered = String::new();
1293 for chunk in &node.chunks {
1294 match chunk {
1295 TomlStringPart::Chunk(chunk) => rendered.push_str(&chunk.value),
1296 TomlStringPart::Interpolation(node) => rendered.push_str(&interpolation_raw_source(
1297 template,
1298 node.interpolation_index,
1299 &node.span,
1300 "TOML string fragment",
1301 )?),
1302 }
1303 }
1304 Ok(render_basic_string(&rendered))
1305}
1306
1307fn interpolation_raw_source(
1308 template: &TemplateInput,
1309 interpolation_index: usize,
1310 span: &SourceSpan,
1311 context: &str,
1312) -> BackendResult<String> {
1313 template
1314 .interpolation_raw_source(interpolation_index)
1315 .map(str::to_owned)
1316 .ok_or_else(|| {
1317 let expression = template.interpolation(interpolation_index).map_or_else(
1318 || format!("slot {interpolation_index}"),
1319 |value| value.expression_label().to_owned(),
1320 );
1321 BackendError::semantic_at(
1322 "toml.format",
1323 format!(
1324 "Cannot format {context} interpolation {expression:?} without raw source text."
1325 ),
1326 Some(span.clone()),
1327 )
1328 })
1329}
1330
1331fn is_bare_key(value: &str) -> bool {
1332 value
1333 .chars()
1334 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
1335}
1336
1337fn render_basic_string(value: &str) -> String {
1338 let mut rendered = String::with_capacity(value.len() + 2);
1339 rendered.push('"');
1340 for ch in value.chars() {
1341 match ch {
1342 '\u{0008}' => rendered.push_str("\\b"),
1343 '\t' => rendered.push_str("\\t"),
1344 '\n' => rendered.push_str("\\n"),
1345 '\u{000c}' => rendered.push_str("\\f"),
1346 '\r' => rendered.push_str("\\r"),
1347 '"' => rendered.push_str("\\\""),
1348 '\\' => rendered.push_str("\\\\"),
1349 '\u{0000}'..='\u{001f}' | '\u{007f}' => {
1350 rendered.push_str(&format!("\\u{:04X}", ch as u32));
1351 }
1352 _ => rendered.push(ch),
1353 }
1354 }
1355 rendered.push('"');
1356 rendered
1357}
1358
1359fn normalize_float(value: f64) -> NormalizedFloat {
1360 if value.is_nan() {
1361 return NormalizedFloat::NaN;
1362 }
1363 if value.is_infinite() {
1364 return if value.is_sign_negative() {
1365 NormalizedFloat::NegInf
1366 } else {
1367 NormalizedFloat::PosInf
1368 };
1369 }
1370 NormalizedFloat::finite(value)
1371}
1372
1373fn normalize_datetime(value: &toml::value::Datetime) -> BackendResult<NormalizedTemporal> {
1374 match (&value.date, &value.time, &value.offset) {
1375 (Some(date), Some(time), Some(offset)) => Ok(NormalizedTemporal::OffsetDateTime(
1376 NormalizedOffsetDateTime {
1377 date: normalize_date(*date),
1378 time: normalize_time(*time),
1379 offset_minutes: match offset {
1380 toml::value::Offset::Z => 0,
1381 toml::value::Offset::Custom { minutes } => *minutes,
1382 },
1383 },
1384 )),
1385 (Some(date), Some(time), None) => {
1386 Ok(NormalizedTemporal::LocalDateTime(NormalizedLocalDateTime {
1387 date: normalize_date(*date),
1388 time: normalize_time(*time),
1389 }))
1390 }
1391 (Some(date), None, None) => Ok(NormalizedTemporal::LocalDate(normalize_date(*date))),
1392 (None, Some(time), None) => Ok(NormalizedTemporal::LocalTime(normalize_time(*time))),
1393 _ => Err(BackendError::semantic(format!(
1394 "Unsupported TOML datetime shape: {value}"
1395 ))),
1396 }
1397}
1398
1399fn normalize_date(value: toml::value::Date) -> NormalizedDate {
1400 NormalizedDate {
1401 year: i32::from(value.year),
1402 month: value.month,
1403 day: value.day,
1404 }
1405}
1406
1407fn normalize_time(value: toml::value::Time) -> NormalizedTime {
1408 NormalizedTime {
1409 hour: value.hour,
1410 minute: value.minute,
1411 second: value.second,
1412 nanosecond: value.nanosecond,
1413 }
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418 use super::{TomlKeySegmentValue, TomlStatementNode, TomlValueNode, parse_template};
1419 use pyo3::prelude::*;
1420 use tstring_pyo3_bindings::{extract_template, toml::render_document};
1421 use tstring_syntax::{BackendError, BackendResult, ErrorKind};
1422
1423 fn parse_rendered_toml(text: &str) -> BackendResult<toml::Value> {
1424 toml::from_str(text).map_err(|err| {
1425 BackendError::parse(format!(
1426 "Rendered TOML could not be reparsed during test verification: {err}"
1427 ))
1428 })
1429 }
1430
1431 #[test]
1432 fn parses_toml_string_families() {
1433 Python::with_gil(|py| {
1434 let module = PyModule::from_code(
1435 py,
1436 pyo3::ffi::c_str!("template=t'basic = \"hi-{1}\"\\nliteral = \\'hi-{2}\\''\n"),
1437 pyo3::ffi::c_str!("test_toml.py"),
1438 pyo3::ffi::c_str!("test_toml"),
1439 )
1440 .unwrap();
1441 let template = module.getattr("template").unwrap();
1442 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1443 let document = parse_template(&template).unwrap();
1444 assert_eq!(document.statements.len(), 2);
1445 let TomlStatementNode::Assignment(first) = &document.statements[0] else {
1446 panic!("expected assignment");
1447 };
1448 let TomlValueNode::String(first_value) = &first.value else {
1449 panic!("expected string");
1450 };
1451 assert_eq!(first_value.style, "basic");
1452 });
1453 }
1454
1455 #[test]
1456 fn parses_headers_and_interpolated_key_segments() {
1457 Python::with_gil(|py| {
1458 let module = PyModule::from_code(
1459 py,
1460 pyo3::ffi::c_str!(
1461 "env='prod'\nname='api'\ntemplate=t'[servers.{env}]\\nservice = \"{name}\"\\n[[services]]\\nid = 1\\n'\n"
1462 ),
1463 pyo3::ffi::c_str!("test_toml_headers.py"),
1464 pyo3::ffi::c_str!("test_toml_headers"),
1465 )
1466 .unwrap();
1467 let template = module.getattr("template").unwrap();
1468 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1469 let document = parse_template(&template).unwrap();
1470
1471 assert_eq!(document.statements.len(), 4);
1472 let TomlStatementNode::TableHeader(header) = &document.statements[0] else {
1473 panic!("expected table header");
1474 };
1475 assert_eq!(header.key_path.segments.len(), 2);
1476 assert!(matches!(
1477 header.key_path.segments[1].value,
1478 TomlKeySegmentValue::Interpolation(_)
1479 ));
1480 assert!(matches!(
1481 document.statements[2],
1482 TomlStatementNode::ArrayTableHeader(_)
1483 ));
1484 });
1485 }
1486
1487 #[test]
1488 fn parses_quoted_keys_and_multiline_array_comments() {
1489 Python::with_gil(|py| {
1490 let module = PyModule::from_code(
1491 py,
1492 pyo3::ffi::c_str!(
1493 "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"
1494 ),
1495 pyo3::ffi::c_str!("test_toml_quoted_keys.py"),
1496 pyo3::ffi::c_str!("test_toml_quoted_keys"),
1497 )
1498 .unwrap();
1499 let template = module.getattr("template").unwrap();
1500 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1501 let document = parse_template(&template).unwrap();
1502 let rendered = render_document(py, &document).unwrap();
1503 let table = rendered.data.as_table().expect("expected TOML table");
1504
1505 assert_eq!(table["a.b"].as_integer(), Some(1));
1506 assert_eq!(table["site"]["google.com"]["value"].as_integer(), Some(2));
1507 assert_eq!(
1508 table["value"]
1509 .as_array()
1510 .expect("array")
1511 .iter()
1512 .filter_map(toml::Value::as_integer)
1513 .collect::<Vec<_>>(),
1514 vec![1, 2]
1515 );
1516
1517 let empty_basic = module.getattr("empty_basic").unwrap();
1518 let empty_basic = extract_template(py, &empty_basic, "toml_t/toml_t_str").unwrap();
1519 let rendered = render_document(py, &parse_template(&empty_basic).unwrap()).unwrap();
1520 let table = rendered.data.as_table().expect("expected TOML table");
1521 assert_eq!(table[""].as_integer(), Some(1));
1522
1523 let empty_literal = module.getattr("empty_literal").unwrap();
1524 let empty_literal = extract_template(py, &empty_literal, "toml_t/toml_t_str").unwrap();
1525 let rendered = render_document(py, &parse_template(&empty_literal).unwrap()).unwrap();
1526 let table = rendered.data.as_table().expect("expected TOML table");
1527 assert_eq!(table[""].as_integer(), Some(1));
1528
1529 let empty_segment = module.getattr("empty_segment").unwrap();
1530 let empty_segment = extract_template(py, &empty_segment, "toml_t/toml_t_str").unwrap();
1531 let rendered = render_document(py, &parse_template(&empty_segment).unwrap()).unwrap();
1532 let table = rendered.data.as_table().expect("expected TOML table");
1533 assert_eq!(table["a"][""]["b"].as_integer(), Some(1));
1534 });
1535 }
1536
1537 #[test]
1538 fn renders_temporal_values_and_inline_tables() {
1539 Python::with_gil(|py| {
1540 let module = PyModule::from_code(
1541 py,
1542 pyo3::ffi::c_str!(
1543 "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"
1544 ),
1545 pyo3::ffi::c_str!("test_toml_render.py"),
1546 pyo3::ffi::c_str!("test_toml_render"),
1547 )
1548 .unwrap();
1549 let template = module.getattr("template").unwrap();
1550 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1551 let document = parse_template(&template).unwrap();
1552 let rendered = render_document(py, &document).unwrap();
1553 let table = rendered.data.as_table().expect("expected TOML table");
1554
1555 assert!(rendered.text.contains("2025-01-02T03:04:05"));
1556 assert_eq!(table["meta"]["count"].as_integer(), Some(2));
1557 assert_eq!(table["meta"]["active"].as_bool(), Some(true));
1558 });
1559 }
1560
1561 #[test]
1562 fn rejects_null_like_interpolations() {
1563 Python::with_gil(|py| {
1564 let module = PyModule::from_code(
1565 py,
1566 pyo3::ffi::c_str!("missing=None\ntemplate=t'value = {missing}\\n'\n"),
1567 pyo3::ffi::c_str!("test_toml_error.py"),
1568 pyo3::ffi::c_str!("test_toml_error"),
1569 )
1570 .unwrap();
1571 let template = module.getattr("template").unwrap();
1572 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1573 let document = parse_template(&template).unwrap();
1574 let err = match render_document(py, &document) {
1575 Ok(_) => panic!("expected TOML render failure"),
1576 Err(err) => err,
1577 };
1578
1579 assert_eq!(err.kind, ErrorKind::Unrepresentable);
1580 assert!(err.message.contains("TOML has no null"));
1581 });
1582 }
1583
1584 #[test]
1585 fn trims_multiline_basic_line_end_backslashes() {
1586 Python::with_gil(|py| {
1587 let module = PyModule::from_code(
1588 py,
1589 pyo3::ffi::c_str!(
1590 "from string.templatelib import Template\ntrimmed=t'value = \"\"\"\\nalpha\\\\\\n beta\\n\"\"\"'\ncrlf=Template('value = \"\"\"\\r\\na\\\\\\r\\n b\\r\\n\"\"\"\\n')\n"
1591 ),
1592 pyo3::ffi::c_str!("test_toml_multiline.py"),
1593 pyo3::ffi::c_str!("test_toml_multiline"),
1594 )
1595 .unwrap();
1596 for (name, expected) in [("trimmed", "alphabeta\n"), ("crlf", "ab\n")] {
1597 let template = module.getattr(name).unwrap();
1598 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1599 let document = parse_template(&template).unwrap();
1600 let rendered = render_document(py, &document).unwrap();
1601 let table = rendered.data.as_table().expect("expected TOML table");
1602 assert_eq!(table["value"].as_str(), Some(expected));
1603 }
1604 });
1605 }
1606
1607 #[test]
1608 fn parses_multiline_strings_with_one_or_two_quotes_before_terminator() {
1609 Python::with_gil(|py| {
1610 let module = PyModule::from_code(
1611 py,
1612 pyo3::ffi::c_str!(
1613 "template=t'value = \"\"\"\"\"\"\"\\none = \"\"\"\"\"\"\"\"\\nliteral = \\'\\'\\'\\'\\'\\'\\'\\nliteral_two = \\'\\'\\'\\'\\'\\'\\'\\'\\n'\n"
1614 ),
1615 pyo3::ffi::c_str!("test_toml_quote_run.py"),
1616 pyo3::ffi::c_str!("test_toml_quote_run"),
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_str(), Some("\""));
1626 assert_eq!(table["one"].as_str(), Some("\"\""));
1627 assert_eq!(table["literal"].as_str(), Some("'"));
1628 assert_eq!(table["literal_two"].as_str(), Some("''"));
1629 });
1630 }
1631
1632 #[test]
1633 fn parses_numeric_forms_and_local_datetimes() {
1634 Python::with_gil(|py| {
1635 let module = PyModule::from_code(
1636 py,
1637 pyo3::ffi::c_str!(
1638 "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"
1639 ),
1640 pyo3::ffi::c_str!("test_toml_numeric_forms.py"),
1641 pyo3::ffi::c_str!("test_toml_numeric_forms"),
1642 )
1643 .unwrap();
1644 let template = module.getattr("template").unwrap();
1645 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1646 let document = parse_template(&template).unwrap();
1647 let rendered = render_document(py, &document).unwrap();
1648 let table = rendered.data.as_table().expect("expected TOML table");
1649
1650 assert_eq!(table["value"].as_integer(), Some(3_735_928_559));
1651 assert_eq!(table["hex_underscore"].as_integer(), Some(3_735_928_559));
1652 assert_eq!(table["binary"].as_integer(), Some(13));
1653 assert_eq!(table["octal"].as_integer(), Some(493));
1654 assert_eq!(table["underscored"].as_integer(), Some(1_000_000));
1655 assert_eq!(table["float"].as_float(), Some(1.0));
1656 assert_eq!(table["exp"].as_float(), Some(-0.02));
1657 assert_eq!(
1658 table["local"]
1659 .as_datetime()
1660 .map(std::string::ToString::to_string),
1661 Some("2024-01-02T03:04:05".to_owned())
1662 );
1663 });
1664 }
1665
1666 #[test]
1667 fn parses_empty_strings_and_quoted_empty_table_headers() {
1668 Python::with_gil(|py| {
1669 let module = PyModule::from_code(
1670 py,
1671 pyo3::ffi::c_str!(
1672 "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"
1673 ),
1674 pyo3::ffi::c_str!("test_toml_empty_strings.py"),
1675 pyo3::ffi::c_str!("test_toml_empty_strings"),
1676 )
1677 .unwrap();
1678
1679 for name in ["basic", "literal"] {
1680 let template = module.getattr(name).unwrap();
1681 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1682 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
1683 let table = rendered.data.as_table().expect("expected TOML table");
1684 assert_eq!(table["value"].as_str(), Some(""));
1685 }
1686
1687 let escaped_quote = module.getattr("escaped_quote").unwrap();
1688 let escaped_quote = extract_template(py, &escaped_quote, "toml_t/toml_t_str").unwrap();
1689 let rendered = render_document(py, &parse_template(&escaped_quote).unwrap()).unwrap();
1690 let table = rendered.data.as_table().expect("expected TOML table");
1691 assert_eq!(table["value"].as_str(), Some("a\"b"));
1692
1693 let header = module.getattr("header").unwrap();
1694 let header = extract_template(py, &header, "toml_t/toml_t_str").unwrap();
1695 let rendered = render_document(py, &parse_template(&header).unwrap()).unwrap();
1696 let table = rendered.data.as_table().expect("expected TOML table");
1697 assert_eq!(table[""]["value"].as_integer(), Some(1));
1698
1699 let header_subtable = module.getattr("header_subtable").unwrap();
1700 let header_subtable =
1701 extract_template(py, &header_subtable, "toml_t/toml_t_str").unwrap();
1702 let rendered = render_document(py, &parse_template(&header_subtable).unwrap()).unwrap();
1703 let table = rendered.data.as_table().expect("expected TOML table");
1704 assert_eq!(table[""]["inner"]["name"].as_str(), Some("x"));
1705 });
1706 }
1707
1708 #[test]
1709 fn parses_empty_collections_and_quoted_empty_dotted_tables() {
1710 Python::with_gil(|py| {
1711 let module = PyModule::from_code(
1712 py,
1713 pyo3::ffi::c_str!(
1714 "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"
1715 ),
1716 pyo3::ffi::c_str!("test_toml_empty_collections.py"),
1717 pyo3::ffi::c_str!("test_toml_empty_collections"),
1718 )
1719 .unwrap();
1720
1721 let empty_array = module.getattr("empty_array").unwrap();
1722 let empty_array = extract_template(py, &empty_array, "toml_t/toml_t_str").unwrap();
1723 let rendered = render_document(py, &parse_template(&empty_array).unwrap()).unwrap();
1724 let table = rendered.data.as_table().expect("expected TOML table");
1725 assert_eq!(table["value"].as_array().expect("array").len(), 0);
1726
1727 let empty_inline_table = module.getattr("empty_inline_table").unwrap();
1728 let empty_inline_table =
1729 extract_template(py, &empty_inline_table, "toml_t/toml_t_str").unwrap();
1730 let rendered =
1731 render_document(py, &parse_template(&empty_inline_table).unwrap()).unwrap();
1732 let table = rendered.data.as_table().expect("expected TOML table");
1733 assert_eq!(table["value"].as_table().expect("table").len(), 0);
1734
1735 let quoted_empty_dotted_table = module.getattr("quoted_empty_dotted_table").unwrap();
1736 let quoted_empty_dotted_table =
1737 extract_template(py, "ed_empty_dotted_table, "toml_t/toml_t_str").unwrap();
1738 let rendered =
1739 render_document(py, &parse_template("ed_empty_dotted_table).unwrap()).unwrap();
1740 let table = rendered.data.as_table().expect("expected TOML table");
1741 assert_eq!(table["a"][""]["b"]["value"].as_integer(), Some(1));
1742
1743 let quoted_empty_subsegments = module.getattr("quoted_empty_subsegments").unwrap();
1744 let quoted_empty_subsegments =
1745 extract_template(py, "ed_empty_subsegments, "toml_t/toml_t_str").unwrap();
1746 let rendered =
1747 render_document(py, &parse_template("ed_empty_subsegments).unwrap()).unwrap();
1748 let table = rendered.data.as_table().expect("expected TOML table");
1749 assert_eq!(table[""][""]["leaf"]["value"].as_integer(), Some(1));
1750
1751 let quoted_empty_leaf_chain = module.getattr("quoted_empty_leaf_chain").unwrap();
1752 let quoted_empty_leaf_chain =
1753 extract_template(py, "ed_empty_leaf_chain, "toml_t/toml_t_str").unwrap();
1754 let rendered =
1755 render_document(py, &parse_template("ed_empty_leaf_chain).unwrap()).unwrap();
1756 let table = rendered.data.as_table().expect("expected TOML table");
1757 assert_eq!(table[""][""]["leaf"]["value"].as_integer(), Some(1));
1758
1759 let mixed_array_tables = module.getattr("mixed_array_tables").unwrap();
1760 let mixed_array_tables =
1761 extract_template(py, &mixed_array_tables, "toml_t/toml_t_str").unwrap();
1762 let rendered =
1763 render_document(py, &parse_template(&mixed_array_tables).unwrap()).unwrap();
1764 let table = rendered.data.as_table().expect("expected TOML table");
1765 assert_eq!(table["a"].as_array().expect("array").len(), 2);
1766 });
1767 }
1768
1769 #[test]
1770 fn parses_additional_numeric_and_datetime_forms() {
1771 Python::with_gil(|py| {
1772 let module = PyModule::from_code(
1773 py,
1774 pyo3::ffi::c_str!(
1775 "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"
1776 ),
1777 pyo3::ffi::c_str!("test_toml_more_numeric_forms.py"),
1778 pyo3::ffi::c_str!("test_toml_more_numeric_forms"),
1779 )
1780 .unwrap();
1781 let template = module.getattr("template").unwrap();
1782 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
1783 let document = parse_template(&template).unwrap();
1784 let rendered = render_document(py, &document).unwrap();
1785 let table = rendered.data.as_table().expect("expected TOML table");
1786
1787 assert_eq!(table["plus_int"].as_integer(), Some(1));
1788 assert_eq!(table["plus_zero"].as_integer(), Some(0));
1789 assert_eq!(table["plus_zero_float"].as_float(), Some(0.0));
1790 assert_eq!(table["zero_float_exp"].as_float(), Some(0.0));
1791 assert_eq!(table["plus_zero_float_exp"].as_float(), Some(0.0));
1792 assert_eq!(table["plus_zero_fraction_exp"].as_float(), Some(0.0));
1793 assert_eq!(table["exp_underscore"].as_float(), Some(1e10));
1794 assert_eq!(table["frac_underscore"].as_float(), Some(12.34));
1795 assert_eq!(
1796 table["local_space"]
1797 .as_datetime()
1798 .map(std::string::ToString::to_string),
1799 Some("2024-01-02T03:04:05".to_owned())
1800 );
1801 assert_eq!(
1802 table["local_lower_t"]
1803 .as_datetime()
1804 .map(std::string::ToString::to_string),
1805 Some("2024-01-02T03:04:05".to_owned())
1806 );
1807 assert_eq!(
1808 table["local_date"]
1809 .as_datetime()
1810 .map(std::string::ToString::to_string),
1811 Some("2024-01-02".to_owned())
1812 );
1813 assert_eq!(
1814 table["array_of_dates_trailing"]
1815 .as_array()
1816 .expect("array")
1817 .len(),
1818 2
1819 );
1820 assert_eq!(
1821 table["mixed_date_time_array"]
1822 .as_array()
1823 .expect("array")
1824 .len(),
1825 2
1826 );
1827 assert_eq!(
1828 table["local_time_fraction"]
1829 .as_datetime()
1830 .map(std::string::ToString::to_string),
1831 Some("03:04:05.123456".to_owned())
1832 );
1833 assert_eq!(table["array_of_dates"].as_array().expect("array").len(), 2);
1834 assert_eq!(
1835 table["array_of_local_times"]
1836 .as_array()
1837 .expect("array")
1838 .len(),
1839 2
1840 );
1841 assert_eq!(
1842 table["nested_array_mixed_dates"]
1843 .as_array()
1844 .expect("array")
1845 .len(),
1846 2
1847 );
1848 assert_eq!(table["offset_array"].as_array().expect("array").len(), 2);
1849 assert_eq!(
1850 table["offset_array_positive"]
1851 .as_array()
1852 .expect("array")
1853 .len(),
1854 1
1855 );
1856 assert_eq!(
1857 table["datetime_array_trailing"]
1858 .as_array()
1859 .expect("array")
1860 .len(),
1861 2
1862 );
1863 assert_eq!(
1864 table["lowercase_offset_array_trailing"]
1865 .as_array()
1866 .expect("array")
1867 .len(),
1868 2
1869 );
1870 assert_eq!(
1871 table["offset_fraction_dt"]
1872 .as_datetime()
1873 .map(std::string::ToString::to_string),
1874 Some("1979-05-27T07:32:00.999999-07:00".to_owned())
1875 );
1876 assert_eq!(
1877 table["offset_fraction_space"]
1878 .as_datetime()
1879 .map(std::string::ToString::to_string),
1880 Some("1979-05-27T07:32:00.999999-07:00".to_owned())
1881 );
1882 assert_eq!(
1883 table["array_offset_fraction"]
1884 .as_array()
1885 .expect("array")
1886 .len(),
1887 2
1888 );
1889 assert_eq!(
1890 table["fraction_lower_z"]
1891 .as_datetime()
1892 .map(std::string::ToString::to_string),
1893 Some("2024-01-02T03:04:05.123456Z".to_owned())
1894 );
1895 assert_eq!(
1896 table["array_fraction_lower_z"]
1897 .as_array()
1898 .expect("array")
1899 .len(),
1900 1
1901 );
1902 assert_eq!(
1903 table["utc_fraction_lower_array"]
1904 .as_array()
1905 .expect("array")
1906 .len(),
1907 2
1908 );
1909 assert_eq!(
1910 table["utc_fraction_lower_array_trailing"]
1911 .as_array()
1912 .expect("array")
1913 .len(),
1914 2
1915 );
1916 assert_eq!(table["lower_hex"].as_integer(), Some(0xdead_beef));
1917 assert_eq!(table["upper_exp"].as_float(), Some(100.0));
1918 assert_eq!(
1919 table["signed_int_array"]
1920 .as_array()
1921 .expect("array")
1922 .iter()
1923 .filter_map(toml::Value::as_integer)
1924 .collect::<Vec<_>>(),
1925 vec![1, 0, -1]
1926 );
1927 let special_floats = table["special_float_array"].as_array().expect("array");
1928 assert!(special_floats[0].as_float().expect("float").is_infinite());
1929 assert!(
1930 special_floats[1]
1931 .as_float()
1932 .expect("float")
1933 .is_sign_negative()
1934 );
1935 assert!(special_floats[2].as_float().expect("float").is_nan());
1936 assert_eq!(
1937 table["special_float_nested_arrays"]
1938 .as_array()
1939 .expect("array")
1940 .len(),
1941 3
1942 );
1943 let special_float_deeper_arrays = table["special_float_deeper_arrays"]
1944 .as_array()
1945 .expect("array");
1946 assert!(
1947 special_float_deeper_arrays[0][0][0]
1948 .as_float()
1949 .expect("float")
1950 .is_infinite()
1951 );
1952 assert!(
1953 special_float_deeper_arrays[1][0][0]
1954 .as_float()
1955 .expect("float")
1956 .is_sign_negative()
1957 );
1958 assert!(
1959 special_float_deeper_arrays[2][0][0]
1960 .as_float()
1961 .expect("float")
1962 .is_nan()
1963 );
1964 assert_eq!(
1965 table["upper_exp_nested_mixed"]
1966 .as_array()
1967 .expect("array")
1968 .len(),
1969 2
1970 );
1971 assert!(
1972 table["special_float_inline_table"]["pos"]
1973 .as_float()
1974 .expect("float")
1975 .is_infinite()
1976 );
1977 assert!(
1978 table["special_float_inline_table"]["nan"]
1979 .as_float()
1980 .expect("float")
1981 .is_nan()
1982 );
1983 assert_eq!(
1984 table["special_float_mixed_nested"]
1985 .as_array()
1986 .expect("array")
1987 .len(),
1988 2
1989 );
1990 assert_eq!(
1991 table["nested_datetime_arrays"]
1992 .as_array()
1993 .expect("array")
1994 .len(),
1995 2
1996 );
1997 assert_eq!(
1998 table["upper_exp_nested_array"]
1999 .as_array()
2000 .expect("array")
2001 .len(),
2002 3
2003 );
2004 assert_eq!(
2005 table["positive_negative_offsets"]
2006 .as_array()
2007 .expect("array")
2008 .len(),
2009 2
2010 );
2011 assert_eq!(
2012 table["positive_offset_scalar_space"]
2013 .as_datetime()
2014 .map(std::string::ToString::to_string),
2015 Some("1979-05-27T07:32:00+07:00".to_owned())
2016 );
2017 assert_eq!(
2018 table["positive_offset_array_space"]
2019 .as_array()
2020 .expect("array")
2021 .len(),
2022 2
2023 );
2024 assert_eq!(
2025 table["utc_z"]
2026 .as_datetime()
2027 .map(std::string::ToString::to_string),
2028 Some("2024-01-02T03:04:05Z".to_owned())
2029 );
2030 assert_eq!(
2031 table["utc_lower_z"]
2032 .as_datetime()
2033 .map(std::string::ToString::to_string),
2034 Some("2024-01-02T03:04:05Z".to_owned())
2035 );
2036 assert_eq!(
2037 table["utc_fraction"]
2038 .as_datetime()
2039 .map(std::string::ToString::to_string),
2040 Some("2024-01-02T03:04:05.123456Z".to_owned())
2041 );
2042 assert_eq!(
2043 table["utc_fraction_array"].as_array().expect("array").len(),
2044 2
2045 );
2046 });
2047 }
2048
2049 #[test]
2050 fn rejects_newlines_in_single_line_strings() {
2051 Python::with_gil(|py| {
2052 let module = PyModule::from_code(
2053 py,
2054 pyo3::ffi::c_str!("template=t'value = \"a\\nb\"'\n"),
2055 pyo3::ffi::c_str!("test_toml_newline_error.py"),
2056 pyo3::ffi::c_str!("test_toml_newline_error"),
2057 )
2058 .unwrap();
2059 let template = module.getattr("template").unwrap();
2060 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2061 let err = match parse_template(&template) {
2062 Ok(_) => panic!("expected TOML parse failure"),
2063 Err(err) => err,
2064 };
2065 assert_eq!(err.kind, ErrorKind::Parse);
2066 assert!(
2067 err.message
2068 .contains("single-line basic strings cannot contain newlines")
2069 );
2070 });
2071 }
2072
2073 #[test]
2074 fn renders_toml_special_floats() {
2075 Python::with_gil(|py| {
2076 let module = PyModule::from_code(
2077 py,
2078 pyo3::ffi::c_str!(
2079 "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"
2080 ),
2081 pyo3::ffi::c_str!("test_toml_special_floats.py"),
2082 pyo3::ffi::c_str!("test_toml_special_floats"),
2083 )
2084 .unwrap();
2085 let template = module.getattr("template").unwrap();
2086 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2087 let document = parse_template(&template).unwrap();
2088 let rendered = render_document(py, &document).unwrap();
2089 let table = rendered.data.as_table().expect("expected TOML table");
2090
2091 assert!(rendered.text.contains("pos = inf"));
2092 assert!(rendered.text.contains("plus_inf = +inf"));
2093 assert!(rendered.text.contains("neg = -inf"));
2094 assert!(rendered.text.contains("value = nan"));
2095 assert!(rendered.text.contains("plus_nan = +nan"));
2096 assert!(rendered.text.contains("minus_nan = -nan"));
2097 assert!(table["pos"].as_float().expect("float").is_infinite());
2098 assert!(table["plus_inf"].as_float().expect("float").is_infinite());
2099 assert!(table["neg"].as_float().expect("float").is_sign_negative());
2100 assert!(table["value"].as_float().expect("float").is_nan());
2101 assert!(table["plus_nan"].as_float().expect("float").is_nan());
2102 assert!(table["minus_nan"].as_float().expect("float").is_nan());
2103 });
2104 }
2105
2106 #[test]
2107 fn parses_arrays_with_trailing_commas() {
2108 Python::with_gil(|py| {
2109 let module = PyModule::from_code(
2110 py,
2111 pyo3::ffi::c_str!(
2112 "from string.templatelib import Template\ntemplate=Template('value = [1, 2,]\\nnested = [[ ], [1, 2,],]\\nempty_inline_tables = [{}, {}]\\nnested_empty_inline_arrays = { inner = [[], [1]] }\\n')\n"
2113 ),
2114 pyo3::ffi::c_str!("test_toml_trailing_comma.py"),
2115 pyo3::ffi::c_str!("test_toml_trailing_comma"),
2116 )
2117 .unwrap();
2118 let template = module.getattr("template").unwrap();
2119 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2120 let document = parse_template(&template).unwrap();
2121 let rendered = render_document(py, &document).unwrap();
2122 let table = rendered.data.as_table().expect("expected TOML table");
2123
2124 assert_eq!(
2125 table["value"]
2126 .as_array()
2127 .expect("array")
2128 .iter()
2129 .filter_map(toml::Value::as_integer)
2130 .collect::<Vec<_>>(),
2131 vec![1, 2]
2132 );
2133 assert_eq!(table["nested"].as_array().expect("array").len(), 2);
2134 assert_eq!(
2135 table["empty_inline_tables"]
2136 .as_array()
2137 .expect("array")
2138 .len(),
2139 2
2140 );
2141 assert_eq!(
2142 table["nested_empty_inline_arrays"]["inner"]
2143 .as_array()
2144 .expect("array")
2145 .len(),
2146 2
2147 );
2148 });
2149 }
2150
2151 #[test]
2152 fn renders_nested_collections_and_array_tables() {
2153 Python::with_gil(|py| {
2154 let module = PyModule::from_code(
2155 py,
2156 pyo3::ffi::c_str!(
2157 "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"
2158 ),
2159 pyo3::ffi::c_str!("test_toml_nested_collections.py"),
2160 pyo3::ffi::c_str!("test_toml_nested_collections"),
2161 )
2162 .unwrap();
2163 let template = module.getattr("template").unwrap();
2164 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2165 let document = parse_template(&template).unwrap();
2166 let rendered = render_document(py, &document).unwrap();
2167 let table = rendered.data.as_table().expect("expected TOML table");
2168
2169 assert_eq!(table["matrix"].as_array().expect("array").len(), 2);
2170 assert_eq!(table["meta"]["inner"]["value"].as_integer(), Some(1));
2171 assert_eq!(
2172 table["nested_inline_arrays"]["items"]
2173 .as_array()
2174 .expect("array")
2175 .len(),
2176 2
2177 );
2178 assert_eq!(
2179 table["deep_nested_inline"]["inner"]["deep"]["value"].as_integer(),
2180 Some(1)
2181 );
2182 assert_eq!(
2183 table["inline_table_array"].as_array().expect("array").len(),
2184 2
2185 );
2186 assert_eq!(table["inline_table_array"][0]["a"].as_integer(), Some(1));
2187 assert_eq!(table["inline_table_array"][1]["a"].as_integer(), Some(2));
2188 assert_eq!(
2189 table["inline_table_array_nested"]
2190 .as_array()
2191 .expect("array")
2192 .len(),
2193 2
2194 );
2195 assert_eq!(
2196 table["inline_table_array_nested"][0]
2197 .as_array()
2198 .expect("array")[0]["a"]
2199 .as_integer(),
2200 Some(1)
2201 );
2202 assert_eq!(
2203 table["inline_table_array_nested"][1]
2204 .as_array()
2205 .expect("array")[0]["a"]
2206 .as_integer(),
2207 Some(2)
2208 );
2209 assert_eq!(table["a"]["value"].as_integer(), Some(1));
2210 assert_eq!(table["a"]["b"].as_array().expect("array").len(), 1);
2211 assert_eq!(table["a"]["b"][0]["name"].as_str(), Some("x"));
2212 assert_eq!(table["services"].as_array().expect("array").len(), 2);
2213 });
2214 }
2215
2216 #[test]
2217 fn parses_headers_comments_and_crlf_literal_strings() {
2218 Python::with_gil(|py| {
2219 let module = PyModule::from_code(
2220 py,
2221 pyo3::ffi::c_str!(
2222 "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"
2223 ),
2224 pyo3::ffi::c_str!("test_toml_additional_surface.py"),
2225 pyo3::ffi::c_str!("test_toml_additional_surface"),
2226 )
2227 .unwrap();
2228
2229 let quoted_header = module.getattr("quoted_header").unwrap();
2230 let quoted_header = extract_template(py, "ed_header, "toml_t/toml_t_str").unwrap();
2231 let rendered = render_document(py, &parse_template("ed_header).unwrap()).unwrap();
2232 let table = rendered.data.as_table().expect("expected TOML table");
2233 assert_eq!(table["a.b"]["value"].as_integer(), Some(1));
2234
2235 let dotted_header = module.getattr("dotted_header").unwrap();
2236 let dotted_header = extract_template(py, &dotted_header, "toml_t/toml_t_str").unwrap();
2237 let rendered = render_document(py, &parse_template(&dotted_header).unwrap()).unwrap();
2238 let table = rendered.data.as_table().expect("expected TOML table");
2239 assert_eq!(table["site"]["google.com"]["value"].as_integer(), Some(1));
2240
2241 let quoted_segments = module.getattr("quoted_segments").unwrap();
2242 let quoted_segments =
2243 extract_template(py, "ed_segments, "toml_t/toml_t_str").unwrap();
2244 let rendered = render_document(py, &parse_template("ed_segments).unwrap()).unwrap();
2245 let table = rendered.data.as_table().expect("expected TOML table");
2246 assert_eq!(table["a"]["b"]["value"].as_integer(), Some(1));
2247
2248 let quoted_header_then_dotted = module.getattr("quoted_header_then_dotted").unwrap();
2249 let quoted_header_then_dotted =
2250 extract_template(py, "ed_header_then_dotted, "toml_t/toml_t_str").unwrap();
2251 let rendered =
2252 render_document(py, &parse_template("ed_header_then_dotted).unwrap()).unwrap();
2253 let table = rendered.data.as_table().expect("expected TOML table");
2254 assert_eq!(table["a.b"]["value"].as_integer(), Some(1));
2255 assert_eq!(table["a.b"]["c"]["name"].as_str(), Some("x"));
2256
2257 let inline_comment = module.getattr("inline_comment").unwrap();
2258 let inline_comment =
2259 extract_template(py, &inline_comment, "toml_t/toml_t_str").unwrap();
2260 let rendered = render_document(py, &parse_template(&inline_comment).unwrap()).unwrap();
2261 let table = rendered.data.as_table().expect("expected TOML table");
2262 assert_eq!(table["value"]["a"].as_integer(), Some(1));
2263
2264 let commented_array = module.getattr("commented_array").unwrap();
2265 let commented_array =
2266 extract_template(py, &commented_array, "toml_t/toml_t_str").unwrap();
2267 let rendered = render_document(py, &parse_template(&commented_array).unwrap()).unwrap();
2268 let table = rendered.data.as_table().expect("expected TOML table");
2269 assert_eq!(
2270 table["value"]
2271 .as_array()
2272 .expect("array")
2273 .iter()
2274 .filter_map(toml::Value::as_integer)
2275 .collect::<Vec<_>>(),
2276 vec![1, 2]
2277 );
2278
2279 let literal_crlf = module.getattr("literal_crlf").unwrap();
2280 let literal_crlf = extract_template(py, &literal_crlf, "toml_t/toml_t_str").unwrap();
2281 let rendered = render_document(py, &parse_template(&literal_crlf).unwrap()).unwrap();
2282 let table = rendered.data.as_table().expect("expected TOML table");
2283 assert_eq!(table["value"].as_str(), Some("a\nb"));
2284
2285 let array_then_table = module.getattr("array_then_table").unwrap();
2286 let array_then_table =
2287 extract_template(py, &array_then_table, "toml_t/toml_t_str").unwrap();
2288 let rendered =
2289 render_document(py, &parse_template(&array_then_table).unwrap()).unwrap();
2290 let table = rendered.data.as_table().expect("expected TOML table");
2291 assert_eq!(table["items"].as_array().expect("array").len(), 1);
2292 assert_eq!(table["tool"]["value"].as_integer(), Some(1));
2293 });
2294 }
2295
2296 #[test]
2297 fn rejects_multiline_inline_tables() {
2298 Python::with_gil(|py| {
2299 let module = PyModule::from_code(
2300 py,
2301 pyo3::ffi::c_str!(
2302 "from string.templatelib import Template\ntemplate=Template('value = { a = 1,\\n b = 2 }\\n')\n"
2303 ),
2304 pyo3::ffi::c_str!("test_toml_inline_table_newline.py"),
2305 pyo3::ffi::c_str!("test_toml_inline_table_newline"),
2306 )
2307 .unwrap();
2308 let template = module.getattr("template").unwrap();
2309 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2310 let err = match parse_template(&template) {
2311 Ok(_) => panic!("expected TOML parse failure"),
2312 Err(err) => err,
2313 };
2314 assert_eq!(err.kind, ErrorKind::Parse);
2315 assert!(err.message.contains("Expected a TOML key segment"));
2316 });
2317 }
2318
2319 #[test]
2320 fn rejects_invalid_table_redefinitions_and_newlines_after_dots() {
2321 Python::with_gil(|py| {
2322 let module = PyModule::from_code(
2323 py,
2324 pyo3::ffi::c_str!(
2325 "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"
2326 ),
2327 pyo3::ffi::c_str!("test_toml_invalid_tables.py"),
2328 pyo3::ffi::c_str!("test_toml_invalid_tables"),
2329 )
2330 .unwrap();
2331
2332 for name in ["table_redefine", "array_redefine"] {
2333 let template = module.getattr(name).unwrap();
2334 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2335 let document = parse_template(&template).unwrap();
2336 let rendered = render_document(py, &document).unwrap();
2337 let err =
2338 parse_rendered_toml(&rendered.text).expect_err("expected TOML parse failure");
2339 assert_eq!(err.kind, ErrorKind::Parse);
2340 assert!(
2341 err.message.contains("duplicate key"),
2342 "{name}: {}",
2343 err.message
2344 );
2345 }
2346
2347 let template = module.getattr("newline_after_dot").unwrap();
2348 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2349 let err = match parse_template(&template) {
2350 Ok(_) => panic!("expected TOML parse failure for newline_after_dot"),
2351 Err(err) => err,
2352 };
2353 assert_eq!(err.kind, ErrorKind::Parse);
2354 assert!(err.message.contains("Expected a TOML key segment"));
2355
2356 for (name, expected) in [
2357 ("extra_array_comma", "Expected a TOML value"),
2358 ("inline_double_comma", "Expected a TOML key segment"),
2359 ("array_leading_comma", "Expected a TOML value"),
2360 ("inline_trailing_comma", "Expected a TOML key segment"),
2361 ("invalid_decimal_underscore", "Invalid TOML literal"),
2362 ("invalid_hex_underscore", "Invalid TOML literal"),
2363 ("header_trailing_dot", "Expected a TOML key segment"),
2364 ("invalid_octal_underscore", "Invalid TOML literal"),
2365 ("invalid_fraction_underscore", "Invalid TOML literal"),
2366 ("invalid_plus_zero_float_underscore", "Invalid TOML literal"),
2367 ("invalid_fraction_double_underscore", "Invalid TOML literal"),
2368 ("invalid_exp_double_underscore", "Invalid TOML literal"),
2369 ("invalid_double_plus", "Invalid TOML literal"),
2370 ("invalid_double_plus_inline_table", "Invalid TOML literal"),
2371 ("invalid_double_plus_nested", "Invalid TOML literal"),
2372 (
2373 "invalid_double_plus_nested_inline_table",
2374 "Invalid TOML literal",
2375 ),
2376 ("invalid_double_plus_array_nested", "Invalid TOML literal"),
2377 ("invalid_double_plus_array_mixed", "Invalid TOML literal"),
2378 ("invalid_double_plus_after_scalar", "Invalid TOML literal"),
2379 ("leading_zero", "Invalid TOML literal"),
2380 ("leading_zero_plus", "Invalid TOML literal"),
2381 ("leading_zero_float", "Invalid TOML literal"),
2382 ("binary_leading_underscore", "Invalid TOML literal"),
2383 ("signed_binary", "Invalid TOML literal"),
2384 ("time_with_offset", "Invalid TOML literal"),
2385 ("plus_inf_underscore", "Invalid TOML literal"),
2386 ("plus_nan_underscore", "Invalid TOML literal"),
2387 ("time_lower_z", "Invalid TOML literal"),
2388 ("invalid_exp_leading_underscore", "Invalid TOML literal"),
2389 ("invalid_exp_trailing_underscore", "Invalid TOML literal"),
2390 ("double_sign_exp", "Invalid TOML literal"),
2391 ("double_sign_float", "Invalid TOML literal"),
2392 ("double_dot_dotted_key", "Expected a TOML key segment"),
2393 ("hex_float_like", "Invalid TOML literal"),
2394 ("signed_octal", "Invalid TOML literal"),
2395 ("inline_table_missing_comma", "Invalid TOML literal"),
2396 ] {
2397 let template = module.getattr(name).unwrap();
2398 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2399 let err = parse_template(&template).expect_err("expected TOML parse failure");
2400 assert_eq!(err.kind, ErrorKind::Parse);
2401 assert!(err.message.contains(expected), "{name}: {}", err.message);
2402 }
2403 });
2404 }
2405
2406 #[test]
2407 fn rejects_value_contracts() {
2408 Python::with_gil(|py| {
2409 let module = PyModule::from_code(
2410 py,
2411 pyo3::ffi::c_str!(
2412 "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"
2413 ),
2414 pyo3::ffi::c_str!("test_toml_value_contracts.py"),
2415 pyo3::ffi::c_str!("test_toml_value_contracts"),
2416 )
2417 .unwrap();
2418
2419 for (name, expected) in [
2420 ("key_template", "TOML keys must be str"),
2421 ("null_template", "TOML has no null value"),
2422 ("time_template", "timezone"),
2423 ("fragment_template", "string fragment"),
2424 ] {
2425 let template = module.getattr(name).unwrap();
2426 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2427 let document = parse_template(&template).unwrap();
2428 let err = match render_document(py, &document) {
2429 Ok(_) => panic!("expected TOML render failure"),
2430 Err(err) => err,
2431 };
2432 assert_eq!(err.kind, ErrorKind::Unrepresentable);
2433 assert!(err.message.contains(expected), "{name}: {}", err.message);
2434 }
2435
2436 let duplicate_table = module.getattr("duplicate_table").unwrap();
2437 let duplicate_table =
2438 extract_template(py, &duplicate_table, "toml_t/toml_t_str").unwrap();
2439 let document = parse_template(&duplicate_table).unwrap();
2440 let rendered = render_document(py, &document).unwrap();
2441 let err = parse_rendered_toml(&rendered.text)
2442 .expect_err("expected TOML duplicate-key parse failure");
2443 assert_eq!(err.kind, ErrorKind::Parse);
2444 assert!(err.message.contains("duplicate key"));
2445 });
2446 }
2447
2448 #[test]
2449 fn rejects_invalid_numeric_literal_families() {
2450 Python::with_gil(|py| {
2451 let module = PyModule::from_code(
2452 py,
2453 pyo3::ffi::c_str!(
2454 "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"
2455 ),
2456 pyo3::ffi::c_str!("test_toml_invalid_literals.py"),
2457 pyo3::ffi::c_str!("test_toml_invalid_literals"),
2458 )
2459 .unwrap();
2460
2461 for name in [
2462 "leading_zero",
2463 "positive_leading_zero",
2464 "double_underscore",
2465 "hex_underscore",
2466 "double_plus",
2467 ] {
2468 let template = module.getattr(name).unwrap();
2469 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2470 let err = parse_template(&template).expect_err("expected TOML parse failure");
2471 assert_eq!(err.kind, ErrorKind::Parse);
2472 assert!(
2473 err.message.contains("Invalid TOML literal"),
2474 "{name}: {}",
2475 err.message
2476 );
2477 }
2478 });
2479 }
2480
2481 #[test]
2482 fn rejects_additional_invalid_literal_families() {
2483 Python::with_gil(|py| {
2484 let module = PyModule::from_code(
2485 py,
2486 pyo3::ffi::c_str!(
2487 "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"
2488 ),
2489 pyo3::ffi::c_str!("test_toml_additional_invalid_literals.py"),
2490 pyo3::ffi::c_str!("test_toml_additional_invalid_literals"),
2491 )
2492 .unwrap();
2493
2494 for name in [
2495 "invalid_exp_mixed_sign",
2496 "invalid_float_then_exp",
2497 "invalid_inline_pos",
2498 "invalid_nested_inline_pos",
2499 "invalid_triple_nested_plus",
2500 ] {
2501 let template = module.getattr(name).unwrap();
2502 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2503 let err = parse_template(&template).expect_err("expected TOML parse failure");
2504 assert_eq!(err.kind, ErrorKind::Parse);
2505 assert!(
2506 err.message.contains("Invalid TOML literal"),
2507 "{name}: {}",
2508 err.message
2509 );
2510 }
2511 });
2512 }
2513
2514 #[test]
2515 fn rejects_bare_literal_fragment_and_suffix_families() {
2516 Python::with_gil(|py| {
2517 let module = PyModule::from_code(
2518 py,
2519 pyo3::ffi::c_str!(
2520 "count=1\nfragment=t'value = 2{count}\\n'\nsuffix=t'value = {count}ms\\n'\n"
2521 ),
2522 pyo3::ffi::c_str!("test_toml_literal_fragments.py"),
2523 pyo3::ffi::c_str!("test_toml_literal_fragments"),
2524 )
2525 .unwrap();
2526
2527 for (name, expected) in [
2528 (
2529 "fragment",
2530 "TOML bare literals cannot contain fragment interpolations.",
2531 ),
2532 (
2533 "suffix",
2534 "Whole-value TOML interpolations cannot have bare suffix text.",
2535 ),
2536 ] {
2537 let template = module.getattr(name).unwrap();
2538 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2539 let err = parse_template(&template).expect_err("expected TOML parse failure");
2540 assert_eq!(err.kind, ErrorKind::Parse);
2541 assert!(err.message.contains(expected), "{name}: {}", err.message);
2542 }
2543 });
2544 }
2545
2546 #[test]
2547 fn renders_header_progressions_comments_and_crlf_text() {
2548 Python::with_gil(|py| {
2549 let module = PyModule::from_code(
2550 py,
2551 pyo3::ffi::c_str!(
2552 "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"
2553 ),
2554 pyo3::ffi::c_str!("test_toml_render_progressions.py"),
2555 pyo3::ffi::c_str!("test_toml_render_progressions"),
2556 )
2557 .unwrap();
2558
2559 let quoted_header_then_dotted = module.getattr("quoted_header_then_dotted").unwrap();
2560 let quoted_header_then_dotted =
2561 extract_template(py, "ed_header_then_dotted, "toml_t/toml_t_str").unwrap();
2562 let rendered =
2563 render_document(py, &parse_template("ed_header_then_dotted).unwrap()).unwrap();
2564 assert_eq!(
2565 rendered.text,
2566 "[\"a.b\"]\nvalue = 1\n[\"a.b\".c]\nname = \"x\""
2567 );
2568 let table = rendered.data.as_table().expect("expected TOML table");
2569 assert_eq!(table["a.b"]["c"]["name"].as_str(), Some("x"));
2570
2571 let commented_array = module.getattr("commented_array").unwrap();
2572 let commented_array =
2573 extract_template(py, &commented_array, "toml_t/toml_t_str").unwrap();
2574 let rendered = render_document(py, &parse_template(&commented_array).unwrap()).unwrap();
2575 assert_eq!(rendered.text, "value = [1, 2]");
2576 assert_eq!(
2577 rendered.data["value"]
2578 .as_array()
2579 .expect("array")
2580 .iter()
2581 .filter_map(toml::Value::as_integer)
2582 .collect::<Vec<_>>(),
2583 vec![1, 2]
2584 );
2585
2586 let literal_crlf = module.getattr("literal_crlf").unwrap();
2587 let literal_crlf = extract_template(py, &literal_crlf, "toml_t/toml_t_str").unwrap();
2588 let rendered = render_document(py, &parse_template(&literal_crlf).unwrap()).unwrap();
2589 assert_eq!(rendered.text, "value = \"a\\nb\"");
2590 assert_eq!(rendered.data["value"].as_str(), Some("a\nb"));
2591
2592 let array_then_table = module.getattr("array_then_table").unwrap();
2593 let array_then_table =
2594 extract_template(py, &array_then_table, "toml_t/toml_t_str").unwrap();
2595 let rendered =
2596 render_document(py, &parse_template(&array_then_table).unwrap()).unwrap();
2597 assert_eq!(rendered.text, "[[items]]\nname = \"a\"\n[tool]\nvalue = 1");
2598 let table = rendered.data.as_table().expect("expected TOML table");
2599 assert_eq!(table["items"].as_array().expect("array").len(), 1);
2600 assert_eq!(table["tool"]["value"].as_integer(), Some(1));
2601 });
2602 }
2603
2604 #[test]
2605 fn renders_temporal_values_and_special_float_arrays() {
2606 Python::with_gil(|py| {
2607 let module = PyModule::from_code(
2608 py,
2609 pyo3::ffi::c_str!(
2610 "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"
2611 ),
2612 pyo3::ffi::c_str!("test_toml_temporal_arrays.py"),
2613 pyo3::ffi::c_str!("test_toml_temporal_arrays"),
2614 )
2615 .unwrap();
2616
2617 let doc = module.getattr("doc").unwrap();
2618 let doc = extract_template(py, &doc, "toml_t/toml_t_str").unwrap();
2619 let rendered = render_document(py, &parse_template(&doc).unwrap()).unwrap();
2620 assert_eq!(
2621 rendered.text,
2622 "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]"
2623 );
2624 assert_eq!(
2625 rendered.data["local_date"]
2626 .as_datetime()
2627 .map(ToString::to_string),
2628 Some("2024-01-02".to_string())
2629 );
2630 assert_eq!(
2631 rendered.data["local_time"]
2632 .as_datetime()
2633 .map(ToString::to_string),
2634 Some("03:04:05.678901".to_string())
2635 );
2636 assert_eq!(
2637 rendered.data["offset_times"][0]
2638 .as_datetime()
2639 .map(ToString::to_string),
2640 Some("1979-05-27T07:32:00.999999-07:00".to_string())
2641 );
2642 assert_eq!(
2643 rendered.data["special"]
2644 .as_array()
2645 .expect("special array")
2646 .iter()
2647 .map(ToString::to_string)
2648 .collect::<Vec<_>>(),
2649 vec!["inf".to_string(), "-inf".to_string(), "nan".to_string()]
2650 );
2651 });
2652 }
2653
2654 #[test]
2655 fn renders_end_to_end_supported_positions_text_and_data() {
2656 Python::with_gil(|py| {
2657 let module = PyModule::from_code(
2658 py,
2659 pyo3::ffi::c_str!(
2660 "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"
2661 ),
2662 pyo3::ffi::c_str!("test_toml_end_to_end_positions.py"),
2663 pyo3::ffi::c_str!("test_toml_end_to_end_positions"),
2664 )
2665 .unwrap();
2666
2667 let template = module.getattr("template").unwrap();
2668 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2669 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2670 assert_eq!(
2671 rendered.text,
2672 "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\" }"
2673 );
2674 let table = rendered.data.as_table().expect("expected TOML table");
2675 assert_eq!(table["title"].as_str(), Some("item-prefix"));
2676 assert_eq!(table["root"]["leaf"]["name"].as_str(), Some("suffix"));
2677 assert_eq!(
2678 table["root"]["leaf"]["label"].as_str(),
2679 Some("prefix-suffix")
2680 );
2681 assert_eq!(
2682 table["root"]["leaf"]["created"]
2683 .as_datetime()
2684 .map(ToString::to_string),
2685 Some("2024-01-02T03:04:05+00:00".to_string())
2686 );
2687 assert_eq!(
2688 table["root"]["leaf"]["rows"]
2689 .as_array()
2690 .expect("rows")
2691 .iter()
2692 .filter_map(toml::Value::as_str)
2693 .collect::<Vec<_>>(),
2694 vec!["prefix", "suffix"]
2695 );
2696 assert_eq!(
2697 table["root"]["leaf"]["meta"]["target"].as_str(),
2698 Some("suffix")
2699 );
2700 });
2701 }
2702
2703 #[test]
2704 fn renders_string_families_exact_text_and_data() {
2705 Python::with_gil(|py| {
2706 let module = PyModule::from_code(
2707 py,
2708 pyo3::ffi::c_str!(
2709 "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"
2710 ),
2711 pyo3::ffi::c_str!("test_toml_string_families_render.py"),
2712 pyo3::ffi::c_str!("test_toml_string_families_render"),
2713 )
2714 .unwrap();
2715
2716 for (name, expected_key) in [
2717 ("basic", "basic"),
2718 ("literal", "literal"),
2719 ("multi_basic", "multi_basic"),
2720 ("multi_literal", "multi_literal"),
2721 ] {
2722 let template = module.getattr(name).unwrap();
2723 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2724 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2725 assert_eq!(rendered.data[expected_key].as_str(), Some("hi-name"));
2726 assert!(
2727 rendered.text.contains("hi-name"),
2728 "{name}: {}",
2729 rendered.text
2730 );
2731 }
2732 });
2733 }
2734
2735 #[test]
2736 fn renders_date_and_time_round_trip_shapes() {
2737 Python::with_gil(|py| {
2738 let module = PyModule::from_code(
2739 py,
2740 pyo3::ffi::c_str!(
2741 "from datetime import date, time\nday=date(2024, 1, 2)\nmoment=time(4, 5, 6)\ntemplate=t'day = {day}\\nmoment = {moment}'\n"
2742 ),
2743 pyo3::ffi::c_str!("test_toml_date_time_round_trip.py"),
2744 pyo3::ffi::c_str!("test_toml_date_time_round_trip"),
2745 )
2746 .unwrap();
2747
2748 let template = module.getattr("template").unwrap();
2749 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2750 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2751 assert_eq!(rendered.text, "day = 2024-01-02\nmoment = 04:05:06");
2752 assert_eq!(
2753 rendered.data["day"].as_datetime().map(ToString::to_string),
2754 Some("2024-01-02".to_string())
2755 );
2756 assert_eq!(
2757 rendered.data["moment"]
2758 .as_datetime()
2759 .map(ToString::to_string),
2760 Some("04:05:06".to_string())
2761 );
2762 });
2763 }
2764
2765 #[test]
2766 fn renders_array_tables_and_comment_preserving_shapes() {
2767 Python::with_gil(|py| {
2768 let module = PyModule::from_code(
2769 py,
2770 pyo3::ffi::c_str!(
2771 "name='api'\nworker='worker'\ntemplate=t'''\n# comment before content\n[[services]]\nname = {name} # inline comment\n\n[[services]]\nname = {worker}\n'''\n"
2772 ),
2773 pyo3::ffi::c_str!("test_toml_array_tables_comments.py"),
2774 pyo3::ffi::c_str!("test_toml_array_tables_comments"),
2775 )
2776 .unwrap();
2777
2778 let template = module.getattr("template").unwrap();
2779 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2780 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2781 assert_eq!(
2782 rendered.text,
2783 "[[services]]\nname = \"api\"\n[[services]]\nname = \"worker\""
2784 );
2785 let services = rendered.data["services"]
2786 .as_array()
2787 .expect("services array");
2788 assert_eq!(services.len(), 2);
2789 assert_eq!(services[0]["name"].as_str(), Some("api"));
2790 assert_eq!(services[1]["name"].as_str(), Some("worker"));
2791 });
2792 }
2793
2794 #[test]
2795 fn renders_array_of_tables_spec_example_text_and_data() {
2796 Python::with_gil(|py| {
2797 let module = PyModule::from_code(
2798 py,
2799 pyo3::ffi::c_str!(
2800 "template=t'[[products]]\\nname = \"Hammer\"\\nsku = 738594937\\n\\n[[products]]\\nname = \"Nail\"\\nsku = 284758393\\ncolor = \"gray\"\\n'\n"
2801 ),
2802 pyo3::ffi::c_str!("test_toml_array_of_tables_spec_example.py"),
2803 pyo3::ffi::c_str!("test_toml_array_of_tables_spec_example"),
2804 )
2805 .unwrap();
2806
2807 let template = module.getattr("template").unwrap();
2808 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2809 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2810 assert_eq!(
2811 rendered.text,
2812 "[[products]]\nname = \"Hammer\"\nsku = 738594937\n[[products]]\nname = \"Nail\"\nsku = 284758393\ncolor = \"gray\""
2813 );
2814 let products = rendered.data["products"]
2815 .as_array()
2816 .expect("products array");
2817 assert_eq!(products.len(), 2);
2818 assert_eq!(products[0]["name"].as_str(), Some("Hammer"));
2819 assert_eq!(products[0]["sku"].as_integer(), Some(738594937));
2820 assert_eq!(products[1]["name"].as_str(), Some("Nail"));
2821 assert_eq!(products[1]["sku"].as_integer(), Some(284758393));
2822 assert_eq!(products[1]["color"].as_str(), Some("gray"));
2823 });
2824 }
2825
2826 #[test]
2827 fn renders_nested_array_of_tables_spec_hierarchy_text_and_data() {
2828 Python::with_gil(|py| {
2829 let module = PyModule::from_code(
2830 py,
2831 pyo3::ffi::c_str!(
2832 "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"
2833 ),
2834 pyo3::ffi::c_str!("test_toml_nested_array_tables_spec_example.py"),
2835 pyo3::ffi::c_str!("test_toml_nested_array_tables_spec_example"),
2836 )
2837 .unwrap();
2838
2839 let template = module.getattr("template").unwrap();
2840 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2841 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2842 assert_eq!(
2843 rendered.text,
2844 "[[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\""
2845 );
2846 let fruit = rendered.data["fruit"].as_array().expect("fruit array");
2847 assert_eq!(fruit.len(), 2);
2848 assert_eq!(fruit[0]["name"].as_str(), Some("apple"));
2849 assert_eq!(fruit[0]["physical"]["color"].as_str(), Some("red"));
2850 assert_eq!(fruit[0]["physical"]["shape"].as_str(), Some("round"));
2851 let varieties = fruit[0]["variety"].as_array().expect("apple varieties");
2852 assert_eq!(varieties.len(), 2);
2853 assert_eq!(varieties[0]["name"].as_str(), Some("red delicious"));
2854 assert_eq!(varieties[1]["name"].as_str(), Some("granny smith"));
2855 assert_eq!(fruit[1]["name"].as_str(), Some("banana"));
2856 let varieties = fruit[1]["variety"].as_array().expect("banana varieties");
2857 assert_eq!(varieties.len(), 1);
2858 assert_eq!(varieties[0]["name"].as_str(), Some("plantain"));
2859 });
2860 }
2861
2862 #[test]
2863 fn renders_main_spec_example_text_and_data() {
2864 Python::with_gil(|py| {
2865 let module = PyModule::from_code(
2866 py,
2867 pyo3::ffi::c_str!(
2868 "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"
2869 ),
2870 pyo3::ffi::c_str!("test_toml_main_spec_example.py"),
2871 pyo3::ffi::c_str!("test_toml_main_spec_example"),
2872 )
2873 .unwrap();
2874
2875 let template = module.getattr("template").unwrap();
2876 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2877 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2878 assert_eq!(
2879 rendered.text,
2880 "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\""
2881 );
2882 assert_eq!(rendered.data["title"].as_str(), Some("TOML Example"));
2883 assert_eq!(
2884 rendered.data["owner"]["name"].as_str(),
2885 Some("Tom Preston-Werner")
2886 );
2887 assert_eq!(rendered.data["database"]["enabled"].as_bool(), Some(true));
2888 assert_eq!(
2889 rendered.data["database"]["ports"]
2890 .as_array()
2891 .expect("ports array")
2892 .len(),
2893 3
2894 );
2895 assert_eq!(
2896 rendered.data["servers"]["alpha"]["ip"].as_str(),
2897 Some("10.0.0.1")
2898 );
2899 assert_eq!(
2900 rendered.data["servers"]["beta"]["role"].as_str(),
2901 Some("backend")
2902 );
2903 });
2904 }
2905
2906 #[test]
2907 fn renders_empty_headers_and_empty_path_segments() {
2908 Python::with_gil(|py| {
2909 let module = PyModule::from_code(
2910 py,
2911 pyo3::ffi::c_str!(
2912 "empty_header=t'[\"\"]\\nvalue = 1\\n'\nempty_segment=t'a.\"\".b = 1\\n'\nempty_subsegments=t'[\"\".\"\".leaf]\\nvalue = 1\\n'\n"
2913 ),
2914 pyo3::ffi::c_str!("test_toml_empty_path_shapes.py"),
2915 pyo3::ffi::c_str!("test_toml_empty_path_shapes"),
2916 )
2917 .unwrap();
2918
2919 let empty_header = module.getattr("empty_header").unwrap();
2920 let empty_header = extract_template(py, &empty_header, "toml_t/toml_t_str").unwrap();
2921 let rendered = render_document(py, &parse_template(&empty_header).unwrap()).unwrap();
2922 assert_eq!(rendered.text, "[\"\"]\nvalue = 1");
2923 assert_eq!(rendered.data[""]["value"].as_integer(), Some(1));
2924
2925 let empty_segment = module.getattr("empty_segment").unwrap();
2926 let empty_segment = extract_template(py, &empty_segment, "toml_t/toml_t_str").unwrap();
2927 let rendered = render_document(py, &parse_template(&empty_segment).unwrap()).unwrap();
2928 assert_eq!(rendered.text, "a.\"\".b = 1");
2929 assert_eq!(rendered.data["a"][""]["b"].as_integer(), Some(1));
2930
2931 let empty_subsegments = module.getattr("empty_subsegments").unwrap();
2932 let empty_subsegments =
2933 extract_template(py, &empty_subsegments, "toml_t/toml_t_str").unwrap();
2934 let rendered =
2935 render_document(py, &parse_template(&empty_subsegments).unwrap()).unwrap();
2936 assert_eq!(rendered.text, "[\"\".\"\".leaf]\nvalue = 1");
2937 assert_eq!(rendered.data[""][""]["leaf"]["value"].as_integer(), Some(1));
2938 });
2939 }
2940
2941 #[test]
2942 fn renders_special_float_nested_shapes() {
2943 Python::with_gil(|py| {
2944 let module = PyModule::from_code(
2945 py,
2946 pyo3::ffi::c_str!(
2947 "template=t'special_float_inline_table = {{ pos = +inf, neg = -inf, nan = nan }}\\nspecial_float_mixed_nested = [[+inf, -inf], [nan]]\\n'\n"
2948 ),
2949 pyo3::ffi::c_str!("test_toml_special_float_nested.py"),
2950 pyo3::ffi::c_str!("test_toml_special_float_nested"),
2951 )
2952 .unwrap();
2953
2954 let template = module.getattr("template").unwrap();
2955 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
2956 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2957 assert_eq!(
2958 rendered.text,
2959 "special_float_inline_table = { pos = +inf, neg = -inf, nan = nan }\nspecial_float_mixed_nested = [[+inf, -inf], [nan]]"
2960 );
2961 assert!(
2962 rendered.data["special_float_inline_table"]["pos"]
2963 .as_float()
2964 .expect("pos float")
2965 .is_infinite()
2966 );
2967 assert!(
2968 rendered.data["special_float_inline_table"]["neg"]
2969 .as_float()
2970 .expect("neg float")
2971 .is_sign_negative()
2972 );
2973 assert!(
2974 rendered.data["special_float_inline_table"]["nan"]
2975 .as_float()
2976 .expect("nan float")
2977 .is_nan()
2978 );
2979 assert!(
2980 rendered.data["special_float_mixed_nested"][0][0]
2981 .as_float()
2982 .expect("nested pos")
2983 .is_infinite()
2984 );
2985 assert!(
2986 rendered.data["special_float_mixed_nested"][0][1]
2987 .as_float()
2988 .expect("nested neg")
2989 .is_sign_negative()
2990 );
2991 assert!(
2992 rendered.data["special_float_mixed_nested"][1][0]
2993 .as_float()
2994 .expect("nested nan")
2995 .is_nan()
2996 );
2997 });
2998 }
2999
3000 #[test]
3001 fn renders_numeric_and_datetime_literal_shapes() {
3002 Python::with_gil(|py| {
3003 let module = PyModule::from_code(
3004 py,
3005 pyo3::ffi::c_str!(
3006 "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"
3007 ),
3008 pyo3::ffi::c_str!("test_toml_numeric_datetime_literal_shapes.py"),
3009 pyo3::ffi::c_str!("test_toml_numeric_datetime_literal_shapes"),
3010 )
3011 .unwrap();
3012
3013 let template = module.getattr("template").unwrap();
3014 let template = extract_template(py, &template, "toml_t/toml_t_str").unwrap();
3015 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
3016 assert_eq!(
3017 rendered.text,
3018 "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]"
3019 );
3020 assert_eq!(rendered.data["plus_int"].as_integer(), Some(1));
3021 assert_eq!(rendered.data["plus_zero"].as_integer(), Some(0));
3022 assert_eq!(rendered.data["plus_zero_float"].as_float(), Some(0.0));
3023 assert_eq!(
3024 rendered.data["local_date"]
3025 .as_datetime()
3026 .and_then(|value| value.date.as_ref())
3027 .map(ToString::to_string),
3028 Some("2024-01-02".to_string())
3029 );
3030 assert_eq!(
3031 rendered.data["local_time_fraction"]
3032 .as_datetime()
3033 .and_then(|value| value.time.as_ref())
3034 .map(ToString::to_string),
3035 Some("03:04:05.123456".to_string())
3036 );
3037 assert_eq!(
3038 rendered.data["offset_fraction_dt"]
3039 .as_datetime()
3040 .map(ToString::to_string),
3041 Some("1979-05-27T07:32:00.999999-07:00".to_string())
3042 );
3043 assert_eq!(
3044 rendered.data["utc_fraction_lower_array"]
3045 .as_array()
3046 .expect("utc array")
3047 .len(),
3048 2
3049 );
3050 assert_eq!(
3051 rendered.data["signed_int_array"]
3052 .as_array()
3053 .expect("signed array")
3054 .iter()
3055 .filter_map(toml::Value::as_integer)
3056 .collect::<Vec<_>>(),
3057 vec![1, 0, -1]
3058 );
3059 });
3060 }
3061
3062 #[test]
3063 fn test_parse_rendered_toml_surfaces_parse_failures() {
3064 let err = parse_rendered_toml("[a]\nvalue = 1\n[a]\nname = \"x\"\n")
3065 .expect_err("expected TOML parse failure");
3066 assert_eq!(err.kind, ErrorKind::Parse);
3067 assert!(err.message.contains("Rendered TOML could not be reparsed"));
3068 assert!(err.message.contains("duplicate key"));
3069 }
3070}