Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support if-then-else #37 #53

Merged
merged 22 commits into from
Jun 29, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b6ff3b1
use JSONPointer backend
YongHee-Kim Oct 3, 2020
45f9c04
fix test for exceptions
YongHee-Kim Oct 3, 2020
5c87171
fix uri fragment pointer
YongHee-Kim Oct 4, 2020
31ea926
fix index shfit bug
YongHee-Kim Oct 4, 2020
29cdf20
revert unnessary line chagne
YongHee-Kim Oct 4, 2020
1e4c162
remove `isnothing` for julia 1.0
YongHee-Kim Oct 6, 2020
270bc76
Merge branch 'master' of https://github.com/YongHee-Kim/JSONSchema.jl
YongHee-Kim Jul 11, 2021
d66b48d
Merge branch 'master' of https://github.com/fredo-dedup/JSONSchema.jl…
YongHee-Kim Sep 12, 2021
d5674e3
Merge remote-tracking branch 'upstream/master'
YongHee-Kim Jun 24, 2024
efc6bd0
added JSONPointer dependency
YongHee-Kim Jun 26, 2024
064ab5b
Replaced `get_element` methods with JSONPointer package
YongHee-Kim Jun 26, 2024
7a481ec
added `_if_then_esle` validation for draft7 #37
YongHee-Kim Jun 27, 2024
e9b81d1
Included draft 7 in the testt
YongHee-Kim Jun 27, 2024
6ee43bd
fixed docstring for _if_then_else
YongHee-Kim Jun 27, 2024
b758a35
Applied JuliadFormatter
YongHee-Kim Jun 27, 2024
e2e551a
Revert "Replaced `get_element` methods with JSONPointer package"
YongHee-Kim Jun 28, 2024
f30ca92
cleanup _if_then_else
YongHee-Kim Jun 28, 2024
6191fb6
`return nothing` -> `return` to keep the same rule with orginal codes
YongHee-Kim Jun 28, 2024
2c21138
Update validation.jl
odow Jun 29, 2024
24e52d1
Update validation.jl
odow Jun 29, 2024
7891e71
Update validation.jl
odow Jun 29, 2024
6ccd592
Update src/validation.jl
odow Jun 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions src/JSONSchema.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module JSONSchema
import Downloads
import JSON
import JSON3
using JSONPointer
import URIs

export Schema, validate
Expand Down
48 changes: 4 additions & 44 deletions src/schema.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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.
Expand Down
71 changes: 69 additions & 2 deletions src/validation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ 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
Expand Down Expand Up @@ -160,10 +166,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

"""
_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 │
└─────┴──────┴──────┴────────┘
https://json-schema.org/understanding-json-schema/reference/conditionals#ifthenelse
```
"""
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have made a mistake, but I think you can write this code as:

function _if_then_else(x, schema, path)
    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
    return
end

but what about the case where you have a if-then and the if fails to validate?


###
### Checks for Arrays.
Expand Down
16 changes: 11 additions & 5 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -265,7 +267,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"}},
Expand All @@ -274,23 +276,27 @@ 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"}},
"definitions": 1
}""")
)
@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"}},
"definitions": [1, 2]
}""")
)
@test_throws(
ErrorException("item index 3 is larger than array $(Any[1, 2])."),
BoundsError,
JSONSchema.Schema("""{
"type": "object",
"properties": {"version": {"\$ref": "#/definitions/3"}},
Expand Down
Loading