1use minijinja::value::{Kwargs, Rest, Value, ValueKind};
26use minijinja::{Environment, Error, ErrorKind};
27
28#[derive(Debug, Clone, Default)]
30pub struct DumpsOptions {
31 pub ensure_ascii: bool,
32 pub indent: Option<String>,
35 pub separators: Option<(String, String)>,
39 pub sort_keys: bool,
40}
41
42pub(crate) fn environment<'a>() -> Environment<'a> {
44 let mut env = Environment::new();
45 env.set_trim_blocks(true);
46 env.set_lstrip_blocks(true);
47 env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback);
49 env.add_filter("tojson", tojson_filter);
50 env.set_formatter(|out, state, value| {
59 if needs_py_str(value) {
60 write!(out, "{}", py_str(value))
61 .map_err(|_| Error::new(ErrorKind::WriteFailure, "formatter write failed"))
62 } else {
63 minijinja::escape_formatter(out, state, value)
64 }
65 });
66 env.add_filter("string", |value: Value| -> Value {
67 if value.kind() == ValueKind::String {
68 value
69 } else if needs_py_str(&value) {
70 Value::from(py_str(&value))
71 } else {
72 Value::from(value.to_string())
73 }
74 });
75 env.add_function("raise_exception", |msg: String| -> Result<Value, Error> {
79 Err(Error::new(ErrorKind::InvalidOperation, msg))
80 });
81 env.add_function("visible_text", |v: Value| -> String {
86 if let Some(s) = v.as_str() {
87 return s.to_string();
88 }
89 if let Ok(iter) = v.try_iter() {
90 let mut out = Vec::new();
91 for item in iter {
92 if let Some(s) = item.as_str() {
93 out.push(s.to_string());
94 } else if let Ok(t) = item.get_attr("text") {
95 if let Some(s) = t.as_str() {
96 out.push(s.to_string());
97 }
98 }
99 }
100 return out.join("\n");
101 }
102 String::new()
103 });
104 env
105}
106
107fn tojson_filter(value: &Value, args: Rest<Value>) -> Result<Value, Error> {
113 let mut positional: Vec<Value> = args.0;
115 let kwargs = match positional.last() {
116 Some(last) if last.is_kwargs() => Kwargs::try_from(positional.pop().unwrap())?,
117 _ => Kwargs::from_iter(std::iter::empty::<(String, Value)>()),
118 };
119 if positional.len() > 4 {
120 return Err(Error::new(
121 ErrorKind::TooManyArguments,
122 "tojson() takes at most 4 positional arguments",
123 ));
124 }
125 let mut positional = positional.into_iter();
126 let ensure_ascii = positional.next();
127 let indent = positional.next();
128 let separators = positional.next();
129 let sort_keys = positional.next();
130 let pick = |pos: Option<Value>, name: &str| -> Result<Option<Value>, Error> {
131 let kw: Option<Value> = kwargs.get(name)?;
132 match (pos, kw) {
133 (Some(_), Some(_)) => Err(Error::new(
134 ErrorKind::InvalidOperation,
135 format!("tojson() got multiple values for argument '{name}'"),
136 )),
137 (p, k) => Ok(p.or(k).filter(|v| !v.is_none() && !v.is_undefined())),
138 }
139 };
140 let ensure_ascii = pick(ensure_ascii, "ensure_ascii")?;
141 let indent = pick(indent, "indent")?;
142 let separators = pick(separators, "separators")?;
143 let sort_keys = pick(sort_keys, "sort_keys")?;
144 kwargs.assert_all_used()?;
145
146 let indent = match indent {
147 None => None,
148 Some(v) if v.kind() == ValueKind::String => Some(v.as_str().unwrap_or("").to_string()),
149 Some(v) if v.kind() == ValueKind::Bool => Some(" ".repeat(v.is_true() as usize)),
150 Some(v) => {
151 let n = v.as_i64().ok_or_else(|| {
152 Error::new(
153 ErrorKind::InvalidOperation,
154 format!("tojson(): indent must be an int or a string, got {v}"),
155 )
156 })?;
157 Some(" ".repeat(n.max(0) as usize))
158 }
159 };
160 let separators = match separators {
161 None => None,
162 Some(v) => {
163 let items: Vec<Value> = v.try_iter().map(|it| it.collect()).map_err(|_| {
164 Error::new(
165 ErrorKind::InvalidOperation,
166 "tojson(): separators must be a (item, key) pair",
167 )
168 })?;
169 match items.as_slice() {
170 [a, b] if a.as_str().is_some() && b.as_str().is_some() => Some((
171 a.as_str().unwrap().to_string(),
172 b.as_str().unwrap().to_string(),
173 )),
174 _ => {
175 return Err(Error::new(
176 ErrorKind::InvalidOperation,
177 "tojson(): separators must be a pair of strings",
178 ));
179 }
180 }
181 }
182 };
183 let opts = DumpsOptions {
184 ensure_ascii: ensure_ascii.is_some_and(|v| v.is_true()),
185 indent,
186 separators,
187 sort_keys: sort_keys.is_some_and(|v| v.is_true()),
188 };
189 py_json_dumps(value, &opts).map(Value::from_safe_string)
192}
193
194fn needs_py_str(v: &Value) -> bool {
196 match v.kind() {
197 ValueKind::None | ValueKind::Bool | ValueKind::Seq | ValueKind::Map => true,
198 ValueKind::Number => !v.is_integer(),
199 _ => false,
200 }
201}
202
203pub fn py_str(v: &Value) -> String {
205 match v.kind() {
206 ValueKind::String => v.as_str().unwrap_or("").to_string(),
207 ValueKind::Undefined => String::new(),
208 _ => py_repr(v),
209 }
210}
211
212pub fn py_repr(v: &Value) -> String {
214 match v.kind() {
215 ValueKind::Undefined | ValueKind::None => "None".into(),
216 ValueKind::Bool => (if v.is_true() { "True" } else { "False" }).into(),
217 ValueKind::Number if v.is_integer() => v.to_string(),
218 ValueKind::Number => {
219 let f = f64::try_from(v.clone()).unwrap_or(f64::NAN);
220 if f.is_nan() {
221 "nan".into()
222 } else if f.is_infinite() {
223 (if f > 0.0 { "inf" } else { "-inf" }).into()
224 } else {
225 py_float_repr(f)
226 }
227 }
228 ValueKind::String => py_str_repr(v.as_str().unwrap_or("")),
229 ValueKind::Seq => {
230 let items: Vec<String> = v
231 .try_iter()
232 .map(|it| it.map(|x| py_repr(&x)).collect())
233 .unwrap_or_default();
234 format!("[{}]", items.join(", "))
235 }
236 ValueKind::Map => {
237 let mut items = Vec::new();
238 if let Ok(keys) = v.try_iter() {
239 for k in keys {
240 let item = v.get_item(&k).unwrap_or(Value::UNDEFINED);
241 items.push(format!("{}: {}", py_repr(&k), py_repr(&item)));
242 }
243 }
244 format!("{{{}}}", items.join(", "))
245 }
246 _ => v.to_string(),
247 }
248}
249
250fn py_str_repr(s: &str) -> String {
254 let quote = if s.contains('\'') && !s.contains('"') {
255 '"'
256 } else {
257 '\''
258 };
259 let mut out = String::with_capacity(s.len() + 2);
260 out.push(quote);
261 for c in s.chars() {
262 match c {
263 '\\' => out.push_str("\\\\"),
264 '\n' => out.push_str("\\n"),
265 '\r' => out.push_str("\\r"),
266 '\t' => out.push_str("\\t"),
267 c if c == quote => {
268 out.push('\\');
269 out.push(c);
270 }
271 c if (c as u32) < 0x20 || (0x7f..0xa0).contains(&(c as u32)) => {
272 out.push_str(&format!("\\x{:02x}", c as u32));
273 }
274 c => out.push(c),
275 }
276 }
277 out.push(quote);
278 out
279}
280
281pub fn py_json_dumps(value: &Value, opts: &DumpsOptions) -> Result<String, Error> {
283 let (item_sep, key_sep) = match &opts.separators {
284 Some((i, k)) => (i.as_str(), k.as_str()),
285 None if opts.indent.is_some() => (",", ": "),
286 None => (", ", ": "),
287 };
288 let mut out = String::new();
289 let mut d = Dumper {
290 opts,
291 item_sep,
292 key_sep,
293 out: &mut out,
294 };
295 d.value(value, 0)?;
296 Ok(out)
297}
298
299struct Dumper<'o> {
300 opts: &'o DumpsOptions,
301 item_sep: &'o str,
302 key_sep: &'o str,
303 out: &'o mut String,
304}
305
306impl Dumper<'_> {
307 fn newline(&mut self, level: usize) {
308 if let Some(ind) = &self.opts.indent {
309 self.out.push('\n');
310 for _ in 0..level {
311 self.out.push_str(ind);
312 }
313 }
314 }
315
316 fn value(&mut self, v: &Value, level: usize) -> Result<(), Error> {
317 match v.kind() {
318 ValueKind::Undefined | ValueKind::None => self.out.push_str("null"),
322 ValueKind::Bool => self
323 .out
324 .push_str(if v.is_true() { "true" } else { "false" }),
325 ValueKind::Number => {
326 if v.is_integer() {
327 self.out.push_str(&v.to_string());
328 } else {
329 let f = f64::try_from(v.clone()).map_err(|_| {
330 Error::new(ErrorKind::InvalidOperation, "tojson(): bad number")
331 })?;
332 self.out.push_str(&py_float_repr(f));
333 }
334 }
335 ValueKind::String => self.string(v.as_str().unwrap_or("")),
336 ValueKind::Seq | ValueKind::Iterable => {
337 let items: Vec<Value> = v.try_iter()?.collect();
338 if items.is_empty() {
339 self.out.push_str("[]");
340 return Ok(());
341 }
342 self.out.push('[');
343 for (i, item) in items.iter().enumerate() {
344 if i > 0 {
345 self.out.push_str(self.item_sep);
346 }
347 self.newline(level + 1);
348 self.value(item, level + 1)?;
349 }
350 self.newline(level);
351 self.out.push(']');
352 }
353 ValueKind::Map | ValueKind::Plain => {
354 let mut entries: Vec<(String, Value)> = Vec::new();
355 for key in v.try_iter()? {
356 let item = v.get_item(&key)?;
357 entries.push((self.key_text(&key)?, item));
358 }
359 if self.opts.sort_keys {
360 entries.sort_by(|a, b| a.0.cmp(&b.0));
361 }
362 if entries.is_empty() {
363 self.out.push_str("{}");
364 return Ok(());
365 }
366 self.out.push('{');
367 for (i, (k, item)) in entries.iter().enumerate() {
368 if i > 0 {
369 self.out.push_str(self.item_sep);
370 }
371 self.newline(level + 1);
372 self.string(k);
373 self.out.push_str(self.key_sep);
374 self.value(item, level + 1)?;
375 }
376 self.newline(level);
377 self.out.push('}');
378 }
379 other => {
380 return Err(Error::new(
381 ErrorKind::InvalidOperation,
382 format!("tojson(): object of type {other} is not JSON serializable"),
383 ));
384 }
385 }
386 Ok(())
387 }
388
389 fn key_text(&self, k: &Value) -> Result<String, Error> {
392 Ok(match k.kind() {
393 ValueKind::String => k.as_str().unwrap_or("").to_string(),
394 ValueKind::None | ValueKind::Undefined => "null".into(),
395 ValueKind::Bool => (if k.is_true() { "true" } else { "false" }).into(),
396 ValueKind::Number if k.is_integer() => k.to_string(),
397 ValueKind::Number => py_float_repr(f64::try_from(k.clone()).unwrap_or(f64::NAN)),
398 other => {
399 return Err(Error::new(
400 ErrorKind::InvalidOperation,
401 format!("tojson(): keys must be str, int, float, bool or None, not {other}"),
402 ));
403 }
404 })
405 }
406
407 fn string(&mut self, s: &str) {
408 self.out.push('"');
409 for c in s.chars() {
410 match c {
411 '"' => self.out.push_str("\\\""),
412 '\\' => self.out.push_str("\\\\"),
413 '\n' => self.out.push_str("\\n"),
414 '\r' => self.out.push_str("\\r"),
415 '\t' => self.out.push_str("\\t"),
416 '\u{08}' => self.out.push_str("\\b"),
417 '\u{0c}' => self.out.push_str("\\f"),
418 c if (c as u32) < 0x20 => {
419 self.out.push_str(&format!("\\u{:04x}", c as u32));
420 }
421 c if self.opts.ensure_ascii && !(' '..='~').contains(&c) => {
425 let mut buf = [0u16; 2];
426 for unit in c.encode_utf16(&mut buf) {
427 self.out.push_str(&format!("\\u{:04x}", unit));
428 }
429 }
430 c => self.out.push(c),
431 }
432 }
433 self.out.push('"');
434 }
435}
436
437pub fn py_float_repr(f: f64) -> String {
442 if f.is_nan() {
443 return "NaN".into();
444 }
445 if f.is_infinite() {
446 return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
447 }
448 if f == 0.0 {
449 return if f.is_sign_negative() { "-0.0" } else { "0.0" }.into();
450 }
451 let e = format!("{:e}", f.abs());
453 let (mant, exp) = e.split_once('e').expect("LowerExp has an exponent");
454 let exp: i32 = exp.parse().expect("LowerExp exponent");
455 let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
456 let decpt = exp + 1; let mut s = String::new();
458 if f < 0.0 {
459 s.push('-');
460 }
461 if -4 < decpt && decpt <= 16 {
462 let n = digits.len() as i32;
463 if decpt <= 0 {
464 s.push_str("0.");
465 for _ in 0..(-decpt) {
466 s.push('0');
467 }
468 s.push_str(&digits);
469 } else if decpt >= n {
470 s.push_str(&digits);
471 for _ in 0..(decpt - n) {
472 s.push('0');
473 }
474 s.push_str(".0");
475 } else {
476 s.push_str(&digits[..decpt as usize]);
477 s.push('.');
478 s.push_str(&digits[decpt as usize..]);
479 }
480 } else {
481 s.push_str(&digits[..1]);
482 if digits.len() > 1 {
483 s.push('.');
484 s.push_str(&digits[1..]);
485 }
486 s.push('e');
487 s.push(if exp < 0 { '-' } else { '+' });
488 s.push_str(&format!("{:02}", exp.abs()));
489 }
490 s
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 fn render(tpl: &str, ctx: serde_json::Value) -> Result<String, Error> {
498 let mut env = environment();
499 env.add_template("t", tpl)?;
500 env.get_template("t")?.render(Value::from_serialize(&ctx))
501 }
502
503 fn tool() -> serde_json::Value {
504 serde_json::json!({
506 "type": "function",
507 "function": {
508 "name": "get_weather",
509 "description": "Погода <city> & \"quotes\" 🌧",
510 "parameters": {
511 "type": "object",
512 "properties": {"unit": {"type": "string"}, "city": {"type": "string"}},
513 "required": ["city"]
514 }
515 }
516 })
517 }
518
519 #[test]
523 fn default_matches_python_json_dumps() {
524 let got = render("{{ t | tojson }}", serde_json::json!({"t": tool()})).unwrap();
525 assert_eq!(
526 got,
527 r#"{"type": "function", "function": {"name": "get_weather", "description": "Погода <city> & \"quotes\" 🌧", "parameters": {"type": "object", "properties": {"unit": {"type": "string"}, "city": {"type": "string"}}, "required": ["city"]}}}"#
528 );
529 let explicit = render(
531 "{{ t | tojson(ensure_ascii=False) }}",
532 serde_json::json!({"t": tool()}),
533 )
534 .unwrap();
535 assert_eq!(explicit, got);
536 }
537
538 #[test]
539 fn ensure_ascii_escapes_like_python() {
540 let got = render(
541 "{{ s | tojson(ensure_ascii=True) }}",
542 serde_json::json!({"s": "é\u{7f}🌧\u{1}\t'"}),
543 )
544 .unwrap();
545 assert_eq!(got, r#""\u00e9\u007f\ud83c\udf27\u0001\t'""#);
547 }
548
549 #[test]
550 fn sort_keys_indent_and_separators() {
551 let v = serde_json::json!({"b": [1, 2], "a": {}, "c": []});
552 let ctx = serde_json::json!({"v": v});
553 assert_eq!(
554 render("{{ v | tojson(sort_keys=True) }}", ctx.clone()).unwrap(),
555 r#"{"a": {}, "b": [1, 2], "c": []}"#
556 );
557 assert_eq!(
559 render("{{ v | tojson(indent=2) }}", ctx.clone()).unwrap(),
560 "{\n \"b\": [\n 1,\n 2\n ],\n \"a\": {},\n \"c\": []\n}"
561 );
562 assert_eq!(
563 render(
564 "{{ v | tojson(indent='\\t', sort_keys=true) }}",
565 ctx.clone()
566 )
567 .unwrap(),
568 "{\n\t\"a\": {},\n\t\"b\": [\n\t\t1,\n\t\t2\n\t],\n\t\"c\": []\n}"
569 );
570 assert_eq!(
571 render("{{ v | tojson(separators=(',', ':')) }}", ctx.clone()).unwrap(),
572 r#"{"b":[1,2],"a":{},"c":[]}"#
573 );
574 assert_eq!(
576 render("{{ [1] | tojson(indent=0) }}", ctx.clone()).unwrap(),
577 "[\n1\n]"
578 );
579 assert_eq!(
582 render("{{ 'é' | tojson(true) }}", ctx.clone()).unwrap(),
583 r#""\u00e9""#
584 );
585 }
586
587 #[test]
588 fn unknown_keyword_is_an_error() {
589 assert!(render("{{ 1 | tojson(bogus=1) }}", serde_json::json!({})).is_err());
590 }
591
592 #[test]
593 fn scalars_follow_python() {
594 let got = render(
595 "{{ v | tojson }}",
596 serde_json::json!({"v": [1, -7, 1.5, 18.0, 1e16, 1.0e-5, 0.0001, 123456789012345678u64, true, null]}),
597 )
598 .unwrap();
599 assert_eq!(
601 got,
602 "[1, -7, 1.5, 18.0, 1e+16, 1e-05, 0.0001, 123456789012345678, true, null]"
603 );
604 }
605
606 #[test]
607 fn float_repr_table() {
608 for (f, want) in [
609 (0.1, "0.1"),
610 (1.0 / 3.0, "0.3333333333333333"),
611 (1234567890123456.0, "1234567890123456.0"),
612 (12345678901234567.0, "1.2345678901234568e+16"),
613 (-2.5e-7, "-2.5e-07"),
614 (1e100, "1e+100"),
615 (-0.0, "-0.0"),
616 (f64::INFINITY, "Infinity"),
617 ] {
618 assert_eq!(py_float_repr(f), want, "{f:e}");
619 }
620 }
621
622 #[test]
626 fn dict_items_keep_insertion_order() {
627 let got = render(
628 "{% for k, v in d.items() %}{{ k }}={{ v }};{% endfor %}",
629 serde_json::json!({"d": {"zeta": 1, "alpha": 2, "mid": 3}}),
630 )
631 .unwrap();
632 assert_eq!(got, "zeta=1;alpha=2;mid=3;");
633 }
634
635 #[test]
638 fn printing_follows_python_str() {
639 let ctx = serde_json::json!({
640 "b": true, "n": null, "f": 1.5e-7, "i": 42, "s": "plain",
641 "l": ["a", "it's", 2, false, null], "d": {"k": true, "z": [1.0]}
642 });
643 let got = render(
644 "{{ b }}|{{ n }}|{{ f }}|{{ i }}|{{ s }}|{{ l }}|{{ d }}|{{ b | string }}|{{ 'x' ~ i }}",
645 ctx,
646 )
647 .unwrap();
648 assert_eq!(
649 got,
650 r#"True|None|1.5e-07|42|plain|['a', "it's", 2, False, None]|{'k': True, 'z': [1.0]}|True|x42"#
651 );
652 assert_eq!(py_str_repr("a\nb'\"\\"), r#"'a\nb\'"\\'"#);
654 }
655
656 #[test]
657 fn raise_exception_carries_the_template_message() {
658 let err = render(
659 "{{ raise_exception('roles must alternate') }}",
660 serde_json::json!({}),
661 )
662 .unwrap_err();
663 assert!(
664 format!("{err:#}").contains("roles must alternate"),
665 "{err:#}"
666 );
667 }
668}