From b6ff3b1b59c9674bdd43c805b29bb0907bfc3e76 Mon Sep 17 00:00:00 2001 From: YongHeeKim Date: Sat, 3 Oct 2020 19:12:22 +0900 Subject: [PATCH 01/19] use JSONPointer backend --- Project.toml | 4 +++- src/JSONSchema.jl | 1 + src/schema.jl | 51 +++++++++-------------------------------------- 3 files changed, 13 insertions(+), 43 deletions(-) diff --git a/Project.toml b/Project.toml index bc41ce7..81cfb8f 100644 --- a/Project.toml +++ b/Project.toml @@ -5,17 +5,19 @@ version = "0.3.2" [deps] HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +JSONPointer = "cc3ff66e-924d-4e6b-b111-1d9960e4bba9" ZipFile = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea" [compat] HTTP = "0.8" JSON = "0.21" ZipFile = "0.8, 0.9" +JSONPointer = "0.2.3" julia = "1" [extras] -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["Test", "OrderedCollections"] diff --git a/src/JSONSchema.jl b/src/JSONSchema.jl index 05816ef..de08285 100644 --- a/src/JSONSchema.jl +++ b/src/JSONSchema.jl @@ -1,6 +1,7 @@ module JSONSchema using JSON +using JSONPointer import HTTP export Schema, validate diff --git a/src/schema.jl b/src/schema.jl index be217ba..721f41a 100644 --- a/src/schema.jl +++ b/src/schema.jl @@ -1,15 +1,3 @@ -# Transform escaped characters in JPaths back to their original value. -function unescape_jpath(raw::String) - ret = replace(replace(raw, "~0" => "~"), "~1" => "/") - m = match(r"%([0-9A-F]{2})", ret) - if m !== nothing - for c in m.captures - ret = replace(ret, "%$(c)" => Char(parse(UInt8, "0x$(c)"))) - end - end - return ret -end - function type_to_dict(x) return Dict(name => getfield(x, name) for name in fieldnames(typeof(x))) end @@ -29,34 +17,6 @@ function update_id(uri::HTTP.URI, s::String) return HTTP.URI(; els...) end -function get_element(schema, path::AbstractString) - for element in split(path, "/"; keepempty = false) - schema = _recurse_get_element(schema, unescape_jpath(String(element))) - end - return schema -end - -function _recurse_get_element(schema::Any, ::String) - error("unmanaged type in ref resolution $(typeof(schema)): $(schema).") -end - -function _recurse_get_element(schema::AbstractDict, element::String) - if !haskey(schema, element) - error("missing property '$(element)' in $(schema).") - end - return schema[element] -end - -function _recurse_get_element(schema::Vector, element::String) - index = tryparse(Int, element) # Remember that `index` is 0-indexed! - if index === nothing - error("expected integer array index instead of '$(element)'.") - elseif index >= length(schema) - error("item index $(index) is larger than array $(schema).") - end - return schema[index + 1] -end - function get_remote_schema(uri::HTTP.URI) r = HTTP.get(uri) if r.status != 200 @@ -71,7 +31,8 @@ function find_ref( if path == "" || path == "#" # This path refers to the root schema. return id_map[string(uri)] elseif startswith(path, "#/") # This path is a JPointer. - return get_element(id_map[string(uri)], path[3:end]) + p = JSONPointer.Pointer(path[2:end]; shift_index = true) + return id_map[string(uri)][p] end uri = update_id(uri, path) els = type_to_dict(uri) @@ -93,7 +54,13 @@ function find_ref( Schema(JSON.parsefile(uri2.path); parent_dir = dirname(uri2.path)).data end end - return get_element(id_map[string(uri2)], uri.fragment) + @show uri + if isempty(uri.fragment) + return id_map[string(uri2)] + else + p = JSONPointer.Pointer(uri.fragment) + return id_map[string(uri2)][p] + end end # Recursively find all "$ref" fields and resolve their path. From 45f9c047fe0a7ded37d2d9b797dd8c37bee212c8 Mon Sep 17 00:00:00 2001 From: YongHeeKim Date: Sun, 4 Oct 2020 08:19:11 +0900 Subject: [PATCH 02/19] fix test for exceptions --- test/runtests.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 69741a2..28dbd38 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -211,7 +211,7 @@ end @testset "errors" begin @test_throws( - ErrorException("missing property 'Foo' in $(Dict{String,Any}())."), + KeyError, Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/Foo"}}, @@ -220,7 +220,7 @@ end ) @test_throws( - ErrorException("unmanaged type in ref resolution $(Int64): 1."), + MethodError, Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/Foo"}}, @@ -228,7 +228,7 @@ end }""") ) @test_throws( - ErrorException("expected integer array index instead of 'Foo'."), + ArgumentError, Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/Foo"}}, @@ -236,7 +236,7 @@ end }""") ) @test_throws( - ErrorException("item index 3 is larger than array $(Any[1, 2])."), + BoundsError, Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/3"}}, @@ -294,6 +294,6 @@ end data_pass = OrderedDict("foo" => true) data_fail = OrderedDict("bar" => 12.5) @test JSONSchema.validate(data_pass, schema) === nothing - @test JSONSchema.validate(data_fail, schema) != nothing + @test JSONSchema.validate(data_fail, schema) !== nothing end \ No newline at end of file From 5c871715ea52b63bec5827bf0399ed2ca7984bb6 Mon Sep 17 00:00:00 2001 From: YongHeeKim Date: Sun, 4 Oct 2020 09:06:29 +0900 Subject: [PATCH 03/19] fix uri fragment pointer --- src/schema.jl | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/schema.jl b/src/schema.jl index 721f41a..5ae46e6 100644 --- a/src/schema.jl +++ b/src/schema.jl @@ -12,7 +12,7 @@ function update_id(uri::HTTP.URI, s::String) els[:fragment] = id2.fragment if !isempty(id2.path) oldpath = match(r"^(.*/).*$", uri.path) - els[:path] = oldpath == nothing ? id2.path : oldpath.captures[1] * id2.path + els[:path] = isnothing(oldpath) ? id2.path : oldpath.captures[1] * id2.path end return HTTP.URI(; els...) end @@ -31,7 +31,7 @@ function find_ref( if path == "" || path == "#" # This path refers to the root schema. return id_map[string(uri)] elseif startswith(path, "#/") # This path is a JPointer. - p = JSONPointer.Pointer(path[2:end]; shift_index = true) + p = JSONPointer.Pointer(path; shift_index = true) return id_map[string(uri)][p] end uri = update_id(uri, path) @@ -54,13 +54,9 @@ function find_ref( Schema(JSON.parsefile(uri2.path); parent_dir = dirname(uri2.path)).data end end - @show uri - if isempty(uri.fragment) - return id_map[string(uri2)] - else - p = JSONPointer.Pointer(uri.fragment) - return id_map[string(uri2)][p] - end + + p = JSONPointer.Pointer(uri.fragment) + return id_map[string(uri2)][p] end # Recursively find all "$ref" fields and resolve their path. @@ -199,4 +195,7 @@ Create a schema for document validation by parsing the string `schema`. """ Schema(schema::String; kwargs...) = Schema(JSON.parse(schema); kwargs...) -Base.show(io::IO, ::Schema) = print(io, "A JSONSchema") +function Base.show(io::IO, ::Schema) + print(io, "A JSONSchema") + +end \ No newline at end of file From 31ea926da5aef492675860d3c2a8edcf315c9951 Mon Sep 17 00:00:00 2001 From: YongHeeKim Date: Sun, 4 Oct 2020 09:09:51 +0900 Subject: [PATCH 04/19] fix index shfit bug --- src/schema.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schema.jl b/src/schema.jl index 5ae46e6..f95f075 100644 --- a/src/schema.jl +++ b/src/schema.jl @@ -55,7 +55,7 @@ function find_ref( end end - p = JSONPointer.Pointer(uri.fragment) + p = JSONPointer.Pointer(uri.fragment; shift_index = true) return id_map[string(uri2)][p] end From 29cdf200fc6f933ab4361550c69ff39705352fe3 Mon Sep 17 00:00:00 2001 From: YongHeeKim Date: Sun, 4 Oct 2020 09:12:50 +0900 Subject: [PATCH 05/19] revert unnessary line chagne --- src/schema.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/schema.jl b/src/schema.jl index f95f075..e5aa3a3 100644 --- a/src/schema.jl +++ b/src/schema.jl @@ -195,7 +195,4 @@ Create a schema for document validation by parsing the string `schema`. """ Schema(schema::String; kwargs...) = Schema(JSON.parse(schema); kwargs...) -function Base.show(io::IO, ::Schema) - print(io, "A JSONSchema") - -end \ No newline at end of file +Base.show(io::IO, ::Schema) = print(io, "A JSONSchema") From 1e4c16200d022328554d331134410afba1433545 Mon Sep 17 00:00:00 2001 From: YongheeKim Date: Tue, 6 Oct 2020 23:15:51 +0900 Subject: [PATCH 06/19] remove `isnothing` for julia 1.0 --- src/schema.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schema.jl b/src/schema.jl index e5aa3a3..d267cdf 100644 --- a/src/schema.jl +++ b/src/schema.jl @@ -12,7 +12,7 @@ function update_id(uri::HTTP.URI, s::String) els[:fragment] = id2.fragment if !isempty(id2.path) oldpath = match(r"^(.*/).*$", uri.path) - els[:path] = isnothing(oldpath) ? id2.path : oldpath.captures[1] * id2.path + els[:path] = oldpath === nothing ? id2.path : oldpath.captures[1] * id2.path end return HTTP.URI(; els...) end From efc6bd0e7647410e9332562b569d1ef1b26fe5ab Mon Sep 17 00:00:00 2001 From: YongheeKim Date: Wed, 26 Jun 2024 18:49:19 +0900 Subject: [PATCH 07/19] added JSONPointer dependency --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index ffb2da4..8c15bad 100644 --- a/Project.toml +++ b/Project.toml @@ -6,6 +6,7 @@ version = "1.3.0" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +JSONPointer = "cc3ff66e-924d-4e6b-b111-1d9960e4bba9" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" [compat] From 064ab5bb562eacf4994d50e32fb2be138223fd43 Mon Sep 17 00:00:00 2001 From: YongheeKim Date: Thu, 27 Jun 2024 00:42:53 +0900 Subject: [PATCH 08/19] Replaced `get_element` methods with JSONPointer package --- src/schema.jl | 48 ++++-------------------------------------------- test/runtests.jl | 8 ++++---- 2 files changed, 8 insertions(+), 48 deletions(-) diff --git a/src/schema.jl b/src/schema.jl index 5406e65..401ae95 100644 --- a/src/schema.jl +++ b/src/schema.jl @@ -3,18 +3,6 @@ # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. -# Transform escaped characters in JPaths back to their original value. -function unescape_jpath(raw::String) - ret = replace(replace(raw, "~0" => "~"), "~1" => "/") - m = match(r"%([0-9A-F]{2})", ret) - if m !== nothing - for c in m.captures - ret = replace(ret, "%$(c)" => Char(parse(UInt8, "0x$(c)"))) - end - end - return ret -end - function type_to_dict(x) return Dict(name => getfield(x, name) for name in fieldnames(typeof(x))) end @@ -35,36 +23,6 @@ function update_id(uri::URIs.URI, s::String) return URIs.URI(; els...) end -function get_element(schema, path::AbstractString) - for element in split(path, "/"; keepempty = false) - schema = _recurse_get_element(schema, unescape_jpath(String(element))) - end - return schema -end - -function _recurse_get_element(schema::Any, ::String) - return error( - "unmanaged type in ref resolution $(typeof(schema)): $(schema).", - ) -end - -function _recurse_get_element(schema::AbstractDict, element::String) - if !haskey(schema, element) - error("missing property '$(element)' in $(schema).") - end - return schema[element] -end - -function _recurse_get_element(schema::AbstractVector, element::String) - index = tryparse(Int, element) # Remember that `index` is 0-indexed! - if index === nothing - error("expected integer array index instead of '$(element)'.") - elseif index >= length(schema) - error("item index $(index) is larger than array $(schema).") - end - return schema[index+1] -end - function get_remote_schema(uri::URIs.URI) io = IOBuffer() r = Downloads.request(string(uri); output = io, throw = false) @@ -89,7 +47,8 @@ function find_ref( if path == "" || path == "#" # This path refers to the root schema. return id_map[string(uri)] elseif startswith(path, "#/") # This path is a JPointer. - return get_element(id_map[string(uri)], path[3:end]) + p = JSONPointer.Pointer(path; shift_index=true) + return get_pointer(id_map[string(uri)], p) end uri = update_id(uri, path) els = type_to_dict(uri) @@ -114,7 +73,8 @@ function find_ref( ).data end end - return get_element(id_map[string(uri2)], uri.fragment) + p = JSONPointer.Pointer(uri.fragment; shift_index = true) + return get_pointer(id_map[string(uri2)], p) end # Recursively find all "$ref" fields and resolve their path. diff --git a/test/runtests.jl b/test/runtests.jl index 1fa2e67..a0608c3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -265,7 +265,7 @@ end @testset "errors" begin @test_throws( - ErrorException("missing property 'Foo' in $(Dict{String,Any}())."), + KeyError("Foo"), JSONSchema.Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/Foo"}}, @@ -274,7 +274,7 @@ end ) @test_throws( - ErrorException("unmanaged type in ref resolution $(Int64): 1."), + ArgumentError("JSON pointer does not match the data-structure. I tried (and failed) to index 1 with the key: Foo"), JSONSchema.Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/Foo"}}, @@ -282,7 +282,7 @@ end }""") ) @test_throws( - ErrorException("expected integer array index instead of 'Foo'."), + ArgumentError("JSON pointer does not match the data-structure. I tried (and failed) to index Any[1, 2] with the key: Foo"), JSONSchema.Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/Foo"}}, @@ -290,7 +290,7 @@ end }""") ) @test_throws( - ErrorException("item index 3 is larger than array $(Any[1, 2])."), + BoundsError, JSONSchema.Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/3"}}, From 7a481ec0d2cdd82ab438b62c01f97fa010f9357c Mon Sep 17 00:00:00 2001 From: YongheeKim Date: Thu, 27 Jun 2024 17:41:05 +0900 Subject: [PATCH 09/19] added `_if_then_esle` validation for draft7 #37 --- src/validation.jl | 75 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/src/validation.jl b/src/validation.jl index d51c425..948d255 100644 --- a/src/validation.jl +++ b/src/validation.jl @@ -98,6 +98,16 @@ function _validate_entry(x, schema::Bool, path::String) return schema ? nothing : SingleIssue(x, path, "schema", schema) end +""" + _resolve_refs(schema, explored_refs = Any[]) + +Resolves any `"\$ref"` keys it encounters. +Note: This is recursive function and will continue to resolve references until no more are found. + +# Arguments +- `schema`: The schema or part of a schema to resolve references in. +- `explored_refs`: An array to keep track of explored references to detect circular references. +""" function _resolve_refs(schema::AbstractDict, explored_refs = Any[schema]) if !haskey(schema, "\$ref") return schema @@ -160,10 +170,71 @@ function _validate(x, schema, ::Val{:not}, val, path::String) end # 9.2.2.1: if - +function _validate(x, schema, ::Val{:if}, val, path::String) + # ignore if without then or else + if haskey(schema, "then") || haskey(schema, "else") + ret = _if_then_else(x, schema, path) + return ret + end + return nothing +end # 9.2.2.2: then - +function _validate(x, schema, ::Val{:then}, val, path::String) + # ignore then without if + if haskey(schema, "if") + ret = _if_then_else(x, schema, path) + return ret + end + return nothing +end # 9.2.2.3: else +function _validate(x, schema, ::Val{:else}, val, path::String) + # ignore else without if + if haskey(schema, "if") + ret = _if_then_else(x, schema, path) + return ret + end + return nothing +end + +""" + + +https://json-schema.org/understanding-json-schema/reference/conditionals#ifthenelse + +The if, then and else keywords allow the application of a subschema based on the outcome of another schema. Details are in the link and the truth table is as follows: + + ┌─────┬──────┬──────┬────────┐ + │ if │ then │ else │ result │ + ├─────┼──────┼──────┼────────┤ + │ T │ T │ n/a │ T │ + │ T │ F │ n/a │ F │ + │ F │ n/a │ T │ T │ + │ F │ n/a │ F │ F │ + │ n/a │ n/a │ n/a │ T │ + └─────┴──────┴──────┴────────┘ +""" +function _if_then_else(x, schema, path) + val_if = _validate(x, schema["if"], path) + val_then = if haskey(schema, "then") + _validate(x, schema["then"], path) + end + val_else = if haskey(schema, "else") + _validate(x, schema["else"], path) + end + if isnothing(val_if) + if isnothing(val_then) && isnothing(val_else) + return nothing + else + return val_then + end + end + if isa(val_if, SingleIssue) + return val_else + end + return nothing +end + ### ### Checks for Arrays. From e9b81d1963594c4b4ba0c683b834f39cf281ff97 Mon Sep 17 00:00:00 2001 From: YongheeKim Date: Thu, 27 Jun 2024 21:29:09 +0900 Subject: [PATCH 10/19] Included draft 7 in the testt --- test/runtests.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index a0608c3..c290582 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -131,7 +131,7 @@ write( }]""", ) -@testset "Draft 4/6" begin +@testset "Draft 4/6/7" begin # Note(odow): I didn't want to use a mutable reference like this for the web-server. # The obvious thing to do is to start a new server for each value of `draft_folder`, # however, shutting down the webserver asynchronously doesn't play well with @@ -148,6 +148,7 @@ write( @testset "$(draft_folder)" for draft_folder in [ "draft4", "draft6", + "draft7", basename(abspath(LOCAL_TEST_DIR)), ] test_dir = joinpath(SCHEMA_TEST_DIR, draft_folder) @@ -190,6 +191,7 @@ end @testset "$(draft_folder)" for draft_folder in [ "draft4", "draft6", + "draft7", basename(abspath(LOCAL_TEST_DIR)), ] test_dir = joinpath(SCHEMA_TEST_DIR, draft_folder) From 6ee43bd31224f37507a6644ac3529671d5b1a40d Mon Sep 17 00:00:00 2001 From: YongheeKim Date: Thu, 27 Jun 2024 21:29:24 +0900 Subject: [PATCH 11/19] fixed docstring for _if_then_else --- src/validation.jl | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/validation.jl b/src/validation.jl index 948d255..fdaf2aa 100644 --- a/src/validation.jl +++ b/src/validation.jl @@ -103,10 +103,6 @@ end Resolves any `"\$ref"` keys it encounters. Note: This is recursive function and will continue to resolve references until no more are found. - -# Arguments -- `schema`: The schema or part of a schema to resolve references in. -- `explored_refs`: An array to keep track of explored references to detect circular references. """ function _resolve_refs(schema::AbstractDict, explored_refs = Any[schema]) if !haskey(schema, "\$ref") @@ -198,21 +194,22 @@ function _validate(x, schema, ::Val{:else}, val, path::String) end """ - - -https://json-schema.org/understanding-json-schema/reference/conditionals#ifthenelse + _if_then_else(x, schema, path) The if, then and else keywords allow the application of a subschema based on the outcome of another schema. Details are in the link and the truth table is as follows: - ┌─────┬──────┬──────┬────────┐ - │ if │ then │ else │ result │ - ├─────┼──────┼──────┼────────┤ - │ T │ T │ n/a │ T │ - │ T │ F │ n/a │ F │ - │ F │ n/a │ T │ T │ - │ F │ n/a │ F │ F │ - │ n/a │ n/a │ n/a │ T │ - └─────┴──────┴──────┴────────┘ +``` +┌─────┬──────┬──────┬────────┐ +│ if │ then │ else │ result │ +├─────┼──────┼──────┼────────┤ +│ T │ T │ n/a │ T │ +│ T │ F │ n/a │ F │ +│ F │ n/a │ T │ T │ +│ F │ n/a │ F │ F │ +│ n/a │ n/a │ n/a │ T │ +└─────┴──────┴──────┴────────┘ +https://json-schema.org/understanding-json-schema/reference/conditionals#ifthenelse +``` """ function _if_then_else(x, schema, path) val_if = _validate(x, schema["if"], path) From b758a3516da5a15c4e9baae30f5ddf813bc85204 Mon Sep 17 00:00:00 2001 From: YongheeKim Date: Thu, 27 Jun 2024 21:32:24 +0900 Subject: [PATCH 12/19] Applied JuliadFormatter --- src/JSONSchema.jl | 1 - src/schema.jl | 2 +- src/validation.jl | 9 ++++----- test/runtests.jl | 8 ++++++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/JSONSchema.jl b/src/JSONSchema.jl index 7b6ccfa..70d749f 100644 --- a/src/JSONSchema.jl +++ b/src/JSONSchema.jl @@ -5,7 +5,6 @@ module JSONSchema - import Downloads import JSON import JSON3 diff --git a/src/schema.jl b/src/schema.jl index 401ae95..b50c64d 100644 --- a/src/schema.jl +++ b/src/schema.jl @@ -47,7 +47,7 @@ function find_ref( if path == "" || path == "#" # This path refers to the root schema. return id_map[string(uri)] elseif startswith(path, "#/") # This path is a JPointer. - p = JSONPointer.Pointer(path; shift_index=true) + p = JSONPointer.Pointer(path; shift_index = true) return get_pointer(id_map[string(uri)], p) end uri = update_id(uri, path) diff --git a/src/validation.jl b/src/validation.jl index fdaf2aa..629897b 100644 --- a/src/validation.jl +++ b/src/validation.jl @@ -171,7 +171,7 @@ function _validate(x, schema, ::Val{:if}, val, path::String) if haskey(schema, "then") || haskey(schema, "else") ret = _if_then_else(x, schema, path) return ret - end + end return nothing end # 9.2.2.2: then @@ -180,7 +180,7 @@ function _validate(x, schema, ::Val{:then}, val, path::String) if haskey(schema, "if") ret = _if_then_else(x, schema, path) return ret - end + end return nothing end # 9.2.2.3: else @@ -189,7 +189,7 @@ function _validate(x, schema, ::Val{:else}, val, path::String) if haskey(schema, "if") ret = _if_then_else(x, schema, path) return ret - end + end return nothing end @@ -218,7 +218,7 @@ function _if_then_else(x, schema, path) end val_else = if haskey(schema, "else") _validate(x, schema["else"], path) - end + end if isnothing(val_if) if isnothing(val_then) && isnothing(val_else) return nothing @@ -232,7 +232,6 @@ function _if_then_else(x, schema, path) return nothing end - ### ### Checks for Arrays. ### diff --git a/test/runtests.jl b/test/runtests.jl index c290582..fe61bce 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -276,7 +276,9 @@ end ) @test_throws( - ArgumentError("JSON pointer does not match the data-structure. I tried (and failed) to index 1 with the key: Foo"), + ArgumentError( + "JSON pointer does not match the data-structure. I tried (and failed) to index 1 with the key: Foo", + ), JSONSchema.Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/Foo"}}, @@ -284,7 +286,9 @@ end }""") ) @test_throws( - ArgumentError("JSON pointer does not match the data-structure. I tried (and failed) to index Any[1, 2] with the key: Foo"), + ArgumentError( + "JSON pointer does not match the data-structure. I tried (and failed) to index Any[1, 2] with the key: Foo", + ), JSONSchema.Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/Foo"}}, From e2e551a76a3f484434cee887c5bc297c3c0bd97d Mon Sep 17 00:00:00 2001 From: YongheeKim Date: Fri, 28 Jun 2024 14:32:32 +0900 Subject: [PATCH 13/19] Revert "Replaced `get_element` methods with JSONPointer package" This reverts commit 064ab5bb562eacf4994d50e32fb2be138223fd43. --- Project.toml | 1 - src/JSONSchema.jl | 1 - src/schema.jl | 48 +++++++++++++++++++++++++++++++++++++++++++---- test/runtests.jl | 12 ++++-------- 4 files changed, 48 insertions(+), 14 deletions(-) diff --git a/Project.toml b/Project.toml index 8c15bad..ffb2da4 100644 --- a/Project.toml +++ b/Project.toml @@ -6,7 +6,6 @@ version = "1.3.0" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" -JSONPointer = "cc3ff66e-924d-4e6b-b111-1d9960e4bba9" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" [compat] diff --git a/src/JSONSchema.jl b/src/JSONSchema.jl index 70d749f..396486f 100644 --- a/src/JSONSchema.jl +++ b/src/JSONSchema.jl @@ -8,7 +8,6 @@ module JSONSchema import Downloads import JSON import JSON3 -using JSONPointer import URIs export Schema, validate diff --git a/src/schema.jl b/src/schema.jl index b50c64d..5406e65 100644 --- a/src/schema.jl +++ b/src/schema.jl @@ -3,6 +3,18 @@ # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. +# Transform escaped characters in JPaths back to their original value. +function unescape_jpath(raw::String) + ret = replace(replace(raw, "~0" => "~"), "~1" => "/") + m = match(r"%([0-9A-F]{2})", ret) + if m !== nothing + for c in m.captures + ret = replace(ret, "%$(c)" => Char(parse(UInt8, "0x$(c)"))) + end + end + return ret +end + function type_to_dict(x) return Dict(name => getfield(x, name) for name in fieldnames(typeof(x))) end @@ -23,6 +35,36 @@ function update_id(uri::URIs.URI, s::String) return URIs.URI(; els...) end +function get_element(schema, path::AbstractString) + for element in split(path, "/"; keepempty = false) + schema = _recurse_get_element(schema, unescape_jpath(String(element))) + end + return schema +end + +function _recurse_get_element(schema::Any, ::String) + return error( + "unmanaged type in ref resolution $(typeof(schema)): $(schema).", + ) +end + +function _recurse_get_element(schema::AbstractDict, element::String) + if !haskey(schema, element) + error("missing property '$(element)' in $(schema).") + end + return schema[element] +end + +function _recurse_get_element(schema::AbstractVector, element::String) + index = tryparse(Int, element) # Remember that `index` is 0-indexed! + if index === nothing + error("expected integer array index instead of '$(element)'.") + elseif index >= length(schema) + error("item index $(index) is larger than array $(schema).") + end + return schema[index+1] +end + function get_remote_schema(uri::URIs.URI) io = IOBuffer() r = Downloads.request(string(uri); output = io, throw = false) @@ -47,8 +89,7 @@ function find_ref( if path == "" || path == "#" # This path refers to the root schema. return id_map[string(uri)] elseif startswith(path, "#/") # This path is a JPointer. - p = JSONPointer.Pointer(path; shift_index = true) - return get_pointer(id_map[string(uri)], p) + return get_element(id_map[string(uri)], path[3:end]) end uri = update_id(uri, path) els = type_to_dict(uri) @@ -73,8 +114,7 @@ function find_ref( ).data end end - p = JSONPointer.Pointer(uri.fragment; shift_index = true) - return get_pointer(id_map[string(uri2)], p) + return get_element(id_map[string(uri2)], uri.fragment) end # Recursively find all "$ref" fields and resolve their path. diff --git a/test/runtests.jl b/test/runtests.jl index fe61bce..aa65798 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -267,7 +267,7 @@ end @testset "errors" begin @test_throws( - KeyError("Foo"), + ErrorException("missing property 'Foo' in $(Dict{String,Any}())."), JSONSchema.Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/Foo"}}, @@ -276,9 +276,7 @@ end ) @test_throws( - ArgumentError( - "JSON pointer does not match the data-structure. I tried (and failed) to index 1 with the key: Foo", - ), + ErrorException("unmanaged type in ref resolution $(Int64): 1."), JSONSchema.Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/Foo"}}, @@ -286,9 +284,7 @@ end }""") ) @test_throws( - ArgumentError( - "JSON pointer does not match the data-structure. I tried (and failed) to index Any[1, 2] with the key: Foo", - ), + ErrorException("expected integer array index instead of 'Foo'."), JSONSchema.Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/Foo"}}, @@ -296,7 +292,7 @@ end }""") ) @test_throws( - BoundsError, + ErrorException("item index 3 is larger than array $(Any[1, 2])."), JSONSchema.Schema("""{ "type": "object", "properties": {"version": {"\$ref": "#/definitions/3"}}, From f30ca920d2ce0dac0e6785a7eeef56ce140ef75b Mon Sep 17 00:00:00 2001 From: YongheeKim Date: Fri, 28 Jun 2024 14:37:29 +0900 Subject: [PATCH 14/19] cleanup _if_then_else --- src/validation.jl | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/validation.jl b/src/validation.jl index 629897b..3176e39 100644 --- a/src/validation.jl +++ b/src/validation.jl @@ -212,24 +212,14 @@ https://json-schema.org/understanding-json-schema/reference/conditionals#ifthene ``` """ function _if_then_else(x, schema, path) - val_if = _validate(x, schema["if"], path) - val_then = if haskey(schema, "then") - _validate(x, schema["then"], path) - end - val_else = if haskey(schema, "else") - _validate(x, schema["else"], path) - end - if isnothing(val_if) - if isnothing(val_then) && isnothing(val_else) - return nothing - else - return val_then + if _validate(x, schema["if"], path) !== nothing + if haskey(schema, "else") + return _validate(x, schema["else"], path) end + elseif haskey(schema, "then") + return _validate(x, schema["then"], path) end - if isa(val_if, SingleIssue) - return val_else - end - return nothing + return end ### From 6191fb60da44fd5303159ec9866e376e5e6752b3 Mon Sep 17 00:00:00 2001 From: YongheeKim Date: Fri, 28 Jun 2024 14:39:55 +0900 Subject: [PATCH 15/19] `return nothing` -> `return` to keep the same rule with orginal codes --- src/validation.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/validation.jl b/src/validation.jl index 3176e39..f121be0 100644 --- a/src/validation.jl +++ b/src/validation.jl @@ -172,7 +172,7 @@ function _validate(x, schema, ::Val{:if}, val, path::String) ret = _if_then_else(x, schema, path) return ret end - return nothing + return end # 9.2.2.2: then function _validate(x, schema, ::Val{:then}, val, path::String) @@ -181,7 +181,7 @@ function _validate(x, schema, ::Val{:then}, val, path::String) ret = _if_then_else(x, schema, path) return ret end - return nothing + return end # 9.2.2.3: else function _validate(x, schema, ::Val{:else}, val, path::String) @@ -190,7 +190,7 @@ function _validate(x, schema, ::Val{:else}, val, path::String) ret = _if_then_else(x, schema, path) return ret end - return nothing + return end """ From 2c21138a59130a1abe79a1366adc7585a2894eef Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sat, 29 Jun 2024 12:19:37 +1200 Subject: [PATCH 16/19] Update validation.jl --- src/validation.jl | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/validation.jl b/src/validation.jl index f121be0..680f9ef 100644 --- a/src/validation.jl +++ b/src/validation.jl @@ -169,26 +169,25 @@ end function _validate(x, schema, ::Val{:if}, val, path::String) # ignore if without then or else if haskey(schema, "then") || haskey(schema, "else") - ret = _if_then_else(x, schema, path) - return ret + return _if_then_else(x, schema, path) end return end + # 9.2.2.2: then function _validate(x, schema, ::Val{:then}, val, path::String) # ignore then without if if haskey(schema, "if") - ret = _if_then_else(x, schema, path) - return ret + return _if_then_else(x, schema, path) end return end + # 9.2.2.3: else function _validate(x, schema, ::Val{:else}, val, path::String) # ignore else without if if haskey(schema, "if") - ret = _if_then_else(x, schema, path) - return ret + return _if_then_else(x, schema, path) end return end @@ -196,7 +195,9 @@ end """ _if_then_else(x, schema, path) -The if, then and else keywords allow the application of a subschema based on the outcome of another schema. Details are in the link and the truth table is as follows: +The if, then and else keywords allow the application of a subschema based on the +outcome of another schema. Details are in the link and the truth table is as +follows: ``` ┌─────┬──────┬──────┬────────┐ @@ -208,8 +209,10 @@ The if, then and else keywords allow the application of a subschema based on the │ F │ n/a │ F │ F │ │ n/a │ n/a │ n/a │ T │ └─────┴──────┴──────┴────────┘ -https://json-schema.org/understanding-json-schema/reference/conditionals#ifthenelse ``` + +See https://json-schema.org/understanding-json-schema/reference/conditionals#ifthenelse +for details. """ function _if_then_else(x, schema, path) if _validate(x, schema["if"], path) !== nothing From 24e52d14d7507d2c6de764390e0a32ec12019c7d Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sat, 29 Jun 2024 12:21:52 +1200 Subject: [PATCH 17/19] Update validation.jl --- src/validation.jl | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/validation.jl b/src/validation.jl index 680f9ef..bebddeb 100644 --- a/src/validation.jl +++ b/src/validation.jl @@ -98,12 +98,6 @@ function _validate_entry(x, schema::Bool, path::String) return schema ? nothing : SingleIssue(x, path, "schema", schema) end -""" - _resolve_refs(schema, explored_refs = Any[]) - -Resolves any `"\$ref"` keys it encounters. -Note: This is recursive function and will continue to resolve references until no more are found. -""" function _resolve_refs(schema::AbstractDict, explored_refs = Any[schema]) if !haskey(schema, "\$ref") return schema @@ -171,7 +165,7 @@ function _validate(x, schema, ::Val{:if}, val, path::String) if haskey(schema, "then") || haskey(schema, "else") return _if_then_else(x, schema, path) end - return + return end # 9.2.2.2: then @@ -180,7 +174,7 @@ function _validate(x, schema, ::Val{:then}, val, path::String) if haskey(schema, "if") return _if_then_else(x, schema, path) end - return + return end # 9.2.2.3: else @@ -189,7 +183,7 @@ function _validate(x, schema, ::Val{:else}, val, path::String) if haskey(schema, "if") return _if_then_else(x, schema, path) end - return + return end """ From 7891e71f10b1b98e43cb924a87486ad66329dd61 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sat, 29 Jun 2024 14:32:15 +1200 Subject: [PATCH 18/19] Update validation.jl --- src/validation.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/validation.jl b/src/validation.jl index bebddeb..9a3f4ab 100644 --- a/src/validation.jl +++ b/src/validation.jl @@ -209,6 +209,7 @@ See https://json-schema.org/understanding-json-schema/reference/conditionals#ift for details. """ function _if_then_else(x, schema, path) + error("Test that this actually gets run") if _validate(x, schema["if"], path) !== nothing if haskey(schema, "else") return _validate(x, schema["else"], path) From 6ccd592a17c4a193989d1362394df96ee26aa0ed Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sat, 29 Jun 2024 14:39:53 +1200 Subject: [PATCH 19/19] Update src/validation.jl --- src/validation.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/validation.jl b/src/validation.jl index 9a3f4ab..bebddeb 100644 --- a/src/validation.jl +++ b/src/validation.jl @@ -209,7 +209,6 @@ See https://json-schema.org/understanding-json-schema/reference/conditionals#ift for details. """ function _if_then_else(x, schema, path) - error("Test that this actually gets run") if _validate(x, schema["if"], path) !== nothing if haskey(schema, "else") return _validate(x, schema["else"], path)