Skip to main content

tstring_toml/
lib.rs

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    // The template parser enforces profile-specific grammar boundaries first.
1084    // We still materialize with `toml::from_str` afterwards so literals and
1085    // formatted interpolation payloads share the same `toml` crate semantics.
1086    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    // TOML does not add format-specific post-parse validation yet. Keep the
1121    // validated entry point aligned with the other backends so callers can rely
1122    // on one API shape as backend-specific validation rules are introduced.
1123    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, &quoted_empty_dotted_table, "toml_t/toml_t_str").unwrap();
1842            let rendered =
1843                render_document(py, &parse_template(&quoted_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, &quoted_empty_subsegments, "toml_t/toml_t_str").unwrap();
1850            let rendered =
1851                render_document(py, &parse_template(&quoted_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, &quoted_empty_leaf_chain, "toml_t/toml_t_str").unwrap();
1858            let rendered =
1859                render_document(py, &parse_template(&quoted_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, &quoted_header, "toml_t/toml_t_str").unwrap();
2335            let rendered = render_document(py, &parse_template(&quoted_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, &quoted_segments, "toml_t/toml_t_str").unwrap();
2348            let rendered = render_document(py, &parse_template(&quoted_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, &quoted_header_then_dotted, "toml_t/toml_t_str").unwrap();
2355            let rendered =
2356                render_document(py, &parse_template(&quoted_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, &quoted_header_then_dotted, "toml_t/toml_t_str").unwrap();
2666            let rendered =
2667                render_document(py, &parse_template(&quoted_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}