Skip to content

Commit b729022

Browse files
authored
Merge pull request #62 from ropensci/issue-61
2 parents 33f286a + c97e780 commit b729022

File tree

7 files changed

+270
-11
lines changed

7 files changed

+270
-11
lines changed

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: jsonvalidate
22
Title: Validate 'JSON' Schema
3-
Version: 1.4.0
3+
Version: 1.4.1
44
Authors@R: c(person("Rich", "FitzJohn", role = c("aut", "cre"),
55
email = "[email protected]"),
66
person("Rob", "Ashton", role = "aut"),

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# jsonvalidate 1.4.1
2+
3+
* Add support for subfolders in nested schema references. (#61)
4+
15
# jsonvalidate 1.4.0
26

37
* Support for safely serialising objects to json, guided by the schema, with new function `json_serialise`

R/read.R

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,26 @@ read_schema <- function(x, v8) {
4343

4444

4545
read_schema_filename <- function(filename, children, parent, v8) {
46-
if (!file.exists(filename)) {
47-
stop(sprintf("Did not find schema file '%s'", filename))
46+
## '$ref' path should be relative to schema ID so if parent is in a
47+
## subdir we need to add the dir to the filename so it can be sourced
48+
file_path <- filename
49+
if (path_includes_dir(parent[1])) {
50+
file_path <- file.path(dirname(parent[1]), file_path)
4851
}
4952

50-
schema <- paste(readLines(filename), collapse = "\n")
53+
if (!file.exists(file_path)) {
54+
additional_msg <- ""
55+
if (file_path != filename) {
56+
additional_msg <- sprintf(" relative to '%s'", parent[1])
57+
}
58+
stop(sprintf("Did not find schema file '%s'%s", filename, additional_msg))
59+
}
60+
61+
schema <- paste(readLines(file_path), collapse = "\n")
5162

5263
meta_schema_version <- read_meta_schema_version(schema, v8)
53-
read_schema_dependencies(schema, children, c(filename, parent), v8)
54-
list(schema = schema, filename = filename,
64+
read_schema_dependencies(schema, children, c(file_path, parent), v8)
65+
list(schema = schema, filename = file_path,
5566
meta_schema_version = meta_schema_version)
5667
}
5768

@@ -79,21 +90,27 @@ read_schema_dependencies <- function(schema, children, parent, v8) {
7990
stop("Don't yet support protocol-based sub schemas")
8091
}
8192

93+
if (any(is_absolute_path(extra))) {
94+
abs <- extra[is_absolute_path(extra)]
95+
abs <- paste0("'", paste(abs, collapse = "', '"), "'")
96+
stop(sprintf("'$ref' paths must be relative, got absolute path(s) %s", abs))
97+
}
98+
8299
if (any(grepl("#/", extra))) {
83100
split <- strsplit(extra, "#/")
84101
extra <- lapply(split, "[[", 1)
85102
}
86103

87-
for (p in extra) {
104+
for (ref in extra) {
88105
## Mark name as one that we will not descend further with
89-
children[[p]] <- NULL
106+
children[[ref]] <- NULL
90107
## I feel this should be easier to do with withCallingHandlers,
91108
## but not getting anywhere there.
92-
children[[p]] <- tryCatch(
93-
read_schema_filename(p, children, parent, v8),
109+
children[[ref]] <- tryCatch(
110+
read_schema_filename(ref, children, parent, v8),
94111
error = function(e) {
95112
if (!inherits(e, "jsonvalidate_read_error")) {
96-
chain <- paste(squote(c(rev(parent), p)), collapse = " > ")
113+
chain <- paste(squote(c(rev(parent), ref)), collapse = " > ")
97114
e$message <- sprintf("While reading %s\n%s", chain, e$message)
98115
class(e) <- c("jsonvalidate_read_error", class(e))
99116
e$call <- NULL

R/util.R

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,11 @@ note_imjv <- function(msg, is_interactive = interactive()) {
7070
message(msg)
7171
}
7272
}
73+
74+
path_includes_dir <- function(x) {
75+
!is.null(x) && basename(x) != x
76+
}
77+
78+
is_absolute_path <- function(path) {
79+
grepl("^(/|[A-Za-z]:)", path)
80+
}

tests/testthat/test-util.R

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,9 @@ test_that("control printing imjv notice", {
4949
list(jsonvalidate.no_note_imjv = TRUE),
5050
expect_silent(note_imjv("note", TRUE)))
5151
})
52+
53+
test_that("can check if path includes dir", {
54+
expect_false(path_includes_dir(NULL))
55+
expect_false(path_includes_dir("file.json"))
56+
expect_true(path_includes_dir("the/file.json"))
57+
})

tests/testthat/test-validator.R

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,153 @@ test_that("Referenced schemas have their ids replaced", {
287287
})
288288

289289

290+
test_that("file references in subdirectories work", {
291+
parent <- c(
292+
'{',
293+
' "type": "object",',
294+
' "properties": {',
295+
' "hello": {',
296+
' "$ref": "sub/child.json"',
297+
' }',
298+
' },',
299+
' "required": ["hello"],',
300+
' "additionalProperties": false',
301+
'}')
302+
child <- c(
303+
'{',
304+
' "type": "string"',
305+
'}')
306+
path <- tempfile()
307+
dir.create(path)
308+
subdir <- file.path(path, "sub")
309+
dir.create(subdir)
310+
writeLines(parent, file.path(path, "parent.json"))
311+
writeLines(child, file.path(subdir, "child.json"))
312+
313+
v <- json_validator(file.path(path, "parent.json"), engine = "ajv")
314+
expect_false(v("{}"))
315+
expect_true(v('{"hello": "world"}'))
316+
})
317+
318+
319+
test_that("chained file references work", {
320+
parent <- c(
321+
'{',
322+
' "type": "object",',
323+
' "properties": {',
324+
' "hello": {',
325+
' "$ref": "sub/middle.json"',
326+
' }',
327+
' },',
328+
' "required": ["hello"],',
329+
' "additionalProperties": false',
330+
'}')
331+
middle <- c(
332+
'{',
333+
' "type": "object",',
334+
' "properties": {',
335+
' "greeting": {',
336+
' "$ref": "child.json"',
337+
' }',
338+
' },',
339+
' "required": ["greeting"],',
340+
' "additionalProperties": false',
341+
'}')
342+
child <- c(
343+
'{',
344+
' "type": "string"',
345+
'}')
346+
path <- tempfile()
347+
dir.create(path)
348+
subdir <- file.path(path, "sub")
349+
dir.create(subdir)
350+
writeLines(parent, file.path(path, "parent.json"))
351+
writeLines(middle, file.path(subdir, "middle.json"))
352+
writeLines(child, file.path(subdir, "child.json"))
353+
354+
v <- json_validator(file.path(path, "parent.json"), engine = "ajv")
355+
expect_false(v("{}"))
356+
expect_true(v('{"hello": { "greeting": "world"}}'))
357+
expect_false(v('{"hello": { "greeting": 2}}'))
358+
})
359+
360+
361+
test_that("absolute file references throw error", {
362+
parent <- c(
363+
'{',
364+
' "type": "object",',
365+
' "properties": {',
366+
' "greeting": {',
367+
' "$ref": "%s"',
368+
' },',
369+
' "address": {',
370+
' "$ref": "%s"',
371+
' }',
372+
' },',
373+
' "required": ["greeting", "address"],',
374+
' "additionalProperties": false',
375+
'}')
376+
child <- c(
377+
'{',
378+
' "type": "string"',
379+
'}')
380+
path <- tempfile()
381+
dir.create(path)
382+
child_path1 <- file.path(path, "child1.json")
383+
writeLines(child, child_path1)
384+
child_path2 <- file.path(path, "child2.json")
385+
writeLines(child, child_path2)
386+
parent_path <- file.path(path, "parent.json")
387+
writeLines(sprintf(paste0(parent, collapse = "\n"),
388+
normalizePath(child_path1), normalizePath(child_path2)),
389+
parent_path)
390+
391+
expect_error(json_validator(parent_path, engine = "ajv"),
392+
"'\\$ref' paths must be relative, got absolute path\\(s\\)")
393+
})
394+
395+
396+
test_that("chained file references return useful error", {
397+
parent <- c(
398+
'{',
399+
' "type": "object",',
400+
' "properties": {',
401+
' "hello": {',
402+
' "$ref": "sub/middle.json"',
403+
' }',
404+
' },',
405+
' "required": ["hello"],',
406+
' "additionalProperties": false',
407+
'}')
408+
middle <- c(
409+
'{',
410+
' "type": "object",',
411+
' "properties": {',
412+
' "greeting": {',
413+
' "$ref": "sub/child.json"',
414+
' }',
415+
' },',
416+
' "required": ["greeting"],',
417+
' "additionalProperties": false',
418+
'}')
419+
child <- c(
420+
'{',
421+
' "type": "string"',
422+
'}')
423+
path <- tempfile()
424+
dir.create(path)
425+
subdir <- file.path(path, "sub")
426+
dir.create(subdir)
427+
writeLines(parent, file.path(path, "parent.json"))
428+
writeLines(middle, file.path(subdir, "middle.json"))
429+
writeLines(child, file.path(subdir, "child.json"))
430+
431+
expect_error(
432+
json_validator(file.path(path, "parent.json"), engine = "ajv"),
433+
"Did not find schema file 'sub/child.json' relative to 'sub/middle.json'")
434+
})
435+
436+
290437
test_that("Can validate fraction of a json object", {
291438
schema <- c(
292439
'{',

vignettes/jsonvalidate.Rmd

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,80 @@ v(json)
228228
```
229229

230230
While we do not intend on removing this old interface, new code should prefer both `jsonvalidate::json_schema` and the `ajv` engine.
231+
232+
## Combining schemas
233+
234+
You can combine schemas with `ajv` engine. You can reference definitions within one schema
235+
236+
```{r}
237+
schema <- '{
238+
"$schema": "http://json-schema.org/draft-04/schema#",
239+
"definitions": {
240+
"city": { "type": "string" }
241+
},
242+
"type": "object",
243+
"properties": {
244+
"city": { "$ref": "#/definitions/city" }
245+
}
246+
}'
247+
json <- '{
248+
"city": "Firenze"
249+
}'
250+
jsonvalidate::json_validate(json, schema, engine = "ajv")
251+
```
252+
You can reference schema from other files
253+
254+
```{r}
255+
city_schema <- '{
256+
"$schema": "http://json-schema.org/draft-07/schema",
257+
"type": "string",
258+
"enum": ["Firenze"]
259+
}'
260+
address_schema <- '{
261+
"$schema": "http://json-schema.org/draft-07/schema",
262+
"type":"object",
263+
"properties": {
264+
"city": { "$ref": "city.json" }
265+
}
266+
}'
267+
268+
path <- tempfile()
269+
dir.create(path)
270+
address_path <- file.path(path, "address.json")
271+
city_path <- file.path(path, "city.json")
272+
writeLines(address_schema, address_path)
273+
writeLines(city_schema, city_path)
274+
jsonvalidate::json_validate(json, address_path, engine = "ajv")
275+
```
276+
277+
You can combine schemas in subdirectories. Note that the `$ref` path needs to be relative to the schema path. You cannot use absolute paths in `$ref` and jsonvalidate will throw an error if you try to do so.
278+
279+
```{r}
280+
user_schema = '{
281+
"$schema": "http://json-schema.org/draft-07/schema",
282+
"type": "object",
283+
"required": ["address"],
284+
"properties": {
285+
"address": {
286+
"$ref": "sub/address.json"
287+
}
288+
}
289+
}'
290+
291+
json <- '{
292+
"address": {
293+
"city": "Firenze"
294+
}
295+
}'
296+
297+
path <- tempfile()
298+
subdir <- file.path(path, "sub")
299+
dir.create(subdir, showWarnings = FALSE, recursive = TRUE)
300+
city_path <- file.path(subdir, "city.json")
301+
address_path <- file.path(subdir, "address.json")
302+
user_path <- file.path(path, "schema.json")
303+
writeLines(city_schema, city_path)
304+
writeLines(address_schema, address_path)
305+
writeLines(user_schema, user_path)
306+
jsonvalidate::json_validate(json, user_path, engine = "ajv")
307+
```

0 commit comments

Comments
 (0)