-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
657 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,53 +1,55 @@ | ||
# rjsoncons 1.3.0 | ||
|
||
- (1.2.0.9401) internal C++ code cleanup and refactoring | ||
- (1.2.0.9300) add 'Examples' web-only vignette | ||
- (1.2.0.9201) restore progress bar on NDJSON parsing | ||
- (1.2.0.9500) add JSON patch support with `j_patch_apply()`, | ||
`j_patch_from()`. | ||
- (1.2.0.9401) internal C++ code cleanup and refactoring. | ||
- (1.2.0.9300) add 'Examples' web-only vignette. | ||
- (1.2.0.9201) restore progress bar on NDJSON parsing. | ||
- (1.2.0.9100) `as_r()` supports file and url connections; improved | ||
connection implementation using C++ stream buffer. | ||
- (1.2.0.9000) bug fix: support JSON `j_pivot()` file / url connections | ||
- (1.2.0.9000) bug fix: support JSON `j_pivot()` file / url connections. | ||
|
||
# rjsoncons 1.2.0 | ||
|
||
- (1.2.0) CRAN release | ||
- (1.2.0) CRAN release. | ||
- (1.1.0.9500) update documentation, include NDJSON-specific, web-only | ||
vignette | ||
- (1.1.0.9400) support NDJSON and file / url connections | ||
- (1.1.0.9300) implement `j_query()` (query without requiring path | ||
vignette. | ||
- (1.1.0.9400) support NDJSON and file / url connections. | ||
- (1.1.0.9300) implement `j_query()` (query without requiring path. | ||
specification), `j_pivot()`, and `j_path_type()`. Remove | ||
`jsonpivot()`. | ||
- (1.1.0.9200) implement `jsonpointer()` for querying JSON documents. | ||
- (1.1.0.9100) update jsoncons library to 173.2, relaxing compiler | ||
requirements to c++11. | ||
- (1.1.0.9000) implement `jsonpivot()` to transform JSON | ||
- (1.1.0.9000) implement `jsonpivot()` to transform JSON. | ||
array-of-objects to object-of-arrays, a common step before | ||
representation as a data.frame. | ||
|
||
# rjsoncons 1.1.0 | ||
|
||
- (1.1.0) CRAN release | ||
- (1.0.1.9100) using jsonlite (e.g., 'toJSON()' for parsing R objects) | ||
- (1.1.0) CRAN release. | ||
- (1.0.1.9100) using jsonlite (e.g., 'toJSON()' for parsing R objects). | ||
requires separate installation of jsonlite. | ||
- (1.0.1.9000) update jsoncons library to 0.172.1; addresses segfault | ||
on 'fedora' CRAN builder | ||
on 'fedora' CRAN builder. | ||
|
||
# rjsoncons 1.0.1 | ||
|
||
- (1.0.1) CRAN release | ||
- (1.0.0.9200) use pkgdown | ||
- (1.0.0.9100) parse JSON to R with `as = "R"` argument and `as_r()` | ||
- (1.0.1) CRAN release. | ||
- (1.0.0.9200) use pkgdown. | ||
- (1.0.0.9100) parse JSON to R with `as = "R"` argument and `as_r()`. | ||
|
||
# rjsoncons 1.0.0 | ||
|
||
- (1.0.0) initial CRAN release | ||
- (0.0.99) pre-release version | ||
- (0.0.3) support object names ordering 'asis' or 'sort' | ||
- (1.0.0) initial CRAN release. | ||
- (0.0.99) pre-release version. | ||
- (0.0.3) support object names ordering 'asis' or 'sort'. | ||
- (0.0.3) DESCRIPTION file updates: correct 'Title:' capitalization; | ||
avoid warnings about most misspellings | ||
avoid warnings about most misspellings. | ||
- (0.0.3) Add GitHub action to rebuild README.md from | ||
vignettes/rjsoncons.Rmd | ||
- (0.0.2) jsoncons library update | ||
- (0.0.2) support for R object query in addition to JSON string | ||
- (0.0.2) add unit tests | ||
- (0.0.2) R and minor C++ code refactoring | ||
- (0.0.1) initial C++ / R implementation of `jmespath()` / `jsonpath()` | ||
vignettes/rjsoncons.Rmd. | ||
- (0.0.2) jsoncons library update. | ||
- (0.0.2) support for R object query in addition to JSON string. | ||
- (0.0.2) add unit tests. | ||
- (0.0.2) R and minor C++ code refactoring. | ||
- (0.0.1) initial C++ / R implementation of `jmespath()` / `jsonpath()`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
J_PATCH_OP <- c("add", "remove", "replace", "copy", "move", "test") | ||
|
||
.is_j_patch_type <- | ||
function(x) | ||
{ | ||
identical(x[[1]], "json") || identical(x[[1]], "R") | ||
} | ||
.j_patch_data_from_connection <- | ||
function(data, data_type) | ||
{ | ||
con <- .as_unopened_connection(data, data_type) | ||
open(con, "rb") | ||
on.exit(close(con)) | ||
lines <- readLines(con, warn = FALSE) | ||
paste0(trimws(lines), collapse = "\n") | ||
} | ||
|
||
.j_patch_patch_validate <- | ||
function(patch) | ||
{ | ||
## FIXME: use j_schema_validate() when available | ||
bad_op <- character() | ||
if (!j_query(patch, "type(@)") %in% "array") { | ||
stop("'patch' must be a JSON array") | ||
} | ||
op <- j_query(patch, "[].op", as = "R") | ||
bad_op <- setdiff(op, J_PATCH_OP) | ||
if (length(bad_op)) { | ||
stop( | ||
"'patch' malformed:\n", | ||
" op: ", toString(bad_op), "\n", | ||
" not in: ", toString(J_PATCH_OP), "\n", | ||
call. = FALSE | ||
) | ||
} | ||
|
||
} | ||
|
||
#' @rdname patch | ||
#' | ||
#' @title Patch or compute the difference between two JSON documents | ||
#' | ||
#' @description `j_patch_apply()` uses JSON Patch | ||
#' <https://jsonpatch.com> to transform JSON 'data' according the | ||
#' rules in JSON 'patch'. | ||
#' | ||
#' @param data JSON character vector, file, URL, or an *R* object to | ||
#' be converted to JSON using `jsonline::fromJSON(data, ...)`. | ||
#' | ||
#' @param patch JSON 'patch' as character vector, file, URL, or *R* | ||
#' object. | ||
#' | ||
#' @param as character(1) return type; `"string"` returns a JSON | ||
#' string, `"R"` returns an *R* object using the rules in | ||
#' `as_r()`. | ||
#' | ||
#' @param ... passed to `jsonlite::toJSON` when `data`, `patch`, | ||
#' `data_x`, and / or `data_y` is an _R_ object. Usually, it is | ||
#' appropriate to add the `jsonlite::toJSON()` argument | ||
#' `auto_unbox = TRUE` when `patch` is an *R* object (because the | ||
#' elements of the patch objects are scalar-valued, not arrays of | ||
#' length 1). | ||
#' | ||
#' @return `j_patch_apply()` returns a JSON string or *R* object | ||
#' representing 'data' patched according to 'patch'. | ||
#' | ||
#' @details | ||
#' | ||
#' For `j_patch_apply()`, 'patch' is a JSON array of objects. Each | ||
#' object describes how the patch is to be applied. Simple examples | ||
#' are available at <https://jsonpatch.com>, with verbs 'add', | ||
#' 'remove', 'replace', 'copy' and 'test'. The 'path' element of each | ||
#' operation is a JSON pointer; remember that JSON arrays are 0-based. | ||
#' | ||
#' | ||
#' - `add` -- add elements to an existing document. | ||
#' ``` | ||
#' {"op": "add", "path": "/biscuits/1", "value": {"name": "Ginger Nut"}} | ||
#' ``` | ||
#' - `remove` -- remove elements from a document. | ||
#' ``` | ||
#' {"op": "remove", "path": "/biscuits/0"} | ||
#' ``` | ||
#' - `replace` -- replace one element with another | ||
#' ``` | ||
#' { | ||
#' "op": "replace", "path": "/biscuits/0/name", | ||
#' "value": "Chocolate Digestive" | ||
#' } | ||
#' ``` | ||
#' - `copy` -- copy a path to another location. | ||
#' ``` | ||
#' {"op": "copy", "from": "/biscuits/0", "path": "/best_biscuit"} | ||
#' ``` | ||
#' - `move` -- move a path to another location. | ||
#' ``` | ||
#' {"op": "move", "from": "/biscuits", "path": "/cookies"} | ||
#' ``` | ||
#' - `test` -- test for the existence of a path; if the path does not | ||
#' exist, do not apply any of the patch. | ||
#' ``` | ||
#' {"op": "test", "path": "/best_biscuit/name", "value": "Choco Leibniz"} | ||
#' ``` | ||
#' | ||
#' The examples below illustrate a patch with one (a JSON array with a | ||
#' single object) or several (a JSON array with several arguments) | ||
#' operations. `j_patch_apply()` fits naturally into a pipeline | ||
#' composed with `|>` to transform JSON between representations. | ||
#' | ||
#' @examples | ||
#' data_file <- system.file(package = "rjsoncons", "extdata", "patch_data.json") | ||
#' | ||
#' ## add a biscuit | ||
#' patch <- '[ | ||
#' {"op": "add", "path": "/biscuits/1", "value": {"name": "Ginger Nut"}} | ||
#' ]' | ||
#' j_patch_apply(data_file, patch, as = "R") |> str() | ||
#' | ||
#' ## add a biscuit and choose a favorite | ||
#'patch <- '[ | ||
#' {"op": "add", "path": "/biscuits/1", "value": {"name": "Ginger Nut"}}, | ||
#' {"op": "copy", "from": "/biscuits/2", "path": "/best_biscuit"} | ||
#' ]' | ||
#' biscuits <- j_patch_apply(data_file, patch) | ||
#' as_r(biscuits) |> str() | ||
#' | ||
#' @export | ||
j_patch_apply <- | ||
function(data, patch, as = "string", ...) | ||
{ | ||
data_type <- j_data_type(data) | ||
patch_type <- j_data_type(patch) | ||
stopifnot( | ||
## FIXME: support NDJSON | ||
.is_j_patch_type(data_type), | ||
.is_j_patch_type(patch_type), | ||
as %in% c("string", "R") | ||
) | ||
|
||
if (.is_j_data_type_connection(data_type)) { | ||
data <- .j_patch_data_from_connection(data, data_type) | ||
data_type <- data_type[[1]] | ||
} | ||
if (.is_j_data_type_connection(patch_type)) { | ||
data <- .j_patch_data_from_connection(patch, patch_type) | ||
data_type <- data_type[[1]] | ||
} | ||
|
||
data <- .as_json_string(data, data_type, ...) | ||
patch <- .as_json_string(patch, patch_type, ...) | ||
.j_patch_patch_validate(patch) | ||
|
||
result <- do_cpp( | ||
cpp_j_patch_apply, NULL, | ||
data, data_type, patch, as, | ||
n_records = Inf, verbose = FALSE | ||
) | ||
|
||
result | ||
} | ||
|
||
#' @rdname patch | ||
#' | ||
#' @description `j_patch_from()` computes a JSON patch describing the | ||
#' difference between to JSON documents. | ||
#' | ||
#' @param data_x As for `data`. | ||
#' | ||
#' @param data_y As for `data`. | ||
#' | ||
#' @return `j_patch_from()` returns a JSON string or *R* object | ||
#' representing the difference between 'data_x' and 'data_y'. | ||
#' | ||
#' @examples | ||
#' j_patch_from(biscuits, data_file, as = "R") |> str() | ||
#' | ||
#' @export | ||
j_patch_from <- | ||
function(data_x, data_y, as = "string", ...) | ||
{ | ||
data_type_x <- j_data_type(data_x) | ||
data_type_y <- j_data_type(data_y) | ||
stopifnot( | ||
## FIXME: support NDJSON | ||
.is_j_patch_type(data_type_x), | ||
.is_j_patch_type(data_type_y), | ||
as %in% c("string", "R") | ||
) | ||
|
||
if (.is_j_data_type_connection(data_type_x)) { | ||
data_x <- .j_patch_data_from_connection(data_x, data_type_x) | ||
data_type_x <- data_type_x[[1]] | ||
} else { | ||
data_x <- .as_json_string(data_x, data_type_x, ...) | ||
} | ||
if (.is_j_data_type_connection(data_type_y)) { | ||
data_y <- .j_patch_data_from_connection(data_y, data_type_y) | ||
data_type_y <- data_type_y[[1]] | ||
} else { | ||
data_y <- .as_json_string(data_y, data_type_y, ...) | ||
} | ||
|
||
result <- do_cpp( | ||
cpp_j_patch_from, NULL, | ||
data_x, data_type_x, data_y, data_type_y, | ||
as, n_records = Inf, verbose = FALSE | ||
) | ||
|
||
result | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"biscuits":[ | ||
{"name":"Digestive"}, | ||
{"name":"Choco Leibniz"} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
data <- system.file(package = "rjsoncons", "extdata", "patch_data.json") | ||
json <- paste0(trimws(readLines(data, warn = FALSE)), collapse = "") | ||
|
||
## j_patch_apply() | ||
|
||
expect_identical(j_patch_apply(json, '[]'), json) | ||
expect_identical(j_patch_apply(json, '[]', as = "R"), as_r(json)) | ||
|
||
patch <- '[{"op": "remove", "path": "/biscuits"}]' | ||
expect_identical(j_patch_apply(json, patch), "{}") | ||
|
||
json_r = as_r(json) # 'data' is an R object | ||
expect_identical(j_patch_apply(json_r, patch, auto_unbox = TRUE), "{}") | ||
|
||
patch_r = as_r(patch) # 'patch' is an R object | ||
expect_identical(j_patch_apply(json, patch_r, auto_unbox = TRUE), "{}") | ||
expect_identical(j_patch_apply(json_r, patch_r, auto_unbox = TRUE), "{}") | ||
|
||
patch <- '{"op": "remove", "path": "/biscuits"}' # not an array | ||
expect_error(j_patch_apply(json, patch)) | ||
|
||
patch <- '[{"op": "remover", "path": "/biscuits"}]' # unknown op | ||
expect_error(j_patch_apply(json, patch)) | ||
|
||
patch <- '[{"op": "remove", "path": "/biscuits10"}]' # unknown path | ||
expect_error(j_patch_apply(json, patch)) | ||
|
||
## j_patch_from | ||
|
||
expect_identical(j_patch_from(j_patch_apply(json, '[]'), json), '[]') | ||
expect_identical( | ||
j_patch_from(j_patch_apply(json, '[]'), json, as = "R"), | ||
list() | ||
) | ||
|
||
patch <- '[{"op": "remove", "path": "/biscuits/1"}]' | ||
expect_identical( | ||
j_patch_from(j_patch_apply(json, patch), json), | ||
'[{"op":"add","path":"/biscuits/1","value":{"name":"Choco Leibniz"}}]' | ||
) | ||
expect_identical( | ||
j_patch_from(j_patch_apply(json, patch), json, as = "R"), | ||
list(list( | ||
op = "add", path = "/biscuits/1", | ||
value = list(name = "Choco Leibniz") | ||
)) | ||
) | ||
expect_identical( | ||
j_patch_from(j_patch_apply(json, patch, as = "R"), json, auto_unbox = TRUE), | ||
'[{"op":"add","path":"/biscuits/1","value":{"name":"Choco Leibniz"}}]' | ||
) | ||
expect_identical( | ||
j_patch_from(j_patch_apply(json, patch), json_r, auto_unbox = TRUE), | ||
'[{"op":"add","path":"/biscuits/1","value":{"name":"Choco Leibniz"}}]' | ||
) |
Oops, something went wrong.