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