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

Fixes and improvements for the 23.1.0 test schema #57

Merged
merged 5 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 39 additions & 15 deletions src/schema.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,26 @@ function update_id(uri::URIs.URI, s::String)
delete!(els, :uri)
els[:fragment] = id2.fragment
if !isempty(id2.path)
oldpath = match(r"^(.*/).*$", uri.path)
els[:path] =
oldpath === nothing ? id2.path : oldpath.captures[1] * id2.path
if startswith(id2.path, "/") # Absolute path
els[:path] = id2.path
else # Relative path
old_path = match(r"^(.*/).*$", uri.path)
if old_path === nothing
els[:path] = id2.path
else
els[:path] = old_path.captures[1] * id2.path
end
end
end
return URIs.URI(; els...)
end

function get_element(schema, path::AbstractString)
for element in split(path, "/"; keepempty = false)
elements = split(path, "/"; keepempty = true)
if isempty(first(elements))
popfirst!(elements)
end
for element in elements
schema = _recurse_get_element(schema, unescape_jpath(String(element)))
end
return schema
Expand Down Expand Up @@ -102,16 +113,17 @@ function find_ref(
end
if !haskey(id_map, string(uri2))
# id_map doesn't have this key so, fetch the ref and add it to id_map.
id_map[string(uri2)] = if startswith(uri2.scheme, "http")
if startswith(uri2.scheme, "http")
@info("fetching remote ref $(uri2)")
get_remote_schema(uri2).data
id_map[string(uri2)] = get_remote_schema(uri2).data
else
@assert is_file_uri
@info("loading local ref $(uri2)")
Schema(
local_schema = Schema(
JSON.parsefile(uri2.path);
parent_dir = dirname(uri2.path),
).data
)
id_map[string(uri2)] = local_schema.data
end
end
return get_element(id_map[string(uri2)], uri.fragment)
Expand Down Expand Up @@ -139,6 +151,17 @@ function resolve_refs!(
id_map::AbstractDict,
parent_dir::String,
)
# This $ref has not been resolved yet (otherwise it would not be a String).
# We will replace the path string with the schema element pointed at, thus
# marking it as resolved. This should prevent infinite recursions caused by
# self referencing. We also unpack the $ref first so that fields like $id
# do not interfere with it.
ref = get(schema, "\$ref", nothing)
ref_unpacked = false
if ref isa String
schema["\$ref"] = find_ref(uri, id_map, ref, parent_dir)
ref_unpacked = true
end
if haskey(schema, "id") && schema["id"] isa String
# This block is for draft 4.
uri = update_id(uri, schema["id"])
Expand All @@ -148,12 +171,10 @@ function resolve_refs!(
uri = update_id(uri, schema["\$id"])
end
for (k, v) in schema
if k == "\$ref" && v isa String
# This ref has not been resolved yet (otherwise it would not be a String).
# We will replace the path string with the schema element pointed at, thus
# marking it as resolved. This should prevent infinite recursions caused by
# self referencing.
schema["\$ref"] = find_ref(uri, id_map, v, parent_dir)
if k == "\$ref" && ref_unpacked
continue # We've already unpacked this ref
elseif k in ("enum", "const")
continue # Don't unpack refs inside const and enum.
else
resolve_refs!(v, uri, id_map, parent_dir)
end
Expand Down Expand Up @@ -193,7 +214,10 @@ function build_id_map!(
uri = update_id(uri, schema["\$id"])
id_map[string(uri)] = schema
end
for value in values(schema)
for (k, value) in schema
if k == "enum" || k == "const"
continue
end
build_id_map!(id_map, value, uri)
end
return
Expand Down
56 changes: 42 additions & 14 deletions src/validation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,28 @@ _resolve_refs(schema, explored_refs = Any[]) = schema
# Default fallback
_validate(::Any, ::Any, ::Val, ::Any, ::String) = nothing

# JSON treats == between Bool and Number differently to Julia, so:
# false != 0
# true != 1
# 0 == 0.0
# 1.0 == 1
_isequal(x, y) = x == y

_isequal(::Bool, ::Number) = false

_isequal(::Number, ::Bool) = false

_isequal(x::Bool, y::Bool) = x == y

function _isequal(x::Vector, y::Vector)
return length(x) == length(y) && all(_isequal.(x, y))
end

function _isequal(x::Dict, y::Dict)
return Set(keys(x)) == Set(keys(y)) &&
all(_isequal(v, y[k]) for (k, v) in x)
end

###
### Core JSON Schema
###
Expand Down Expand Up @@ -471,6 +493,7 @@ _is_type(::Any, ::Val) = false
_is_type(::Array, ::Val{:array}) = true
_is_type(::Bool, ::Val{:boolean}) = true
_is_type(::Integer, ::Val{:integer}) = true
_is_type(x::Float64, ::Val{:integer}) = isinteger(x)
_is_type(::Real, ::Val{:number}) = true
_is_type(::Nothing, ::Val{:null}) = true
_is_type(::Missing, ::Val{:null}) = true
Expand All @@ -482,15 +505,15 @@ _is_type(::Bool, ::Val{:integer}) = false

# 6.1.2
function _validate(x, schema, ::Val{:enum}, val, path::String)
if !any(x == v for v in val)
if !any(_isequal(x, v) for v in val)
return SingleIssue(x, path, "enum", val)
end
return
end

# 6.1.3
function _validate(x, schema, ::Val{:const}, val, path::String)
if x != val
if !_isequal(x, val)
return SingleIssue(x, path, "const", val)
end
return
Expand All @@ -508,7 +531,8 @@ function _validate(
val::Number,
path::String,
)
if !isapprox(x / val, round(x / val))
y = x / val
if !isfinite(y) || !isapprox(y, round(y))
return SingleIssue(x, path, "multipleOf", val)
end
return
Expand Down Expand Up @@ -605,7 +629,7 @@ function _validate(
x::String,
schema,
::Val{:maxLength},
val::Integer,
val::Union{Integer,Float64},
path::String,
)
if length(x) > val
Expand All @@ -619,7 +643,7 @@ function _validate(
x::String,
schema,
::Val{:minLength},
val::Integer,
val::Union{Integer,Float64},
path::String,
)
if length(x) < val
Expand Down Expand Up @@ -651,7 +675,7 @@ function _validate(
x::AbstractVector,
schema,
::Val{:maxItems},
val::Integer,
val::Union{Integer,Float64},
path::String,
)
if length(x) > val
Expand All @@ -665,7 +689,7 @@ function _validate(
x::AbstractVector,
schema,
::Val{:minItems},
val::Integer,
val::Union{Integer,Float64},
path::String,
)
if length(x) < val
Expand All @@ -682,11 +706,15 @@ function _validate(
val::Bool,
path::String,
)
# It isn't sufficient to just compare allunique on x, because Julia treats 0 == false,
# but JSON distinguishes them.
y = [(xx, typeof(xx)) for xx in x]
if val && !allunique(y)
return SingleIssue(x, path, "uniqueItems", val)
if !val
return
end
# TODO(odow): O(n^2) here. But probably not too bad, because there shouldn't
# be a large x.
for i in eachindex(x), j in eachindex(x)
if i != j && _isequal(x[i], x[j])
return SingleIssue(x, path, "uniqueItems", val)
end
end
return
end
Expand All @@ -704,7 +732,7 @@ function _validate(
x::AbstractDict,
schema,
::Val{:maxProperties},
val::Integer,
val::Union{Integer,Float64},
path::String,
)
if length(x) > val
Expand All @@ -718,7 +746,7 @@ function _validate(
x::AbstractDict,
schema,
::Val{:minProperties},
val::Integer,
val::Union{Integer,Float64},
path::String,
)
if length(x) < val
Expand Down
Loading