ropensci / jsonvalidate

Compare 4d9f6dc ... +18 ... 33f286a

Coverage Reach
read.R validate.R schema.R util.R serialise.R zzz.R

No flags found

Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.

e.g., #unittest #integration

#production #enterprise

#frontend #backend

Learn more about Codecov Flags here.


@@ -0,0 +1,89 @@
Loading
1 +
##' Safe serialisation of json with unboxing guided by the schema.
2 +
##'
3 +
##' When using [jsonlite::toJSON] we are forced to deal with the
4 +
##' differences between R's types and those available in JSON. In
5 +
##' particular:
6 +
##'
7 +
##' * R has no scalar types so it is not clear if `1` should be
8 +
##'   serialised as a number or a vector of length 1; jsonlite
9 +
##'   provides support for "automatically unboxing" such values
10 +
##'   (assuming that length-1 vectors are scalars) or never unboxing
11 +
##'   them unless asked to using [jsonlite::unbox]
12 +
##' * JSON has no date/time values and there are many possible string
13 +
##'   representations.
14 +
##' * JSON has no [data.frame] or [matrix] type and there are several
15 +
##'   ways of representing these in JSON, all equally valid (e.g., row-wise,
16 +
##'   column-wise or as an array of objects).
17 +
##' * The handling of `NULL` and missing values (`NA`, `NaN`) are different
18 +
##' * We need to chose the number of digits to write numbers out at,
19 +
##'   balancing precision and storage.
20 +
##'
21 +
##' These issues are somewhat lessened when we have a schema because
22 +
##' we know what our target type looks like.  This function attempts
23 +
##' to use the schema to guide serialsation of json safely.  Currently
24 +
##' it only supports detecting the appropriate treatment of length-1
25 +
##' vectors, but we will expand functionality over time.
26 +
##'
27 +
##' For a user, this function provides an argument-free replacement
28 +
##' for `jsonlite::toJSON`, accepting an R object and returning a
29 +
##' string with the JSON representation of the object. Internally the
30 +
##' algorithm is:
31 +
##'
32 +
##' 1. serialise the object with [jsonlite::toJSON], with
33 +
##'    `auto_unbox = FALSE` so that length-1 vectors are serialised as a
34 +
##'    length-1 arrays.
35 +
##' 2. operating entirely within JavaScript, deserialise the object
36 +
##'    with `JSON.parse`, traverse the object and its schema
37 +
##'    simultaneously looking for length-1 arrays where the schema
38 +
##'    says there should be scalar value and unboxing these, and
39 +
##'    re-serialise with `JSON.stringify`
40 +
##'
41 +
##' There are several limitations to our current approach, and not all
42 +
##' unboxable values will be found - at the moment we know that
43 +
##' schemas contained within a `oneOf` block (or similar) will not be
44 +
##' recursed into.
45 +
##'
46 +
##' @section: Warning:
47 +
##'
48 +
##' Direct use of this function will be slow!  If you are going to
49 +
##'   serialise more than one or two objects with a single schema, you
50 +
##'   should use the `serialise` method of a
51 +
##'   [jsonvalidate::json_schema] object which you create once and pass around.
52 +
##'
53 +
##' @title Safe JSON serialisation
54 +
##'
55 +
##' @param object An object to be serialised
56 +
##'
57 +
##' @param schema A schema (string or path to a string, suitable to be
58 +
##'   passed through to [jsonvalidate::json_validator] or a validator
59 +
##'   object itself.
60 +
##'
61 +
##' @param engine The engine to use. Only ajv is supported, and trying
62 +
##'   to use `imjv` will throw an error.
63 +
##'
64 +
##' @inheritParams json_validate
65 +
##'
66 +
##' @return A string, representing `object` in JSON format. As for
67 +
##'   `jsonlite::toJSON` we set the class attribute to be `json` to
68 +
##'   mark it as serialised json.
69 +
##'
70 +
##' @export
71 +
##' @example man-roxygen/example-json_serialise.R
72 +
json_serialise <- function(object, schema, engine = "ajv", reference = NULL,
73 +
                           strict = FALSE) {
74 +
  obj <- json_schema$new(schema, engine, reference, strict)
75 +
  obj$serialise(object)
76 +
}
77 +
78 +
79 +
json_serialise_imjv <- function(v8, object) {
80 +
  stop("json_serialise is only supported with engine 'ajv'")
81 +
}
82 +
83 +
84 +
json_serialise_ajv <- function(v8, object) {
85 +
  str <- jsonlite::toJSON(object, auto_unbox = FALSE)
86 +
  ret <- v8$call("safeSerialise", str)
87 +
  class(ret) <- "json"
88 +
  ret
89 +
}

