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 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 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, "ed_key, "json_t/json_t_str").unwrap();
900 let rendered = render_document(py, &parse_template("ed_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}