Skip to main content

tstring_toml/
lib.rs

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