@@ -3,3 +3,9 @@
Loading
3 3
  ct$source(system.file("bundle.js", package = "jsonvalidate"))
4 4
  ct
5 5
}
6 +
7 +
8 +
## Via Gabor, remove NOTE about Imports while not loading R6 at load.
9 +
function() {
10 +
  R6::R6Class
11 +
}

@@ -0,0 +1,169 @@
Loading
1 +
##' @name json_schema
2 +
##' @rdname json_schema
3 +
##' @title Interact with JSON schemas
4 +
##'
5 +
##' @description Interact with JSON schemas, using them to validate
6 +
##'   json strings or serialise objects to JSON safely.
7 +
##'
8 +
##' This interface supercedes [jsonvalidate::json_schema] and changes
9 +
##'   some default arguments.  While the old interface is not going
10 +
##'   away any time soon, users are encouraged to switch to this
11 +
##'   interface, which is what we will develop in the future.
12 +
##'
13 +
##' @example man-roxygen/example-json_serialise.R
14 +
NULL
15 +
16 +
## Workaround for https://github.com/r-lib/roxygen2/issues/1158
17 +
18 +
##' @rdname json_schema
19 +
##' @export
20 +
json_schema <- R6::R6Class(
21 +
  "json_schema",
22 +
  cloneable = FALSE,
23 +
24 +
  private = list(
25 +
    v8 = NULL,
26 +
    do_validate = NULL,
27 +
    do_serialise = NULL),
28 +
29 +
  public = list(
30 +
    ##' @field schema The parsed schema, cannot be rebound
31 +
    schema = NULL,
32 +
33 +
    ##' @field engine The name of the schema validation engine
34 +
    engine = NULL,
35 +
36 +
    ##' @description Create a new `json_schema` object.
37 +
    ##'
38 +
    ##' @param schema Contents of the json schema, or a filename
39 +
    ##'   containing a schema.
40 +
    ##'
41 +
    ##' @param engine Specify the validation engine to use.  Options are
42 +
    ##'   "ajv" (the default; "Another JSON Schema Validator") or "imjv"
43 +
    ##'  ("is-my-json-valid", the default everywhere in versions prior
44 +
    ##'  to 1.4.0, and the default for [jsonvalidate::json_validator].
45 +
    ##'  *Use of `ajv` is strongly recommended for all new code*.
46 +
    ##'
47 +
    ##' @param reference Reference within schema to use for validating
48 +
    ##'   against a sub-schema instead of the full schema passed in.
49 +
    ##'   For example if the schema has a 'definitions' list including a
50 +
    ##'   definition for a 'Hello' object, one could pass
51 +
    ##'   "#/definitions/Hello" and the validator would check that the json
52 +
    ##'   is a valid "Hello" object. Only available if `engine = "ajv"`.
53 +
    ##'
54 +
    ##' @param strict Set whether the schema should be parsed strictly or not.
55 +
    ##'   If in strict mode schemas will error to "prevent any unexpected
56 +
    ##'   behaviours or silently ignored mistakes in user schema". For example
57 +
    ##'   it will error if encounters unknown formats or unknown keywords. See
58 +
    ##'   https://ajv.js.org/strict-mode.html for details. Only available in
59 +
    ##'   `engine = "ajv"` and silently ignored for "imjv".
60 +
    initialize = function(schema, engine = "ajv", reference = NULL,
61 +
                          strict = FALSE) {
62 +
      v8 <- jsonvalidate_js()
63 +
      schema <- read_schema(schema, v8)
64 +
      if (engine == "imjv") {
65 +
        private$v8 <- json_schema_imjv(schema, v8, reference)
66 +
        private$do_validate <- json_validate_imjv
67 +
        private$do_serialise <- json_serialise_imjv
68 +
      } else if (engine == "ajv") {
69 +
        private$v8 <- json_schema_ajv(schema, v8, reference, strict)
70 +
        private$do_validate <- json_validate_ajv
71 +
        private$do_serialise <- json_serialise_ajv
72 +
      } else {
73 +
        stop(sprintf("Unknown engine '%s'", engine))
74 +
      }
75 +
76 +
      self$engine <- engine
77 +
      self$schema <- schema
78 +
      lockBinding("schema", self)
79 +
      lockBinding("engine", self)
80 +
    },
81 +
82 +
    ##' Validate a json string against a schema.
83 +
    ##'
84 +
    ##' @param json Contents of a json object, or a filename containing
85 +
    ##'   one.
86 +
    ##'
87 +
    ##' @param verbose Be verbose?  If `TRUE`, then an attribute
88 +
    ##'   "errors" will list validation failures as a data.frame
89 +
    ##'
90 +
    ##' @param greedy Continue after the first error?
91 +
    ##'
92 +
    ##' @param error Throw an error on parse failure?  If `TRUE`,
93 +
    ##'   then the function returns `NULL` on success (i.e., call
94 +
    ##'   only for the side-effect of an error on failure, like
95 +
    ##'   `stopifnot`).
96 +
    ##'
97 +
    ##' @param query A string indicating a component of the data to
98 +
    ##'   validate the schema against.  Eventually this may support full
99 +
    ##'   [jsonpath](https://www.npmjs.com/package/jsonpath) syntax, but
100 +
    ##'   for now this must be the name of an element within `json`.  See
101 +
    ##'   the examples for more details.
102 +
    validate = function(json, verbose = FALSE, greedy = FALSE, error = FALSE,
103 +
                        query = NULL) {
104 +
      private$do_validate(private$v8, json, verbose, greedy, error, query)
105 +
    },
106 +
107 +
    ##' Serialise an R object to JSON with unboxing guided by the schema.
108 +
    ##' See [jsonvalidate::json_serialise] for details on the problem and
109 +
    ##' the algorithm.
110 +
    ##'
111 +
    ##' @param object An R object to serialise
112 +
    serialise = function(object) {
113 +
      private$do_serialise(private$v8, object)
114 +
    }
115 +
  ))
