Skip to main content

tstring_json/
lib.rs

1use serde_json::Value;
2use std::str::FromStr;
3use tstring_syntax::{
4    BackendError, BackendResult, NormalizedDocument, NormalizedFloat, NormalizedKey,
5    NormalizedStream, NormalizedValue, SourcePosition, SourceSpan, StreamItem, TemplateInput,
6};
7
8#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
9pub enum JsonProfile {
10    Rfc8259,
11}
12
13impl JsonProfile {
14    #[must_use]
15    pub const fn as_str(self) -> &'static str {
16        match self {
17            Self::Rfc8259 => "rfc8259",
18        }
19    }
20}
21
22impl Default for JsonProfile {
23    fn default() -> Self {
24        Self::Rfc8259
25    }
26}
27
28impl FromStr for JsonProfile {
29    type Err = String;
30
31    fn from_str(value: &str) -> Result<Self, Self::Err> {
32        match value {
33            "rfc8259" => Ok(Self::Rfc8259),
34            other => Err(format!(
35                "Unsupported JSON profile {other:?}. Supported profiles: \"rfc8259\"."
36            )),
37        }
38    }
39}
40
41#[derive(Clone, Debug)]
42pub struct JsonInterpolationNode {
43    pub span: SourceSpan,
44    pub interpolation_index: usize,
45    pub role: String,
46}
47
48#[derive(Clone, Debug)]
49pub struct JsonStringChunkNode {
50    pub span: SourceSpan,
51    pub value: String,
52}
53
54#[derive(Clone, Debug)]
55pub enum JsonStringPart {
56    Chunk(JsonStringChunkNode),
57    Interpolation(JsonInterpolationNode),
58}
59
60#[derive(Clone, Debug)]
61pub struct JsonStringNode {
62    pub span: SourceSpan,
63    pub chunks: Vec<JsonStringPart>,
64    pub quoted: bool,
65}
66
67#[derive(Clone, Debug)]
68pub struct JsonLiteralNode {
69    pub span: SourceSpan,
70    pub source: String,
71    pub value: Value,
72}
73
74#[derive(Clone, Debug)]
75pub struct JsonKeyNode {
76    pub span: SourceSpan,
77    pub value: JsonKeyValue,
78}
79
80#[derive(Clone, Debug)]
81pub enum JsonKeyValue {
82    String(JsonStringNode),
83    Interpolation(JsonInterpolationNode),
84}
85
86#[derive(Clone, Debug)]
87pub struct JsonMemberNode {
88    pub span: SourceSpan,
89    pub key: JsonKeyNode,
90    pub value: JsonValueNode,
91}
92
93#[derive(Clone, Debug)]
94pub struct JsonObjectNode {
95    pub span: SourceSpan,
96    pub members: Vec<JsonMemberNode>,
97}
98
99#[derive(Clone, Debug)]
100pub struct JsonArrayNode {
101    pub span: SourceSpan,
102    pub items: Vec<JsonValueNode>,
103}
104
105#[derive(Clone, Debug)]
106pub struct JsonDocumentNode {
107    pub span: SourceSpan,
108    pub value: JsonValueNode,
109}
110
111#[derive(Clone, Debug)]
112pub enum JsonValueNode {
113    String(JsonStringNode),
114    Literal(JsonLiteralNode),
115    Interpolation(JsonInterpolationNode),
116    Object(JsonObjectNode),
117    Array(JsonArrayNode),
118}
119
120pub struct JsonParser {
121    items: Vec<StreamItem>,
122    index: usize,
123}
124
125impl JsonParser {
126    #[must_use]
127    pub fn new(template: &TemplateInput) -> Self {
128        Self {
129            items: template.flatten(),
130            index: 0,
131        }
132    }
133
134    pub fn parse(&mut self) -> BackendResult<JsonDocumentNode> {
135        let start = self.mark();
136        let value = self.parse_value()?;
137        self.skip_whitespace();
138        if self.current_kind() != "eof" {
139            return Err(self.error("Unexpected trailing content in JSON template."));
140        }
141        Ok(JsonDocumentNode {
142            span: self.span_from(start),
143            value,
144        })
145    }
146
147    fn current(&self) -> &StreamItem {
148        &self.items[self.index]
149    }
150
151    fn current_kind(&self) -> &'static str {
152        self.current().kind()
153    }
154
155    fn current_char(&self) -> Option<char> {
156        self.current().char()
157    }
158
159    fn mark(&self) -> SourcePosition {
160        self.current().span().start.clone()
161    }
162
163    fn previous_end(&self) -> SourcePosition {
164        if self.index == 0 {
165            return self.current().span().start.clone();
166        }
167        self.items[self.index - 1].span().end.clone()
168    }
169
170    fn span_from(&self, start: SourcePosition) -> SourceSpan {
171        SourceSpan::between(start, self.previous_end())
172    }
173
174    fn error(&self, message: impl Into<String>) -> BackendError {
175        BackendError::parse_at("json.parse", message, Some(self.current().span().clone()))
176    }
177
178    fn advance(&mut self) {
179        if self.current_kind() != "eof" {
180            self.index += 1;
181        }
182    }
183
184    fn skip_whitespace(&mut self) {
185        while matches!(self.current_char(), Some(ch) if ch.is_whitespace()) {
186            self.advance();
187        }
188    }
189
190    fn consume_char(&mut self, expected: char) -> BackendResult<()> {
191        if self.current_char() != Some(expected) {
192            return Err(self.error(format!("Expected {expected:?} in JSON template.")));
193        }
194        self.advance();
195        Ok(())
196    }
197
198    fn parse_value(&mut self) -> BackendResult<JsonValueNode> {
199        self.skip_whitespace();
200        let start = self.mark();
201
202        if self.current_kind() == "interpolation" {
203            let interpolation = self.consume_interpolation("value")?;
204            if self.starts_value_terminator() {
205                return Ok(JsonValueNode::Interpolation(interpolation));
206            }
207            return Ok(JsonValueNode::String(self.parse_promoted_string(
208                start,
209                vec![JsonStringPart::Interpolation(interpolation)],
210            )?));
211        }
212
213        if self.current_char() == Some('{') {
214            return Ok(JsonValueNode::Object(self.parse_object()?));
215        }
216        if self.current_char() == Some('[') {
217            return Ok(JsonValueNode::Array(self.parse_array()?));
218        }
219        if self.current_char() == Some('"') {
220            return Ok(JsonValueNode::String(self.parse_string(true)?));
221        }
222        if matches!(self.current_char(), Some('-' | '0'..='9')) {
223            return Ok(JsonValueNode::Literal(self.parse_number()?));
224        }
225
226        if let Some(literal) = self.try_consume_literal("true", Value::Bool(true))? {
227            return Ok(JsonValueNode::Literal(literal));
228        }
229        if let Some(literal) = self.try_consume_literal("false", Value::Bool(false))? {
230            return Ok(JsonValueNode::Literal(literal));
231        }
232        if let Some(literal) = self.try_consume_literal("null", Value::Null)? {
233            return Ok(JsonValueNode::Literal(literal));
234        }
235
236        Ok(JsonValueNode::String(
237            self.parse_promoted_string(start, Vec::new())?,
238        ))
239    }
240
241    fn try_consume_literal(
242        &mut self,
243        text: &str,
244        value: Value,
245    ) -> BackendResult<Option<JsonLiteralNode>> {
246        let start_index = self.index;
247        let start = self.mark();
248        for expected in text.chars() {
249            if self.current_char() != Some(expected) {
250                self.index = start_index;
251                return Ok(None);
252            }
253            self.advance();
254        }
255        if !self.starts_value_terminator() {
256            self.index = start_index;
257            return Ok(None);
258        }
259        Ok(Some(JsonLiteralNode {
260            span: self.span_from(start),
261            source: text.to_owned(),
262            value,
263        }))
264    }
265
266    fn parse_number(&mut self) -> BackendResult<JsonLiteralNode> {
267        let start = self.mark();
268        let mut source = String::new();
269        while matches!(
270            self.current_char(),
271            Some('-' | '+' | '.' | 'e' | 'E' | '0'..='9')
272        ) {
273            source.push(self.current_char().unwrap_or_default());
274            self.advance();
275        }
276        if source.is_empty() {
277            return Err(self.error("Expected a JSON number."));
278        }
279        if !self.starts_value_terminator() {
280            return Err(self.error("Invalid JSON number literal."));
281        }
282        let value: Value = serde_json::from_str(&source)
283            .map_err(|err| self.error(format!("Invalid JSON number literal: {err}")))?;
284        Ok(JsonLiteralNode {
285            span: self.span_from(start),
286            source,
287            value,
288        })
289    }
290
291    fn parse_object(&mut self) -> BackendResult<JsonObjectNode> {
292        let start = self.mark();
293        self.consume_char('{')?;
294        self.skip_whitespace();
295        let mut members = Vec::new();
296        if self.current_char() == Some('}') {
297            self.advance();
298            return Ok(JsonObjectNode {
299                span: self.span_from(start),
300                members,
301            });
302        }
303
304        loop {
305            let member_start = self.mark();
306            let key = self.parse_key()?;
307            self.skip_whitespace();
308            self.consume_char(':')?;
309            let value = self.parse_value()?;
310            members.push(JsonMemberNode {
311                span: self.span_from(member_start),
312                key,
313                value,
314            });
315            self.skip_whitespace();
316            if self.current_char() == Some('}') {
317                self.advance();
318                break;
319            }
320            self.consume_char(',')?;
321            self.skip_whitespace();
322        }
323
324        Ok(JsonObjectNode {
325            span: self.span_from(start),
326            members,
327        })
328    }
329
330    fn parse_key(&mut self) -> BackendResult<JsonKeyNode> {
331        self.skip_whitespace();
332        let start = self.mark();
333        if self.current_kind() == "interpolation" {
334            return Ok(JsonKeyNode {
335                span: self.span_from(start),
336                value: JsonKeyValue::Interpolation(self.consume_interpolation("key")?),
337            });
338        }
339        if self.current_char() != Some('"') {
340            return Err(self.error("JSON object keys must be quoted strings or interpolations."));
341        }
342        Ok(JsonKeyNode {
343            span: self.span_from(start),
344            value: JsonKeyValue::String(self.parse_string(true)?),
345        })
346    }
347
348    fn parse_array(&mut self) -> BackendResult<JsonArrayNode> {
349        let start = self.mark();
350        self.consume_char('[')?;
351        self.skip_whitespace();
352        let mut items = Vec::new();
353        if self.current_char() == Some(']') {
354            self.advance();
355            return Ok(JsonArrayNode {
356                span: self.span_from(start),
357                items,
358            });
359        }
360
361        loop {
362            items.push(self.parse_value()?);
363            self.skip_whitespace();
364            if self.current_char() == Some(']') {
365                self.advance();
366                break;
367            }
368            self.consume_char(',')?;
369            self.skip_whitespace();
370        }
371
372        Ok(JsonArrayNode {
373            span: self.span_from(start),
374            items,
375        })
376    }
377
378    fn parse_string(&mut self, quoted: bool) -> BackendResult<JsonStringNode> {
379        let start = self.mark();
380        if quoted {
381            self.consume_char('"')?;
382        }
383        let mut chunks = Vec::new();
384        let mut buffer = String::new();
385
386        loop {
387            if quoted && self.current_char() == Some('"') {
388                self.advance();
389                break;
390            }
391            if quoted && self.current_kind() == "eof" {
392                return Err(self.error("Unterminated JSON string."));
393            }
394            if self.current_kind() == "interpolation" {
395                self.flush_buffer(&mut buffer, &mut chunks);
396                chunks.push(JsonStringPart::Interpolation(
397                    self.consume_interpolation("string_fragment")?,
398                ));
399                continue;
400            }
401            if self.current_char().is_none() {
402                break;
403            }
404            if quoted && self.current_char() == Some('\\') {
405                buffer.push(self.parse_escape_sequence()?);
406                continue;
407            }
408            if quoted && self.current_char().is_some_and(|ch| ch < ' ') {
409                return Err(self.error("Control characters are not allowed in JSON strings."));
410            }
411
412            buffer.push(self.current_char().unwrap_or_default());
413            self.advance();
414            if !quoted && self.starts_value_terminator() {
415                break;
416            }
417        }
418
419        self.flush_buffer(&mut buffer, &mut chunks);
420        Ok(JsonStringNode {
421            span: self.span_from(start),
422            chunks,
423            quoted,
424        })
425    }
426
427    fn parse_escape_sequence(&mut self) -> BackendResult<char> {
428        self.consume_char('\\')?;
429        let escape_char = self
430            .current_char()
431            .ok_or_else(|| self.error("Incomplete JSON escape sequence."))?;
432        self.advance();
433        let mapped = match escape_char {
434            '"' => Some('"'),
435            '\\' => Some('\\'),
436            '/' => Some('/'),
437            'b' => Some('\u{0008}'),
438            'f' => Some('\u{000c}'),
439            'n' => Some('\n'),
440            'r' => Some('\r'),
441            't' => Some('\t'),
442            _ => None,
443        };
444        if let Some(value) = mapped {
445            return Ok(value);
446        }
447        if escape_char == 'u' {
448            let codepoint = self.parse_unicode_escape_value()?;
449            if (0xD800..=0xDBFF).contains(&codepoint) {
450                self.consume_char('\\')?;
451                self.consume_char('u')?;
452                let low = self.parse_unicode_escape_value()?;
453                if !(0xDC00..=0xDFFF).contains(&low) {
454                    return Err(self.error("Invalid JSON unicode escape."));
455                }
456                let combined = 0x10000 + (((codepoint - 0xD800) << 10) | (low - 0xDC00));
457                return char::from_u32(combined)
458                    .ok_or_else(|| self.error("Invalid JSON unicode escape."));
459            }
460            if (0xDC00..=0xDFFF).contains(&codepoint) {
461                return Err(self.error("Invalid JSON unicode escape."));
462            }
463            return char::from_u32(codepoint)
464                .ok_or_else(|| self.error("Invalid JSON unicode escape."));
465        }
466        Err(self.error("Invalid JSON escape sequence."))
467    }
468
469    fn parse_unicode_escape_value(&mut self) -> BackendResult<u32> {
470        let digits = self.collect_exact_chars(4)?;
471        u32::from_str_radix(&digits, 16).map_err(|_| self.error("Invalid JSON unicode escape."))
472    }
473
474    fn collect_exact_chars(&mut self, count: usize) -> BackendResult<String> {
475        let mut digits = String::new();
476        for _ in 0..count {
477            let ch = self
478                .current_char()
479                .ok_or_else(|| self.error("Unexpected end of JSON escape sequence."))?;
480            digits.push(ch);
481            self.advance();
482        }
483        Ok(digits)
484    }
485
486    fn flush_buffer(&self, buffer: &mut String, chunks: &mut Vec<JsonStringPart>) {
487        if buffer.is_empty() {
488            return;
489        }
490        chunks.push(JsonStringPart::Chunk(JsonStringChunkNode {
491            span: SourceSpan::point(0, 0),
492            value: std::mem::take(buffer),
493        }));
494    }
495
496    fn parse_promoted_string(
497        &mut self,
498        start: SourcePosition,
499        mut chunks: Vec<JsonStringPart>,
500    ) -> BackendResult<JsonStringNode> {
501        if chunks.is_empty() && self.starts_value_terminator() {
502            return Err(self.error("Expected a JSON value."));
503        }
504
505        let mut buffer = String::new();
506        let mut saw_interpolation = chunks
507            .iter()
508            .any(|chunk| matches!(chunk, JsonStringPart::Interpolation(_)));
509
510        while self.current_kind() != "eof" && !self.starts_value_terminator() {
511            if self.current_kind() == "interpolation" {
512                self.flush_buffer(&mut buffer, &mut chunks);
513                chunks.push(JsonStringPart::Interpolation(
514                    self.consume_interpolation("string_fragment")?,
515                ));
516                saw_interpolation = true;
517                continue;
518            }
519            if matches!(self.current_char(), Some('"' | '{' | '[' | ':')) {
520                return Err(self.error("Invalid promoted JSON fragment content."));
521            }
522            if let Some(ch) = self.current_char() {
523                buffer.push(ch);
524                self.advance();
525            } else {
526                break;
527            }
528        }
529
530        self.flush_buffer(&mut buffer, &mut chunks);
531        if !saw_interpolation {
532            return Err(self.error("Expected a JSON value."));
533        }
534        self.trim_trailing_fragment_whitespace(&mut chunks);
535        Ok(JsonStringNode {
536            span: self.span_from(start),
537            chunks,
538            quoted: false,
539        })
540    }
541
542    fn trim_trailing_fragment_whitespace(&self, chunks: &mut Vec<JsonStringPart>) {
543        while let Some(last) = chunks.last_mut() {
544            let JsonStringPart::Chunk(last_chunk) = last else {
545                return;
546            };
547            let trimmed = last_chunk.value.trim_end().to_owned();
548            if trimmed.is_empty() {
549                chunks.pop();
550                continue;
551            }
552            last_chunk.value = trimmed;
553            return;
554        }
555    }
556
557    fn starts_value_terminator(&self) -> bool {
558        let mut probe = self.index;
559        while matches!(self.items[probe].char(), Some(ch) if ch.is_whitespace()) {
560            probe += 1;
561        }
562        matches!(
563            &self.items[probe],
564            StreamItem::Eof { .. }
565                | StreamItem::Char {
566                    ch: ',' | ']' | '}',
567                    ..
568                }
569        )
570    }
571
572    fn consume_interpolation(&mut self, role: &str) -> BackendResult<JsonInterpolationNode> {
573        let (interpolation_index, span) = match self.current() {
574            StreamItem::Interpolation {
575                interpolation_index,
576                span,
577                ..
578            } => (*interpolation_index, span.clone()),
579            _ => return Err(self.error("Expected an interpolation.")),
580        };
581        self.advance();
582        Ok(JsonInterpolationNode {
583            span,
584            interpolation_index,
585            role: role.to_owned(),
586        })
587    }
588}
589
590pub fn parse_template_with_profile(
591    template: &TemplateInput,
592    _profile: JsonProfile,
593) -> BackendResult<JsonDocumentNode> {
594    // JSON only exposes RFC 8259 in this phase, but the parameter is kept so
595    // the backend stays aligned with the public profile-aware API shape.
596    JsonParser::new(template).parse()
597}
598
599pub fn parse_template(template: &TemplateInput) -> BackendResult<JsonDocumentNode> {
600    parse_template_with_profile(template, JsonProfile::default())
601}
602
603pub fn check_template_with_profile(
604    template: &TemplateInput,
605    profile: JsonProfile,
606) -> BackendResult<()> {
607    parse_template_with_profile(template, profile).map(|_| ())
608}
609
610pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
611    check_template_with_profile(template, JsonProfile::default())
612}
613
614pub fn format_template_with_profile(
615    template: &TemplateInput,
616    profile: JsonProfile,
617) -> BackendResult<String> {
618    let document = parse_template_with_profile(template, profile)?;
619    format_json_value(template, &document.value)
620}
621
622pub fn format_template(template: &TemplateInput) -> BackendResult<String> {
623    format_template_with_profile(template, JsonProfile::default())
624}
625
626pub fn normalize_document_with_profile(
627    value: &Value,
628    _profile: JsonProfile,
629) -> BackendResult<NormalizedStream> {
630    // Normalization is currently profile-invariant for JSON, but it remains
631    // profile-aware so future variants do not require another API break.
632    Ok(NormalizedStream::new(vec![NormalizedDocument::Value(
633        normalize_value(value)?,
634    )]))
635}
636
637pub fn normalize_document(value: &Value) -> BackendResult<NormalizedStream> {
638    normalize_document_with_profile(value, JsonProfile::default())
639}
640
641pub fn normalize_value(value: &Value) -> BackendResult<NormalizedValue> {
642    match value {
643        Value::Null => Ok(NormalizedValue::Null),
644        Value::Bool(value) => Ok(NormalizedValue::Bool(*value)),
645        Value::String(value) => Ok(NormalizedValue::String(value.clone())),
646        Value::Array(values) => values
647            .iter()
648            .map(normalize_value)
649            .collect::<BackendResult<Vec<_>>>()
650            .map(NormalizedValue::Sequence),
651        Value::Object(values) => values
652            .iter()
653            .map(|(key, value)| {
654                Ok(tstring_syntax::NormalizedEntry {
655                    key: NormalizedKey::String(key.clone()),
656                    value: normalize_value(value)?,
657                })
658            })
659            .collect::<BackendResult<Vec<_>>>()
660            .map(NormalizedValue::Mapping),
661        Value::Number(number) => normalize_number(number),
662    }
663}
664
665fn format_json_value(template: &TemplateInput, node: &JsonValueNode) -> BackendResult<String> {
666    match node {
667        JsonValueNode::String(node) => format_json_string(template, node),
668        JsonValueNode::Literal(node) => Ok(node.source.clone()),
669        JsonValueNode::Interpolation(node) => {
670            interpolation_raw_source(template, node.interpolation_index, &node.span, "JSON value")
671        }
672        JsonValueNode::Object(node) => {
673            let mut members = Vec::with_capacity(node.members.len());
674            for member in &node.members {
675                members.push(format!(
676                    "{}: {}",
677                    format_json_key(template, &member.key)?,
678                    format_json_value(template, &member.value)?
679                ));
680            }
681            Ok(format!("{{{}}}", members.join(", ")))
682        }
683        JsonValueNode::Array(node) => {
684            let items = node
685                .items
686                .iter()
687                .map(|item| format_json_value(template, item))
688                .collect::<BackendResult<Vec<_>>>()?;
689            Ok(format!("[{}]", items.join(", ")))
690        }
691    }
692}
693
694fn format_json_key(template: &TemplateInput, node: &JsonKeyNode) -> BackendResult<String> {
695    match &node.value {
696        JsonKeyValue::String(node) => format_json_string(template, node),
697        JsonKeyValue::Interpolation(node) => interpolation_raw_source(
698            template,
699            node.interpolation_index,
700            &node.span,
701            "JSON object key",
702        ),
703    }
704}
705
706fn format_json_string(template: &TemplateInput, node: &JsonStringNode) -> BackendResult<String> {
707    let mut rendered = String::from("\"");
708    for chunk in &node.chunks {
709        match chunk {
710            JsonStringPart::Chunk(chunk) => rendered.push_str(&escape_json_fragment(&chunk.value)),
711            JsonStringPart::Interpolation(node) => rendered.push_str(&interpolation_raw_source(
712                template,
713                node.interpolation_index,
714                &node.span,
715                "JSON string fragment",
716            )?),
717        }
718    }
719    rendered.push('"');
720    Ok(rendered)
721}
722
723fn escape_json_fragment(text: &str) -> String {
724    let mut escaped = serde_json::to_string(text).unwrap_or_else(|_| "\"\"".to_owned());
725    escaped.remove(0);
726    escaped.pop();
727    escaped
728}
729
730fn interpolation_raw_source(
731    template: &TemplateInput,
732    interpolation_index: usize,
733    span: &SourceSpan,
734    context: &str,
735) -> BackendResult<String> {
736    template
737        .interpolation_raw_source(interpolation_index)
738        .map(str::to_owned)
739        .ok_or_else(|| {
740            let expression = template.interpolation(interpolation_index).map_or_else(
741                || format!("slot {interpolation_index}"),
742                |value| value.expression_label().to_owned(),
743            );
744            BackendError::semantic_at(
745                "json.format",
746                format!(
747                    "Cannot format {context} interpolation {expression:?} without raw source text."
748                ),
749                Some(span.clone()),
750            )
751        })
752}
753
754fn normalize_number(number: &serde_json::Number) -> BackendResult<NormalizedValue> {
755    let source = number.to_string();
756    if source.contains(['.', 'e', 'E']) {
757        return source
758            .parse::<f64>()
759            .map(NormalizedFloat::finite)
760            .map(NormalizedValue::Float)
761            .map_err(|err| {
762                BackendError::semantic(format!(
763                    "Validated JSON number {source} could not be normalized as a finite float: {err}"
764                ))
765            });
766    }
767
768    source.parse().map(NormalizedValue::Integer).map_err(|err| {
769        BackendError::semantic(format!(
770            "Validated JSON number {source} could not be normalized as an exact integer: {err}"
771        ))
772    })
773}
774
775#[cfg(test)]
776mod tests {
777    use super::{parse_template, JsonKeyValue, JsonStringPart, JsonValueNode};
778    use pyo3::prelude::*;
779    use serde_json::{json, Map, Number, Value};
780    use tstring_pyo3_bindings::{extract_template, json::render_document};
781    use tstring_syntax::{BackendError, BackendResult, ErrorKind};
782
783    fn parse_rendered_json(text: &str) -> BackendResult<Value> {
784        serde_json::from_str(text).map_err(|err| {
785            BackendError::parse(format!(
786                "Rendered JSON could not be reparsed during test verification: {err}"
787            ))
788        })
789    }
790
791    #[test]
792    fn parses_json_structure() {
793        Python::with_gil(|py| {
794            let module = PyModule::from_code(
795                py,
796                pyo3::ffi::c_str!(
797                    "from string.templatelib import Template\nleft='prefix'\nright='suffix'\ntemplate=t'{{\"prefix-{left}\": {left}-{right}}}'\n"
798                ),
799                pyo3::ffi::c_str!("test_json.py"),
800                pyo3::ffi::c_str!("test_json"),
801            )
802            .unwrap();
803            let template = module.getattr("template").unwrap();
804            let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
805            let document = parse_template(&template).unwrap();
806            let JsonValueNode::Object(object) = document.value else {
807                panic!("expected object");
808            };
809            assert_eq!(object.members.len(), 1);
810            let JsonKeyValue::String(key) = &object.members[0].key.value else {
811                panic!("expected interpolated string key");
812            };
813            assert_eq!(key.chunks.len(), 2);
814            assert!(matches!(key.chunks[1], JsonStringPart::Interpolation(_)));
815            let JsonValueNode::String(value) = &object.members[0].value else {
816                panic!("expected promoted string value");
817            };
818            assert_eq!(value.chunks.len(), 3);
819            assert!(matches!(value.chunks[0], JsonStringPart::Interpolation(_)));
820        });
821    }
822
823    #[test]
824    fn renders_nested_collections_and_validates() {
825        Python::with_gil(|py| {
826            let module = PyModule::from_code(
827                py,
828                pyo3::ffi::c_str!(
829                    "items=[1, {'name': 'Ada'}]\nname='Ada'\ntemplate=t'{{\"items\": {items}, \"message\": \"hi-{name}\"}}'\n"
830                ),
831                pyo3::ffi::c_str!("test_json_render.py"),
832                pyo3::ffi::c_str!("test_json_render"),
833            )
834            .unwrap();
835            let template = module.getattr("template").unwrap();
836            let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
837            let document = parse_template(&template).unwrap();
838            let rendered = render_document(py, &document).unwrap();
839
840            assert_eq!(rendered.data["message"], Value::String("hi-Ada".to_owned()));
841            assert_eq!(rendered.data["items"][0], Value::Number(Number::from(1)));
842            assert_eq!(
843                rendered.data["items"][1]["name"],
844                Value::String("Ada".to_owned())
845            );
846        });
847    }
848
849    #[test]
850    fn renders_top_level_scalar_text() {
851        Python::with_gil(|py| {
852            let module = PyModule::from_code(
853                py,
854                pyo3::ffi::c_str!(
855                    "template=t'{1}'\nbool_template=t'{True}'\nnull_template=t'{None}'\n"
856                ),
857                pyo3::ffi::c_str!("test_json_scalar.py"),
858                pyo3::ffi::c_str!("test_json_scalar"),
859            )
860            .unwrap();
861            let template = module.getattr("template").unwrap();
862            let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
863            let document = parse_template(&template).unwrap();
864            let rendered = render_document(py, &document).unwrap();
865
866            assert_eq!(rendered.text, "1");
867            assert_eq!(rendered.data, Value::Number(Number::from(1)));
868
869            let bool_template = module.getattr("bool_template").unwrap();
870            let bool_template = extract_template(py, &bool_template, "json_t/json_t_str").unwrap();
871            let document = parse_template(&bool_template).unwrap();
872            let rendered = render_document(py, &document).unwrap();
873            assert_eq!(rendered.text, "true");
874            assert_eq!(rendered.data, Value::Bool(true));
875
876            let null_template = module.getattr("null_template").unwrap();
877            let null_template = extract_template(py, &null_template, "json_t/json_t_str").unwrap();
878            let document = parse_template(&null_template).unwrap();
879            let rendered = render_document(py, &document).unwrap();
880            assert_eq!(rendered.text, "null");
881            assert_eq!(rendered.data, Value::Null);
882        });
883    }
884
885    #[test]
886    fn renders_quoted_key_fragments_and_promoted_fragments() {
887        Python::with_gil(|py| {
888            let module = PyModule::from_code(
889                py,
890                pyo3::ffi::c_str!(
891                    "left='prefix'\nright='suffix'\nrows=[{'name': 'one'}, {'name': 'two'}]\nquoted_key=t'{{\"{left}-{right}\": {1}, \"value\": {True}}}'\nfragment=t'{{\"label\": {left}-{right}}}'\npromoted=t'{rows}'\n"
892                ),
893                pyo3::ffi::c_str!("test_json_fragments.py"),
894                pyo3::ffi::c_str!("test_json_fragments"),
895            )
896            .unwrap();
897
898            let quoted_key = module.getattr("quoted_key").unwrap();
899            let quoted_key = extract_template(py, &quoted_key, "json_t/json_t_str").unwrap();
900            let rendered = render_document(py, &parse_template(&quoted_key).unwrap()).unwrap();
901            assert_eq!(rendered.data, json!({"prefix-suffix": 1, "value": true}));
902
903            let fragment = module.getattr("fragment").unwrap();
904            let fragment = extract_template(py, &fragment, "json_t/json_t_str").unwrap();
905            let rendered = render_document(py, &parse_template(&fragment).unwrap()).unwrap();
906            assert_eq!(rendered.text, "{\"label\": \"prefix-suffix\"}");
907            assert_eq!(rendered.data, json!({"label": "prefix-suffix"}));
908
909            let promoted = module.getattr("promoted").unwrap();
910            let promoted = extract_template(py, &promoted, "json_t/json_t_str").unwrap();
911            let rendered = render_document(py, &parse_template(&promoted).unwrap()).unwrap();
912            assert_eq!(rendered.text, r#"[{"name": "one"}, {"name": "two"}]"#);
913            assert_eq!(rendered.data, json!([{"name": "one"}, {"name": "two"}]));
914        });
915    }
916
917    #[test]
918    fn rejects_non_string_key_interpolation() {
919        Python::with_gil(|py| {
920            let module = PyModule::from_code(
921                py,
922                pyo3::ffi::c_str!("key=1\ntemplate=t'{{{key}: 1}}'\n"),
923                pyo3::ffi::c_str!("test_json_error.py"),
924                pyo3::ffi::c_str!("test_json_error"),
925            )
926            .unwrap();
927            let template = module.getattr("template").unwrap();
928            let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
929            let document = parse_template(&template).unwrap();
930            let err = match render_document(py, &document) {
931                Ok(_) => panic!("expected JSON render failure"),
932                Err(err) => err,
933            };
934
935            assert_eq!(err.kind, ErrorKind::Unrepresentable);
936            assert!(err.message.contains("JSON object keys must be str"));
937        });
938    }
939
940    #[test]
941    fn rejects_unrepresentable_render_value_families() {
942        Python::with_gil(|py| {
943            let module = PyModule::from_code(
944                py,
945                pyo3::ffi::c_str!(
946                    "import math\nbad_map={1: 'x'}\nbad_nan=math.nan\nbad_set={1, 2}\nmap_template=t'{bad_map}'\nnan_template=t'{bad_nan}'\nset_template=t'{bad_set}'\n"
947                ),
948                pyo3::ffi::c_str!("test_json_unrepresentable.py"),
949                pyo3::ffi::c_str!("test_json_unrepresentable"),
950            )
951            .unwrap();
952
953            for (name, expected) in [
954                ("map_template", "JSON object keys must be str"),
955                ("nan_template", "non-finite float"),
956                ("set_template", "could not be rendered as JSON"),
957            ] {
958                let template = module.getattr(name).unwrap();
959                let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
960                let document = parse_template(&template).unwrap();
961                let err = match render_document(py, &document) {
962                    Ok(_) => panic!("expected JSON render failure"),
963                    Err(err) => err,
964                };
965                assert_eq!(err.kind, ErrorKind::Unrepresentable);
966                assert!(err.message.contains(expected), "{name}: {}", err.message);
967            }
968        });
969    }
970
971    #[test]
972    fn parses_unicode_surrogate_pairs() {
973        Python::with_gil(|py| {
974            let module = PyModule::from_code(
975                py,
976                pyo3::ffi::c_str!("template=t'\"\\\\uD834\\\\uDD1E\"'\n"),
977                pyo3::ffi::c_str!("test_json_unicode.py"),
978                pyo3::ffi::c_str!("test_json_unicode"),
979            )
980            .unwrap();
981            let template = module.getattr("template").unwrap();
982            let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
983            let document = parse_template(&template).unwrap();
984            let rendered = render_document(py, &document).unwrap();
985
986            assert_eq!(rendered.data, Value::String("𝄞".to_owned()));
987        });
988    }
989
990    #[test]
991    fn parses_numbers_and_escape_sequences() {
992        Python::with_gil(|py| {
993            let module = PyModule::from_code(
994                py,
995                pyo3::ffi::c_str!(
996                    "template=t'{{\"int\": -0, \"exp\": 1.5e2, \"escapes\": \"\\\\b\\\\f\\\\n\\\\r\\\\t\\\\/\", \"unicode\": \"\\\\u00DF\\\\u6771\\\\uD834\\\\uDD1E\"}}'\n"
997                ),
998                pyo3::ffi::c_str!("test_json_numbers.py"),
999                pyo3::ffi::c_str!("test_json_numbers"),
1000            )
1001            .unwrap();
1002            let template = module.getattr("template").unwrap();
1003            let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1004            let document = parse_template(&template).unwrap();
1005            let rendered = render_document(py, &document).unwrap();
1006
1007            assert_eq!(
1008                rendered.data["exp"],
1009                Value::Number(Number::from_f64(150.0).unwrap())
1010            );
1011            assert_eq!(rendered.data["unicode"], Value::String("ß東𝄞".to_owned()));
1012            assert_eq!(
1013                rendered.data["escapes"],
1014                Value::String("\u{0008}\u{000c}\n\r\t/".to_owned())
1015            );
1016        });
1017    }
1018
1019    #[test]
1020    fn parses_whitespace_and_escaped_solidus_cases() {
1021        Python::with_gil(|py| {
1022            let module = PyModule::from_code(
1023                py,
1024                pyo3::ffi::c_str!(
1025                    "from string.templatelib import Template\ntop_bool_ws=Template(' \\n true \\t ')\ntop_null_ws=Template(' \\r\\n null \\n')\nempty_string=t'\"\"'\nempty_object=Template('{ \\n\\t }')\nempty_array=Template('[ \\n\\t ]')\narray_empty_values=Template('[\"\", 0, false, null, {}, []]')\nempty_object_in_array=Template('[{}, {\"a\": []}]')\ntop_level_empty_object_ws=Template(' \\n { } \\t ')\nescaped_controls=t'\"\\\\b\\\\f\\\\n\\\\r\\\\t\"'\nescaped_solidus=t'\"\\\\/\"'\nescaped_backslash=t'\"\\\\\\\\\"'\nunicode_backslash_escape=t'\"\\\\u005C\"'\nreverse_solidus_u=t'\"\\\\u005C/\"'\nescaped_quote_backslash=t'\"\\\\\\\"\\\\\\\\\"'\nescaped_null_and_unit_separator=t'\"\\\\u0000\\\\u001f\"'\nnested_upper_unicode=t'\"\\\\u00DF\\\\u6771\"'\nunicode_line_sep=t'\"\\\\u2028\"'\nunicode_para_sep=t'\"\\\\u2029\"'\narray_with_line_sep=t'[\"\\\\u2028\", \"\\\\u2029\"]'\nunicode_escapes_array=Template('[\"\\\\u005C\", \"\\\\/\", \"\\\\u00DF\"]')\nunicode_mix_nested_obj=Template('{\"x\": {\"a\": \"\\\\u005C\", \"b\": \"\\\\u00DF\", \"c\": \"\\\\u2029\"}}')\nnested_unicode_object_array=Template('{\"a\": [{\"b\": \"\\\\u005C\", \"c\": \"\\\\u00DF\"}]}')\nupper_unicode_mix_array=Template('[\"\\\\u00DF\", \"\\\\u6771\", \"\\\\u2028\"]')\nescaped_slash_backslash_quote=t'\"\\\\/\\\\\\\\\\\\\\\"\"'\nescaped_reverse_solidus_solidus=t'\"\\\\\\\\/\"'\nnested_escaped_mix=Template('{\"x\":\"\\\\b\\\\u2028\\\\u2029\\\\/\"}')\nupper_exp=t'1E2'\nupper_exp_plus=t'1E+2'\nupper_exp_negative=t'-1E+2'\nupper_exp_zero_fraction=t'0E+0'\nupper_zero_negative_exp=t'-0E-0'\nnested_upper_exp=Template('{\"value\": 1E+2}')\nneg_exp_zero=t'-1e-0'\nupper_exp_negative_zero=t'1E-0'\nexp_with_fraction_zero=t'1.0e-0'\nnegative_zero_exp_upper=t'-0E0'\nnested_bool_null_mix=Template('{\"v\": [true, null, false, {\"x\": 1}]}')\nkeyword_array=Template('[true,false,null]')\nempty_name_nested_keywords=Template('{\"\": [null, true, false]}')\nnested_empty_mix=Template('{\"a\": [{}, [], \"\", 0, false, null]}')\nnested_empty_collections_mix=Template('{\"a\": {\"b\": []}, \"c\": [{}, []]}')\narray_nested_mixed_scalars=Template('[{\"a\": []}, {\"b\": {}}, \"\", 0, false, null]')\nnested_negative_exp_mix=Template('{\"x\":[-1E-2,0,\"\",{\"y\":[null]}]}')\nmixed_nested_keywords=Template('{\"a\": [true, false, null], \"b\": {\"c\": -1e-0}}')\nnested_number_combo=Template('{\"a\": [0, -0, -0.0, 1e0, -1E-0]}')\nnested_number_whitespace=Template('{\"a\": [ 0 , -0 , 1.5E-2 ] }')\nnested_empty_names=Template('{\"\": {\"\": []}}')\nnested_empty_name_array=Template('{\"\": [\"\", {\"\": 0}]}')\nnested_nulls=Template('{\"a\": null, \"b\": [null, {\"c\": null}]}')\nnested_top_ws=Template('\\r\\n {\"a\": [1, {\"b\": \"c\"}], \"\": \"\"} \\n')\ntop_ws_string=Template('\\n\\r\\t \"x\" \\n')\nzero_fraction_exp=t'0.0e+0'\nnested=Template('[\\n {\"a\": 1, \"b\": [true, false, null]}\\n]')\n"
1026                ),
1027                pyo3::ffi::c_str!("test_json_whitespace.py"),
1028                pyo3::ffi::c_str!("test_json_whitespace"),
1029            )
1030            .unwrap();
1031
1032            let top_bool_ws = module.getattr("top_bool_ws").unwrap();
1033            let top_bool_ws = extract_template(py, &top_bool_ws, "json_t/json_t_str").unwrap();
1034            let rendered = render_document(py, &parse_template(&top_bool_ws).unwrap()).unwrap();
1035            assert_eq!(rendered.data, Value::Bool(true));
1036
1037            let top_null_ws = module.getattr("top_null_ws").unwrap();
1038            let top_null_ws = extract_template(py, &top_null_ws, "json_t/json_t_str").unwrap();
1039            let rendered = render_document(py, &parse_template(&top_null_ws).unwrap()).unwrap();
1040            assert_eq!(rendered.data, Value::Null);
1041
1042            let empty_string = module.getattr("empty_string").unwrap();
1043            let empty_string = extract_template(py, &empty_string, "json_t/json_t_str").unwrap();
1044            let rendered = render_document(py, &parse_template(&empty_string).unwrap()).unwrap();
1045            assert_eq!(rendered.data, Value::String(String::new()));
1046
1047            let empty_object = module.getattr("empty_object").unwrap();
1048            let empty_object = extract_template(py, &empty_object, "json_t/json_t_str").unwrap();
1049            let rendered = render_document(py, &parse_template(&empty_object).unwrap()).unwrap();
1050            assert_eq!(rendered.data, Value::Object(Default::default()));
1051
1052            let empty_array = module.getattr("empty_array").unwrap();
1053            let empty_array = extract_template(py, &empty_array, "json_t/json_t_str").unwrap();
1054            let rendered = render_document(py, &parse_template(&empty_array).unwrap()).unwrap();
1055            assert_eq!(rendered.data, Value::Array(Vec::new()));
1056
1057            let escaped_controls = module.getattr("escaped_controls").unwrap();
1058            let escaped_controls =
1059                extract_template(py, &escaped_controls, "json_t/json_t_str").unwrap();
1060            let rendered =
1061                render_document(py, &parse_template(&escaped_controls).unwrap()).unwrap();
1062            assert_eq!(
1063                rendered.data,
1064                Value::String("\u{0008}\u{000c}\n\r\t".to_owned())
1065            );
1066
1067            let escaped_solidus = module.getattr("escaped_solidus").unwrap();
1068            let escaped_solidus =
1069                extract_template(py, &escaped_solidus, "json_t/json_t_str").unwrap();
1070            let rendered = render_document(py, &parse_template(&escaped_solidus).unwrap()).unwrap();
1071            assert_eq!(rendered.data, Value::String("/".to_owned()));
1072
1073            let escaped_backslash = module.getattr("escaped_backslash").unwrap();
1074            let escaped_backslash =
1075                extract_template(py, &escaped_backslash, "json_t/json_t_str").unwrap();
1076            let rendered =
1077                render_document(py, &parse_template(&escaped_backslash).unwrap()).unwrap();
1078            assert_eq!(rendered.data, Value::String("\\".to_owned()));
1079
1080            let unicode_backslash_escape = module.getattr("unicode_backslash_escape").unwrap();
1081            let unicode_backslash_escape =
1082                extract_template(py, &unicode_backslash_escape, "json_t/json_t_str").unwrap();
1083            let rendered =
1084                render_document(py, &parse_template(&unicode_backslash_escape).unwrap()).unwrap();
1085            assert_eq!(rendered.data, Value::String("\\".to_owned()));
1086
1087            let reverse_solidus_u = module.getattr("reverse_solidus_u").unwrap();
1088            let reverse_solidus_u =
1089                extract_template(py, &reverse_solidus_u, "json_t/json_t_str").unwrap();
1090            let rendered =
1091                render_document(py, &parse_template(&reverse_solidus_u).unwrap()).unwrap();
1092            assert_eq!(rendered.data, Value::String("\\/".to_owned()));
1093
1094            let escaped_quote_backslash = module.getattr("escaped_quote_backslash").unwrap();
1095            let escaped_quote_backslash =
1096                extract_template(py, &escaped_quote_backslash, "json_t/json_t_str").unwrap();
1097            let rendered =
1098                render_document(py, &parse_template(&escaped_quote_backslash).unwrap()).unwrap();
1099            assert_eq!(rendered.data, Value::String("\"\\".to_owned()));
1100
1101            let escaped_null_and_unit_separator =
1102                module.getattr("escaped_null_and_unit_separator").unwrap();
1103            let escaped_null_and_unit_separator =
1104                extract_template(py, &escaped_null_and_unit_separator, "json_t/json_t_str")
1105                    .unwrap();
1106            let rendered = render_document(
1107                py,
1108                &parse_template(&escaped_null_and_unit_separator).unwrap(),
1109            )
1110            .unwrap();
1111            assert_eq!(rendered.data, Value::String("\u{0000}\u{001f}".to_owned()));
1112
1113            let nested_upper_unicode = module.getattr("nested_upper_unicode").unwrap();
1114            let nested_upper_unicode =
1115                extract_template(py, &nested_upper_unicode, "json_t/json_t_str").unwrap();
1116            let rendered =
1117                render_document(py, &parse_template(&nested_upper_unicode).unwrap()).unwrap();
1118            assert_eq!(rendered.data, Value::String("ß東".to_owned()));
1119
1120            let unicode_line_sep = module.getattr("unicode_line_sep").unwrap();
1121            let unicode_line_sep =
1122                extract_template(py, &unicode_line_sep, "json_t/json_t_str").unwrap();
1123            let rendered =
1124                render_document(py, &parse_template(&unicode_line_sep).unwrap()).unwrap();
1125            assert_eq!(rendered.data, Value::String("\u{2028}".to_owned()));
1126
1127            let unicode_para_sep = module.getattr("unicode_para_sep").unwrap();
1128            let unicode_para_sep =
1129                extract_template(py, &unicode_para_sep, "json_t/json_t_str").unwrap();
1130            let rendered =
1131                render_document(py, &parse_template(&unicode_para_sep).unwrap()).unwrap();
1132            assert_eq!(rendered.data, Value::String("\u{2029}".to_owned()));
1133
1134            let array_with_line_sep = module.getattr("array_with_line_sep").unwrap();
1135            let array_with_line_sep =
1136                extract_template(py, &array_with_line_sep, "json_t/json_t_str").unwrap();
1137            let rendered =
1138                render_document(py, &parse_template(&array_with_line_sep).unwrap()).unwrap();
1139            assert_eq!(rendered.data.as_array().expect("array").len(), 2);
1140            assert_eq!(rendered.data[0], Value::String("\u{2028}".to_owned()));
1141            assert_eq!(rendered.data[1], Value::String("\u{2029}".to_owned()));
1142
1143            let unicode_escapes_array = module.getattr("unicode_escapes_array").unwrap();
1144            let unicode_escapes_array =
1145                extract_template(py, &unicode_escapes_array, "json_t/json_t_str").unwrap();
1146            let rendered =
1147                render_document(py, &parse_template(&unicode_escapes_array).unwrap()).unwrap();
1148            let values = rendered.data.as_array().expect("array");
1149            assert_eq!(values[0], Value::String("\\".to_owned()));
1150            assert_eq!(values[1], Value::String("/".to_owned()));
1151            assert_eq!(values[2], Value::String("ß".to_owned()));
1152
1153            let unicode_mix_nested_obj = module.getattr("unicode_mix_nested_obj").unwrap();
1154            let unicode_mix_nested_obj =
1155                extract_template(py, &unicode_mix_nested_obj, "json_t/json_t_str").unwrap();
1156            let rendered =
1157                render_document(py, &parse_template(&unicode_mix_nested_obj).unwrap()).unwrap();
1158            assert_eq!(rendered.data["x"]["a"], Value::String("\\".to_owned()));
1159            assert_eq!(rendered.data["x"]["b"], Value::String("ß".to_owned()));
1160            assert_eq!(
1161                rendered.data["x"]["c"],
1162                Value::String("\u{2029}".to_owned())
1163            );
1164
1165            let nested_unicode_object_array =
1166                module.getattr("nested_unicode_object_array").unwrap();
1167            let nested_unicode_object_array =
1168                extract_template(py, &nested_unicode_object_array, "json_t/json_t_str").unwrap();
1169            let rendered =
1170                render_document(py, &parse_template(&nested_unicode_object_array).unwrap())
1171                    .unwrap();
1172            assert_eq!(rendered.data["a"][0]["b"], Value::String("\\".to_owned()));
1173            assert_eq!(rendered.data["a"][0]["c"], Value::String("ß".to_owned()));
1174
1175            let upper_unicode_mix_array = module.getattr("upper_unicode_mix_array").unwrap();
1176            let upper_unicode_mix_array =
1177                extract_template(py, &upper_unicode_mix_array, "json_t/json_t_str").unwrap();
1178            let rendered =
1179                render_document(py, &parse_template(&upper_unicode_mix_array).unwrap()).unwrap();
1180            let values = rendered.data.as_array().expect("array");
1181            assert_eq!(values[0], Value::String("ß".to_owned()));
1182            assert_eq!(values[1], Value::String("東".to_owned()));
1183            assert_eq!(values[2], Value::String("\u{2028}".to_owned()));
1184
1185            let escaped_slash_backslash_quote =
1186                module.getattr("escaped_slash_backslash_quote").unwrap();
1187            let escaped_slash_backslash_quote =
1188                extract_template(py, &escaped_slash_backslash_quote, "json_t/json_t_str").unwrap();
1189            let rendered =
1190                render_document(py, &parse_template(&escaped_slash_backslash_quote).unwrap())
1191                    .unwrap();
1192            assert_eq!(rendered.data, Value::String("/\\\"".to_owned()));
1193
1194            let array_empty_values = module.getattr("array_empty_values").unwrap();
1195            let array_empty_values =
1196                extract_template(py, &array_empty_values, "json_t/json_t_str").unwrap();
1197            let rendered =
1198                render_document(py, &parse_template(&array_empty_values).unwrap()).unwrap();
1199            let values = rendered.data.as_array().expect("array");
1200            assert_eq!(values.len(), 6);
1201            assert_eq!(values[0], Value::String(String::new()));
1202            assert_eq!(values[1], Value::Number(Number::from(0)));
1203            assert_eq!(values[2], Value::Bool(false));
1204            assert_eq!(values[3], Value::Null);
1205            assert_eq!(values[4], Value::Object(Map::new()));
1206            assert_eq!(values[5], Value::Array(Vec::new()));
1207
1208            let empty_object_in_array = module.getattr("empty_object_in_array").unwrap();
1209            let empty_object_in_array =
1210                extract_template(py, &empty_object_in_array, "json_t/json_t_str").unwrap();
1211            let rendered =
1212                render_document(py, &parse_template(&empty_object_in_array).unwrap()).unwrap();
1213            let values = rendered.data.as_array().expect("array");
1214            assert_eq!(values.len(), 2);
1215            assert_eq!(values[0], Value::Object(Map::new()));
1216            assert_eq!(values[1]["a"], Value::Array(Vec::new()));
1217
1218            let top_level_empty_object_ws = module.getattr("top_level_empty_object_ws").unwrap();
1219            let top_level_empty_object_ws =
1220                extract_template(py, &top_level_empty_object_ws, "json_t/json_t_str").unwrap();
1221            let rendered =
1222                render_document(py, &parse_template(&top_level_empty_object_ws).unwrap()).unwrap();
1223            assert_eq!(rendered.data, Value::Object(Map::new()));
1224
1225            let nested_escaped_mix = module.getattr("nested_escaped_mix").unwrap();
1226            let nested_escaped_mix =
1227                extract_template(py, &nested_escaped_mix, "json_t/json_t_str").unwrap();
1228            let rendered =
1229                render_document(py, &parse_template(&nested_escaped_mix).unwrap()).unwrap();
1230            assert_eq!(
1231                rendered.data["x"],
1232                Value::String("\u{0008}\u{2028}\u{2029}/".to_owned())
1233            );
1234
1235            let escaped_reverse_solidus_solidus =
1236                module.getattr("escaped_reverse_solidus_solidus").unwrap();
1237            let escaped_reverse_solidus_solidus =
1238                extract_template(py, &escaped_reverse_solidus_solidus, "json_t/json_t_str")
1239                    .unwrap();
1240            let rendered = render_document(
1241                py,
1242                &parse_template(&escaped_reverse_solidus_solidus).unwrap(),
1243            )
1244            .unwrap();
1245            assert_eq!(rendered.data, Value::String("\\/".to_owned()));
1246
1247            let upper_exp = module.getattr("upper_exp").unwrap();
1248            let upper_exp = extract_template(py, &upper_exp, "json_t/json_t_str").unwrap();
1249            let rendered = render_document(py, &parse_template(&upper_exp).unwrap()).unwrap();
1250            assert_eq!(
1251                rendered.data,
1252                Value::Number(Number::from_f64(100.0).unwrap())
1253            );
1254
1255            let upper_exp_plus = module.getattr("upper_exp_plus").unwrap();
1256            let upper_exp_plus =
1257                extract_template(py, &upper_exp_plus, "json_t/json_t_str").unwrap();
1258            let rendered = render_document(py, &parse_template(&upper_exp_plus).unwrap()).unwrap();
1259            assert_eq!(
1260                rendered.data,
1261                Value::Number(Number::from_f64(100.0).unwrap())
1262            );
1263
1264            let upper_exp_negative = module.getattr("upper_exp_negative").unwrap();
1265            let upper_exp_negative =
1266                extract_template(py, &upper_exp_negative, "json_t/json_t_str").unwrap();
1267            let rendered =
1268                render_document(py, &parse_template(&upper_exp_negative).unwrap()).unwrap();
1269            assert_eq!(
1270                rendered.data,
1271                Value::Number(Number::from_f64(-100.0).unwrap())
1272            );
1273
1274            let upper_exp_zero_fraction = module.getattr("upper_exp_zero_fraction").unwrap();
1275            let upper_exp_zero_fraction =
1276                extract_template(py, &upper_exp_zero_fraction, "json_t/json_t_str").unwrap();
1277            let rendered =
1278                render_document(py, &parse_template(&upper_exp_zero_fraction).unwrap()).unwrap();
1279            assert_eq!(rendered.data, Value::Number(Number::from_f64(0.0).unwrap()));
1280
1281            let upper_zero_negative_exp = module.getattr("upper_zero_negative_exp").unwrap();
1282            let upper_zero_negative_exp =
1283                extract_template(py, &upper_zero_negative_exp, "json_t/json_t_str").unwrap();
1284            let rendered =
1285                render_document(py, &parse_template(&upper_zero_negative_exp).unwrap()).unwrap();
1286            assert_eq!(
1287                rendered.data,
1288                Value::Number(Number::from_f64(-0.0).unwrap())
1289            );
1290
1291            let nested_upper_exp = module.getattr("nested_upper_exp").unwrap();
1292            let nested_upper_exp =
1293                extract_template(py, &nested_upper_exp, "json_t/json_t_str").unwrap();
1294            let rendered =
1295                render_document(py, &parse_template(&nested_upper_exp).unwrap()).unwrap();
1296            assert_eq!(
1297                rendered.data["value"],
1298                Value::Number(Number::from_f64(100.0).unwrap())
1299            );
1300
1301            let neg_exp_zero = module.getattr("neg_exp_zero").unwrap();
1302            let neg_exp_zero = extract_template(py, &neg_exp_zero, "json_t/json_t_str").unwrap();
1303            let rendered = render_document(py, &parse_template(&neg_exp_zero).unwrap()).unwrap();
1304            assert_eq!(
1305                rendered.data,
1306                Value::Number(Number::from_f64(-1.0).unwrap())
1307            );
1308
1309            let upper_exp_negative_zero = module.getattr("upper_exp_negative_zero").unwrap();
1310            let upper_exp_negative_zero =
1311                extract_template(py, &upper_exp_negative_zero, "json_t/json_t_str").unwrap();
1312            let rendered =
1313                render_document(py, &parse_template(&upper_exp_negative_zero).unwrap()).unwrap();
1314            assert_eq!(rendered.data, Value::Number(Number::from_f64(1.0).unwrap()));
1315
1316            let exp_with_fraction_zero = module.getattr("exp_with_fraction_zero").unwrap();
1317            let exp_with_fraction_zero =
1318                extract_template(py, &exp_with_fraction_zero, "json_t/json_t_str").unwrap();
1319            let rendered =
1320                render_document(py, &parse_template(&exp_with_fraction_zero).unwrap()).unwrap();
1321            assert_eq!(rendered.data, Value::Number(Number::from_f64(1.0).unwrap()));
1322
1323            let negative_zero_exp_upper = module.getattr("negative_zero_exp_upper").unwrap();
1324            let negative_zero_exp_upper =
1325                extract_template(py, &negative_zero_exp_upper, "json_t/json_t_str").unwrap();
1326            let rendered =
1327                render_document(py, &parse_template(&negative_zero_exp_upper).unwrap()).unwrap();
1328            assert_eq!(
1329                rendered.data,
1330                Value::Number(Number::from_f64(-0.0).unwrap())
1331            );
1332
1333            let nested_negative_exp_mix = module.getattr("nested_negative_exp_mix").unwrap();
1334            let nested_negative_exp_mix =
1335                extract_template(py, &nested_negative_exp_mix, "json_t/json_t_str").unwrap();
1336            let rendered =
1337                render_document(py, &parse_template(&nested_negative_exp_mix).unwrap()).unwrap();
1338            assert_eq!(
1339                rendered.data["x"][0],
1340                Value::Number(Number::from_f64(-0.01).unwrap())
1341            );
1342            assert_eq!(rendered.data["x"][1], Value::Number(Number::from(0)));
1343            assert_eq!(rendered.data["x"][2], Value::String(String::new()));
1344            assert_eq!(rendered.data["x"][3]["y"][0], Value::Null);
1345
1346            let mixed_nested_keywords = module.getattr("mixed_nested_keywords").unwrap();
1347            let mixed_nested_keywords =
1348                extract_template(py, &mixed_nested_keywords, "json_t/json_t_str").unwrap();
1349            let rendered =
1350                render_document(py, &parse_template(&mixed_nested_keywords).unwrap()).unwrap();
1351            assert_eq!(rendered.data["a"][0], Value::Bool(true));
1352            assert_eq!(rendered.data["a"][1], Value::Bool(false));
1353            assert_eq!(rendered.data["a"][2], Value::Null);
1354            assert_eq!(
1355                rendered.data["b"]["c"],
1356                Value::Number(Number::from_f64(-1.0).unwrap())
1357            );
1358
1359            let nested_empty_names = module.getattr("nested_empty_names").unwrap();
1360            let nested_empty_names =
1361                extract_template(py, &nested_empty_names, "json_t/json_t_str").unwrap();
1362            let rendered =
1363                render_document(py, &parse_template(&nested_empty_names).unwrap()).unwrap();
1364            assert_eq!(rendered.data[""][""], Value::Array(Vec::new()));
1365
1366            let nested_empty_name_array = module.getattr("nested_empty_name_array").unwrap();
1367            let nested_empty_name_array =
1368                extract_template(py, &nested_empty_name_array, "json_t/json_t_str").unwrap();
1369            let rendered =
1370                render_document(py, &parse_template(&nested_empty_name_array).unwrap()).unwrap();
1371            assert_eq!(rendered.data[""][0], Value::String(String::new()));
1372            assert_eq!(rendered.data[""][1][""], Value::Number(Number::from(0)));
1373
1374            let nested_nulls = module.getattr("nested_nulls").unwrap();
1375            let nested_nulls = extract_template(py, &nested_nulls, "json_t/json_t_str").unwrap();
1376            let rendered = render_document(py, &parse_template(&nested_nulls).unwrap()).unwrap();
1377            assert_eq!(rendered.data["a"], Value::Null);
1378            assert_eq!(rendered.data["b"][0], Value::Null);
1379            assert_eq!(rendered.data["b"][1]["c"], Value::Null);
1380
1381            let nested_top_ws = module.getattr("nested_top_ws").unwrap();
1382            let nested_top_ws = extract_template(py, &nested_top_ws, "json_t/json_t_str").unwrap();
1383            let rendered = render_document(py, &parse_template(&nested_top_ws).unwrap()).unwrap();
1384            assert_eq!(rendered.data["a"][1]["b"], Value::String("c".to_owned()));
1385            assert_eq!(rendered.data[""], Value::String(String::new()));
1386
1387            let top_ws_string = module.getattr("top_ws_string").unwrap();
1388            let top_ws_string = extract_template(py, &top_ws_string, "json_t/json_t_str").unwrap();
1389            let rendered = render_document(py, &parse_template(&top_ws_string).unwrap()).unwrap();
1390            assert_eq!(rendered.data, Value::String("x".to_owned()));
1391
1392            let nested_number_whitespace = module.getattr("nested_number_whitespace").unwrap();
1393            let nested_number_whitespace =
1394                extract_template(py, &nested_number_whitespace, "json_t/json_t_str").unwrap();
1395            let rendered =
1396                render_document(py, &parse_template(&nested_number_whitespace).unwrap()).unwrap();
1397            let values = rendered.data["a"].as_array().expect("array");
1398            assert_eq!(values.len(), 3);
1399            assert_eq!(values[0], Value::Number(Number::from(0)));
1400            assert_eq!(values[1], Value::Number(Number::from_f64(-0.0).unwrap()));
1401            assert_eq!(values[2], Value::Number(Number::from_f64(0.015).unwrap()));
1402
1403            let nested_number_combo = module.getattr("nested_number_combo").unwrap();
1404            let nested_number_combo =
1405                extract_template(py, &nested_number_combo, "json_t/json_t_str").unwrap();
1406            let rendered =
1407                render_document(py, &parse_template(&nested_number_combo).unwrap()).unwrap();
1408            let values = rendered.data["a"].as_array().expect("array");
1409            assert_eq!(values[0], Value::Number(Number::from(0)));
1410            assert_eq!(values[1], Value::Number(Number::from_f64(-0.0).unwrap()));
1411            assert_eq!(values[2], Value::Number(Number::from_f64(-0.0).unwrap()));
1412            assert_eq!(values[3], Value::Number(Number::from_f64(1.0).unwrap()));
1413            assert_eq!(values[4], Value::Number(Number::from_f64(-1.0).unwrap()));
1414
1415            let nested_bool_null_mix = module.getattr("nested_bool_null_mix").unwrap();
1416            let nested_bool_null_mix =
1417                extract_template(py, &nested_bool_null_mix, "json_t/json_t_str").unwrap();
1418            let rendered =
1419                render_document(py, &parse_template(&nested_bool_null_mix).unwrap()).unwrap();
1420            assert_eq!(rendered.data["v"][0], Value::Bool(true));
1421            assert_eq!(rendered.data["v"][1], Value::Null);
1422            assert_eq!(rendered.data["v"][2], Value::Bool(false));
1423            assert_eq!(rendered.data["v"][3]["x"], Value::Number(Number::from(1)));
1424
1425            let keyword_array = module.getattr("keyword_array").unwrap();
1426            let keyword_array = extract_template(py, &keyword_array, "json_t/json_t_str").unwrap();
1427            let rendered = render_document(py, &parse_template(&keyword_array).unwrap()).unwrap();
1428            let values = rendered.data.as_array().expect("array");
1429            assert_eq!(values[0], Value::Bool(true));
1430            assert_eq!(values[1], Value::Bool(false));
1431            assert_eq!(values[2], Value::Null);
1432
1433            let empty_name_nested_keywords = module.getattr("empty_name_nested_keywords").unwrap();
1434            let empty_name_nested_keywords =
1435                extract_template(py, &empty_name_nested_keywords, "json_t/json_t_str").unwrap();
1436            let rendered =
1437                render_document(py, &parse_template(&empty_name_nested_keywords).unwrap()).unwrap();
1438            let values = rendered.data[""].as_array().expect("array");
1439            assert_eq!(values[0], Value::Null);
1440            assert_eq!(values[1], Value::Bool(true));
1441            assert_eq!(values[2], Value::Bool(false));
1442
1443            let nested_empty_mix = module.getattr("nested_empty_mix").unwrap();
1444            let nested_empty_mix =
1445                extract_template(py, &nested_empty_mix, "json_t/json_t_str").unwrap();
1446            let rendered =
1447                render_document(py, &parse_template(&nested_empty_mix).unwrap()).unwrap();
1448            let values = rendered.data["a"].as_array().expect("array");
1449            assert_eq!(values[0], Value::Object(Map::new()));
1450            assert_eq!(values[1], Value::Array(Vec::new()));
1451            assert_eq!(values[2], Value::String(String::new()));
1452            assert_eq!(values[3], Value::Number(Number::from(0)));
1453            assert_eq!(values[4], Value::Bool(false));
1454            assert_eq!(values[5], Value::Null);
1455
1456            let nested_empty_collections_mix =
1457                module.getattr("nested_empty_collections_mix").unwrap();
1458            let nested_empty_collections_mix =
1459                extract_template(py, &nested_empty_collections_mix, "json_t/json_t_str").unwrap();
1460            let rendered =
1461                render_document(py, &parse_template(&nested_empty_collections_mix).unwrap())
1462                    .unwrap();
1463            assert_eq!(rendered.data["a"]["b"], Value::Array(Vec::new()));
1464            assert_eq!(rendered.data["c"][0], Value::Object(Map::new()));
1465            assert_eq!(rendered.data["c"][1], Value::Array(Vec::new()));
1466
1467            let array_nested_mixed_scalars = module.getattr("array_nested_mixed_scalars").unwrap();
1468            let array_nested_mixed_scalars =
1469                extract_template(py, &array_nested_mixed_scalars, "json_t/json_t_str").unwrap();
1470            let rendered =
1471                render_document(py, &parse_template(&array_nested_mixed_scalars).unwrap()).unwrap();
1472            let values = rendered.data.as_array().expect("array");
1473            assert_eq!(values[0]["a"], Value::Array(Vec::new()));
1474            assert_eq!(values[1]["b"], Value::Object(Map::new()));
1475            assert_eq!(values[2], Value::String(String::new()));
1476            assert_eq!(values[3], Value::Number(Number::from(0)));
1477            assert_eq!(values[4], Value::Bool(false));
1478            assert_eq!(values[5], Value::Null);
1479
1480            let zero_fraction_exp = module.getattr("zero_fraction_exp").unwrap();
1481            let zero_fraction_exp =
1482                extract_template(py, &zero_fraction_exp, "json_t/json_t_str").unwrap();
1483            let rendered =
1484                render_document(py, &parse_template(&zero_fraction_exp).unwrap()).unwrap();
1485            assert_eq!(rendered.data, Value::Number(Number::from_f64(0.0).unwrap()));
1486
1487            let nested = module.getattr("nested").unwrap();
1488            let nested = extract_template(py, &nested, "json_t/json_t_str").unwrap();
1489            let rendered = render_document(py, &parse_template(&nested).unwrap()).unwrap();
1490            assert_eq!(rendered.data.as_array().expect("array").len(), 1);
1491            assert_eq!(rendered.data[0]["a"], Value::Number(Number::from(1)));
1492            assert_eq!(rendered.data[0]["b"][0], Value::Bool(true));
1493            assert_eq!(rendered.data[0]["b"][1], Value::Bool(false));
1494            assert_eq!(rendered.data[0]["b"][2], Value::Null);
1495        });
1496    }
1497
1498    #[test]
1499    fn rejects_invalid_unicode_and_control_sequences() {
1500        Python::with_gil(|py| {
1501            let module = PyModule::from_code(
1502                py,
1503                pyo3::ffi::c_str!(
1504                    "from string.templatelib import Template\nincomplete=Template('\"\\\\u12\"')\ncontrol=Template('\"a\\nb\"')\nlow=Template('\"\\\\uDD1E\"')\nexp=Template('1e+')\nexp2=Template('1e')\ndot=Template('1.')\nminus=Template('-')\nobject_exp=Template('{\"a\": 1e+}')\nobject_dot=Template('{\"a\": 1.}')\ntrailing_true=Template('true false')\ntrailing_string=Template('\"a\" \"b\"')\nextra_close=Template('[1]]')\nobject_extra_close=Template('{\"a\":1}}')\ndouble_trailing_close=Template('{\"a\": [1]}]')\nmissing_array_comma=Template('[1 2]')\nmissing_object_comma=Template('{\"a\":1 \"b\":2}')\nnested_missing_object_array_comma=Template('{\"a\": [1 2]}')\nextra_array_comma=Template('[1,,2]')\nleading_comma_array=Template('[,1]')\nmissing_value=Template('{\"a\":,1}')\nmissing_key=Template('{,}')\nmissing_colon=Template('{\"a\" 1}')\ntrailing_object_comma=Template('{\"a\":1,}')\ntrailing_array_comma=Template('[1,2,]')\nnested_trailing_object_comma=Template('[{\"a\":1,}]')\nplus_number=Template('+1')\nleading_decimal=Template('.1')\nleading_zero=Template('00')\nnegative_leading_zero=Template('-01')\nleading_zero_exp=Template('01e0')\nnegative_leading_zero_exp=Template('-01e0')\nmissing_exp_digits_minus=Template('1e-')\nobject_missing_exp_digits_minus=Template('{\"x\": 1e-}')\nobject_negative_leading_zero=Template('{\"a\": -01}')\narray_double_value_no_comma=Template('[true false]')\nnested_missing_comma_bool=Template('{\"a\": [true false]}')\narray_true_number_no_comma=Template('[true 1]')\narray_false_object_no_comma=Template('[false {\"a\":1}]')\nnested_missing_comma_obj_after_null=Template('[null {\"a\":1}]')\nobject_double_value_no_comma=Template('{\"a\": true false}')\nobject_keyword_number_no_comma=Template('{\"a\": true 1}')\ntruncated_true=Template('tru')\ntruncated_false=Template('fals')\ntruncated_null=Template('nul')\nobject_truncated_true=Template('{\"a\": tru}')\nobject_truncated_false=Template('{\"a\": fals}')\nobject_truncated_null=Template('{\"a\": nul}')\narray_truncated_true=Template('[tru]')\narray_truncated_false=Template('[fals]')\nnested_array_truncated_true=Template('{\"a\": [tru]}')\nnested_array_truncated_false=Template('{\"a\": [fals]}')\narray_truncated_null=Template('[nul]')\narray_bad_null_case=Template('[nulL]')\nnested_array_bad_null_case=Template('{\"a\": [nulL]}')\nnested_object_truncated_null=Template('[{\"a\": nul}]')\nkeyword_prefix=Template('[truee]')\nbad_true_case=Template('truE')\narray_bad_true_case=Template('[truE]')\nbad_false_case=Template('falsE')\nbad_null_case=Template('nulL')\nobject_bad_true_case=Template('{\"a\": truE}')\nobject_bad_false_case=Template('{\"a\": falsE}')\nobject_bad_null_case=Template('{\"a\": nulL}')\narray_bad_false_case=Template('[falsE]')\narray_missing_comma_after_string=Template('[\"a\" true]')\nobject_missing_comma_after_null=Template('{\"a\": null \"b\": 1}')\ndouble_decimal_point=Template('1.2.3')\nobject_bad_leading_zero=Template('{\"a\": 00}')\nnested_extra_close=Template('[{\"a\":1}]]')\n"
1505                ),
1506                pyo3::ffi::c_str!("test_json_invalid_unicode.py"),
1507                pyo3::ffi::c_str!("test_json_invalid_unicode"),
1508            )
1509            .unwrap();
1510
1511            for (name, expected) in [
1512                ("incomplete", "Unexpected end of JSON escape sequence"),
1513                ("control", "Control characters are not allowed"),
1514                ("low", "Invalid JSON unicode escape"),
1515                ("exp", "Invalid JSON number literal"),
1516                ("exp2", "Invalid JSON number literal"),
1517                ("dot", "Invalid JSON number literal"),
1518                ("minus", "Invalid JSON number literal"),
1519                ("object_exp", "Invalid JSON number literal"),
1520                ("object_dot", "Invalid JSON number literal"),
1521                ("trailing_true", "Expected a JSON value"),
1522                (
1523                    "trailing_string",
1524                    "Unexpected trailing content in JSON template",
1525                ),
1526                (
1527                    "extra_close",
1528                    "Unexpected trailing content in JSON template",
1529                ),
1530                (
1531                    "object_extra_close",
1532                    "Unexpected trailing content in JSON template",
1533                ),
1534                (
1535                    "double_trailing_close",
1536                    "Unexpected trailing content in JSON template",
1537                ),
1538                ("missing_array_comma", "Invalid JSON number literal"),
1539                ("missing_object_comma", "Invalid JSON number literal"),
1540                (
1541                    "nested_missing_object_array_comma",
1542                    "Invalid JSON number literal",
1543                ),
1544                ("extra_array_comma", "Expected a JSON value"),
1545                ("leading_comma_array", "Expected a JSON value"),
1546                ("missing_value", "Expected a JSON value"),
1547                (
1548                    "missing_key",
1549                    "JSON object keys must be quoted strings or interpolations",
1550                ),
1551                ("missing_colon", "Expected ':' in JSON template"),
1552                (
1553                    "trailing_object_comma",
1554                    "JSON object keys must be quoted strings or interpolations",
1555                ),
1556                ("trailing_array_comma", "Expected a JSON value"),
1557                (
1558                    "nested_trailing_object_comma",
1559                    "quoted strings or interpolations",
1560                ),
1561                ("plus_number", "Expected a JSON value"),
1562                ("leading_decimal", "Expected a JSON value"),
1563                ("leading_zero", "Invalid JSON number literal"),
1564                ("negative_leading_zero", "Invalid JSON number literal"),
1565                ("leading_zero_exp", "Invalid JSON number literal"),
1566                ("negative_leading_zero_exp", "Invalid JSON number literal"),
1567                ("missing_exp_digits_minus", "Invalid JSON number literal"),
1568                (
1569                    "object_missing_exp_digits_minus",
1570                    "Invalid JSON number literal",
1571                ),
1572                (
1573                    "object_negative_leading_zero",
1574                    "Invalid JSON number literal",
1575                ),
1576                ("array_double_value_no_comma", "Expected a JSON value"),
1577                ("nested_missing_comma_bool", "Expected a JSON value"),
1578                ("array_true_number_no_comma", "Expected a JSON value"),
1579                (
1580                    "array_false_object_no_comma",
1581                    "Invalid promoted JSON fragment content",
1582                ),
1583                (
1584                    "nested_missing_comma_obj_after_null",
1585                    "Invalid promoted JSON fragment content",
1586                ),
1587                ("object_double_value_no_comma", "Expected a JSON value"),
1588                ("object_keyword_number_no_comma", "Expected a JSON value"),
1589                ("truncated_true", "Expected a JSON value"),
1590                ("truncated_false", "Expected a JSON value"),
1591                ("truncated_null", "Expected a JSON value"),
1592                ("object_truncated_true", "Expected a JSON value"),
1593                ("object_truncated_false", "Expected a JSON value"),
1594                ("object_truncated_null", "Expected a JSON value"),
1595                ("array_truncated_true", "Expected a JSON value"),
1596                ("array_truncated_false", "Expected a JSON value"),
1597                ("nested_array_truncated_true", "Expected a JSON value"),
1598                ("nested_array_truncated_false", "Expected a JSON value"),
1599                ("array_truncated_null", "Expected a JSON value"),
1600                ("nested_object_truncated_null", "Expected a JSON value"),
1601                ("keyword_prefix", "Expected a JSON value"),
1602                ("bad_true_case", "Expected a JSON value"),
1603                ("array_bad_true_case", "Expected a JSON value"),
1604                ("bad_false_case", "Expected a JSON value"),
1605                ("bad_null_case", "Expected a JSON value"),
1606                ("object_bad_true_case", "Expected a JSON value"),
1607                ("object_bad_false_case", "Expected a JSON value"),
1608                ("object_bad_null_case", "Expected a JSON value"),
1609                ("array_bad_false_case", "Expected a JSON value"),
1610                ("array_bad_null_case", "Expected a JSON value"),
1611                ("nested_array_bad_null_case", "Expected a JSON value"),
1612                (
1613                    "array_missing_comma_after_string",
1614                    "Expected ',' in JSON template",
1615                ),
1616                (
1617                    "object_missing_comma_after_null",
1618                    "Invalid promoted JSON fragment content",
1619                ),
1620                ("double_decimal_point", "Invalid JSON number literal"),
1621                ("object_bad_leading_zero", "Invalid JSON number literal"),
1622                (
1623                    "nested_extra_close",
1624                    "Unexpected trailing content in JSON template",
1625                ),
1626            ] {
1627                let template = module.getattr(name).unwrap();
1628                let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1629                let err = match parse_template(&template) {
1630                    Ok(_) => panic!("expected JSON parse failure for {name}"),
1631                    Err(err) => err,
1632                };
1633                assert_eq!(err.kind, ErrorKind::Parse);
1634                assert!(err.message.contains(expected), "{name}: {}", err.message);
1635            }
1636        });
1637    }
1638
1639    #[test]
1640    fn rejects_structural_invalid_message_families() {
1641        Python::with_gil(|py| {
1642            let module = PyModule::from_code(
1643                py,
1644                pyo3::ffi::c_str!(
1645                    "from string.templatelib import Template\nmissing_comma=Template('[\"a\" true]')\ntrailing_comma=Template('{\"a\":1,}')\ninvalid_fragment=Template('[null {\"a\":1}]')\nunexpected_trailing=Template('{\"a\":1}}')\n"
1646                ),
1647                pyo3::ffi::c_str!("test_json_structural_invalids.py"),
1648                pyo3::ffi::c_str!("test_json_structural_invalids"),
1649            )
1650            .unwrap();
1651
1652            for (name, expected) in [
1653                ("missing_comma", "Expected ',' in JSON template"),
1654                (
1655                    "trailing_comma",
1656                    "JSON object keys must be quoted strings or interpolations",
1657                ),
1658                ("invalid_fragment", "Invalid promoted JSON fragment content"),
1659                (
1660                    "unexpected_trailing",
1661                    "Unexpected trailing content in JSON template",
1662                ),
1663            ] {
1664                let template = module.getattr(name).unwrap();
1665                let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1666                let err = parse_template(&template).expect_err("expected JSON parse failure");
1667                assert_eq!(err.kind, ErrorKind::Parse);
1668                assert!(err.message.contains(expected), "{name}: {}", err.message);
1669            }
1670        });
1671    }
1672
1673    #[test]
1674    fn rejects_keyword_truncation_and_collection_separator_errors() {
1675        Python::with_gil(|py| {
1676            let module = PyModule::from_code(
1677                py,
1678                pyo3::ffi::c_str!(
1679                    "from string.templatelib import Template\nbad_true=Template('[tru]')\nbad_false=Template('{\"a\": falsE}')\nbad_null=Template('[nulL]')\nmissing_comma_array=Template('[null {\"a\": 1}]')\ntrailing_array_comma=Template('[1,2,]')\ntrailing_object_comma=Template('{\"a\":1,}')\n"
1680                ),
1681                pyo3::ffi::c_str!("test_json_invalid_keywords.py"),
1682                pyo3::ffi::c_str!("test_json_invalid_keywords"),
1683            )
1684            .unwrap();
1685
1686            for (name, expected) in [
1687                ("bad_true", "Expected a JSON value"),
1688                ("bad_false", "Expected a JSON value"),
1689                ("bad_null", "Expected a JSON value"),
1690                (
1691                    "missing_comma_array",
1692                    "Invalid promoted JSON fragment content",
1693                ),
1694                ("trailing_array_comma", "Expected a JSON value"),
1695                ("trailing_object_comma", "quoted strings or interpolations"),
1696            ] {
1697                let template = module.getattr(name).unwrap();
1698                let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1699                let err = parse_template(&template).expect_err("expected JSON parse failure");
1700                assert_eq!(err.kind, ErrorKind::Parse);
1701                assert!(err.message.contains(expected), "{name}: {}", err.message);
1702            }
1703        });
1704    }
1705
1706    #[test]
1707    fn rejects_additional_number_and_trailing_content_families() {
1708        Python::with_gil(|py| {
1709            let module = PyModule::from_code(
1710                py,
1711                pyo3::ffi::c_str!(
1712                    "from string.templatelib import Template\nleading_zero_exp=Template('01e0')\nleading_zero_exp_negative=Template('-01e0')\nmissing_exp_digits=Template('1e-')\nembedded_missing_exp_digits=Template('{\"x\": 1e-}')\ndouble_sign_number=Template('-+1')\nleading_plus_minus=Template('+-1')\nbad_exp_plus_minus=Template('1e+-1')\nbad_exp_minus_plus=Template('1e-+1')\nextra_decimal=Template('1.2.3')\narray_space_number=Template('[1 2]')\nobject_space_number=Template('{\"a\":1 \"b\":2}')\ntruee=Template('[truee]')\ntrue_then_number=Template('[true 1]')\nobject_true_then_number=Template('{\"a\": true 1}')\nfalse_fragment=Template('[false {\"a\":1}]')\narray_trailing_object=Template('{\"a\": [1]}]')\nobject_trailing_array=Template('[{\"a\":1}]]')\ndeep_object_trailing=Template('{\"a\": {\"b\": 1}}}')\n"
1713                ),
1714                pyo3::ffi::c_str!("test_json_additional_parse_errors.py"),
1715                pyo3::ffi::c_str!("test_json_additional_parse_errors"),
1716            )
1717            .unwrap();
1718
1719            for (name, expected) in [
1720                ("leading_zero_exp", "Invalid JSON number literal"),
1721                ("leading_zero_exp_negative", "Invalid JSON number literal"),
1722                ("missing_exp_digits", "Invalid JSON number literal"),
1723                ("embedded_missing_exp_digits", "Invalid JSON number literal"),
1724                ("double_sign_number", "Invalid JSON number literal"),
1725                ("leading_plus_minus", "Expected a JSON value"),
1726                ("bad_exp_plus_minus", "Invalid JSON number literal"),
1727                ("bad_exp_minus_plus", "Invalid JSON number literal"),
1728                ("extra_decimal", "Invalid JSON number literal"),
1729                ("array_space_number", "Invalid JSON number literal"),
1730                ("object_space_number", "Invalid JSON number literal"),
1731                ("truee", "Expected a JSON value"),
1732                ("true_then_number", "Expected a JSON value"),
1733                ("object_true_then_number", "Expected a JSON value"),
1734                ("false_fragment", "Invalid promoted JSON fragment content"),
1735                (
1736                    "array_trailing_object",
1737                    "Unexpected trailing content in JSON template",
1738                ),
1739                (
1740                    "object_trailing_array",
1741                    "Unexpected trailing content in JSON template",
1742                ),
1743                (
1744                    "deep_object_trailing",
1745                    "Unexpected trailing content in JSON template",
1746                ),
1747            ] {
1748                let template = module.getattr(name).unwrap();
1749                let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1750                let err = parse_template(&template).expect_err("expected JSON parse failure");
1751                assert_eq!(err.kind, ErrorKind::Parse);
1752                assert!(err.message.contains(expected), "{name}: {}", err.message);
1753            }
1754        });
1755    }
1756
1757    #[test]
1758    fn renders_keyword_and_empty_name_collection_shapes() {
1759        Python::with_gil(|py| {
1760            let module = PyModule::from_code(
1761                py,
1762                pyo3::ffi::c_str!(
1763                    "from string.templatelib import Template\nkeyword_array=Template('[true,false,null]')\nempty_name_nested_keywords=Template('{\"\": [null, true, false]}')\nnested_empty_mix=Template('{\"a\": [{}, [], \"\", 0, false, null]}')\narray_nested_mixed_scalars=Template('[{\"a\": []}, {\"b\": {}}, \"\", 0, false, null]')\n"
1764                ),
1765                pyo3::ffi::c_str!("test_json_keyword_empty_shapes.py"),
1766                pyo3::ffi::c_str!("test_json_keyword_empty_shapes"),
1767            )
1768            .unwrap();
1769
1770            let keyword_array = module.getattr("keyword_array").unwrap();
1771            let keyword_array = extract_template(py, &keyword_array, "json_t/json_t_str").unwrap();
1772            let rendered = render_document(py, &parse_template(&keyword_array).unwrap()).unwrap();
1773            assert_eq!(rendered.text, "[true, false, null]");
1774            assert_eq!(rendered.data, json!([true, false, null]));
1775
1776            let empty_name_nested_keywords = module.getattr("empty_name_nested_keywords").unwrap();
1777            let empty_name_nested_keywords =
1778                extract_template(py, &empty_name_nested_keywords, "json_t/json_t_str").unwrap();
1779            let rendered =
1780                render_document(py, &parse_template(&empty_name_nested_keywords).unwrap()).unwrap();
1781            assert_eq!(rendered.text, "{\"\": [null, true, false]}");
1782            assert_eq!(rendered.data, json!({"": [null, true, false]}));
1783
1784            let nested_empty_mix = module.getattr("nested_empty_mix").unwrap();
1785            let nested_empty_mix =
1786                extract_template(py, &nested_empty_mix, "json_t/json_t_str").unwrap();
1787            let rendered =
1788                render_document(py, &parse_template(&nested_empty_mix).unwrap()).unwrap();
1789            assert_eq!(rendered.data, json!({"a": [{}, [], "", 0, false, null]}));
1790
1791            let array_nested_mixed_scalars = module.getattr("array_nested_mixed_scalars").unwrap();
1792            let array_nested_mixed_scalars =
1793                extract_template(py, &array_nested_mixed_scalars, "json_t/json_t_str").unwrap();
1794            let rendered =
1795                render_document(py, &parse_template(&array_nested_mixed_scalars).unwrap()).unwrap();
1796            assert_eq!(
1797                rendered.data,
1798                json!([{"a": []}, {"b": {}}, "", 0, false, null])
1799            );
1800        });
1801    }
1802
1803    #[test]
1804    fn renders_top_level_whitespace_and_nested_number_shapes() {
1805        Python::with_gil(|py| {
1806            let module = PyModule::from_code(
1807                py,
1808                pyo3::ffi::c_str!(
1809                    "from string.templatelib import Template\ntop_ws_string=Template('\\n\\r\\t \"x\" \\n')\nnested_top_ws=Template('\\r\\n {\"a\": [1, {\"b\": \"c\"}], \"\": \"\"} \\n')\nnested_number_whitespace=Template('{\"a\": [ 0 , -0 , 1.5E-2 ] }')\n"
1810                ),
1811                pyo3::ffi::c_str!("test_json_whitespace_shapes.py"),
1812                pyo3::ffi::c_str!("test_json_whitespace_shapes"),
1813            )
1814            .unwrap();
1815
1816            let top_ws_string = module.getattr("top_ws_string").unwrap();
1817            let top_ws_string = extract_template(py, &top_ws_string, "json_t/json_t_str").unwrap();
1818            let rendered = render_document(py, &parse_template(&top_ws_string).unwrap()).unwrap();
1819            assert_eq!(rendered.text, "\"x\"");
1820            assert_eq!(rendered.data, json!("x"));
1821
1822            let nested_top_ws = module.getattr("nested_top_ws").unwrap();
1823            let nested_top_ws = extract_template(py, &nested_top_ws, "json_t/json_t_str").unwrap();
1824            let rendered = render_document(py, &parse_template(&nested_top_ws).unwrap()).unwrap();
1825            assert_eq!(rendered.text, "{\"a\": [1, {\"b\": \"c\"}], \"\": \"\"}");
1826            assert_eq!(rendered.data, json!({"a": [1, {"b": "c"}], "": ""}));
1827
1828            let nested_number_whitespace = module.getattr("nested_number_whitespace").unwrap();
1829            let nested_number_whitespace =
1830                extract_template(py, &nested_number_whitespace, "json_t/json_t_str").unwrap();
1831            let rendered =
1832                render_document(py, &parse_template(&nested_number_whitespace).unwrap()).unwrap();
1833            assert_eq!(rendered.text, "{\"a\": [0, -0, 1.5E-2]}");
1834            assert_eq!(
1835                rendered.data,
1836                serde_json::from_str::<Value>("{\"a\": [0, -0, 1.5E-2]}").unwrap()
1837            );
1838        });
1839    }
1840
1841    #[test]
1842    fn renders_end_to_end_supported_positions_text_and_data() {
1843        Python::with_gil(|py| {
1844            let module = PyModule::from_code(
1845                py,
1846                pyo3::ffi::c_str!(
1847                    "key='user'\nleft='prefix'\nright='suffix'\npayload={'enabled': True, 'count': 2}\ntemplate=t'''\n{{\n  {key}: {payload},\n  \"prefix-{left}\": \"item-{right}\",\n  \"label\": {left}-{right}\n}}\n'''\n"
1848                ),
1849                pyo3::ffi::c_str!("test_json_end_to_end_positions.py"),
1850                pyo3::ffi::c_str!("test_json_end_to_end_positions"),
1851            )
1852            .unwrap();
1853
1854            let template = module.getattr("template").unwrap();
1855            let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1856            let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
1857            assert_eq!(
1858                rendered.text,
1859                "{\"user\": {\"enabled\": true, \"count\": 2}, \"prefix-prefix\": \"item-suffix\", \"label\": \"prefix-suffix\"}"
1860            );
1861            assert_eq!(
1862                rendered.data,
1863                json!({
1864                    "user": {"enabled": true, "count": 2},
1865                    "prefix-prefix": "item-suffix",
1866                    "label": "prefix-suffix",
1867                })
1868            );
1869        });
1870    }
1871
1872    #[test]
1873    fn renders_rfc_8259_image_example_text_and_data() {
1874        Python::with_gil(|py| {
1875            let module = PyModule::from_code(
1876                py,
1877                pyo3::ffi::c_str!(
1878                    "template=t'''{{\n  \"Image\": {{\n    \"Width\": 800,\n    \"Height\": 600,\n    \"Title\": \"View from 15th Floor\",\n    \"Thumbnail\": {{\n      \"Url\": \"http://www.example.com/image/481989943\",\n      \"Height\": 125,\n      \"Width\": 100\n    }},\n    \"Animated\": false,\n    \"IDs\": [116, 943, 234, 38793]\n  }}\n}}'''\n"
1879                ),
1880                pyo3::ffi::c_str!("test_json_rfc_8259_image_example.py"),
1881                pyo3::ffi::c_str!("test_json_rfc_8259_image_example"),
1882            )
1883            .unwrap();
1884
1885            let template = module.getattr("template").unwrap();
1886            let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1887            let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
1888            assert_eq!(
1889                rendered.text,
1890                "{\"Image\": {\"Width\": 800, \"Height\": 600, \"Title\": \"View from 15th Floor\", \"Thumbnail\": {\"Url\": \"http://www.example.com/image/481989943\", \"Height\": 125, \"Width\": 100}, \"Animated\": false, \"IDs\": [116, 943, 234, 38793]}}"
1891            );
1892            assert_eq!(
1893                rendered.data,
1894                json!({
1895                    "Image": {
1896                        "Width": 800,
1897                        "Height": 600,
1898                        "Title": "View from 15th Floor",
1899                        "Thumbnail": {
1900                            "Url": "http://www.example.com/image/481989943",
1901                            "Height": 125,
1902                            "Width": 100,
1903                        },
1904                        "Animated": false,
1905                        "IDs": [116, 943, 234, 38793],
1906                    }
1907                })
1908            );
1909        });
1910    }
1911
1912    #[test]
1913    fn renders_rfc_8259_value_examples_text_and_data() {
1914        Python::with_gil(|py| {
1915            let module = PyModule::from_code(
1916                py,
1917                pyo3::ffi::c_str!(
1918                    "array=t'''[\n  {{\n     \"precision\": \"zip\",\n     \"Latitude\":  37.7668,\n     \"Longitude\": -122.3959,\n     \"Address\":   \"\",\n     \"City\":      \"SAN FRANCISCO\",\n     \"State\":     \"CA\",\n     \"Zip\":       \"94107\",\n     \"Country\":   \"US\"\n  }},\n  {{\n     \"precision\": \"zip\",\n     \"Latitude\":  37.371991,\n     \"Longitude\": -122.026020,\n     \"Address\":   \"\",\n     \"City\":      \"SUNNYVALE\",\n     \"State\":     \"CA\",\n     \"Zip\":       \"94085\",\n     \"Country\":   \"US\"\n  }}\n]'''\nstring=t'\"Hello world!\"'\nnumber=t'42'\nboolean=t'true'\n"
1919                ),
1920                pyo3::ffi::c_str!("test_json_rfc_8259_value_examples.py"),
1921                pyo3::ffi::c_str!("test_json_rfc_8259_value_examples"),
1922            )
1923            .unwrap();
1924
1925            let array = module.getattr("array").unwrap();
1926            let array = extract_template(py, &array, "json_t/json_t_str").unwrap();
1927            let rendered = render_document(py, &parse_template(&array).unwrap()).unwrap();
1928            assert_eq!(
1929                rendered.text,
1930                "[{\"precision\": \"zip\", \"Latitude\": 37.7668, \"Longitude\": -122.3959, \"Address\": \"\", \"City\": \"SAN FRANCISCO\", \"State\": \"CA\", \"Zip\": \"94107\", \"Country\": \"US\"}, {\"precision\": \"zip\", \"Latitude\": 37.371991, \"Longitude\": -122.026020, \"Address\": \"\", \"City\": \"SUNNYVALE\", \"State\": \"CA\", \"Zip\": \"94085\", \"Country\": \"US\"}]"
1931            );
1932            assert_eq!(
1933                rendered.data,
1934                json!([
1935                    {
1936                        "precision": "zip",
1937                        "Latitude": 37.7668,
1938                        "Longitude": -122.3959,
1939                        "Address": "",
1940                        "City": "SAN FRANCISCO",
1941                        "State": "CA",
1942                        "Zip": "94107",
1943                        "Country": "US",
1944                    },
1945                    {
1946                        "precision": "zip",
1947                        "Latitude": 37.371991,
1948                        "Longitude": -122.026020,
1949                        "Address": "",
1950                        "City": "SUNNYVALE",
1951                        "State": "CA",
1952                        "Zip": "94085",
1953                        "Country": "US",
1954                    }
1955                ])
1956            );
1957
1958            for (name, expected_text, expected_value) in [
1959                ("string", "\"Hello world!\"", json!("Hello world!")),
1960                ("number", "42", json!(42)),
1961                ("boolean", "true", json!(true)),
1962            ] {
1963                let template = module.getattr(name).unwrap();
1964                let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1965                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
1966                assert_eq!(rendered.text, expected_text, "{name}");
1967                assert_eq!(rendered.data, expected_value, "{name}");
1968            }
1969        });
1970    }
1971
1972    #[test]
1973    fn renders_unicode_and_escape_mix_shapes() {
1974        Python::with_gil(|py| {
1975            let module = PyModule::from_code(
1976                py,
1977                pyo3::ffi::c_str!(
1978                    "unicode_array=t'[\"\\\\u2028\", \"\\\\u2029\", \"\\\\u00DF\"]'\nescape_object=t'{{\"x\":\"\\\\b\\\\u2028\\\\u2029\\\\/\"}}'\n"
1979                ),
1980                pyo3::ffi::c_str!("test_json_unicode_escape_mix.py"),
1981                pyo3::ffi::c_str!("test_json_unicode_escape_mix"),
1982            )
1983            .unwrap();
1984
1985            let unicode_array = module.getattr("unicode_array").unwrap();
1986            let unicode_array = extract_template(py, &unicode_array, "json_t/json_t_str").unwrap();
1987            let rendered = render_document(py, &parse_template(&unicode_array).unwrap()).unwrap();
1988            assert_eq!(rendered.text, "[\"\u{2028}\", \"\u{2029}\", \"ß\"]");
1989            assert_eq!(rendered.data, json!(["\u{2028}", "\u{2029}", "ß"]));
1990
1991            let escape_object = module.getattr("escape_object").unwrap();
1992            let escape_object = extract_template(py, &escape_object, "json_t/json_t_str").unwrap();
1993            let rendered = render_document(py, &parse_template(&escape_object).unwrap()).unwrap();
1994            assert_eq!(rendered.text, "{\"x\": \"\\b\u{2028}\u{2029}/\"}");
1995            assert_eq!(rendered.data, json!({"x": "\u{0008}\u{2028}\u{2029}/"}));
1996        });
1997    }
1998
1999    #[test]
2000    fn renders_control_escapes_and_reverse_solidus_variants() {
2001        Python::with_gil(|py| {
2002            let module = PyModule::from_code(
2003                py,
2004                pyo3::ffi::c_str!(
2005                    "escaped_controls=t'\"\\\\b\\\\f\\\\n\\\\r\\\\t\"'\nreverse_solidus_u=t'\"\\\\u005C/\"'\nescaped_quote_backslash=t'\"\\\\\\\"\\\\\\\\\"'\n"
2006                ),
2007                pyo3::ffi::c_str!("test_json_escape_variants.py"),
2008                pyo3::ffi::c_str!("test_json_escape_variants"),
2009            )
2010            .unwrap();
2011
2012            let escaped_controls = module.getattr("escaped_controls").unwrap();
2013            let escaped_controls =
2014                extract_template(py, &escaped_controls, "json_t/json_t_str").unwrap();
2015            let rendered =
2016                render_document(py, &parse_template(&escaped_controls).unwrap()).unwrap();
2017            assert_eq!(rendered.text, "\"\\b\\f\\n\\r\\t\"");
2018            assert_eq!(rendered.data, json!("\u{0008}\u{000c}\n\r\t"));
2019
2020            let reverse_solidus_u = module.getattr("reverse_solidus_u").unwrap();
2021            let reverse_solidus_u =
2022                extract_template(py, &reverse_solidus_u, "json_t/json_t_str").unwrap();
2023            let rendered =
2024                render_document(py, &parse_template(&reverse_solidus_u).unwrap()).unwrap();
2025            assert_eq!(rendered.text, "\"\\\\/\"");
2026            assert_eq!(rendered.data, json!("\\/"));
2027
2028            let escaped_quote_backslash = module.getattr("escaped_quote_backslash").unwrap();
2029            let escaped_quote_backslash =
2030                extract_template(py, &escaped_quote_backslash, "json_t/json_t_str").unwrap();
2031            let rendered =
2032                render_document(py, &parse_template(&escaped_quote_backslash).unwrap()).unwrap();
2033            assert_eq!(rendered.text, "\"\\\"\\\\\"");
2034            assert_eq!(rendered.data, json!("\"\\"));
2035        });
2036    }
2037
2038    #[test]
2039    fn renders_promoted_rows_and_fragment_text_shapes() {
2040        Python::with_gil(|py| {
2041            let module = PyModule::from_code(
2042                py,
2043                pyo3::ffi::c_str!(
2044                    "left='prefix'\nright='suffix'\nrows=[{'name': 'one'}, {'name': 'two'}]\nfragment=t'{{\"label\": {left}-{right}}}'\npromoted=t'{rows}'\n"
2045                ),
2046                pyo3::ffi::c_str!("test_json_promoted_rows.py"),
2047                pyo3::ffi::c_str!("test_json_promoted_rows"),
2048            )
2049            .unwrap();
2050
2051            let fragment = module.getattr("fragment").unwrap();
2052            let fragment = extract_template(py, &fragment, "json_t/json_t_str").unwrap();
2053            let rendered = render_document(py, &parse_template(&fragment).unwrap()).unwrap();
2054            assert_eq!(rendered.text, "{\"label\": \"prefix-suffix\"}");
2055            assert_eq!(rendered.data, json!({"label": "prefix-suffix"}));
2056
2057            let promoted = module.getattr("promoted").unwrap();
2058            let promoted = extract_template(py, &promoted, "json_t/json_t_str").unwrap();
2059            let rendered = render_document(py, &parse_template(&promoted).unwrap()).unwrap();
2060            assert_eq!(rendered.text, r#"[{"name": "one"}, {"name": "two"}]"#);
2061            assert_eq!(rendered.data, json!([{"name": "one"}, {"name": "two"}]));
2062        });
2063    }
2064
2065    #[test]
2066    fn renders_negative_zero_number_shapes() {
2067        Python::with_gil(|py| {
2068            let module = PyModule::from_code(
2069                py,
2070                pyo3::ffi::c_str!(
2071                    "neg_zero=t'-0'\nneg_zero_float=t'-0.0'\nneg_zero_exp=t'-0E0'\ncombo=t'{{\"a\": [0, -0, -0.0, 1e0, -1E-0]}}'\n"
2072                ),
2073                pyo3::ffi::c_str!("test_json_negative_zero_shapes.py"),
2074                pyo3::ffi::c_str!("test_json_negative_zero_shapes"),
2075            )
2076            .unwrap();
2077
2078            for (name, expected_text, expected_data) in [
2079                ("neg_zero", "-0", json!(-0.0)),
2080                ("neg_zero_float", "-0.0", json!(-0.0)),
2081                ("neg_zero_exp", "-0E0", json!(-0.0)),
2082            ] {
2083                let template = module.getattr(name).unwrap();
2084                let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
2085                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2086                assert_eq!(rendered.text, expected_text);
2087                assert_eq!(rendered.data, expected_data);
2088            }
2089
2090            let combo = module.getattr("combo").unwrap();
2091            let combo = extract_template(py, &combo, "json_t/json_t_str").unwrap();
2092            let rendered = render_document(py, &parse_template(&combo).unwrap()).unwrap();
2093            assert_eq!(rendered.text, "{\"a\": [0, -0, -0.0, 1e0, -1E-0]}");
2094            assert_eq!(
2095                rendered.data,
2096                serde_json::from_str::<Value>("{\"a\": [0, -0, -0.0, 1e0, -1E-0]}").unwrap()
2097            );
2098        });
2099    }
2100
2101    #[test]
2102    fn renders_top_level_keywords_and_empty_collections() {
2103        Python::with_gil(|py| {
2104            let module = PyModule::from_code(
2105                py,
2106                pyo3::ffi::c_str!(
2107                    "from string.templatelib import Template\ntop_bool_ws=Template(' \\n true \\t ')\ntop_null_ws=Template(' \\r\\n null \\n')\nempty_object=Template('{ \\n\\t }')\nempty_array=Template('[ \\n\\t ]')\n"
2108                ),
2109                pyo3::ffi::c_str!("test_json_top_level_keywords.py"),
2110                pyo3::ffi::c_str!("test_json_top_level_keywords"),
2111            )
2112            .unwrap();
2113
2114            for (name, expected_text, expected_data) in [
2115                ("top_bool_ws", "true", json!(true)),
2116                ("top_null_ws", "null", Value::Null),
2117                ("empty_object", "{}", json!({})),
2118                ("empty_array", "[]", json!([])),
2119            ] {
2120                let template = module.getattr(name).unwrap();
2121                let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
2122                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2123                assert_eq!(rendered.text, expected_text);
2124                assert_eq!(rendered.data, expected_data);
2125            }
2126        });
2127    }
2128
2129    #[test]
2130    fn renders_escape_unicode_and_keyword_text_shapes() {
2131        Python::with_gil(|py| {
2132            let module = PyModule::from_code(
2133                py,
2134                pyo3::ffi::c_str!(
2135                    "from string.templatelib import Template\ntop_bool_ws=Template(' \\n true \\t ')\ntop_null_ws=Template(' \\r\\n null \\n')\narray_with_line_sep=t'[\"\\\\u2028\", \"\\\\u2029\"]'\nunicode_mix_nested_obj=Template('{\"x\": {\"a\": \"\\\\u005C\", \"b\": \"\\\\u00DF\", \"c\": \"\\\\u2029\"}}')\nkeyword_array=Template('[true,false,null]')\n"
2136                ),
2137                pyo3::ffi::c_str!("test_json_escape_unicode_keyword_shapes.py"),
2138                pyo3::ffi::c_str!("test_json_escape_unicode_keyword_shapes"),
2139            )
2140            .unwrap();
2141
2142            for (name, expected_text, expected_data) in [
2143                ("top_bool_ws", "true", json!(true)),
2144                ("top_null_ws", "null", Value::Null),
2145                (
2146                    "array_with_line_sep",
2147                    "[\"\u{2028}\", \"\u{2029}\"]",
2148                    json!(["\u{2028}", "\u{2029}"]),
2149                ),
2150                (
2151                    "unicode_mix_nested_obj",
2152                    "{\"x\": {\"a\": \"\\\\\", \"b\": \"ß\", \"c\": \"\u{2029}\"}}",
2153                    json!({"x": {"a": "\\", "b": "ß", "c": "\u{2029}"}}),
2154                ),
2155                (
2156                    "keyword_array",
2157                    "[true, false, null]",
2158                    json!([true, false, null]),
2159                ),
2160            ] {
2161                let template = module.getattr(name).unwrap();
2162                let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
2163                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2164                assert_eq!(rendered.text, expected_text, "{name}");
2165                assert_eq!(rendered.data, expected_data, "{name}");
2166            }
2167        });
2168    }
2169
2170    #[test]
2171    fn test_parse_rendered_json_surfaces_parse_failures() {
2172        let err = parse_rendered_json("{\"a\":,}").expect_err("expected JSON parse failure");
2173        assert_eq!(err.kind, ErrorKind::Parse);
2174        assert!(err.message.contains("Rendered JSON could not be reparsed"));
2175    }
2176}