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