Line data Source code
1 : use postgres_client::Row;
2 : use postgres_client::types::{Kind, Type};
3 : use serde_json::{Map, Value};
4 :
5 : //
6 : // Convert json non-string types to strings, so that they can be passed to Postgres
7 : // as parameters.
8 : //
9 10 : pub(crate) fn json_to_pg_text(json: Vec<Value>) -> Vec<Option<String>> {
10 10 : json.iter().map(json_value_to_pg_text).collect()
11 10 : }
12 :
13 11 : fn json_value_to_pg_text(value: &Value) -> Option<String> {
14 11 : match value {
15 : // special care for nulls
16 1 : Value::Null => None,
17 :
18 : // convert to text with escaping
19 3 : v @ (Value::Bool(_) | Value::Number(_) | Value::Object(_)) => Some(v.to_string()),
20 :
21 : // avoid escaping here, as we pass this as a parameter
22 4 : Value::String(s) => Some(s.clone()),
23 :
24 : // special care for arrays
25 3 : Value::Array(_) => json_array_to_pg_array(value),
26 : }
27 11 : }
28 :
29 : //
30 : // Serialize a JSON array to a Postgres array. Contrary to the strings in the params
31 : // in the array we need to escape the strings. Postgres is okay with arrays of form
32 : // '{1,"2",3}'::int[], so we don't check that array holds values of the same type, leaving
33 : // it for Postgres to check.
34 : //
35 : // Example of the same escaping in node-postgres: packages/pg/lib/utils.js
36 : //
37 23 : fn json_array_to_pg_array(value: &Value) -> Option<String> {
38 23 : match value {
39 : // special care for nulls
40 2 : Value::Null => None,
41 :
42 : // convert to text with escaping
43 : // here string needs to be escaped, as it is part of the array
44 13 : v @ (Value::Bool(_) | Value::Number(_) | Value::String(_)) => Some(v.to_string()),
45 2 : v @ Value::Object(_) => json_array_to_pg_array(&Value::String(v.to_string())),
46 :
47 : // recurse into array
48 6 : Value::Array(arr) => {
49 6 : let vals = arr
50 6 : .iter()
51 6 : .map(json_array_to_pg_array)
52 18 : .map(|v| v.unwrap_or_else(|| "NULL".to_string()))
53 6 : .collect::<Vec<_>>()
54 6 : .join(",");
55 :
56 6 : Some(format!("{{{vals}}}"))
57 : }
58 : }
59 23 : }
60 :
61 : #[derive(Debug, thiserror::Error)]
62 : pub(crate) enum JsonConversionError {
63 : #[error("internal error compute returned invalid data: {0}")]
64 : AsTextError(postgres_client::Error),
65 : #[error("parse int error: {0}")]
66 : ParseIntError(#[from] std::num::ParseIntError),
67 : #[error("parse float error: {0}")]
68 : ParseFloatError(#[from] std::num::ParseFloatError),
69 : #[error("parse json error: {0}")]
70 : ParseJsonError(#[from] serde_json::Error),
71 : #[error("unbalanced array")]
72 : UnbalancedArray,
73 : #[error("unbalanced quoted string")]
74 : UnbalancedString,
75 : }
76 :
77 : enum OutputMode {
78 : Array(Vec<Value>),
79 : Object(Map<String, Value>),
80 : }
81 :
82 : impl OutputMode {
83 0 : fn key(&mut self, key: &str) -> &mut Value {
84 0 : match self {
85 0 : OutputMode::Array(values) => push_entry(values, Value::Null),
86 0 : OutputMode::Object(map) => map.entry(key.to_string()).or_insert(Value::Null),
87 : }
88 0 : }
89 :
90 0 : fn finish(self) -> Value {
91 0 : match self {
92 0 : OutputMode::Array(values) => Value::Array(values),
93 0 : OutputMode::Object(map) => Value::Object(map),
94 : }
95 0 : }
96 : }
97 :
98 90 : fn push_entry<T>(arr: &mut Vec<T>, t: T) -> &mut T {
99 90 : arr.push(t);
100 90 : arr.last_mut().expect("a value was just inserted")
101 90 : }
102 :
103 : //
104 : // Convert postgres row with text-encoded values to JSON object
105 : //
106 0 : pub(crate) fn pg_text_row_to_json(
107 0 : row: &Row,
108 0 : raw_output: bool,
109 0 : array_mode: bool,
110 0 : ) -> Result<Value, JsonConversionError> {
111 0 : let mut entries = if array_mode {
112 0 : OutputMode::Array(Vec::with_capacity(row.columns().len()))
113 : } else {
114 0 : OutputMode::Object(Map::with_capacity(row.columns().len()))
115 : };
116 :
117 0 : for (i, column) in row.columns().iter().enumerate() {
118 0 : let pg_value = row.as_text(i).map_err(JsonConversionError::AsTextError)?;
119 :
120 0 : let value = entries.key(column.name());
121 :
122 0 : match pg_value {
123 0 : Some(v) if raw_output => *value = Value::String(v.to_string()),
124 0 : Some(v) => pg_text_to_json(value, v, column.type_())?,
125 0 : None => *value = Value::Null,
126 : }
127 : }
128 :
129 0 : Ok(entries.finish())
130 0 : }
131 :
132 : //
133 : // Convert postgres text-encoded value to JSON value
134 : //
135 76 : fn pg_text_to_json(
136 76 : output: &mut Value,
137 76 : val: &str,
138 76 : pg_type: &Type,
139 76 : ) -> Result<(), JsonConversionError> {
140 76 : if let Kind::Array(elem_type) = pg_type.kind() {
141 : // todo: we should fetch this from postgres.
142 0 : let delimiter = ',';
143 :
144 0 : let mut array = vec![];
145 0 : pg_array_parse(&mut array, val, elem_type, delimiter)?;
146 0 : *output = Value::Array(array);
147 0 : return Ok(());
148 76 : }
149 :
150 76 : match *pg_type {
151 12 : Type::BOOL => *output = Value::Bool(val == "t"),
152 : Type::INT2 | Type::INT4 => {
153 14 : let val = val.parse::<i32>()?;
154 14 : *output = Value::Number(serde_json::Number::from(val));
155 : }
156 : Type::FLOAT4 | Type::FLOAT8 => {
157 23 : let fval = val.parse::<f64>()?;
158 23 : let num = serde_json::Number::from_f64(fval);
159 23 : if let Some(num) = num {
160 14 : *output = Value::Number(num);
161 14 : } else {
162 9 : // Pass Nan, Inf, -Inf as strings
163 9 : // JS JSON.stringify() does converts them to null, but we
164 9 : // want to preserve them, so we pass them as strings
165 9 : *output = Value::String(val.to_string());
166 9 : }
167 : }
168 7 : Type::JSON | Type::JSONB => *output = serde_json::from_str(val)?,
169 20 : _ => *output = Value::String(val.to_string()),
170 : }
171 :
172 76 : Ok(())
173 76 : }
174 :
175 : /// Parse postgres array into JSON array.
176 : ///
177 : /// This is a bit involved because we need to handle nested arrays and quoted
178 : /// values. Unlike postgres we don't check that all nested arrays have the same
179 : /// dimensions, we just return them as is.
180 : ///
181 : /// <https://www.postgresql.org/docs/current/arrays.html#ARRAYS-IO>
182 : ///
183 : /// The external text representation of an array value consists of items that are interpreted
184 : /// according to the I/O conversion rules for the array's element type, plus decoration that
185 : /// indicates the array structure. The decoration consists of curly braces (`{` and `}`) around
186 : /// the array value plus delimiter characters between adjacent items. The delimiter character
187 : /// is usually a comma (,) but can be something else: it is determined by the typdelim setting
188 : /// for the array's element type. Among the standard data types provided in the PostgreSQL
189 : /// distribution, all use a comma, except for type box, which uses a semicolon (;).
190 : ///
191 : /// In a multidimensional array, each dimension (row, plane, cube, etc.)
192 : /// gets its own level of curly braces, and delimiters must be written between adjacent
193 : /// curly-braced entities of the same level.
194 22 : fn pg_array_parse(
195 22 : elements: &mut Vec<Value>,
196 22 : mut pg_array: &str,
197 22 : elem: &Type,
198 22 : delim: char,
199 22 : ) -> Result<(), JsonConversionError> {
200 : // skip bounds decoration, eg:
201 : // `[1:1][-2:-1][3:5]={{{1,2,3},{4,5,6}}}`
202 : // technically these are significant, but we have no way to represent them in json.
203 22 : if let Some('[') = pg_array.chars().next() {
204 1 : let Some((_bounds, array)) = pg_array.split_once('=') else {
205 0 : return Err(JsonConversionError::UnbalancedArray);
206 : };
207 1 : pg_array = array;
208 21 : }
209 :
210 : // whitespace might preceed a `{`.
211 22 : let pg_array = pg_array.trim_start();
212 :
213 22 : let rest = pg_array_parse_inner(elements, pg_array, elem, delim)?;
214 22 : if !rest.is_empty() {
215 0 : return Err(JsonConversionError::UnbalancedArray);
216 22 : }
217 :
218 22 : Ok(())
219 22 : }
220 :
221 : /// reads a single array from the `pg_array` string and pushes each values to `elements`.
222 : /// returns the rest of the `pg_array` string that was not read.
223 39 : fn pg_array_parse_inner<'a>(
224 39 : elements: &mut Vec<Value>,
225 39 : mut pg_array: &'a str,
226 39 : elem: &Type,
227 39 : delim: char,
228 39 : ) -> Result<&'a str, JsonConversionError> {
229 : // array should have a `{` prefix.
230 39 : pg_array = pg_array
231 39 : .strip_prefix('{')
232 39 : .ok_or(JsonConversionError::UnbalancedArray)?;
233 :
234 39 : let mut q = String::new();
235 :
236 : loop {
237 90 : let value = push_entry(elements, Value::Null);
238 90 : pg_array = pg_array_parse_item(value, &mut q, pg_array, elem, delim)?;
239 :
240 : // check for separator.
241 90 : if let Some(next) = pg_array.strip_prefix(delim) {
242 51 : // next item.
243 51 : pg_array = next;
244 51 : } else {
245 39 : break;
246 : }
247 : }
248 :
249 39 : let Some(next) = pg_array.strip_prefix('}') else {
250 : // missing `}` terminator.
251 0 : return Err(JsonConversionError::UnbalancedArray);
252 : };
253 :
254 : // whitespace might follow a `}`.
255 39 : Ok(next.trim_start())
256 39 : }
257 :
258 : /// reads a single item from the `pg_array` string.
259 : /// returns the rest of the `pg_array` string that was not read.
260 : ///
261 : /// `quoted` is a scratch allocation that has no defined output.
262 90 : fn pg_array_parse_item<'a>(
263 90 : output: &mut Value,
264 90 : quoted: &mut String,
265 90 : mut pg_array: &'a str,
266 90 : elem: &Type,
267 90 : delim: char,
268 90 : ) -> Result<&'a str, JsonConversionError> {
269 : // We are trying to parse an array item.
270 : // This could be a new array, if this is a multi-dimentional array.
271 : // This could be a quoted string representing `elem`.
272 : // This could be an unquoted string representing `elem`.
273 :
274 : // whitespace might preceed an item.
275 90 : pg_array = pg_array.trim_start();
276 :
277 90 : if pg_array.starts_with('{') {
278 : // nested array.
279 17 : let mut nested = vec![];
280 17 : pg_array = pg_array_parse_inner(&mut nested, pg_array, elem, delim)?;
281 17 : *output = Value::Array(nested);
282 17 : return Ok(pg_array);
283 73 : }
284 :
285 73 : if let Some(mut pg_array) = pg_array.strip_prefix('"') {
286 : // the parsed string is un-escaped and written into quoted.
287 15 : pg_array = pg_array_parse_quoted(quoted, pg_array)?;
288 :
289 : // we have un-escaped the string, parse it as pgtext.
290 15 : pg_text_to_json(output, quoted, elem)?;
291 :
292 15 : return Ok(pg_array);
293 58 : }
294 :
295 : // we need to parse an item. read until we find a delimiter or `}`.
296 58 : let index = pg_array
297 58 : .find([delim, '}'])
298 58 : .ok_or(JsonConversionError::UnbalancedArray)?;
299 :
300 : let item;
301 58 : (item, pg_array) = pg_array.split_at(index);
302 :
303 : // item might have trailing whitespace that we need to ignore.
304 58 : let item = item.trim_end();
305 :
306 : // we might have an item string:
307 : // check for null
308 58 : if item == "NULL" {
309 7 : *output = Value::Null;
310 7 : } else {
311 51 : pg_text_to_json(output, item, elem)?;
312 : }
313 :
314 58 : Ok(pg_array)
315 90 : }
316 :
317 : /// reads a single quoted item from the `pg_array` string.
318 : ///
319 : /// Returns the rest of the `pg_array` string that was not read.
320 : /// The output is written into `quoted`.
321 : ///
322 : /// The pg_array string must have a `"` terminator, but the `"` initial value
323 : /// must have already been removed from the input. The terminator is removed.
324 15 : fn pg_array_parse_quoted<'a>(
325 15 : quoted: &mut String,
326 15 : mut pg_array: &'a str,
327 15 : ) -> Result<&'a str, JsonConversionError> {
328 : // The array output routine will put double quotes around element values if they are empty strings,
329 : // contain curly braces, delimiter characters, double quotes, backslashes, or white space,
330 : // or match the word `NULL`. Double quotes and backslashes embedded in element values will be backslash-escaped.
331 : // For numeric data types it is safe to assume that double quotes will never appear,
332 : // but for textual data types one should be prepared to cope with either the presence or absence of quotes.
333 :
334 15 : quoted.clear();
335 :
336 : // We write to quoted in chunks terminated by an escape character.
337 : // Eg if we have the input `foo\"bar"`, then we write `foo`, then `"`, then finally `bar`.
338 :
339 : loop {
340 : // we need to parse an chunk. read until we find a '\\' or `"`.
341 30 : let i = pg_array
342 30 : .find(['\\', '"'])
343 30 : .ok_or(JsonConversionError::UnbalancedString)?;
344 :
345 : let chunk: &str;
346 30 : (chunk, pg_array) = pg_array
347 30 : .split_at_checked(i)
348 30 : .expect("i is guaranteed to be in-bounds of pg_array");
349 :
350 : // push the chunk.
351 30 : quoted.push_str(chunk);
352 :
353 : // consume the chunk_end character.
354 : let chunk_end: char;
355 30 : (chunk_end, pg_array) =
356 30 : split_first_char(pg_array).expect("pg_array should start with either '\\\\' or '\"'");
357 :
358 : // finished.
359 30 : if chunk_end == '"' {
360 : // whitespace might follow the '"'.
361 15 : pg_array = pg_array.trim_start();
362 :
363 15 : break Ok(pg_array);
364 15 : }
365 :
366 : // consume the escaped character.
367 : let escaped: char;
368 15 : (escaped, pg_array) =
369 15 : split_first_char(pg_array).ok_or(JsonConversionError::UnbalancedString)?;
370 :
371 15 : quoted.push(escaped);
372 : }
373 15 : }
374 :
375 45 : fn split_first_char(s: &str) -> Option<(char, &str)> {
376 45 : let mut chars = s.chars();
377 45 : let c = chars.next()?;
378 45 : Some((c, chars.as_str()))
379 45 : }
380 :
381 : #[cfg(test)]
382 : mod tests {
383 : use serde_json::json;
384 :
385 : use super::*;
386 :
387 : #[test]
388 1 : fn test_atomic_types_to_pg_params() {
389 1 : let json = vec![Value::Bool(true), Value::Bool(false)];
390 1 : let pg_params = json_to_pg_text(json);
391 1 : assert_eq!(
392 : pg_params,
393 1 : vec![Some("true".to_owned()), Some("false".to_owned())]
394 : );
395 :
396 1 : let json = vec![Value::Number(serde_json::Number::from(42))];
397 1 : let pg_params = json_to_pg_text(json);
398 1 : assert_eq!(pg_params, vec![Some("42".to_owned())]);
399 :
400 1 : let json = vec![Value::String("foo\"".to_string())];
401 1 : let pg_params = json_to_pg_text(json);
402 1 : assert_eq!(pg_params, vec![Some("foo\"".to_owned())]);
403 :
404 1 : let json = vec![Value::Null];
405 1 : let pg_params = json_to_pg_text(json);
406 1 : assert_eq!(pg_params, vec![None]);
407 1 : }
408 :
409 : #[test]
410 1 : fn test_json_array_to_pg_array() {
411 : // atoms and escaping
412 1 : let json = "[true, false, null, \"NULL\", 42, \"foo\", \"bar\\\"-\\\\\"]";
413 1 : let json: Value = serde_json::from_str(json).unwrap();
414 1 : let pg_params = json_to_pg_text(vec![json]);
415 1 : assert_eq!(
416 : pg_params,
417 1 : vec![Some(
418 1 : "{true,false,NULL,\"NULL\",42,\"foo\",\"bar\\\"-\\\\\"}".to_owned()
419 1 : )]
420 : );
421 :
422 : // nested arrays
423 1 : let json = "[[true, false], [null, 42], [\"foo\", \"bar\\\"-\\\\\"]]";
424 1 : let json: Value = serde_json::from_str(json).unwrap();
425 1 : let pg_params = json_to_pg_text(vec![json]);
426 1 : assert_eq!(
427 : pg_params,
428 1 : vec![Some(
429 1 : "{{true,false},{NULL,42},{\"foo\",\"bar\\\"-\\\\\"}}".to_owned()
430 1 : )]
431 : );
432 : // array of objects
433 1 : let json = r#"[{"foo": 1},{"bar": 2}]"#;
434 1 : let json: Value = serde_json::from_str(json).unwrap();
435 1 : let pg_params = json_to_pg_text(vec![json]);
436 1 : assert_eq!(
437 : pg_params,
438 1 : vec![Some(r#"{"{\"foo\":1}","{\"bar\":2}"}"#.to_owned())]
439 : );
440 1 : }
441 :
442 10 : fn pg_text_to_json(val: &str, pg_type: &Type) -> Value {
443 10 : let mut v = Value::Null;
444 10 : super::pg_text_to_json(&mut v, val, pg_type).unwrap();
445 10 : v
446 10 : }
447 :
448 22 : fn pg_array_parse(pg_array: &str, pg_type: &Type) -> Value {
449 22 : let mut array = vec![];
450 22 : super::pg_array_parse(&mut array, pg_array, pg_type, ',').unwrap();
451 22 : Value::Array(array)
452 22 : }
453 :
454 : #[test]
455 1 : fn test_atomic_types_parse() {
456 1 : assert_eq!(pg_text_to_json("foo", &Type::TEXT), json!("foo"));
457 1 : assert_eq!(pg_text_to_json("42", &Type::INT4), json!(42));
458 1 : assert_eq!(pg_text_to_json("42", &Type::INT2), json!(42));
459 1 : assert_eq!(pg_text_to_json("42", &Type::INT8), json!("42"));
460 1 : assert_eq!(pg_text_to_json("42.42", &Type::FLOAT8), json!(42.42));
461 1 : assert_eq!(pg_text_to_json("42.42", &Type::FLOAT4), json!(42.42));
462 1 : assert_eq!(pg_text_to_json("NaN", &Type::FLOAT4), json!("NaN"));
463 1 : assert_eq!(
464 1 : pg_text_to_json("Infinity", &Type::FLOAT4),
465 1 : json!("Infinity")
466 : );
467 1 : assert_eq!(
468 1 : pg_text_to_json("-Infinity", &Type::FLOAT4),
469 1 : json!("-Infinity")
470 : );
471 :
472 1 : let json: Value =
473 1 : serde_json::from_str("{\"s\":\"str\",\"n\":42,\"f\":4.2,\"a\":[null,3,\"a\"]}")
474 1 : .unwrap();
475 1 : assert_eq!(
476 1 : pg_text_to_json(
477 1 : r#"{"s":"str","n":42,"f":4.2,"a":[null,3,"a"]}"#,
478 1 : &Type::JSONB
479 : ),
480 : json
481 : );
482 1 : }
483 :
484 : #[test]
485 1 : fn test_pg_array_parse_text() {
486 4 : fn pt(pg_arr: &str) -> Value {
487 4 : pg_array_parse(pg_arr, &Type::TEXT)
488 4 : }
489 1 : assert_eq!(
490 1 : pt(r#"{"aa\"\\\,a",cha,"bbbb"}"#),
491 1 : json!(["aa\"\\,a", "cha", "bbbb"])
492 : );
493 1 : assert_eq!(
494 1 : pt(r#"{{"foo","bar"},{"bee","bop"}}"#),
495 1 : json!([["foo", "bar"], ["bee", "bop"]])
496 : );
497 1 : assert_eq!(
498 1 : pt(r#"{{{{"foo",NULL,"bop",bup}}}}"#),
499 1 : json!([[[["foo", null, "bop", "bup"]]]])
500 : );
501 1 : assert_eq!(
502 1 : pt(r#"{{"1",2,3},{4,NULL,6},{NULL,NULL,NULL}}"#),
503 1 : json!([["1", "2", "3"], ["4", null, "6"], [null, null, null]])
504 : );
505 1 : }
506 :
507 : #[test]
508 1 : fn test_pg_array_parse_bool() {
509 4 : fn pb(pg_arr: &str) -> Value {
510 4 : pg_array_parse(pg_arr, &Type::BOOL)
511 4 : }
512 1 : assert_eq!(pb(r#"{t,f,t}"#), json!([true, false, true]));
513 1 : assert_eq!(pb(r#"{{t,f,t}}"#), json!([[true, false, true]]));
514 1 : assert_eq!(
515 1 : pb(r#"{{t,f},{f,t}}"#),
516 1 : json!([[true, false], [false, true]])
517 : );
518 1 : assert_eq!(
519 1 : pb(r#"{{t,NULL},{NULL,f}}"#),
520 1 : json!([[true, null], [null, false]])
521 : );
522 1 : }
523 :
524 : #[test]
525 1 : fn test_pg_array_parse_numbers() {
526 9 : fn pn(pg_arr: &str, ty: &Type) -> Value {
527 9 : pg_array_parse(pg_arr, ty)
528 9 : }
529 1 : assert_eq!(pn(r#"{1,2,3}"#, &Type::INT4), json!([1, 2, 3]));
530 1 : assert_eq!(pn(r#"{1,2,3}"#, &Type::INT2), json!([1, 2, 3]));
531 1 : assert_eq!(pn(r#"{1,2,3}"#, &Type::INT8), json!(["1", "2", "3"]));
532 1 : assert_eq!(pn(r#"{1,2,3}"#, &Type::FLOAT4), json!([1.0, 2.0, 3.0]));
533 1 : assert_eq!(pn(r#"{1,2,3}"#, &Type::FLOAT8), json!([1.0, 2.0, 3.0]));
534 1 : assert_eq!(
535 1 : pn(r#"{1.1,2.2,3.3}"#, &Type::FLOAT4),
536 1 : json!([1.1, 2.2, 3.3])
537 : );
538 1 : assert_eq!(
539 1 : pn(r#"{1.1,2.2,3.3}"#, &Type::FLOAT8),
540 1 : json!([1.1, 2.2, 3.3])
541 : );
542 1 : assert_eq!(
543 1 : pn(r#"{NaN,Infinity,-Infinity}"#, &Type::FLOAT4),
544 1 : json!(["NaN", "Infinity", "-Infinity"])
545 : );
546 1 : assert_eq!(
547 1 : pn(r#"{NaN,Infinity,-Infinity}"#, &Type::FLOAT8),
548 1 : json!(["NaN", "Infinity", "-Infinity"])
549 : );
550 1 : }
551 :
552 : #[test]
553 1 : fn test_pg_array_with_decoration() {
554 1 : fn p(pg_arr: &str) -> Value {
555 1 : pg_array_parse(pg_arr, &Type::INT2)
556 1 : }
557 1 : assert_eq!(
558 1 : p(r#"[1:1][-2:-1][3:5]={{{1,2,3},{4,5,6}}}"#),
559 1 : json!([[[1, 2, 3], [4, 5, 6]]])
560 : );
561 1 : }
562 :
563 : #[test]
564 1 : fn test_pg_array_parse_json() {
565 4 : fn pt(pg_arr: &str) -> Value {
566 4 : pg_array_parse(pg_arr, &Type::JSONB)
567 4 : }
568 1 : assert_eq!(pt(r#"{"{}"}"#), json!([{}]));
569 1 : assert_eq!(
570 1 : pt(r#"{"{\"foo\": 1, \"bar\": 2}"}"#),
571 1 : json!([{"foo": 1, "bar": 2}])
572 : );
573 1 : assert_eq!(
574 1 : pt(r#"{"{\"foo\": 1}", "{\"bar\": 2}"}"#),
575 1 : json!([{"foo": 1}, {"bar": 2}])
576 : );
577 1 : assert_eq!(
578 1 : pt(r#"{{"{\"foo\": 1}", "{\"bar\": 2}"}}"#),
579 1 : json!([[{"foo": 1}, {"bar": 2}]])
580 : );
581 1 : }
582 : }
|