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