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