Skip to content

Commit

Permalink
Merge branch 'j_query'
Browse files Browse the repository at this point in the history
  • Loading branch information
mtmorgan committed Jan 1, 2024
2 parents 1df0ea9 + eb829ed commit 9230b50
Show file tree
Hide file tree
Showing 20 changed files with 529 additions and 319 deletions.
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: rjsoncons
Title: 'C++' Header-Only 'jsoncons' Library for 'JSON' Queries
Version: 1.1.0.9202
Version: 1.1.0.9300
Authors@R: c(
person(
"Martin", "Morgan", role = c("aut", "cre"),
Expand All @@ -24,7 +24,7 @@ Description: The 'jsoncons'
'JSONpath' and 'JMESpath' queries into 'JSON' strings or 'R'
objects. The 'jsoncons' library is also be easily linked to other
packages for direct access to 'C++' functionality.
Suggests: jsonlite, tinytest, BiocStyle, knitr, rmarkdown
Suggests: jsonlite, tibble, tinytest, BiocStyle, knitr, rmarkdown
License: BSL-1.0
LinkingTo: cpp11
NeedsCompilation: yes
Expand Down
4 changes: 3 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Generated by roxygen2: do not edit by hand

export(as_r)
export(j_path_type)
export(j_pivot)
export(j_query)
export(jmespath)
export(jsonpath)
export(jsonpivot)
export(jsonpointer)
export(version)
useDynLib(rjsoncons, .registration = TRUE)
9 changes: 6 additions & 3 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# rjsoncons 1.2.0

- (v.1.1.0.9200) implement `jsonpointer()` for querying JSON documents.
- (v.1.1.0.9100) update jsoncons library to 173.2, relaxing compiler
- (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.
- (v.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.

Expand Down
6 changes: 3 additions & 3 deletions R/as_r.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
#'
#' @details
#'
#' The `as = "R"` argument to `jsonpath()`, `jmespath()` and
#' `jsonpivot()`, and the `as_r()` function transform a JSON string
#' representation to an *R* object. Main rules are:
#' The `as = "R"` argument to `j_query()`, `j_pivot()`, etc., and the
#' `as_r()` function transform a JSON string representation to an *R*
#' object. Main rules are:
#'
#' - JSON arrays of a single type (boolean, integer, double, string)
#' are transformed to *R* vectors of the same length and
Expand Down
16 changes: 4 additions & 12 deletions R/cpp11.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,12 @@ cpp_version <- function() {
.Call(`_rjsoncons_cpp_version`)
}

cpp_jsonpath <- function(data, path, jtype, as) {
.Call(`_rjsoncons_cpp_jsonpath`, data, path, jtype, as)
cpp_j_query <- function(data, path, object_names, as, path_type) {
.Call(`_rjsoncons_cpp_j_query`, data, path, object_names, as, path_type)
}

cpp_jmespath <- function(data, path, jtype, as) {
.Call(`_rjsoncons_cpp_jmespath`, data, path, jtype, as)
}

cpp_jsonpointer <- function(data, path, jtype, as) {
.Call(`_rjsoncons_cpp_jsonpointer`, data, path, jtype, as)
}

cpp_jsonpivot <- function(data, jtype, as) {
.Call(`_rjsoncons_cpp_jsonpivot`, data, jtype, as)
cpp_j_pivot <- function(data, path, object_names, as, path_type) {
.Call(`_rjsoncons_cpp_j_pivot`, data, path, object_names, as, path_type)
}

cpp_as_r <- function(data, jtype) {
Expand Down
149 changes: 149 additions & 0 deletions R/j_query.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#' @rdname j_query
#'
#' @title Query and pivot for JSON documents
#'
#' @description `j_query()` executes a query against a JSON
#' document, automatically inferring the type of `path`.
#'
#' @param as character(1) return type. For `j_query()`, `"string"`
#' returns a single JSON string; `"R"` parses the JSON to R using
#' rules in `as_r()`. For `j_pivot()`, use `as = "data.frame"` or
#' `as = "tibble"` to coerce the result to a data.frame or tibble.
#'
#' @inheritParams jsonpath
#'
#' @param path_type character(1) type of `path`; one of
#' `"JSONpointer"`, `"JSONpath"`, `"JMESpath"`. Inferred from
#' `path` using `j_path_type()`.
#'
#' @examples
#' json <- '{
#' "locations": [
#' {"name": "Seattle", "state": "WA"},
#' {"name": "New York", "state": "NY"},
#' {"name": "Bellevue", "state": "WA"},
#' {"name": "Olympia", "state": "WA"}
#' ]
#' }'
#'
#' j_query(json, "/locations/0/name") # JSONpointer
#' j_query(json, "$.locations[*].name", as = "R") # JSONpath
#' j_query(json, "locations[].state", as = "R") # JMESpath
#'
#' @export
j_query <-
function(
data, path = "", object_names = "asis", as = "string", ...,
path_type = j_path_type(path)
)
{
stopifnot(
.is_scalar_character(path, z.ok = TRUE),
object_names %in% c("asis", "sort"),
as %in% c("string", "R"),
path_type %in% c("JSONpointer", "JSONpath", "JMESpath")
)

data <- .as_json_string(data, ...)
cpp_j_query(data, path, object_names, as, path_type)
}

#' @rdname j_query
#'
#' @description `j_pivot()` transforms a JSON array-of-objects to an
#' object-of-arrays; this can be useful when forming a
#' column-based tibble from row-oriented JSON.
#'
#' @details
#'
#' `j_pivot()` transforms an 'array-of-objects' (typical when the
#' JSON is a row-oriented representation of a table) to an
#' 'object-of-arrays'. A simple example transforms an array of two
#' objects each with three fields `'[{"a": 1, "b": 2, "c": 3}, {"a":
#' 4, "b": 5, "c": 6}]'` to an object with with three fields, each a
#' vector of length 2 `'{"a": [1, 4], "b": [2, 5], "c": [3, 6]}'`. The
#' object-of-arrays representation corresponds closely to an _R_
#' data.frame or tibble, as illustrated in the examples.
#'
#' @examples
#' j_pivot(json, "$.locations[[email protected]=='WA']", as = "string")
#' j_pivot(json, "locations[[email protected]=='WA']", as = "R")
#' j_pivot(json, "locations[[email protected]=='WA']", as = "data.frame")
#' j_pivot(json, "locations[[email protected]=='WA']", as = "tibble")
#'
#' @export
j_pivot <-
function(
data, path = "", object_names = "asis", as = "string", ...,
path_type = j_path_type(path)
)
{
stopifnot(
.is_scalar_character(path, z.ok = TRUE),
object_names %in% c("asis", "sort"),
as %in% c("string", "R", "data.frame", "tibble"),
path_type %in% c("JSONpointer", "JSONpath", "JMESpath")
)

data <- .as_json_string(data, ...)
switch(
as,
string = cpp_j_pivot(data, path, object_names, as, path_type),
R = cpp_j_pivot(data, path, object_names, as = "R", path_type),
data.frame =
cpp_j_pivot(data, path, object_names, as = "R", path_type) |>
as.data.frame(),
tibble =
cpp_j_pivot(data, path, object_names, as = "R", path_type) |>
tibble::as_tibble()
)
}

#' @rdname j_query
#'
#' @description `j_path_type()` uses simple rules to identify
#' whether `path` is a JSONpointer, JSONpath, or JMESpath
#' expression.
#'
#' @details
#'
#' `j_path_type()` infers the type of `path` using a simple but
#' incomplete calssification:
#'
#' - `"JSONpointer"` is infered if the the path is `""` or starts with `"/"`.
#' - `"JSONpath"` expressions start with `"$"`.
#' - `"JMESpath"` expressions satisfy niether the `JSONpointer` nor
#' `JSONpath` criteria.
#'
#' Because of these rules, the valid JSONpointer path `"@"` is
#' interpretted as JMESpath; use `jsonpointer()` if JSONpointer
#' behavior is required.
#'
#' @param path `character(1)` used to query the JSON document.
#'
#' @examples
#' j_path_type("")
#' j_path_type("/locations/0/name")
#' j_path_type("$.locations[0].name")
#' j_path_type("locations[0].name")
#'
#' @export
j_path_type <-
function(path)
{
stopifnot(
.is_scalar_character(path, z.ok = TRUE)
)

path <- trimws(path)
if (nzchar(path)) {
switch(
substring(path, 1, 1),
"/" = "JSONpointer",
"$" = "JSONpath",
"JMESpath"
)
} else {
"JSONpointer"
}
}
86 changes: 13 additions & 73 deletions R/jsoncons.R → R/paths_and_pointer.R
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
#' @rdname jsoncons
#' @rdname paths_and_pointer
#'
#' @title Query JSON using the jsoncons C++ library
#' @title JSONpath, JMESpath, or JSONpointer query of JSON documents
#'
#' @description `jsonpath()` executes a query against a JSON string
#' using the 'jsonpath' specification
#'
#' @param data an _R_ object. If `data` is a scalar (length 1)
#' character vector, it is treated as a single JSON
#' string. Otherwise, it is parsed to a JSON string using
#' `jsonlite::toJSON()`. Use `I()` to treat a scalar character
#' vector as an _R_ object rather than JSON string, e.g., `I("A")`
#' will be parsed to `["A"]` before processing.
#' @param data a character(1) JSON string, or an *R* object parsed to
#' a JSON string using `jsonlite::toJSON()`.
#'
#' @param path character(1) jsonpath or jmespath query string.
#'
Expand All @@ -26,7 +22,7 @@
#' TRUE` to automatically 'unbox' vectors of length 1 to JSON
#' scalar values.
#'
#' @return `jsonpath()`, `jmespath()` and `jsonpivot()` return a
#' @return `jsonpath()`, `jmespath()` and `jsonpointer()` return a
#' character(1) JSON string (`as = "string"`, default) or *R*
#' object (`as = "R"`) representing the result of the query.
#'
Expand Down Expand Up @@ -66,24 +62,19 @@
#'
#' ## different ordering of object names -- 'asis' (default) or 'sort'
#' json_obj <- '{"b": "1", "a": "2"}'
#' jsonpath(json_obj, "$") |> cat("\n")
#' jsonpath(json_obj, "$.*") |> cat("\n")
#' jsonpath(json_obj, "$") |> cat("\n")
#' jsonpath(json_obj, "$.*") |> cat("\n")
#' jsonpath(json_obj, "$", "sort") |> cat("\n")
#' jsonpath(json_obj, "$.*", "sort") |> cat("\n")
#'
#' @export
jsonpath <-
function(data, path, object_names = "asis", as = "string", ...)
{
stopifnot(
.is_scalar_character(path),
.is_scalar_character(object_names)
)
data <- .as_json_string(data, ...)
cpp_jsonpath(data, path, object_names, as)
j_query(data, path, object_names, as, ..., path_type = "JSONpath")
}

#' @rdname jsoncons
#' @rdname paths_and_pointer
#'
#' @description `jmespath()` executes a query against a JSON string
#' using the 'jmespath' specification.
Expand All @@ -108,16 +99,10 @@ jsonpath <-
jmespath <-
function(data, path, object_names = "asis", as = "string", ...)
{
stopifnot(
.is_scalar_character(path),
.is_scalar_character(object_names),
.is_scalar_character(as)
)
data <- .as_json_string(data, ...)
cpp_jmespath(data, path, object_names, as)
j_query(data, path, object_names, as, ..., path_type = "JMESpath")
}

#' @rdname jsoncons
#' @rdname paths_and_pointer
#'
#' @description `jsonpointer()` extracts an element from a JSON string
#' using the 'JSON pointer' specification.
Expand All @@ -130,57 +115,12 @@ jmespath <-
#' jsonpointer('{"b": 0, "a": 1}', "", "sort", as = "R") |>
#' str()
#'
#' ## 'Key not found' -- path '/' is searches for a 0-length key
#' ## 'Key not found' -- path '/' searches for a 0-length key
#' try(jsonpointer('{"b": 0, "a": 1}', "/"))
#'
#' @export
jsonpointer <-
function(data, path, object_names = "asis", as = "string", ...)
{
stopifnot(
identical(nchar(path), 0L) || .is_scalar_character(path),
.is_scalar_character(object_names),
.is_scalar_character(as)
)
data <- .as_json_string(data, ...)
cpp_jsonpointer(data, path, object_names, as)
}

#' @rdname jsoncons
#'
#' @description `jsonpivot()` transforms a JSON array-of-objects to
#' an object-of-arrays; this can be useful when forming a
#' column-based tibble from row-oriented JSON.
#'
#' @details
#'
#' `jsonpivot()` transforms an 'array-of-objects' (typical when the
#' JSON is a row-oriented representation of a table) to an
#' 'object-of-arrays'. A simple example transforms an array of two
#' objects each with three fields `'[{"a": 1, "b": 2, "c": 3}, {"a":
#' 4, "b": 5, "c": 6}]'` to an object with with three fields, each a
#' vector of length 2 `'{"a": [1, 4], "b": [2, 5], "c": [3, 6]}'`. The
#' object-of-arrays representation corresponds closely to an _R_
#' data.frame or tibble, as illustrated in the examples.
#'
#' @examples
#' json |>
#' ## 'locations' is a array of objects with 'name' and 'state' scalars...
#' jmespath("locations") |>
#' ## ...pivot to a single object with 'name' and 'state' vectors...
#' jsonpivot(as = "R") |>
#' ## ... easily coerced to a data.frame or dplyr::tibble
#' as.data.frame()
#'
#' @export
jsonpivot <-
function(data, object_names = "asis", as = "string", ...)
{
stopifnot(
.is_scalar_character(object_names),
.is_scalar_character(as)
)

data <- .as_json_string(data, ...)
cpp_jsonpivot(data, object_names, as)
j_query(data, path, object_names, as, ..., path_type = "JSONpointer")
}
4 changes: 2 additions & 2 deletions R/utilities.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
}

.is_scalar_character <-
function(x)
function(x, z.ok = FALSE)
{
.is_scalar(x) && is.character(x) && nzchar(x)
.is_scalar(x) && is.character(x) && (z.ok || nzchar(x))
}

.as_json_string <-
Expand Down
Loading

0 comments on commit 9230b50

Please sign in to comment.