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 parse_validated_template_with_profile(
604 template: &TemplateInput,
605 profile: JsonProfile,
606) -> BackendResult<JsonDocumentNode> {
607 parse_template_with_profile(template, profile)
611}
612
613pub fn parse_validated_template(template: &TemplateInput) -> BackendResult<JsonDocumentNode> {
614 parse_validated_template_with_profile(template, JsonProfile::default())
615}
616
617pub fn validate_template_with_profile(
618 template: &TemplateInput,
619 profile: JsonProfile,
620) -> BackendResult<()> {
621 parse_validated_template_with_profile(template, profile).map(|_| ())
622}
623
624pub fn validate_template(template: &TemplateInput) -> BackendResult<()> {
625 validate_template_with_profile(template, JsonProfile::default())
626}
627
628pub fn check_template_with_profile(
629 template: &TemplateInput,
630 profile: JsonProfile,
631) -> BackendResult<()> {
632 validate_template_with_profile(template, profile)
633}
634
635pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
636 check_template_with_profile(template, JsonProfile::default())
637}
638
639pub fn format_template_with_profile(
640 template: &TemplateInput,
641 profile: JsonProfile,
642) -> BackendResult<String> {
643 let document = parse_validated_template_with_profile(template, profile)?;
644 format_json_value(template, &document.value)
645}
646
647pub fn format_template(template: &TemplateInput) -> BackendResult<String> {
648 format_template_with_profile(template, JsonProfile::default())
649}
650
651pub fn normalize_document_with_profile(
652 value: &Value,
653 _profile: JsonProfile,
654) -> BackendResult<NormalizedStream> {
655 Ok(NormalizedStream::new(vec![NormalizedDocument::Value(
658 normalize_value(value)?,
659 )]))
660}
661
662pub fn normalize_document(value: &Value) -> BackendResult<NormalizedStream> {
663 normalize_document_with_profile(value, JsonProfile::default())
664}
665
666pub fn normalize_value(value: &Value) -> BackendResult<NormalizedValue> {
667 match value {
668 Value::Null => Ok(NormalizedValue::Null),
669 Value::Bool(value) => Ok(NormalizedValue::Bool(*value)),
670 Value::String(value) => Ok(NormalizedValue::String(value.clone())),
671 Value::Array(values) => values
672 .iter()
673 .map(normalize_value)
674 .collect::<BackendResult<Vec<_>>>()
675 .map(NormalizedValue::Sequence),
676 Value::Object(values) => values
677 .iter()
678 .map(|(key, value)| {
679 Ok(tstring_syntax::NormalizedEntry {
680 key: NormalizedKey::String(key.clone()),
681 value: normalize_value(value)?,
682 })
683 })
684 .collect::<BackendResult<Vec<_>>>()
685 .map(NormalizedValue::Mapping),
686 Value::Number(number) => normalize_number(number),
687 }
688}
689
690fn format_json_value(template: &TemplateInput, node: &JsonValueNode) -> BackendResult<String> {
691 match node {
692 JsonValueNode::String(node) => format_json_string(template, node),
693 JsonValueNode::Literal(node) => Ok(node.source.clone()),
694 JsonValueNode::Interpolation(node) => {
695 interpolation_raw_source(template, node.interpolation_index, &node.span, "JSON value")
696 }
697 JsonValueNode::Object(node) => {
698 let mut members = Vec::with_capacity(node.members.len());
699 for member in &node.members {
700 members.push(format!(
701 "{}: {}",
702 format_json_key(template, &member.key)?,
703 format_json_value(template, &member.value)?
704 ));
705 }
706 Ok(format!("{{{}}}", members.join(", ")))
707 }
708 JsonValueNode::Array(node) => {
709 let items = node
710 .items
711 .iter()
712 .map(|item| format_json_value(template, item))
713 .collect::<BackendResult<Vec<_>>>()?;
714 Ok(format!("[{}]", items.join(", ")))
715 }
716 }
717}
718
719fn format_json_key(template: &TemplateInput, node: &JsonKeyNode) -> BackendResult<String> {
720 match &node.value {
721 JsonKeyValue::String(node) => format_json_string(template, node),
722 JsonKeyValue::Interpolation(node) => interpolation_raw_source(
723 template,
724 node.interpolation_index,
725 &node.span,
726 "JSON object key",
727 ),
728 }
729}
730
731fn format_json_string(template: &TemplateInput, node: &JsonStringNode) -> BackendResult<String> {
732 let mut rendered = String::from("\"");
733 for chunk in &node.chunks {
734 match chunk {
735 JsonStringPart::Chunk(chunk) => rendered.push_str(&escape_json_fragment(&chunk.value)),
736 JsonStringPart::Interpolation(node) => rendered.push_str(&interpolation_raw_source(
737 template,
738 node.interpolation_index,
739 &node.span,
740 "JSON string fragment",
741 )?),
742 }
743 }
744 rendered.push('"');
745 Ok(rendered)
746}
747
748fn escape_json_fragment(text: &str) -> String {
749 let mut escaped = serde_json::to_string(text).unwrap_or_else(|_| "\"\"".to_owned());
750 escaped.remove(0);
751 escaped.pop();
752 escaped
753}
754
755fn interpolation_raw_source(
756 template: &TemplateInput,
757 interpolation_index: usize,
758 span: &SourceSpan,
759 context: &str,
760) -> BackendResult<String> {
761 template
762 .interpolation_raw_source(interpolation_index)
763 .map(str::to_owned)
764 .ok_or_else(|| {
765 let expression = template.interpolation(interpolation_index).map_or_else(
766 || format!("slot {interpolation_index}"),
767 |value| value.expression_label().to_owned(),
768 );
769 BackendError::semantic_at(
770 "json.format",
771 format!(
772 "Cannot format {context} interpolation {expression:?} without raw source text."
773 ),
774 Some(span.clone()),
775 )
776 })
777}
778
779fn normalize_number(number: &serde_json::Number) -> BackendResult<NormalizedValue> {
780 let source = number.to_string();
781 if source.contains(['.', 'e', 'E']) {
782 return source
783 .parse::<f64>()
784 .map(NormalizedFloat::finite)
785 .map(NormalizedValue::Float)
786 .map_err(|err| {
787 BackendError::semantic(format!(
788 "Validated JSON number {source} could not be normalized as a finite float: {err}"
789 ))
790 });
791 }
792
793 source.parse().map(NormalizedValue::Integer).map_err(|err| {
794 BackendError::semantic(format!(
795 "Validated JSON number {source} could not be normalized as an exact integer: {err}"
796 ))
797 })
798}
799
800#[cfg(test)]
801mod tests {
802 use super::{JsonKeyValue, JsonStringPart, JsonValueNode, parse_template};
803 use pyo3::prelude::*;
804 use serde_json::{Map, Number, Value, json};
805 use tstring_pyo3_bindings::{extract_template, json::render_document};
806 use tstring_syntax::{BackendError, BackendResult, ErrorKind};
807
808 fn parse_rendered_json(text: &str) -> BackendResult<Value> {
809 serde_json::from_str(text).map_err(|err| {
810 BackendError::parse(format!(
811 "Rendered JSON could not be reparsed during test verification: {err}"
812 ))
813 })
814 }
815
816 #[test]
817 fn parses_json_structure() {
818 Python::with_gil(|py| {
819 let module = PyModule::from_code(
820 py,
821 pyo3::ffi::c_str!(
822 "from string.templatelib import Template\nleft='prefix'\nright='suffix'\ntemplate=t'{{\"prefix-{left}\": {left}-{right}}}'\n"
823 ),
824 pyo3::ffi::c_str!("test_json.py"),
825 pyo3::ffi::c_str!("test_json"),
826 )
827 .unwrap();
828 let template = module.getattr("template").unwrap();
829 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
830 let document = parse_template(&template).unwrap();
831 let JsonValueNode::Object(object) = document.value else {
832 panic!("expected object");
833 };
834 assert_eq!(object.members.len(), 1);
835 let JsonKeyValue::String(key) = &object.members[0].key.value else {
836 panic!("expected interpolated string key");
837 };
838 assert_eq!(key.chunks.len(), 2);
839 assert!(matches!(key.chunks[1], JsonStringPart::Interpolation(_)));
840 let JsonValueNode::String(value) = &object.members[0].value else {
841 panic!("expected promoted string value");
842 };
843 assert_eq!(value.chunks.len(), 3);
844 assert!(matches!(value.chunks[0], JsonStringPart::Interpolation(_)));
845 });
846 }
847
848 #[test]
849 fn renders_nested_collections_and_validates() {
850 Python::with_gil(|py| {
851 let module = PyModule::from_code(
852 py,
853 pyo3::ffi::c_str!(
854 "items=[1, {'name': 'Ada'}]\nname='Ada'\ntemplate=t'{{\"items\": {items}, \"message\": \"hi-{name}\"}}'\n"
855 ),
856 pyo3::ffi::c_str!("test_json_render.py"),
857 pyo3::ffi::c_str!("test_json_render"),
858 )
859 .unwrap();
860 let template = module.getattr("template").unwrap();
861 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
862 let document = parse_template(&template).unwrap();
863 let rendered = render_document(py, &document).unwrap();
864
865 assert_eq!(rendered.data["message"], Value::String("hi-Ada".to_owned()));
866 assert_eq!(rendered.data["items"][0], Value::Number(Number::from(1)));
867 assert_eq!(
868 rendered.data["items"][1]["name"],
869 Value::String("Ada".to_owned())
870 );
871 });
872 }
873
874 #[test]
875 fn renders_top_level_scalar_text() {
876 Python::with_gil(|py| {
877 let module = PyModule::from_code(
878 py,
879 pyo3::ffi::c_str!(
880 "template=t'{1}'\nbool_template=t'{True}'\nnull_template=t'{None}'\n"
881 ),
882 pyo3::ffi::c_str!("test_json_scalar.py"),
883 pyo3::ffi::c_str!("test_json_scalar"),
884 )
885 .unwrap();
886 let template = module.getattr("template").unwrap();
887 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
888 let document = parse_template(&template).unwrap();
889 let rendered = render_document(py, &document).unwrap();
890
891 assert_eq!(rendered.text, "1");
892 assert_eq!(rendered.data, Value::Number(Number::from(1)));
893
894 let bool_template = module.getattr("bool_template").unwrap();
895 let bool_template = extract_template(py, &bool_template, "json_t/json_t_str").unwrap();
896 let document = parse_template(&bool_template).unwrap();
897 let rendered = render_document(py, &document).unwrap();
898 assert_eq!(rendered.text, "true");
899 assert_eq!(rendered.data, Value::Bool(true));
900
901 let null_template = module.getattr("null_template").unwrap();
902 let null_template = extract_template(py, &null_template, "json_t/json_t_str").unwrap();
903 let document = parse_template(&null_template).unwrap();
904 let rendered = render_document(py, &document).unwrap();
905 assert_eq!(rendered.text, "null");
906 assert_eq!(rendered.data, Value::Null);
907 });
908 }
909
910 #[test]
911 fn renders_quoted_key_fragments_and_promoted_fragments() {
912 Python::with_gil(|py| {
913 let module = PyModule::from_code(
914 py,
915 pyo3::ffi::c_str!(
916 "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"
917 ),
918 pyo3::ffi::c_str!("test_json_fragments.py"),
919 pyo3::ffi::c_str!("test_json_fragments"),
920 )
921 .unwrap();
922
923 let quoted_key = module.getattr("quoted_key").unwrap();
924 let quoted_key = extract_template(py, "ed_key, "json_t/json_t_str").unwrap();
925 let rendered = render_document(py, &parse_template("ed_key).unwrap()).unwrap();
926 assert_eq!(rendered.data, json!({"prefix-suffix": 1, "value": true}));
927
928 let fragment = module.getattr("fragment").unwrap();
929 let fragment = extract_template(py, &fragment, "json_t/json_t_str").unwrap();
930 let rendered = render_document(py, &parse_template(&fragment).unwrap()).unwrap();
931 assert_eq!(rendered.text, "{\"label\": \"prefix-suffix\"}");
932 assert_eq!(rendered.data, json!({"label": "prefix-suffix"}));
933
934 let promoted = module.getattr("promoted").unwrap();
935 let promoted = extract_template(py, &promoted, "json_t/json_t_str").unwrap();
936 let rendered = render_document(py, &parse_template(&promoted).unwrap()).unwrap();
937 assert_eq!(rendered.text, r#"[{"name": "one"}, {"name": "two"}]"#);
938 assert_eq!(rendered.data, json!([{"name": "one"}, {"name": "two"}]));
939 });
940 }
941
942 #[test]
943 fn rejects_non_string_key_interpolation() {
944 Python::with_gil(|py| {
945 let module = PyModule::from_code(
946 py,
947 pyo3::ffi::c_str!("key=1\ntemplate=t'{{{key}: 1}}'\n"),
948 pyo3::ffi::c_str!("test_json_error.py"),
949 pyo3::ffi::c_str!("test_json_error"),
950 )
951 .unwrap();
952 let template = module.getattr("template").unwrap();
953 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
954 let document = parse_template(&template).unwrap();
955 let err = match render_document(py, &document) {
956 Ok(_) => panic!("expected JSON render failure"),
957 Err(err) => err,
958 };
959
960 assert_eq!(err.kind, ErrorKind::Unrepresentable);
961 assert!(err.message.contains("JSON object keys must be str"));
962 });
963 }
964
965 #[test]
966 fn rejects_unrepresentable_render_value_families() {
967 Python::with_gil(|py| {
968 let module = PyModule::from_code(
969 py,
970 pyo3::ffi::c_str!(
971 "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"
972 ),
973 pyo3::ffi::c_str!("test_json_unrepresentable.py"),
974 pyo3::ffi::c_str!("test_json_unrepresentable"),
975 )
976 .unwrap();
977
978 for (name, expected) in [
979 ("map_template", "JSON object keys must be str"),
980 ("nan_template", "non-finite float"),
981 ("set_template", "could not be rendered as JSON"),
982 ] {
983 let template = module.getattr(name).unwrap();
984 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
985 let document = parse_template(&template).unwrap();
986 let err = match render_document(py, &document) {
987 Ok(_) => panic!("expected JSON render failure"),
988 Err(err) => err,
989 };
990 assert_eq!(err.kind, ErrorKind::Unrepresentable);
991 assert!(err.message.contains(expected), "{name}: {}", err.message);
992 }
993 });
994 }
995
996 #[test]
997 fn parses_unicode_surrogate_pairs() {
998 Python::with_gil(|py| {
999 let module = PyModule::from_code(
1000 py,
1001 pyo3::ffi::c_str!("template=t'\"\\\\uD834\\\\uDD1E\"'\n"),
1002 pyo3::ffi::c_str!("test_json_unicode.py"),
1003 pyo3::ffi::c_str!("test_json_unicode"),
1004 )
1005 .unwrap();
1006 let template = module.getattr("template").unwrap();
1007 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1008 let document = parse_template(&template).unwrap();
1009 let rendered = render_document(py, &document).unwrap();
1010
1011 assert_eq!(rendered.data, Value::String("𝄞".to_owned()));
1012 });
1013 }
1014
1015 #[test]
1016 fn parses_numbers_and_escape_sequences() {
1017 Python::with_gil(|py| {
1018 let module = PyModule::from_code(
1019 py,
1020 pyo3::ffi::c_str!(
1021 "template=t'{{\"int\": -0, \"exp\": 1.5e2, \"escapes\": \"\\\\b\\\\f\\\\n\\\\r\\\\t\\\\/\", \"unicode\": \"\\\\u00DF\\\\u6771\\\\uD834\\\\uDD1E\"}}'\n"
1022 ),
1023 pyo3::ffi::c_str!("test_json_numbers.py"),
1024 pyo3::ffi::c_str!("test_json_numbers"),
1025 )
1026 .unwrap();
1027 let template = module.getattr("template").unwrap();
1028 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1029 let document = parse_template(&template).unwrap();
1030 let rendered = render_document(py, &document).unwrap();
1031
1032 assert_eq!(
1033 rendered.data["exp"],
1034 Value::Number(Number::from_f64(150.0).unwrap())
1035 );
1036 assert_eq!(rendered.data["unicode"], Value::String("ß東𝄞".to_owned()));
1037 assert_eq!(
1038 rendered.data["escapes"],
1039 Value::String("\u{0008}\u{000c}\n\r\t/".to_owned())
1040 );
1041 });
1042 }
1043
1044 #[test]
1045 fn parses_whitespace_and_escaped_solidus_cases() {
1046 Python::with_gil(|py| {
1047 let module = PyModule::from_code(
1048 py,
1049 pyo3::ffi::c_str!(
1050 "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"
1051 ),
1052 pyo3::ffi::c_str!("test_json_whitespace.py"),
1053 pyo3::ffi::c_str!("test_json_whitespace"),
1054 )
1055 .unwrap();
1056
1057 let top_bool_ws = module.getattr("top_bool_ws").unwrap();
1058 let top_bool_ws = extract_template(py, &top_bool_ws, "json_t/json_t_str").unwrap();
1059 let rendered = render_document(py, &parse_template(&top_bool_ws).unwrap()).unwrap();
1060 assert_eq!(rendered.data, Value::Bool(true));
1061
1062 let top_null_ws = module.getattr("top_null_ws").unwrap();
1063 let top_null_ws = extract_template(py, &top_null_ws, "json_t/json_t_str").unwrap();
1064 let rendered = render_document(py, &parse_template(&top_null_ws).unwrap()).unwrap();
1065 assert_eq!(rendered.data, Value::Null);
1066
1067 let empty_string = module.getattr("empty_string").unwrap();
1068 let empty_string = extract_template(py, &empty_string, "json_t/json_t_str").unwrap();
1069 let rendered = render_document(py, &parse_template(&empty_string).unwrap()).unwrap();
1070 assert_eq!(rendered.data, Value::String(String::new()));
1071
1072 let empty_object = module.getattr("empty_object").unwrap();
1073 let empty_object = extract_template(py, &empty_object, "json_t/json_t_str").unwrap();
1074 let rendered = render_document(py, &parse_template(&empty_object).unwrap()).unwrap();
1075 assert_eq!(rendered.data, Value::Object(Default::default()));
1076
1077 let empty_array = module.getattr("empty_array").unwrap();
1078 let empty_array = extract_template(py, &empty_array, "json_t/json_t_str").unwrap();
1079 let rendered = render_document(py, &parse_template(&empty_array).unwrap()).unwrap();
1080 assert_eq!(rendered.data, Value::Array(Vec::new()));
1081
1082 let escaped_controls = module.getattr("escaped_controls").unwrap();
1083 let escaped_controls =
1084 extract_template(py, &escaped_controls, "json_t/json_t_str").unwrap();
1085 let rendered =
1086 render_document(py, &parse_template(&escaped_controls).unwrap()).unwrap();
1087 assert_eq!(
1088 rendered.data,
1089 Value::String("\u{0008}\u{000c}\n\r\t".to_owned())
1090 );
1091
1092 let escaped_solidus = module.getattr("escaped_solidus").unwrap();
1093 let escaped_solidus =
1094 extract_template(py, &escaped_solidus, "json_t/json_t_str").unwrap();
1095 let rendered = render_document(py, &parse_template(&escaped_solidus).unwrap()).unwrap();
1096 assert_eq!(rendered.data, Value::String("/".to_owned()));
1097
1098 let escaped_backslash = module.getattr("escaped_backslash").unwrap();
1099 let escaped_backslash =
1100 extract_template(py, &escaped_backslash, "json_t/json_t_str").unwrap();
1101 let rendered =
1102 render_document(py, &parse_template(&escaped_backslash).unwrap()).unwrap();
1103 assert_eq!(rendered.data, Value::String("\\".to_owned()));
1104
1105 let unicode_backslash_escape = module.getattr("unicode_backslash_escape").unwrap();
1106 let unicode_backslash_escape =
1107 extract_template(py, &unicode_backslash_escape, "json_t/json_t_str").unwrap();
1108 let rendered =
1109 render_document(py, &parse_template(&unicode_backslash_escape).unwrap()).unwrap();
1110 assert_eq!(rendered.data, Value::String("\\".to_owned()));
1111
1112 let reverse_solidus_u = module.getattr("reverse_solidus_u").unwrap();
1113 let reverse_solidus_u =
1114 extract_template(py, &reverse_solidus_u, "json_t/json_t_str").unwrap();
1115 let rendered =
1116 render_document(py, &parse_template(&reverse_solidus_u).unwrap()).unwrap();
1117 assert_eq!(rendered.data, Value::String("\\/".to_owned()));
1118
1119 let escaped_quote_backslash = module.getattr("escaped_quote_backslash").unwrap();
1120 let escaped_quote_backslash =
1121 extract_template(py, &escaped_quote_backslash, "json_t/json_t_str").unwrap();
1122 let rendered =
1123 render_document(py, &parse_template(&escaped_quote_backslash).unwrap()).unwrap();
1124 assert_eq!(rendered.data, Value::String("\"\\".to_owned()));
1125
1126 let escaped_null_and_unit_separator =
1127 module.getattr("escaped_null_and_unit_separator").unwrap();
1128 let escaped_null_and_unit_separator =
1129 extract_template(py, &escaped_null_and_unit_separator, "json_t/json_t_str")
1130 .unwrap();
1131 let rendered = render_document(
1132 py,
1133 &parse_template(&escaped_null_and_unit_separator).unwrap(),
1134 )
1135 .unwrap();
1136 assert_eq!(rendered.data, Value::String("\u{0000}\u{001f}".to_owned()));
1137
1138 let nested_upper_unicode = module.getattr("nested_upper_unicode").unwrap();
1139 let nested_upper_unicode =
1140 extract_template(py, &nested_upper_unicode, "json_t/json_t_str").unwrap();
1141 let rendered =
1142 render_document(py, &parse_template(&nested_upper_unicode).unwrap()).unwrap();
1143 assert_eq!(rendered.data, Value::String("ß東".to_owned()));
1144
1145 let unicode_line_sep = module.getattr("unicode_line_sep").unwrap();
1146 let unicode_line_sep =
1147 extract_template(py, &unicode_line_sep, "json_t/json_t_str").unwrap();
1148 let rendered =
1149 render_document(py, &parse_template(&unicode_line_sep).unwrap()).unwrap();
1150 assert_eq!(rendered.data, Value::String("\u{2028}".to_owned()));
1151
1152 let unicode_para_sep = module.getattr("unicode_para_sep").unwrap();
1153 let unicode_para_sep =
1154 extract_template(py, &unicode_para_sep, "json_t/json_t_str").unwrap();
1155 let rendered =
1156 render_document(py, &parse_template(&unicode_para_sep).unwrap()).unwrap();
1157 assert_eq!(rendered.data, Value::String("\u{2029}".to_owned()));
1158
1159 let array_with_line_sep = module.getattr("array_with_line_sep").unwrap();
1160 let array_with_line_sep =
1161 extract_template(py, &array_with_line_sep, "json_t/json_t_str").unwrap();
1162 let rendered =
1163 render_document(py, &parse_template(&array_with_line_sep).unwrap()).unwrap();
1164 assert_eq!(rendered.data.as_array().expect("array").len(), 2);
1165 assert_eq!(rendered.data[0], Value::String("\u{2028}".to_owned()));
1166 assert_eq!(rendered.data[1], Value::String("\u{2029}".to_owned()));
1167
1168 let unicode_escapes_array = module.getattr("unicode_escapes_array").unwrap();
1169 let unicode_escapes_array =
1170 extract_template(py, &unicode_escapes_array, "json_t/json_t_str").unwrap();
1171 let rendered =
1172 render_document(py, &parse_template(&unicode_escapes_array).unwrap()).unwrap();
1173 let values = rendered.data.as_array().expect("array");
1174 assert_eq!(values[0], Value::String("\\".to_owned()));
1175 assert_eq!(values[1], Value::String("/".to_owned()));
1176 assert_eq!(values[2], Value::String("ß".to_owned()));
1177
1178 let unicode_mix_nested_obj = module.getattr("unicode_mix_nested_obj").unwrap();
1179 let unicode_mix_nested_obj =
1180 extract_template(py, &unicode_mix_nested_obj, "json_t/json_t_str").unwrap();
1181 let rendered =
1182 render_document(py, &parse_template(&unicode_mix_nested_obj).unwrap()).unwrap();
1183 assert_eq!(rendered.data["x"]["a"], Value::String("\\".to_owned()));
1184 assert_eq!(rendered.data["x"]["b"], Value::String("ß".to_owned()));
1185 assert_eq!(
1186 rendered.data["x"]["c"],
1187 Value::String("\u{2029}".to_owned())
1188 );
1189
1190 let nested_unicode_object_array =
1191 module.getattr("nested_unicode_object_array").unwrap();
1192 let nested_unicode_object_array =
1193 extract_template(py, &nested_unicode_object_array, "json_t/json_t_str").unwrap();
1194 let rendered =
1195 render_document(py, &parse_template(&nested_unicode_object_array).unwrap())
1196 .unwrap();
1197 assert_eq!(rendered.data["a"][0]["b"], Value::String("\\".to_owned()));
1198 assert_eq!(rendered.data["a"][0]["c"], Value::String("ß".to_owned()));
1199
1200 let upper_unicode_mix_array = module.getattr("upper_unicode_mix_array").unwrap();
1201 let upper_unicode_mix_array =
1202 extract_template(py, &upper_unicode_mix_array, "json_t/json_t_str").unwrap();
1203 let rendered =
1204 render_document(py, &parse_template(&upper_unicode_mix_array).unwrap()).unwrap();
1205 let values = rendered.data.as_array().expect("array");
1206 assert_eq!(values[0], Value::String("ß".to_owned()));
1207 assert_eq!(values[1], Value::String("東".to_owned()));
1208 assert_eq!(values[2], Value::String("\u{2028}".to_owned()));
1209
1210 let escaped_slash_backslash_quote =
1211 module.getattr("escaped_slash_backslash_quote").unwrap();
1212 let escaped_slash_backslash_quote =
1213 extract_template(py, &escaped_slash_backslash_quote, "json_t/json_t_str").unwrap();
1214 let rendered =
1215 render_document(py, &parse_template(&escaped_slash_backslash_quote).unwrap())
1216 .unwrap();
1217 assert_eq!(rendered.data, Value::String("/\\\"".to_owned()));
1218
1219 let array_empty_values = module.getattr("array_empty_values").unwrap();
1220 let array_empty_values =
1221 extract_template(py, &array_empty_values, "json_t/json_t_str").unwrap();
1222 let rendered =
1223 render_document(py, &parse_template(&array_empty_values).unwrap()).unwrap();
1224 let values = rendered.data.as_array().expect("array");
1225 assert_eq!(values.len(), 6);
1226 assert_eq!(values[0], Value::String(String::new()));
1227 assert_eq!(values[1], Value::Number(Number::from(0)));
1228 assert_eq!(values[2], Value::Bool(false));
1229 assert_eq!(values[3], Value::Null);
1230 assert_eq!(values[4], Value::Object(Map::new()));
1231 assert_eq!(values[5], Value::Array(Vec::new()));
1232
1233 let empty_object_in_array = module.getattr("empty_object_in_array").unwrap();
1234 let empty_object_in_array =
1235 extract_template(py, &empty_object_in_array, "json_t/json_t_str").unwrap();
1236 let rendered =
1237 render_document(py, &parse_template(&empty_object_in_array).unwrap()).unwrap();
1238 let values = rendered.data.as_array().expect("array");
1239 assert_eq!(values.len(), 2);
1240 assert_eq!(values[0], Value::Object(Map::new()));
1241 assert_eq!(values[1]["a"], Value::Array(Vec::new()));
1242
1243 let top_level_empty_object_ws = module.getattr("top_level_empty_object_ws").unwrap();
1244 let top_level_empty_object_ws =
1245 extract_template(py, &top_level_empty_object_ws, "json_t/json_t_str").unwrap();
1246 let rendered =
1247 render_document(py, &parse_template(&top_level_empty_object_ws).unwrap()).unwrap();
1248 assert_eq!(rendered.data, Value::Object(Map::new()));
1249
1250 let nested_escaped_mix = module.getattr("nested_escaped_mix").unwrap();
1251 let nested_escaped_mix =
1252 extract_template(py, &nested_escaped_mix, "json_t/json_t_str").unwrap();
1253 let rendered =
1254 render_document(py, &parse_template(&nested_escaped_mix).unwrap()).unwrap();
1255 assert_eq!(
1256 rendered.data["x"],
1257 Value::String("\u{0008}\u{2028}\u{2029}/".to_owned())
1258 );
1259
1260 let escaped_reverse_solidus_solidus =
1261 module.getattr("escaped_reverse_solidus_solidus").unwrap();
1262 let escaped_reverse_solidus_solidus =
1263 extract_template(py, &escaped_reverse_solidus_solidus, "json_t/json_t_str")
1264 .unwrap();
1265 let rendered = render_document(
1266 py,
1267 &parse_template(&escaped_reverse_solidus_solidus).unwrap(),
1268 )
1269 .unwrap();
1270 assert_eq!(rendered.data, Value::String("\\/".to_owned()));
1271
1272 let upper_exp = module.getattr("upper_exp").unwrap();
1273 let upper_exp = extract_template(py, &upper_exp, "json_t/json_t_str").unwrap();
1274 let rendered = render_document(py, &parse_template(&upper_exp).unwrap()).unwrap();
1275 assert_eq!(
1276 rendered.data,
1277 Value::Number(Number::from_f64(100.0).unwrap())
1278 );
1279
1280 let upper_exp_plus = module.getattr("upper_exp_plus").unwrap();
1281 let upper_exp_plus =
1282 extract_template(py, &upper_exp_plus, "json_t/json_t_str").unwrap();
1283 let rendered = render_document(py, &parse_template(&upper_exp_plus).unwrap()).unwrap();
1284 assert_eq!(
1285 rendered.data,
1286 Value::Number(Number::from_f64(100.0).unwrap())
1287 );
1288
1289 let upper_exp_negative = module.getattr("upper_exp_negative").unwrap();
1290 let upper_exp_negative =
1291 extract_template(py, &upper_exp_negative, "json_t/json_t_str").unwrap();
1292 let rendered =
1293 render_document(py, &parse_template(&upper_exp_negative).unwrap()).unwrap();
1294 assert_eq!(
1295 rendered.data,
1296 Value::Number(Number::from_f64(-100.0).unwrap())
1297 );
1298
1299 let upper_exp_zero_fraction = module.getattr("upper_exp_zero_fraction").unwrap();
1300 let upper_exp_zero_fraction =
1301 extract_template(py, &upper_exp_zero_fraction, "json_t/json_t_str").unwrap();
1302 let rendered =
1303 render_document(py, &parse_template(&upper_exp_zero_fraction).unwrap()).unwrap();
1304 assert_eq!(rendered.data, Value::Number(Number::from_f64(0.0).unwrap()));
1305
1306 let upper_zero_negative_exp = module.getattr("upper_zero_negative_exp").unwrap();
1307 let upper_zero_negative_exp =
1308 extract_template(py, &upper_zero_negative_exp, "json_t/json_t_str").unwrap();
1309 let rendered =
1310 render_document(py, &parse_template(&upper_zero_negative_exp).unwrap()).unwrap();
1311 assert_eq!(
1312 rendered.data,
1313 Value::Number(Number::from_f64(-0.0).unwrap())
1314 );
1315
1316 let nested_upper_exp = module.getattr("nested_upper_exp").unwrap();
1317 let nested_upper_exp =
1318 extract_template(py, &nested_upper_exp, "json_t/json_t_str").unwrap();
1319 let rendered =
1320 render_document(py, &parse_template(&nested_upper_exp).unwrap()).unwrap();
1321 assert_eq!(
1322 rendered.data["value"],
1323 Value::Number(Number::from_f64(100.0).unwrap())
1324 );
1325
1326 let neg_exp_zero = module.getattr("neg_exp_zero").unwrap();
1327 let neg_exp_zero = extract_template(py, &neg_exp_zero, "json_t/json_t_str").unwrap();
1328 let rendered = render_document(py, &parse_template(&neg_exp_zero).unwrap()).unwrap();
1329 assert_eq!(
1330 rendered.data,
1331 Value::Number(Number::from_f64(-1.0).unwrap())
1332 );
1333
1334 let upper_exp_negative_zero = module.getattr("upper_exp_negative_zero").unwrap();
1335 let upper_exp_negative_zero =
1336 extract_template(py, &upper_exp_negative_zero, "json_t/json_t_str").unwrap();
1337 let rendered =
1338 render_document(py, &parse_template(&upper_exp_negative_zero).unwrap()).unwrap();
1339 assert_eq!(rendered.data, Value::Number(Number::from_f64(1.0).unwrap()));
1340
1341 let exp_with_fraction_zero = module.getattr("exp_with_fraction_zero").unwrap();
1342 let exp_with_fraction_zero =
1343 extract_template(py, &exp_with_fraction_zero, "json_t/json_t_str").unwrap();
1344 let rendered =
1345 render_document(py, &parse_template(&exp_with_fraction_zero).unwrap()).unwrap();
1346 assert_eq!(rendered.data, Value::Number(Number::from_f64(1.0).unwrap()));
1347
1348 let negative_zero_exp_upper = module.getattr("negative_zero_exp_upper").unwrap();
1349 let negative_zero_exp_upper =
1350 extract_template(py, &negative_zero_exp_upper, "json_t/json_t_str").unwrap();
1351 let rendered =
1352 render_document(py, &parse_template(&negative_zero_exp_upper).unwrap()).unwrap();
1353 assert_eq!(
1354 rendered.data,
1355 Value::Number(Number::from_f64(-0.0).unwrap())
1356 );
1357
1358 let nested_negative_exp_mix = module.getattr("nested_negative_exp_mix").unwrap();
1359 let nested_negative_exp_mix =
1360 extract_template(py, &nested_negative_exp_mix, "json_t/json_t_str").unwrap();
1361 let rendered =
1362 render_document(py, &parse_template(&nested_negative_exp_mix).unwrap()).unwrap();
1363 assert_eq!(
1364 rendered.data["x"][0],
1365 Value::Number(Number::from_f64(-0.01).unwrap())
1366 );
1367 assert_eq!(rendered.data["x"][1], Value::Number(Number::from(0)));
1368 assert_eq!(rendered.data["x"][2], Value::String(String::new()));
1369 assert_eq!(rendered.data["x"][3]["y"][0], Value::Null);
1370
1371 let mixed_nested_keywords = module.getattr("mixed_nested_keywords").unwrap();
1372 let mixed_nested_keywords =
1373 extract_template(py, &mixed_nested_keywords, "json_t/json_t_str").unwrap();
1374 let rendered =
1375 render_document(py, &parse_template(&mixed_nested_keywords).unwrap()).unwrap();
1376 assert_eq!(rendered.data["a"][0], Value::Bool(true));
1377 assert_eq!(rendered.data["a"][1], Value::Bool(false));
1378 assert_eq!(rendered.data["a"][2], Value::Null);
1379 assert_eq!(
1380 rendered.data["b"]["c"],
1381 Value::Number(Number::from_f64(-1.0).unwrap())
1382 );
1383
1384 let nested_empty_names = module.getattr("nested_empty_names").unwrap();
1385 let nested_empty_names =
1386 extract_template(py, &nested_empty_names, "json_t/json_t_str").unwrap();
1387 let rendered =
1388 render_document(py, &parse_template(&nested_empty_names).unwrap()).unwrap();
1389 assert_eq!(rendered.data[""][""], Value::Array(Vec::new()));
1390
1391 let nested_empty_name_array = module.getattr("nested_empty_name_array").unwrap();
1392 let nested_empty_name_array =
1393 extract_template(py, &nested_empty_name_array, "json_t/json_t_str").unwrap();
1394 let rendered =
1395 render_document(py, &parse_template(&nested_empty_name_array).unwrap()).unwrap();
1396 assert_eq!(rendered.data[""][0], Value::String(String::new()));
1397 assert_eq!(rendered.data[""][1][""], Value::Number(Number::from(0)));
1398
1399 let nested_nulls = module.getattr("nested_nulls").unwrap();
1400 let nested_nulls = extract_template(py, &nested_nulls, "json_t/json_t_str").unwrap();
1401 let rendered = render_document(py, &parse_template(&nested_nulls).unwrap()).unwrap();
1402 assert_eq!(rendered.data["a"], Value::Null);
1403 assert_eq!(rendered.data["b"][0], Value::Null);
1404 assert_eq!(rendered.data["b"][1]["c"], Value::Null);
1405
1406 let nested_top_ws = module.getattr("nested_top_ws").unwrap();
1407 let nested_top_ws = extract_template(py, &nested_top_ws, "json_t/json_t_str").unwrap();
1408 let rendered = render_document(py, &parse_template(&nested_top_ws).unwrap()).unwrap();
1409 assert_eq!(rendered.data["a"][1]["b"], Value::String("c".to_owned()));
1410 assert_eq!(rendered.data[""], Value::String(String::new()));
1411
1412 let top_ws_string = module.getattr("top_ws_string").unwrap();
1413 let top_ws_string = extract_template(py, &top_ws_string, "json_t/json_t_str").unwrap();
1414 let rendered = render_document(py, &parse_template(&top_ws_string).unwrap()).unwrap();
1415 assert_eq!(rendered.data, Value::String("x".to_owned()));
1416
1417 let nested_number_whitespace = module.getattr("nested_number_whitespace").unwrap();
1418 let nested_number_whitespace =
1419 extract_template(py, &nested_number_whitespace, "json_t/json_t_str").unwrap();
1420 let rendered =
1421 render_document(py, &parse_template(&nested_number_whitespace).unwrap()).unwrap();
1422 let values = rendered.data["a"].as_array().expect("array");
1423 assert_eq!(values.len(), 3);
1424 assert_eq!(values[0], Value::Number(Number::from(0)));
1425 assert_eq!(values[1], Value::Number(Number::from_f64(-0.0).unwrap()));
1426 assert_eq!(values[2], Value::Number(Number::from_f64(0.015).unwrap()));
1427
1428 let nested_number_combo = module.getattr("nested_number_combo").unwrap();
1429 let nested_number_combo =
1430 extract_template(py, &nested_number_combo, "json_t/json_t_str").unwrap();
1431 let rendered =
1432 render_document(py, &parse_template(&nested_number_combo).unwrap()).unwrap();
1433 let values = rendered.data["a"].as_array().expect("array");
1434 assert_eq!(values[0], Value::Number(Number::from(0)));
1435 assert_eq!(values[1], Value::Number(Number::from_f64(-0.0).unwrap()));
1436 assert_eq!(values[2], Value::Number(Number::from_f64(-0.0).unwrap()));
1437 assert_eq!(values[3], Value::Number(Number::from_f64(1.0).unwrap()));
1438 assert_eq!(values[4], Value::Number(Number::from_f64(-1.0).unwrap()));
1439
1440 let nested_bool_null_mix = module.getattr("nested_bool_null_mix").unwrap();
1441 let nested_bool_null_mix =
1442 extract_template(py, &nested_bool_null_mix, "json_t/json_t_str").unwrap();
1443 let rendered =
1444 render_document(py, &parse_template(&nested_bool_null_mix).unwrap()).unwrap();
1445 assert_eq!(rendered.data["v"][0], Value::Bool(true));
1446 assert_eq!(rendered.data["v"][1], Value::Null);
1447 assert_eq!(rendered.data["v"][2], Value::Bool(false));
1448 assert_eq!(rendered.data["v"][3]["x"], Value::Number(Number::from(1)));
1449
1450 let keyword_array = module.getattr("keyword_array").unwrap();
1451 let keyword_array = extract_template(py, &keyword_array, "json_t/json_t_str").unwrap();
1452 let rendered = render_document(py, &parse_template(&keyword_array).unwrap()).unwrap();
1453 let values = rendered.data.as_array().expect("array");
1454 assert_eq!(values[0], Value::Bool(true));
1455 assert_eq!(values[1], Value::Bool(false));
1456 assert_eq!(values[2], Value::Null);
1457
1458 let empty_name_nested_keywords = module.getattr("empty_name_nested_keywords").unwrap();
1459 let empty_name_nested_keywords =
1460 extract_template(py, &empty_name_nested_keywords, "json_t/json_t_str").unwrap();
1461 let rendered =
1462 render_document(py, &parse_template(&empty_name_nested_keywords).unwrap()).unwrap();
1463 let values = rendered.data[""].as_array().expect("array");
1464 assert_eq!(values[0], Value::Null);
1465 assert_eq!(values[1], Value::Bool(true));
1466 assert_eq!(values[2], Value::Bool(false));
1467
1468 let nested_empty_mix = module.getattr("nested_empty_mix").unwrap();
1469 let nested_empty_mix =
1470 extract_template(py, &nested_empty_mix, "json_t/json_t_str").unwrap();
1471 let rendered =
1472 render_document(py, &parse_template(&nested_empty_mix).unwrap()).unwrap();
1473 let values = rendered.data["a"].as_array().expect("array");
1474 assert_eq!(values[0], Value::Object(Map::new()));
1475 assert_eq!(values[1], Value::Array(Vec::new()));
1476 assert_eq!(values[2], Value::String(String::new()));
1477 assert_eq!(values[3], Value::Number(Number::from(0)));
1478 assert_eq!(values[4], Value::Bool(false));
1479 assert_eq!(values[5], Value::Null);
1480
1481 let nested_empty_collections_mix =
1482 module.getattr("nested_empty_collections_mix").unwrap();
1483 let nested_empty_collections_mix =
1484 extract_template(py, &nested_empty_collections_mix, "json_t/json_t_str").unwrap();
1485 let rendered =
1486 render_document(py, &parse_template(&nested_empty_collections_mix).unwrap())
1487 .unwrap();
1488 assert_eq!(rendered.data["a"]["b"], Value::Array(Vec::new()));
1489 assert_eq!(rendered.data["c"][0], Value::Object(Map::new()));
1490 assert_eq!(rendered.data["c"][1], Value::Array(Vec::new()));
1491
1492 let array_nested_mixed_scalars = module.getattr("array_nested_mixed_scalars").unwrap();
1493 let array_nested_mixed_scalars =
1494 extract_template(py, &array_nested_mixed_scalars, "json_t/json_t_str").unwrap();
1495 let rendered =
1496 render_document(py, &parse_template(&array_nested_mixed_scalars).unwrap()).unwrap();
1497 let values = rendered.data.as_array().expect("array");
1498 assert_eq!(values[0]["a"], Value::Array(Vec::new()));
1499 assert_eq!(values[1]["b"], Value::Object(Map::new()));
1500 assert_eq!(values[2], Value::String(String::new()));
1501 assert_eq!(values[3], Value::Number(Number::from(0)));
1502 assert_eq!(values[4], Value::Bool(false));
1503 assert_eq!(values[5], Value::Null);
1504
1505 let zero_fraction_exp = module.getattr("zero_fraction_exp").unwrap();
1506 let zero_fraction_exp =
1507 extract_template(py, &zero_fraction_exp, "json_t/json_t_str").unwrap();
1508 let rendered =
1509 render_document(py, &parse_template(&zero_fraction_exp).unwrap()).unwrap();
1510 assert_eq!(rendered.data, Value::Number(Number::from_f64(0.0).unwrap()));
1511
1512 let nested = module.getattr("nested").unwrap();
1513 let nested = extract_template(py, &nested, "json_t/json_t_str").unwrap();
1514 let rendered = render_document(py, &parse_template(&nested).unwrap()).unwrap();
1515 assert_eq!(rendered.data.as_array().expect("array").len(), 1);
1516 assert_eq!(rendered.data[0]["a"], Value::Number(Number::from(1)));
1517 assert_eq!(rendered.data[0]["b"][0], Value::Bool(true));
1518 assert_eq!(rendered.data[0]["b"][1], Value::Bool(false));
1519 assert_eq!(rendered.data[0]["b"][2], Value::Null);
1520 });
1521 }
1522
1523 #[test]
1524 fn rejects_invalid_unicode_and_control_sequences() {
1525 Python::with_gil(|py| {
1526 let module = PyModule::from_code(
1527 py,
1528 pyo3::ffi::c_str!(
1529 "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"
1530 ),
1531 pyo3::ffi::c_str!("test_json_invalid_unicode.py"),
1532 pyo3::ffi::c_str!("test_json_invalid_unicode"),
1533 )
1534 .unwrap();
1535
1536 for (name, expected) in [
1537 ("incomplete", "Unexpected end of JSON escape sequence"),
1538 ("control", "Control characters are not allowed"),
1539 ("low", "Invalid JSON unicode escape"),
1540 ("exp", "Invalid JSON number literal"),
1541 ("exp2", "Invalid JSON number literal"),
1542 ("dot", "Invalid JSON number literal"),
1543 ("minus", "Invalid JSON number literal"),
1544 ("object_exp", "Invalid JSON number literal"),
1545 ("object_dot", "Invalid JSON number literal"),
1546 ("trailing_true", "Expected a JSON value"),
1547 (
1548 "trailing_string",
1549 "Unexpected trailing content in JSON template",
1550 ),
1551 (
1552 "extra_close",
1553 "Unexpected trailing content in JSON template",
1554 ),
1555 (
1556 "object_extra_close",
1557 "Unexpected trailing content in JSON template",
1558 ),
1559 (
1560 "double_trailing_close",
1561 "Unexpected trailing content in JSON template",
1562 ),
1563 ("missing_array_comma", "Invalid JSON number literal"),
1564 ("missing_object_comma", "Invalid JSON number literal"),
1565 (
1566 "nested_missing_object_array_comma",
1567 "Invalid JSON number literal",
1568 ),
1569 ("extra_array_comma", "Expected a JSON value"),
1570 ("leading_comma_array", "Expected a JSON value"),
1571 ("missing_value", "Expected a JSON value"),
1572 (
1573 "missing_key",
1574 "JSON object keys must be quoted strings or interpolations",
1575 ),
1576 ("missing_colon", "Expected ':' in JSON template"),
1577 (
1578 "trailing_object_comma",
1579 "JSON object keys must be quoted strings or interpolations",
1580 ),
1581 ("trailing_array_comma", "Expected a JSON value"),
1582 (
1583 "nested_trailing_object_comma",
1584 "quoted strings or interpolations",
1585 ),
1586 ("plus_number", "Expected a JSON value"),
1587 ("leading_decimal", "Expected a JSON value"),
1588 ("leading_zero", "Invalid JSON number literal"),
1589 ("negative_leading_zero", "Invalid JSON number literal"),
1590 ("leading_zero_exp", "Invalid JSON number literal"),
1591 ("negative_leading_zero_exp", "Invalid JSON number literal"),
1592 ("missing_exp_digits_minus", "Invalid JSON number literal"),
1593 (
1594 "object_missing_exp_digits_minus",
1595 "Invalid JSON number literal",
1596 ),
1597 (
1598 "object_negative_leading_zero",
1599 "Invalid JSON number literal",
1600 ),
1601 ("array_double_value_no_comma", "Expected a JSON value"),
1602 ("nested_missing_comma_bool", "Expected a JSON value"),
1603 ("array_true_number_no_comma", "Expected a JSON value"),
1604 (
1605 "array_false_object_no_comma",
1606 "Invalid promoted JSON fragment content",
1607 ),
1608 (
1609 "nested_missing_comma_obj_after_null",
1610 "Invalid promoted JSON fragment content",
1611 ),
1612 ("object_double_value_no_comma", "Expected a JSON value"),
1613 ("object_keyword_number_no_comma", "Expected a JSON value"),
1614 ("truncated_true", "Expected a JSON value"),
1615 ("truncated_false", "Expected a JSON value"),
1616 ("truncated_null", "Expected a JSON value"),
1617 ("object_truncated_true", "Expected a JSON value"),
1618 ("object_truncated_false", "Expected a JSON value"),
1619 ("object_truncated_null", "Expected a JSON value"),
1620 ("array_truncated_true", "Expected a JSON value"),
1621 ("array_truncated_false", "Expected a JSON value"),
1622 ("nested_array_truncated_true", "Expected a JSON value"),
1623 ("nested_array_truncated_false", "Expected a JSON value"),
1624 ("array_truncated_null", "Expected a JSON value"),
1625 ("nested_object_truncated_null", "Expected a JSON value"),
1626 ("keyword_prefix", "Expected a JSON value"),
1627 ("bad_true_case", "Expected a JSON value"),
1628 ("array_bad_true_case", "Expected a JSON value"),
1629 ("bad_false_case", "Expected a JSON value"),
1630 ("bad_null_case", "Expected a JSON value"),
1631 ("object_bad_true_case", "Expected a JSON value"),
1632 ("object_bad_false_case", "Expected a JSON value"),
1633 ("object_bad_null_case", "Expected a JSON value"),
1634 ("array_bad_false_case", "Expected a JSON value"),
1635 ("array_bad_null_case", "Expected a JSON value"),
1636 ("nested_array_bad_null_case", "Expected a JSON value"),
1637 (
1638 "array_missing_comma_after_string",
1639 "Expected ',' in JSON template",
1640 ),
1641 (
1642 "object_missing_comma_after_null",
1643 "Invalid promoted JSON fragment content",
1644 ),
1645 ("double_decimal_point", "Invalid JSON number literal"),
1646 ("object_bad_leading_zero", "Invalid JSON number literal"),
1647 (
1648 "nested_extra_close",
1649 "Unexpected trailing content in JSON template",
1650 ),
1651 ] {
1652 let template = module.getattr(name).unwrap();
1653 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1654 let err = match parse_template(&template) {
1655 Ok(_) => panic!("expected JSON parse failure for {name}"),
1656 Err(err) => err,
1657 };
1658 assert_eq!(err.kind, ErrorKind::Parse);
1659 assert!(err.message.contains(expected), "{name}: {}", err.message);
1660 }
1661 });
1662 }
1663
1664 #[test]
1665 fn rejects_structural_invalid_message_families() {
1666 Python::with_gil(|py| {
1667 let module = PyModule::from_code(
1668 py,
1669 pyo3::ffi::c_str!(
1670 "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"
1671 ),
1672 pyo3::ffi::c_str!("test_json_structural_invalids.py"),
1673 pyo3::ffi::c_str!("test_json_structural_invalids"),
1674 )
1675 .unwrap();
1676
1677 for (name, expected) in [
1678 ("missing_comma", "Expected ',' in JSON template"),
1679 (
1680 "trailing_comma",
1681 "JSON object keys must be quoted strings or interpolations",
1682 ),
1683 ("invalid_fragment", "Invalid promoted JSON fragment content"),
1684 (
1685 "unexpected_trailing",
1686 "Unexpected trailing content in JSON template",
1687 ),
1688 ] {
1689 let template = module.getattr(name).unwrap();
1690 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1691 let err = parse_template(&template).expect_err("expected JSON parse failure");
1692 assert_eq!(err.kind, ErrorKind::Parse);
1693 assert!(err.message.contains(expected), "{name}: {}", err.message);
1694 }
1695 });
1696 }
1697
1698 #[test]
1699 fn rejects_keyword_truncation_and_collection_separator_errors() {
1700 Python::with_gil(|py| {
1701 let module = PyModule::from_code(
1702 py,
1703 pyo3::ffi::c_str!(
1704 "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"
1705 ),
1706 pyo3::ffi::c_str!("test_json_invalid_keywords.py"),
1707 pyo3::ffi::c_str!("test_json_invalid_keywords"),
1708 )
1709 .unwrap();
1710
1711 for (name, expected) in [
1712 ("bad_true", "Expected a JSON value"),
1713 ("bad_false", "Expected a JSON value"),
1714 ("bad_null", "Expected a JSON value"),
1715 (
1716 "missing_comma_array",
1717 "Invalid promoted JSON fragment content",
1718 ),
1719 ("trailing_array_comma", "Expected a JSON value"),
1720 ("trailing_object_comma", "quoted strings or interpolations"),
1721 ] {
1722 let template = module.getattr(name).unwrap();
1723 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1724 let err = parse_template(&template).expect_err("expected JSON parse failure");
1725 assert_eq!(err.kind, ErrorKind::Parse);
1726 assert!(err.message.contains(expected), "{name}: {}", err.message);
1727 }
1728 });
1729 }
1730
1731 #[test]
1732 fn rejects_additional_number_and_trailing_content_families() {
1733 Python::with_gil(|py| {
1734 let module = PyModule::from_code(
1735 py,
1736 pyo3::ffi::c_str!(
1737 "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"
1738 ),
1739 pyo3::ffi::c_str!("test_json_additional_parse_errors.py"),
1740 pyo3::ffi::c_str!("test_json_additional_parse_errors"),
1741 )
1742 .unwrap();
1743
1744 for (name, expected) in [
1745 ("leading_zero_exp", "Invalid JSON number literal"),
1746 ("leading_zero_exp_negative", "Invalid JSON number literal"),
1747 ("missing_exp_digits", "Invalid JSON number literal"),
1748 ("embedded_missing_exp_digits", "Invalid JSON number literal"),
1749 ("double_sign_number", "Invalid JSON number literal"),
1750 ("leading_plus_minus", "Expected a JSON value"),
1751 ("bad_exp_plus_minus", "Invalid JSON number literal"),
1752 ("bad_exp_minus_plus", "Invalid JSON number literal"),
1753 ("extra_decimal", "Invalid JSON number literal"),
1754 ("array_space_number", "Invalid JSON number literal"),
1755 ("object_space_number", "Invalid JSON number literal"),
1756 ("truee", "Expected a JSON value"),
1757 ("true_then_number", "Expected a JSON value"),
1758 ("object_true_then_number", "Expected a JSON value"),
1759 ("false_fragment", "Invalid promoted JSON fragment content"),
1760 (
1761 "array_trailing_object",
1762 "Unexpected trailing content in JSON template",
1763 ),
1764 (
1765 "object_trailing_array",
1766 "Unexpected trailing content in JSON template",
1767 ),
1768 (
1769 "deep_object_trailing",
1770 "Unexpected trailing content in JSON template",
1771 ),
1772 ] {
1773 let template = module.getattr(name).unwrap();
1774 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1775 let err = parse_template(&template).expect_err("expected JSON parse failure");
1776 assert_eq!(err.kind, ErrorKind::Parse);
1777 assert!(err.message.contains(expected), "{name}: {}", err.message);
1778 }
1779 });
1780 }
1781
1782 #[test]
1783 fn renders_keyword_and_empty_name_collection_shapes() {
1784 Python::with_gil(|py| {
1785 let module = PyModule::from_code(
1786 py,
1787 pyo3::ffi::c_str!(
1788 "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"
1789 ),
1790 pyo3::ffi::c_str!("test_json_keyword_empty_shapes.py"),
1791 pyo3::ffi::c_str!("test_json_keyword_empty_shapes"),
1792 )
1793 .unwrap();
1794
1795 let keyword_array = module.getattr("keyword_array").unwrap();
1796 let keyword_array = extract_template(py, &keyword_array, "json_t/json_t_str").unwrap();
1797 let rendered = render_document(py, &parse_template(&keyword_array).unwrap()).unwrap();
1798 assert_eq!(rendered.text, "[true, false, null]");
1799 assert_eq!(rendered.data, json!([true, false, null]));
1800
1801 let empty_name_nested_keywords = module.getattr("empty_name_nested_keywords").unwrap();
1802 let empty_name_nested_keywords =
1803 extract_template(py, &empty_name_nested_keywords, "json_t/json_t_str").unwrap();
1804 let rendered =
1805 render_document(py, &parse_template(&empty_name_nested_keywords).unwrap()).unwrap();
1806 assert_eq!(rendered.text, "{\"\": [null, true, false]}");
1807 assert_eq!(rendered.data, json!({"": [null, true, false]}));
1808
1809 let nested_empty_mix = module.getattr("nested_empty_mix").unwrap();
1810 let nested_empty_mix =
1811 extract_template(py, &nested_empty_mix, "json_t/json_t_str").unwrap();
1812 let rendered =
1813 render_document(py, &parse_template(&nested_empty_mix).unwrap()).unwrap();
1814 assert_eq!(rendered.data, json!({"a": [{}, [], "", 0, false, null]}));
1815
1816 let array_nested_mixed_scalars = module.getattr("array_nested_mixed_scalars").unwrap();
1817 let array_nested_mixed_scalars =
1818 extract_template(py, &array_nested_mixed_scalars, "json_t/json_t_str").unwrap();
1819 let rendered =
1820 render_document(py, &parse_template(&array_nested_mixed_scalars).unwrap()).unwrap();
1821 assert_eq!(
1822 rendered.data,
1823 json!([{"a": []}, {"b": {}}, "", 0, false, null])
1824 );
1825 });
1826 }
1827
1828 #[test]
1829 fn renders_top_level_whitespace_and_nested_number_shapes() {
1830 Python::with_gil(|py| {
1831 let module = PyModule::from_code(
1832 py,
1833 pyo3::ffi::c_str!(
1834 "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"
1835 ),
1836 pyo3::ffi::c_str!("test_json_whitespace_shapes.py"),
1837 pyo3::ffi::c_str!("test_json_whitespace_shapes"),
1838 )
1839 .unwrap();
1840
1841 let top_ws_string = module.getattr("top_ws_string").unwrap();
1842 let top_ws_string = extract_template(py, &top_ws_string, "json_t/json_t_str").unwrap();
1843 let rendered = render_document(py, &parse_template(&top_ws_string).unwrap()).unwrap();
1844 assert_eq!(rendered.text, "\"x\"");
1845 assert_eq!(rendered.data, json!("x"));
1846
1847 let nested_top_ws = module.getattr("nested_top_ws").unwrap();
1848 let nested_top_ws = extract_template(py, &nested_top_ws, "json_t/json_t_str").unwrap();
1849 let rendered = render_document(py, &parse_template(&nested_top_ws).unwrap()).unwrap();
1850 assert_eq!(rendered.text, "{\"a\": [1, {\"b\": \"c\"}], \"\": \"\"}");
1851 assert_eq!(rendered.data, json!({"a": [1, {"b": "c"}], "": ""}));
1852
1853 let nested_number_whitespace = module.getattr("nested_number_whitespace").unwrap();
1854 let nested_number_whitespace =
1855 extract_template(py, &nested_number_whitespace, "json_t/json_t_str").unwrap();
1856 let rendered =
1857 render_document(py, &parse_template(&nested_number_whitespace).unwrap()).unwrap();
1858 assert_eq!(rendered.text, "{\"a\": [0, -0, 1.5E-2]}");
1859 assert_eq!(
1860 rendered.data,
1861 serde_json::from_str::<Value>("{\"a\": [0, -0, 1.5E-2]}").unwrap()
1862 );
1863 });
1864 }
1865
1866 #[test]
1867 fn renders_end_to_end_supported_positions_text_and_data() {
1868 Python::with_gil(|py| {
1869 let module = PyModule::from_code(
1870 py,
1871 pyo3::ffi::c_str!(
1872 "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"
1873 ),
1874 pyo3::ffi::c_str!("test_json_end_to_end_positions.py"),
1875 pyo3::ffi::c_str!("test_json_end_to_end_positions"),
1876 )
1877 .unwrap();
1878
1879 let template = module.getattr("template").unwrap();
1880 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1881 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
1882 assert_eq!(
1883 rendered.text,
1884 "{\"user\": {\"enabled\": true, \"count\": 2}, \"prefix-prefix\": \"item-suffix\", \"label\": \"prefix-suffix\"}"
1885 );
1886 assert_eq!(
1887 rendered.data,
1888 json!({
1889 "user": {"enabled": true, "count": 2},
1890 "prefix-prefix": "item-suffix",
1891 "label": "prefix-suffix",
1892 })
1893 );
1894 });
1895 }
1896
1897 #[test]
1898 fn renders_rfc_8259_image_example_text_and_data() {
1899 Python::with_gil(|py| {
1900 let module = PyModule::from_code(
1901 py,
1902 pyo3::ffi::c_str!(
1903 "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"
1904 ),
1905 pyo3::ffi::c_str!("test_json_rfc_8259_image_example.py"),
1906 pyo3::ffi::c_str!("test_json_rfc_8259_image_example"),
1907 )
1908 .unwrap();
1909
1910 let template = module.getattr("template").unwrap();
1911 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1912 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
1913 assert_eq!(
1914 rendered.text,
1915 "{\"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]}}"
1916 );
1917 assert_eq!(
1918 rendered.data,
1919 json!({
1920 "Image": {
1921 "Width": 800,
1922 "Height": 600,
1923 "Title": "View from 15th Floor",
1924 "Thumbnail": {
1925 "Url": "http://www.example.com/image/481989943",
1926 "Height": 125,
1927 "Width": 100,
1928 },
1929 "Animated": false,
1930 "IDs": [116, 943, 234, 38793],
1931 }
1932 })
1933 );
1934 });
1935 }
1936
1937 #[test]
1938 fn renders_rfc_8259_value_examples_text_and_data() {
1939 Python::with_gil(|py| {
1940 let module = PyModule::from_code(
1941 py,
1942 pyo3::ffi::c_str!(
1943 "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"
1944 ),
1945 pyo3::ffi::c_str!("test_json_rfc_8259_value_examples.py"),
1946 pyo3::ffi::c_str!("test_json_rfc_8259_value_examples"),
1947 )
1948 .unwrap();
1949
1950 let array = module.getattr("array").unwrap();
1951 let array = extract_template(py, &array, "json_t/json_t_str").unwrap();
1952 let rendered = render_document(py, &parse_template(&array).unwrap()).unwrap();
1953 assert_eq!(
1954 rendered.text,
1955 "[{\"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\"}]"
1956 );
1957 assert_eq!(
1958 rendered.data,
1959 json!([
1960 {
1961 "precision": "zip",
1962 "Latitude": 37.7668,
1963 "Longitude": -122.3959,
1964 "Address": "",
1965 "City": "SAN FRANCISCO",
1966 "State": "CA",
1967 "Zip": "94107",
1968 "Country": "US",
1969 },
1970 {
1971 "precision": "zip",
1972 "Latitude": 37.371991,
1973 "Longitude": -122.026020,
1974 "Address": "",
1975 "City": "SUNNYVALE",
1976 "State": "CA",
1977 "Zip": "94085",
1978 "Country": "US",
1979 }
1980 ])
1981 );
1982
1983 for (name, expected_text, expected_value) in [
1984 ("string", "\"Hello world!\"", json!("Hello world!")),
1985 ("number", "42", json!(42)),
1986 ("boolean", "true", json!(true)),
1987 ] {
1988 let template = module.getattr(name).unwrap();
1989 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
1990 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
1991 assert_eq!(rendered.text, expected_text, "{name}");
1992 assert_eq!(rendered.data, expected_value, "{name}");
1993 }
1994 });
1995 }
1996
1997 #[test]
1998 fn renders_unicode_and_escape_mix_shapes() {
1999 Python::with_gil(|py| {
2000 let module = PyModule::from_code(
2001 py,
2002 pyo3::ffi::c_str!(
2003 "unicode_array=t'[\"\\\\u2028\", \"\\\\u2029\", \"\\\\u00DF\"]'\nescape_object=t'{{\"x\":\"\\\\b\\\\u2028\\\\u2029\\\\/\"}}'\n"
2004 ),
2005 pyo3::ffi::c_str!("test_json_unicode_escape_mix.py"),
2006 pyo3::ffi::c_str!("test_json_unicode_escape_mix"),
2007 )
2008 .unwrap();
2009
2010 let unicode_array = module.getattr("unicode_array").unwrap();
2011 let unicode_array = extract_template(py, &unicode_array, "json_t/json_t_str").unwrap();
2012 let rendered = render_document(py, &parse_template(&unicode_array).unwrap()).unwrap();
2013 assert_eq!(rendered.text, "[\"\u{2028}\", \"\u{2029}\", \"ß\"]");
2014 assert_eq!(rendered.data, json!(["\u{2028}", "\u{2029}", "ß"]));
2015
2016 let escape_object = module.getattr("escape_object").unwrap();
2017 let escape_object = extract_template(py, &escape_object, "json_t/json_t_str").unwrap();
2018 let rendered = render_document(py, &parse_template(&escape_object).unwrap()).unwrap();
2019 assert_eq!(rendered.text, "{\"x\": \"\\b\u{2028}\u{2029}/\"}");
2020 assert_eq!(rendered.data, json!({"x": "\u{0008}\u{2028}\u{2029}/"}));
2021 });
2022 }
2023
2024 #[test]
2025 fn renders_control_escapes_and_reverse_solidus_variants() {
2026 Python::with_gil(|py| {
2027 let module = PyModule::from_code(
2028 py,
2029 pyo3::ffi::c_str!(
2030 "escaped_controls=t'\"\\\\b\\\\f\\\\n\\\\r\\\\t\"'\nreverse_solidus_u=t'\"\\\\u005C/\"'\nescaped_quote_backslash=t'\"\\\\\\\"\\\\\\\\\"'\n"
2031 ),
2032 pyo3::ffi::c_str!("test_json_escape_variants.py"),
2033 pyo3::ffi::c_str!("test_json_escape_variants"),
2034 )
2035 .unwrap();
2036
2037 let escaped_controls = module.getattr("escaped_controls").unwrap();
2038 let escaped_controls =
2039 extract_template(py, &escaped_controls, "json_t/json_t_str").unwrap();
2040 let rendered =
2041 render_document(py, &parse_template(&escaped_controls).unwrap()).unwrap();
2042 assert_eq!(rendered.text, "\"\\b\\f\\n\\r\\t\"");
2043 assert_eq!(rendered.data, json!("\u{0008}\u{000c}\n\r\t"));
2044
2045 let reverse_solidus_u = module.getattr("reverse_solidus_u").unwrap();
2046 let reverse_solidus_u =
2047 extract_template(py, &reverse_solidus_u, "json_t/json_t_str").unwrap();
2048 let rendered =
2049 render_document(py, &parse_template(&reverse_solidus_u).unwrap()).unwrap();
2050 assert_eq!(rendered.text, "\"\\\\/\"");
2051 assert_eq!(rendered.data, json!("\\/"));
2052
2053 let escaped_quote_backslash = module.getattr("escaped_quote_backslash").unwrap();
2054 let escaped_quote_backslash =
2055 extract_template(py, &escaped_quote_backslash, "json_t/json_t_str").unwrap();
2056 let rendered =
2057 render_document(py, &parse_template(&escaped_quote_backslash).unwrap()).unwrap();
2058 assert_eq!(rendered.text, "\"\\\"\\\\\"");
2059 assert_eq!(rendered.data, json!("\"\\"));
2060 });
2061 }
2062
2063 #[test]
2064 fn renders_promoted_rows_and_fragment_text_shapes() {
2065 Python::with_gil(|py| {
2066 let module = PyModule::from_code(
2067 py,
2068 pyo3::ffi::c_str!(
2069 "left='prefix'\nright='suffix'\nrows=[{'name': 'one'}, {'name': 'two'}]\nfragment=t'{{\"label\": {left}-{right}}}'\npromoted=t'{rows}'\n"
2070 ),
2071 pyo3::ffi::c_str!("test_json_promoted_rows.py"),
2072 pyo3::ffi::c_str!("test_json_promoted_rows"),
2073 )
2074 .unwrap();
2075
2076 let fragment = module.getattr("fragment").unwrap();
2077 let fragment = extract_template(py, &fragment, "json_t/json_t_str").unwrap();
2078 let rendered = render_document(py, &parse_template(&fragment).unwrap()).unwrap();
2079 assert_eq!(rendered.text, "{\"label\": \"prefix-suffix\"}");
2080 assert_eq!(rendered.data, json!({"label": "prefix-suffix"}));
2081
2082 let promoted = module.getattr("promoted").unwrap();
2083 let promoted = extract_template(py, &promoted, "json_t/json_t_str").unwrap();
2084 let rendered = render_document(py, &parse_template(&promoted).unwrap()).unwrap();
2085 assert_eq!(rendered.text, r#"[{"name": "one"}, {"name": "two"}]"#);
2086 assert_eq!(rendered.data, json!([{"name": "one"}, {"name": "two"}]));
2087 });
2088 }
2089
2090 #[test]
2091 fn renders_negative_zero_number_shapes() {
2092 Python::with_gil(|py| {
2093 let module = PyModule::from_code(
2094 py,
2095 pyo3::ffi::c_str!(
2096 "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"
2097 ),
2098 pyo3::ffi::c_str!("test_json_negative_zero_shapes.py"),
2099 pyo3::ffi::c_str!("test_json_negative_zero_shapes"),
2100 )
2101 .unwrap();
2102
2103 for (name, expected_text, expected_data) in [
2104 ("neg_zero", "-0", json!(-0.0)),
2105 ("neg_zero_float", "-0.0", json!(-0.0)),
2106 ("neg_zero_exp", "-0E0", json!(-0.0)),
2107 ] {
2108 let template = module.getattr(name).unwrap();
2109 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
2110 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2111 assert_eq!(rendered.text, expected_text);
2112 assert_eq!(rendered.data, expected_data);
2113 }
2114
2115 let combo = module.getattr("combo").unwrap();
2116 let combo = extract_template(py, &combo, "json_t/json_t_str").unwrap();
2117 let rendered = render_document(py, &parse_template(&combo).unwrap()).unwrap();
2118 assert_eq!(rendered.text, "{\"a\": [0, -0, -0.0, 1e0, -1E-0]}");
2119 assert_eq!(
2120 rendered.data,
2121 serde_json::from_str::<Value>("{\"a\": [0, -0, -0.0, 1e0, -1E-0]}").unwrap()
2122 );
2123 });
2124 }
2125
2126 #[test]
2127 fn renders_top_level_keywords_and_empty_collections() {
2128 Python::with_gil(|py| {
2129 let module = PyModule::from_code(
2130 py,
2131 pyo3::ffi::c_str!(
2132 "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"
2133 ),
2134 pyo3::ffi::c_str!("test_json_top_level_keywords.py"),
2135 pyo3::ffi::c_str!("test_json_top_level_keywords"),
2136 )
2137 .unwrap();
2138
2139 for (name, expected_text, expected_data) in [
2140 ("top_bool_ws", "true", json!(true)),
2141 ("top_null_ws", "null", Value::Null),
2142 ("empty_object", "{}", json!({})),
2143 ("empty_array", "[]", json!([])),
2144 ] {
2145 let template = module.getattr(name).unwrap();
2146 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
2147 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2148 assert_eq!(rendered.text, expected_text);
2149 assert_eq!(rendered.data, expected_data);
2150 }
2151 });
2152 }
2153
2154 #[test]
2155 fn renders_escape_unicode_and_keyword_text_shapes() {
2156 Python::with_gil(|py| {
2157 let module = PyModule::from_code(
2158 py,
2159 pyo3::ffi::c_str!(
2160 "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"
2161 ),
2162 pyo3::ffi::c_str!("test_json_escape_unicode_keyword_shapes.py"),
2163 pyo3::ffi::c_str!("test_json_escape_unicode_keyword_shapes"),
2164 )
2165 .unwrap();
2166
2167 for (name, expected_text, expected_data) in [
2168 ("top_bool_ws", "true", json!(true)),
2169 ("top_null_ws", "null", Value::Null),
2170 (
2171 "array_with_line_sep",
2172 "[\"\u{2028}\", \"\u{2029}\"]",
2173 json!(["\u{2028}", "\u{2029}"]),
2174 ),
2175 (
2176 "unicode_mix_nested_obj",
2177 "{\"x\": {\"a\": \"\\\\\", \"b\": \"ß\", \"c\": \"\u{2029}\"}}",
2178 json!({"x": {"a": "\\", "b": "ß", "c": "\u{2029}"}}),
2179 ),
2180 (
2181 "keyword_array",
2182 "[true, false, null]",
2183 json!([true, false, null]),
2184 ),
2185 ] {
2186 let template = module.getattr(name).unwrap();
2187 let template = extract_template(py, &template, "json_t/json_t_str").unwrap();
2188 let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
2189 assert_eq!(rendered.text, expected_text, "{name}");
2190 assert_eq!(rendered.data, expected_data, "{name}");
2191 }
2192 });
2193 }
2194
2195 #[test]
2196 fn test_parse_rendered_json_surfaces_parse_failures() {
2197 let err = parse_rendered_json("{\"a\":,}").expect_err("expected JSON parse failure");
2198 assert_eq!(err.kind, ErrorKind::Parse);
2199 assert!(err.message.contains("Rendered JSON could not be reparsed"));
2200 }
2201}