116 +
117 +
118 +
json_schema_imjv <- function(schema, v8, reference) {
119 +
  meta_schema_version <- schema$meta_schema_version %||% "draft-04"
120 +
121 +
  if (!is.null(reference)) {
122 +
    ## This one has to be an error; it has never worked and makes no
123 +
    ## sense.
124 +
    stop("subschema validation only supported with engine 'ajv'")
125 +
  }
126 +
127 +
  if (meta_schema_version != "draft-04") {
128 +
    ## We detect the version, so let the user know they are not really
129 +
    ## getting what they're asking for
130 +
    note_imjv(paste(
131 +
      "meta schema version other than 'draft-04' is only supported with",
132 +
      sprintf("engine 'ajv' (requested: '%s')", meta_schema_version),
133 +
      "- falling back to use 'draft-04'"))
134 +
    meta_schema_version <- "draft-04"
135 +
  }
136 +
137 +
  if (length(schema$dependencies) > 0L) {
138 +
    ## We've found references, but can't support them. Let the user
139 +
    ## know.
140 +
    note_imjv("Schema references are only supported with engine 'ajv'")
141 +
  }
142 +
143 +
  v8$call("imjv_create", meta_schema_version, V8::JS(schema$schema))
144 +
145 +
  v8
146 +
}
147 +
148 +
149 +
json_schema_ajv <- function(schema, v8, reference, strict) {
150 +
  meta_schema_version <- schema$meta_schema_version %||% "draft-07"
151 +
152 +
  versions_legal <- c("draft-04", "draft-06", "draft-07", "draft/2019-09",
153 +
                      "draft/2020-12")
154 +
  if (!(meta_schema_version %in% versions_legal)) {
155 +
    stop(sprintf("Unknown meta schema version '%s'", meta_schema_version))
156 +
  }
157 +
158 +
  if (is.null(reference)) {
159 +
    reference <- V8::JS("null")
160 +
  }
161 +
  if (is.null(schema$filename)) {
162 +
    schema$filename <- V8::JS("null")
163 +
  }
164 +
  dependencies <- V8::JS(schema$dependencies %||% "null")
165 +
  v8$call("ajv_create", meta_schema_version, strict,
166 +
          V8::JS(schema$schema), schema$filename, dependencies, reference)
167 +
168 +
  v8
169 +
}

