|
| 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 | +} |
0 commit comments