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