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