@@ -78,22 +78,22 @@
Loading
78 78
##'   does not support reading from URLs (only local files are
79 79
##'   supported).
80 80
##'
81 +
##' @return A function that can be used to validate a
82 +
##'   schema. Additionally, the function has two attributes assigned:
83 +
##'   `v8` which is the javascript context (used internally) and
84 +
##'   `engine`, which contains the name of the engine used.
85 +
##'
81 86
##' @export
82 87
##' @example man-roxygen/example-json_validator.R
83 88
json_validator <- function(schema, engine = "imjv", reference = NULL,
84 89
                           strict = FALSE) {
85 -
  v8 <- jsonvalidate_js()
86 -
  schema <- read_schema(schema, v8)
87 -
88 -
  switch(engine,
89 -
         imjv = json_validator_imjv(schema, v8, reference),
90 -
         ajv = json_validator_ajv(schema, v8, reference, strict),
91 -
         stop(sprintf("Unknown engine '%s'", engine)))
90 +
  json_schema$new(schema, engine, reference, strict)$validate
92 91
}
93 92
94 93
95 94
##' Validate a single json against a schema.  This is a convenience
96 -
##' wrapper around `json_validator(schema)(json)`.  See
95 +
##' wrapper around `json_validator(schema)(json)` or
96 +
##' `json_schema$new(schema, engine = "ajv")$validate(json)`.  See
97 97
##' [jsonvalidate::json_validator()] for further details.
98 98
##'
99 99
##' @title Validate a json file
@@ -130,77 +130,25 @@
Loading
130 130
}
131 131
132 132
133 -
json_validator_imjv <- function(schema, v8, reference) {
134 -
  meta_schema_version <- schema$meta_schema_version %||% "draft-04"
135 -
136 -
  if (!is.null(reference)) {
137 -
    ## This one has to be an error; it has never worked and makes no
138 -
    ## sense.
139 -
    stop("subschema validation only supported with engine 'ajv'")
133 +
json_validate_imjv <- function(v8, json, verbose = FALSE, greedy = FALSE,
134 +
                               error = FALSE, query = NULL) {
135 +
  if (!is.null(query)) {
136 +
    stop("Queries are only supported with engine 'ajv'")
140 137
  }
141 -
142 -
  if (meta_schema_version != "draft-04") {
143 -
    ## We detect the version, so let the user know they are not really
144 -
    ## getting what they're asking for
145 -
    note_imjv(paste(
146 -
      "meta schema version other than 'draft-04' is only supported with",
147 -
      sprintf("engine 'ajv' (requested: '%s')", meta_schema_version),
148 -
      "- falling back to use 'draft-04'"))
149 -
    meta_schema_version <- "draft-04"
150 -
  }
151 -
152 -
  if (length(schema$dependencies) > 0L) {
153 -
    ## We've found references, but can't support them. Let the user
154 -
    ## know.
155 -
    note_imjv("Schema references are only supported with engine 'ajv'")
138 +
  if (error) {
139 +
    verbose <- TRUE
156 140
  }
157 -
158 -
  v8$call("imjv_create", meta_schema_version, V8::JS(schema$schema))
159 -
160 -
  ret <- function(json, verbose = FALSE, greedy = FALSE, error = FALSE,
161 -
                  query = NULL) {
162 -
    if (!is.null(query)) {
163 -
      stop("Queries are only supported with engine 'ajv'")
164 -
    }
165 -
    if (error) {
166 -
      verbose <- TRUE
167 -
    }
168 -
    res <- v8$call("imjv_call", V8::JS(get_string(json)),
169 -
                   verbose, greedy)
170 -
    validation_result(res, error, verbose)
171 -
  }
172 -
173 -
  ret
141 +
  res <- v8$call("imjv_call", V8::JS(get_string(json)),
142 +
                 verbose, greedy)
143 +
  validation_result(res, error, verbose)
174 144
}
175 145
176 146
177 -
json_validator_ajv <- function(schema, v8, reference, strict) {
178 -
  meta_schema_version <- schema$meta_schema_version %||% "draft-07"
179 -
180 -
  versions_legal <- c("draft-04", "draft-06", "draft-07", "draft/2019-09",
181 -
                      "draft/2020-12")
182 -
  if (!(meta_schema_version %in% versions_legal)) {
183 -
    stop(sprintf("Unknown meta schema version '%s'", meta_schema_version))
184 -
  }
185 -
186 -
  if (is.null(reference)) {
187 -
    reference <- V8::JS("null")
188 -
  }
189 -
  if (is.null(schema$filename)) {
190 -
    schema$filename <- V8::JS("null")
191 -
  }
192 -
  dependencies <- V8::JS(schema$dependencies %||% "null")
193 -
  v8$call("ajv_create", meta_schema_version, strict,
194 -
          V8::JS(schema$schema), schema$filename, dependencies, reference)
195 -
196 -
  ret <- function(json, verbose = FALSE, greedy = FALSE, error = FALSE,
197 -
                  query = NULL) {
198 -
    res <- v8$call("ajv_call", V8::JS(get_string(json)),
199 -
                   error || verbose, query_validate(query))
200 -
    validation_result(res, error, verbose)
201 -
  }
202 -
203 -
  ret
147 +
json_validate_ajv <- function(v8, json, verbose = FALSE, greedy = FALSE,
148 +
                              error = FALSE, query = NULL) {
149 +
  res <- v8$call("ajv_call", V8::JS(get_string(json)),
150 +
                 error || verbose, query_validate(query))
151 +
  validation_result(res, error, verbose)
204 152
}
205 153
206 154

Learn more Showing 2 files with coverage changes found.

New file R/serialise.R
New
Loading file...
New file R/schema.R
New
Loading file...
Files Coverage
R 100.00%
Project Totals (6 files) 100.00%
Loading