Skip to content

Commit

Permalink
Fixes and improvements for the 23.1.0 test schema (#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
odow authored Aug 23, 2024
1 parent e152668 commit a1245c8
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 29 deletions.
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

0 comments on commit a1245c8

Please sign in to comment.