From 3a893489c22208d58ab763c0c296f323c84135c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Fri, 24 Nov 2023 12:07:15 +0100 Subject: [PATCH] Update tojson from pkgdepends --- R/advanced_search.R | 2 +- R/api.R | 5 +- R/tojson.R | 126 ++++++++--- tests/testthat/_snaps/tojson.md | 384 ++++++++++++++++++++++++++++---- tests/testthat/test-tojson.R | 192 +++++++++++++++- 5 files changed, 633 insertions(+), 76 deletions(-) diff --git a/R/advanced_search.R b/R/advanced_search.R index e99340e..1a9b007 100644 --- a/R/advanced_search.R +++ b/R/advanced_search.R @@ -86,7 +86,7 @@ advanced_search <- function(..., json = NULL, format = c("short", "long"), default_field = "*" ) ) - )) + ), opts = list(auto_unbox = TRUE, pretty = TRUE)) } else { qstr <- json diff --git a/R/api.R b/R/api.R index a56f1de..9d8e7c8 100644 --- a/R/api.R +++ b/R/api.R @@ -177,7 +177,10 @@ make_query <- function(query) { ) ) - tojson$write_str(query_object) + tojson$write_str( + query_object, + opts = list(auto_unbox = TRUE, pretty = TRUE) + ) } do_query <- function(query, server, port, from, size) { diff --git a/R/tojson.R b/R/tojson.R index 2ed842e..9306e52 100644 --- a/R/tojson.R +++ b/R/tojson.R @@ -3,11 +3,21 @@ tojson <- local({ mapply(fn, x, y, ..., SIMPLIFY = FALSE) } + filter <- function(v, fn) { + keep <- vapply(v, fn, logical(1)) + v[keep] + } + + # FIXME: is this escaping the right things? jq <- function(x) { encodeString(x, quote = "\"", justify = "none") } - comma <- function(x, key = NULL) { + # 1. add a "key": at the begining of each element, unless is.null(key) + # 2. add a comma after each elelemt, except the last one + # Each element can be a character vector, so `key` is added to the first + # element of the character vectors, and comma to the last ones. + comma <- function(x, opts, key = NULL) { len <- length(x) stopifnot(len >= 1) @@ -15,40 +25,79 @@ tojson <- local({ nokey <- is.na(key) | key == "" key[nokey] <- seq_along(x)[nokey] x <- map2(jq(key), x, function(k, el) { - el[1] <- paste0(k, ": ", el[1]) + el[1] <- paste0(k, if (opts$pretty) ": " else ":", el[1]) el }) } - # No commans needed for scalars - if (len == 1) return(x) + # No commas needed for scalars + if (len == 1) { + return(x) + } x2 <- lapply(x, function(el) { el[length(el)] <- paste0(el[length(el)], ",") el }) + + # Keep the last list element as is x2[[len]] <- x[[len]] x2 } - j_null <- function(x) { + j_null <- function(x, opts) { "{}" } - j_list <- function(x) { + # Data frames are done row-wise. + # Atomic columns are unboxed. Atomic NA values are omitted. + # List columns remove the extra wrapping list. + j_df <- function(x, opts) { + sub <- unlist(comma( + lapply(seq_len(nrow(x)), function(i) { + row <- as.list(x[i, ]) + row <- filter(row, function(v) !(is.atomic(v) && is.na(v))) + row[] <- lapply(row, function(v) { + if (is.atomic(v)) unbox(v) else if (is.list(v)) v[[1]] else v + }) + j_list(row, opts) + }) + )) + if (opts$pretty) { + c("[", paste0(" ", sub), "]") + } else { + paste0(c("[", sub, "]"), collapse = "") + } + } + + # Returns a character vector. Named lists are dictionaries, unnnamed + # ones are lists. Missing dictionary keys are filled in. + # Keys do _NOT_ need to be unique. + j_list <- function(x, opts) { if (length(x) == 0L) { if (is.null(names(x))) "[]" else "{}" - } else if (is.null(names(x))) { - c("[", paste0(" ", unlist(comma(lapply(x, j)))), "]") - + sub <- unlist(comma(lapply(x, j, opts), opts)) + if (opts$pretty) { + c("[", paste0(" ", sub), "]") + } else { + paste(c("[", sub, "]"), collapse = "") + } } else { - c("{", paste0(" ", unlist(comma(lapply(x, j), names(x)))), "}") + sub <- unlist(comma(lapply(x, j, opts), opts, names(x))) + if (opts$pretty) { + c("{", paste0(" ", sub), "}") + } else { + paste(c("{", sub, "}"), collapse = "") + } } } - j_atomic <- function(x) { - if (! typeof(x) %in% c("logical", "integer", "double", "character")) { + # Atomic vectors are converted to lists, even if they have names. + # The names are lost. Pretty formatting keeps a vector in one line + # currently. NA is converted to null. + j_atomic <- function(x, opts) { + if (!typeof(x) %in% c("logical", "integer", "double", "character")) { stop("Cannot convert atomic ", typeof(x), " vectors to JSON.") } len <- length(x) @@ -57,51 +106,72 @@ tojson <- local({ return("[]") } + unbox <- (opts$auto_unbox && len == 1) || "unbox" %in% class(x) + if (is.character(x)) { x <- jq(enc2utf8(x)) } if (is.logical(x)) { + # tolower() keeps NAs, we'll sub them later x <- tolower(x) } - if (len == 1L) { + if (unbox) { if (is.na(x) || x == "NA") "null" else paste0(x) - } else { x[is.na(x) | x == "NA"] <- "null" - paste0("[", paste(comma(x), collapse = " "), "]") + sep <- if (opts$pretty) " " else "" + paste0("[", paste(comma(x), collapse = sep), "]") } -} + } - j <- function(x) { + j <- function(x, opts) { if (is.null(x)) { - j_null(x) + j_null(x, opts) + } else if (is.data.frame(x)) { + j_df(x, opts) } else if (is.list(x)) { - j_list(x) + j_list(x, opts) } else if (is.atomic(x)) { - j_atomic(x) + j_atomic(x, opts) } else { stop("Cannot convert type ", typeof(x), " to JSON.") } -} + } - write_str <- function(x) { - paste0(j(x), collapse = "\n") + write_str <- function(x, opts = NULL) { + paste0(write_lines(x, opts), collapse = "\n") } - write_file <- function(x, file) { - writeLines(j(x), file) + write_file <- function(x, file, opts = NULL) { + writeLines(write_lines(x, opts), file) } - write_lines <- function(x) { - j(x) + write_lines <- function(x, opts = NULL) { + opts <- list( + auto_unbox = opts$auto_unbox %||% FALSE, + pretty = opts$pretty %||% FALSE + ) + j(x, opts) + } + + unbox <- function(x) { + if (!is.atomic(x)) { + stop("Can only unbox atomic scalar, not ", typeof(x), ".") + } + if (length(x) != 1) { + stop("Cannot unbox vector of length ", length(x), ".") + } + class(x) <- c("unbox", class(x)) + x } list( .envir = environment(), write_str = write_str, write_file = write_file, - write_lines = write_lines + write_lines = write_lines, + unbox = unbox ) }) diff --git a/tests/testthat/_snaps/tojson.md b/tests/testthat/_snaps/tojson.md index 428ed33..5f350ba 100644 --- a/tests/testthat/_snaps/tojson.md +++ b/tests/testthat/_snaps/tojson.md @@ -43,177 +43,471 @@ Code tojson$write_str(list(1)) Output - [1] "[\n 1\n]" + [1] "[[1]]" Code tojson$write_str(1L) Output - [1] "1" + [1] "[1]" Code tojson$write_str(1) Output - [1] "1" + [1] "[1]" Code tojson$write_str("foo") Output - [1] "\"foo\"" + [1] "[\"foo\"]" Code tojson$write_str(TRUE) Output - [1] "true" + [1] "[true]" --- Code tojson$write_str(list(1, 2)) Output - [1] "[\n 1,\n 2\n]" + [1] "[[1],[2]]" Code tojson$write_str(1:2) Output - [1] "[1, 2]" + [1] "[1,2]" Code tojson$write_str(c(1, 2)) Output - [1] "[1, 2]" + [1] "[1,2]" Code tojson$write_str(c("foo", "bar")) Output - [1] "[\"foo\", \"bar\"]" + [1] "[\"foo\",\"bar\"]" Code tojson$write_str(c(TRUE, FALSE)) Output - [1] "[true, false]" + [1] "[true,false]" --- Code tojson$write_str(NA_integer_) Output - [1] "null" + [1] "[null]" Code tojson$write_str(NA_real_) Output - [1] "null" + [1] "[null]" Code tojson$write_str(NA_character_) Output - [1] "null" + [1] "[null]" Code tojson$write_str(NA) Output - [1] "null" + [1] "[null]" --- Code tojson$write_str(c(1L, NA_integer_)) Output - [1] "[1, null]" + [1] "[1,null]" Code tojson$write_str(c(1, NA_real_)) Output - [1] "[1, null]" + [1] "[1,null]" Code tojson$write_str(c("foo", NA_character_)) Output - [1] "[\"foo\", null]" + [1] "[\"foo\",null]" Code tojson$write_str(c(TRUE, NA)) Output - [1] "[true, null]" + [1] "[true,null]" --- Code tojson$write_str(c(a = 1L, b = 2L)) Output - [1] "[1, 2]" + [1] "[1,2]" Code tojson$write_str(c(a = 1, b = 2)) Output - [1] "[1, 2]" + [1] "[1,2]" Code tojson$write_str(c(a = "foo", b = "bar")) Output - [1] "[\"foo\", \"bar\"]" + [1] "[\"foo\",\"bar\"]" Code tojson$write_str(c(a = TRUE, b = FALSE)) Output - [1] "[true, false]" + [1] "[true,false]" # write_str character encoding and escaping Code cat(tojson$write_str("foo\"\\bar")) Output - "foo\"\\bar" + ["foo\"\\bar"] Code charToRaw(tojson$write_str(iconv(utf8, "UTF-8", "latin1"))) Output - [1] 22 47 c3 a1 62 6f 72 22 + [1] 5b 22 47 c3 a1 62 6f 72 22 5d # lists Code cat(tojson$write_str(list(list(1, 2, 3), list(4, 5, 6), list(7, 8, 9)))) + Output + [[[1],[2],[3]],[[4],[5],[6]],[[7],[8],[9]]] + +--- + + Code + cat(tojson$write_str(list(a = 1, b = 2))) + Output + {"a":[1],"b":[2]} + +--- + + Code + cat(tojson$write_str(list(a = list(a1 = 1, a2 = 2), b = list(b1 = 3, b2 = 4)))) + Output + {"a":{"a1":[1],"a2":[2]},"b":{"b1":[3],"b2":[4]}} + +--- + + Code + cat(tojson$write_str(list(a = list(1, a2 = 2), list(b1 = 3, 4)))) + Output + {"a":{"1":[1],"a2":[2]},"2":{"b1":[3],"2":[4]}} + +# auto-unbox + + Code + tojson$write_str(list(1), opts = list(auto_unbox = TRUE)) + Output + [1] "[1]" + Code + tojson$write_str(1L, opts = list(auto_unbox = TRUE)) + Output + [1] "1" + Code + tojson$write_str(1, opts = list(auto_unbox = TRUE)) + Output + [1] "1" + Code + tojson$write_str("foo", opts = list(auto_unbox = TRUE)) + Output + [1] "\"foo\"" + Code + tojson$write_str(TRUE, opts = list(auto_unbox = TRUE)) + Output + [1] "true" + +--- + + Code + tojson$write_str(NA_integer_, opts = list(auto_unbox = TRUE)) + Output + [1] "null" + Code + tojson$write_str(NA_real_, opts = list(auto_unbox = TRUE)) + Output + [1] "null" + Code + tojson$write_str(NA_character_, opts = list(auto_unbox = TRUE)) + Output + [1] "null" + Code + tojson$write_str(NA, opts = list(auto_unbox = TRUE)) + Output + [1] "null" + +--- + + Code + cat(tojson$write_str(list(list(1, 2, 3), list(4, 5, 6), list(7, 8, 9)), opts = list( + auto_unbox = TRUE))) + Output + [[1,2,3],[4,5,6],[7,8,9]] + +--- + + Code + cat(tojson$write_str(list(a = 1, b = 2), opts = list(auto_unbox = TRUE))) + Output + {"a":1,"b":2} + +--- + + Code + cat(tojson$write_str(list(a = list(a1 = 1, a2 = 2), b = list(b1 = 3, b2 = 4)), + opts = list(auto_unbox = TRUE))) + Output + {"a":{"a1":1,"a2":2},"b":{"b1":3,"b2":4}} + +--- + + Code + cat(tojson$write_str(list(list(1, 2:3, 4), list(4:5, 6, 7:8), list(9:10, 11, 12)), + opts = list(auto_unbox = TRUE))) + Output + [[1,[2,3],4],[[4,5],6,[7,8]],[[9,10],11,12]] + +--- + + Code + cat(tojson$write_str(list(a = 1:3, b = 4), opts = list(auto_unbox = TRUE))) + Output + {"a":[1,2,3],"b":4} + +--- + + Code + cat(tojson$write_str(list(a = list(a1 = 1, a2 = 2:3), b = list(b1 = 4:6, b2 = 7)), + opts = list(auto_unbox = TRUE))) + Output + {"a":{"a1":1,"a2":[2,3]},"b":{"b1":[4,5,6],"b2":7}} + +# unbox + + Code + tojson$write_str(list(tojson$unbox(1))) + Output + [1] "[1]" + Code + tojson$write_str(tojson$unbox(1L)) + Output + [1] "1" + Code + tojson$write_str(tojson$unbox(1)) + Output + [1] "1" + Code + tojson$write_str(tojson$unbox("foo")) + Output + [1] "\"foo\"" + Code + tojson$write_str(tojson$unbox(TRUE)) + Output + [1] "true" + +--- + + Code + tojson$write_str(tojson$unbox(NA_integer_)) + Output + [1] "null" + Code + tojson$write_str(tojson$unbox(NA_real_)) + Output + [1] "null" + Code + tojson$write_str(tojson$unbox(NA_character_)) + Output + [1] "null" + Code + tojson$write_str(tojson$unbox(NA)) + Output + [1] "null" + +--- + + Code + cat(tojson$write_str(list(list(1, tojson$unbox(2), 3), list(tojson$unbox(4), 5, + 6), list(7, 8, 9)))) + Output + [[[1],2,[3]],[4,[5],[6]],[[7],[8],[9]]] + +--- + + Code + cat(tojson$write_str(list(a = 1, b = tojson$unbox(2)))) + Output + {"a":[1],"b":2} + +--- + + Code + cat(tojson$write_str(list(a = list(a1 = tojson$unbox(1), a2 = 2), b = list(b1 = 3, + b2 = tojson$unbox(4))))) + Output + {"a":{"a1":1,"a2":[2]},"b":{"b1":[3],"b2":4}} + +--- + + Code + tojson$unbox(list()) + Condition + Error in `tojson$unbox()`: + ! Can only unbox atomic scalar, not list. + Code + tojson$unbox(1:2) + Condition + Error in `tojson$unbox()`: + ! Cannot unbox vector of length 2. + Code + tojson$unbox(double(2)) + Condition + Error in `tojson$unbox()`: + ! Cannot unbox vector of length 2. + Code + tojson$unbox(character(2)) + Condition + Error in `tojson$unbox()`: + ! Cannot unbox vector of length 2. + Code + tojson$unbox(logical(2)) + Condition + Error in `tojson$unbox()`: + ! Cannot unbox vector of length 2. + +# pretty + + Code + tojson$write_str(1:5, opts = list(pretty = TRUE)) + Output + [1] "[1, 2, 3, 4, 5]" + Code + tojson$write_str(1:5 / 2, opts = list(pretty = TRUE)) + Output + [1] "[0.5, 1, 1.5, 2, 2.5]" + Code + tojson$write_str(letters[1:5], opts = list(pretty = TRUE)) + Output + [1] "[\"a\", \"b\", \"c\", \"d\", \"e\"]" + Code + tojson$write_str(1:5 %% 2 == 0, opts = list(pretty = TRUE)) + Output + [1] "[false, true, false, true, false]" + +--- + + Code + cat(tojson$write_str(list(list(1, tojson$unbox(2), 3), list(tojson$unbox(4), 5, + 6), list(7, 8, 9)), opts = list(pretty = TRUE))) Output [ [ - 1, + [1], 2, - 3 + [3] ], [ 4, - 5, - 6 + [5], + [6] ], [ - 7, - 8, - 9 + [7], + [8], + [9] ] ] --- Code - cat(tojson$write_str(list(a = 1, b = 2))) + cat(tojson$write_str(list(a = 1, b = tojson$unbox(2)), opts = list(pretty = TRUE))) Output { - "a": 1, + "a": [1], "b": 2 } --- Code - cat(tojson$write_str(list(a = list(a1 = 1, a2 = 2), b = list(b1 = 3, b2 = 4)))) + cat(tojson$write_str(list(a = list(a1 = tojson$unbox(1), a2 = 2), b = list(b1 = 3, + b2 = tojson$unbox(4))), opts = list(pretty = TRUE))) Output { "a": { "a1": 1, - "a2": 2 + "a2": [2] }, "b": { - "b1": 3, + "b1": [3], "b2": 4 } } +# data frames + + Code + tojson$write_str(df) + Output + [1] "[{\"mpg\":21,\"cyl\":6,\"disp\":160},{\"mpg\":21,\"cyl\":6,\"disp\":160},{\"mpg\":22.8,\"cyl\":4,\"disp\":108}]" + Code + cat(tojson$write_str(df, opts = list(pretty = TRUE))) + Output + [ + { + "mpg": 21, + "cyl": 6, + "disp": 160 + }, + { + "mpg": 21, + "cyl": 6, + "disp": 160 + }, + { + "mpg": 22.8, + "cyl": 4, + "disp": 108 + } + ] + --- Code - cat(tojson$write_str(list(a = list(1, a2 = 2), list(b1 = 3, 4)))) + tojson$write_str(df) Output - { - "a": { - "1": 1, - "a2": 2 + [1] "[{\"cyl\":6,\"disp\":160},{\"mpg\":21,\"cyl\":6},{\"mpg\":22.8,\"cyl\":4,\"disp\":108}]" + Code + cat(tojson$write_str(df, opts = list(pretty = TRUE))) + Output + [ + { + "cyl": 6, + "disp": 160 }, - "2": { - "b1": 3, - "2": 4 + { + "mpg": 21, + "cyl": 6 + }, + { + "mpg": 22.8, + "cyl": 4, + "disp": 108 } - } + ] + +--- + + Code + tojson$write_str(df) + Output + [1] "[{\"cyl\":6,\"disp\":160,\"list\":[1,2]},{\"mpg\":21,\"cyl\":6,\"list\":[5]},{\"mpg\":22.8,\"cyl\":4,\"disp\":108,\"list\":[\"a\",\"b\",\"c\"]}]" + Code + cat(tojson$write_str(df, opts = list(pretty = TRUE))) + Output + [ + { + "cyl": 6, + "disp": 160, + "list": [1, 2] + }, + { + "mpg": 21, + "cyl": 6, + "list": [5] + }, + { + "mpg": 22.8, + "cyl": 4, + "disp": 108, + "list": ["a", "b", "c"] + } + ] diff --git a/tests/testthat/test-tojson.R b/tests/testthat/test-tojson.R index a483881..4cae657 100644 --- a/tests/testthat/test-tojson.R +++ b/tests/testthat/test-tojson.R @@ -100,4 +100,194 @@ test_that("write_file", { tojson$write_file(mtcars, tmp) lns <- tojson$write_lines(mtcars) expect_equal(readLines(tmp), lns) -}) \ No newline at end of file +}) + +test_that("auto-unbox", { + # scalars + expect_snapshot({ + tojson$write_str(list(1), opts = list(auto_unbox = TRUE)) + tojson$write_str(1L, opts = list(auto_unbox = TRUE)) + tojson$write_str(1.0, opts = list(auto_unbox = TRUE)) + tojson$write_str("foo", opts = list(auto_unbox = TRUE)) + tojson$write_str(TRUE, opts = list(auto_unbox = TRUE)) + }) + + # NA in vectors + expect_snapshot({ + tojson$write_str(NA_integer_, opts = list(auto_unbox = TRUE)) + tojson$write_str(NA_real_, opts = list(auto_unbox = TRUE)) + tojson$write_str(NA_character_, opts = list(auto_unbox = TRUE)) + tojson$write_str(NA, opts = list(auto_unbox = TRUE)) + }) + + # embedded lists + expect_snapshot({ + cat(tojson$write_str( + list(list(1, 2, 3), list(4, 5, 6), list(7, 8, 9)), + opts = list(auto_unbox = TRUE) + )) + }) + + # named lists + expect_snapshot({ + cat(tojson$write_str( + list(a = 1, b = 2), + opts = list(auto_unbox = TRUE) + )) + }) + + # nested named lists + expect_snapshot({ + cat(tojson$write_str( + list( + a = list(a1 = 1, a2 = 2), + b = list(b1 = 3, b2 = 4) + ), + opts = list(auto_unbox = TRUE) + )) + }) + + # mixing scalars and vectors + expect_snapshot({ + cat(tojson$write_str( + list(list(1, 2:3, 4), list(4:5, 6, 7:8), list(9:10, 11, 12)), + opts = list(auto_unbox = TRUE) + )) + }) + + expect_snapshot({ + cat(tojson$write_str( + list(a = 1:3, b = 4), + opts = list(auto_unbox = TRUE) + )) + }) + + expect_snapshot({ + cat(tojson$write_str( + list( + a = list(a1 = 1, a2 = 2:3), + b = list(b1 = 4:6, b2 = 7) + ), + opts = list(auto_unbox = TRUE) + )) + }) +}) + +test_that("unbox", { + # scalars + expect_snapshot({ + tojson$write_str(list(tojson$unbox(1))) + tojson$write_str(tojson$unbox(1L)) + tojson$write_str(tojson$unbox(1.0)) + tojson$write_str(tojson$unbox("foo")) + tojson$write_str(tojson$unbox(TRUE)) + }) + + # NA in vectors + expect_snapshot({ + tojson$write_str(tojson$unbox(NA_integer_)) + tojson$write_str(tojson$unbox(NA_real_)) + tojson$write_str(tojson$unbox(NA_character_)) + tojson$write_str(tojson$unbox(NA)) + }) + + # embedded lists + expect_snapshot({ + cat(tojson$write_str( + list( + list(1, tojson$unbox(2), 3), + list(tojson$unbox(4), 5, 6), + list(7, 8, 9) + ) + )) + }) + + # named lists + expect_snapshot({ + cat(tojson$write_str( + list(a = 1, b = tojson$unbox(2)) + )) + }) + + # nested named lists + expect_snapshot({ + cat(tojson$write_str( + list( + a = list(a1 = tojson$unbox(1), a2 = 2), + b = list(b1 = 3, b2 = tojson$unbox(4)) + ) + )) + }) + + expect_snapshot(error = TRUE, { + tojson$unbox(list()) + tojson$unbox(1:2) + tojson$unbox(double(2)) + tojson$unbox(character(2)) + tojson$unbox(logical(2)) + }) +}) + +test_that("pretty", { + # vectors + expect_snapshot({ + tojson$write_str(1:5, opts = list(pretty = TRUE)) + tojson$write_str(1:5/2, opts = list(pretty = TRUE)) + tojson$write_str(letters[1:5], opts = list(pretty = TRUE)) + tojson$write_str(1:5 %% 2 == 0, opts = list(pretty = TRUE)) + }) + + # lists + expect_snapshot({ + cat(tojson$write_str( + list( + list(1, tojson$unbox(2), 3), + list(tojson$unbox(4), 5, 6), + list(7, 8, 9) + ), + opts = list(pretty = TRUE) + )) + }) + + # named lists + expect_snapshot({ + cat(tojson$write_str( + list(a = 1, b = tojson$unbox(2)), + opts = list(pretty = TRUE) + )) + }) + + # nested named lists + expect_snapshot({ + cat(tojson$write_str( + list( + a = list(a1 = tojson$unbox(1), a2 = 2), + b = list(b1 = 3, b2 = tojson$unbox(4)) + ), + opts = list(pretty = TRUE) + )) + }) +}) + +test_that("data frames", { + # easy + df <- mtcars[1:3, 1:3] + expect_snapshot({ + tojson$write_str(df) + cat(tojson$write_str(df, opts = list(pretty = TRUE))) + }) + + # NAs + df[1, 1] <- df[2, 3] <- NA + expect_snapshot({ + tojson$write_str(df) + cat(tojson$write_str(df, opts = list(pretty = TRUE))) + }) + + # list columns + df$list <- I(list(1:2, 5, letters[1:3])) + expect_snapshot({ + tojson$write_str(df) + cat(tojson$write_str(df, opts = list(pretty = TRUE))) + }) +})