Skip to content
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
34 changes: 34 additions & 0 deletions src/runtime.jl
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ struct Spec
default_server::String
server::Base.RefValue{String}
graphs::Dict{Symbol,Any}
subschemas::Dict{Tuple{Symbol,String,String},Any}
graph_lock::ReentrantLock
# Generated data is required and name-mapped so an omitted keyword or a
# declaration reorder cannot silently substitute an empty or adjacent
Expand All @@ -88,6 +89,7 @@ struct Spec
default_server = normalized_server,
server = Ref(normalized_server),
graphs = Dict{Symbol,Any}(),
subschemas = Dict{Tuple{Symbol,String,String},Any}(),
graph_lock = ReentrantLock(),
)
ordered = map(field -> getproperty(values, field), fieldnames(Spec))
Expand Down Expand Up @@ -221,6 +223,17 @@ end

function _schema_at(spec::Spec, descriptor, direction::Symbol = :neutral)
descriptor === nothing && return nothing
# Every validation of a generated model asks for the same few views, and building one
# parses the resource URI and walks its pointer; keep each view once it exists.
key = (direction, String(descriptor.resource), String(descriptor.pointer))
cached = lock(() -> get(spec.subschemas, key, nothing), spec.graph_lock)
cached === nothing || return cached
schema = _build_schema_at(spec, descriptor, direction)
lock(() -> (spec.subschemas[key] = schema), spec.graph_lock)
return schema
end

function _build_schema_at(spec::Spec, descriptor, direction::Symbol)
graph = _schema_graph(spec, direction)
graph === nothing && throw(ArgumentError(
"generated schema metadata has a descriptor but no schema roots; regenerate the module",
Expand Down Expand Up @@ -639,6 +652,27 @@ function _decode_union(
if value === nothing && Nothing in variants
return nothing
end
# `Union{Absent, Nothing, X}` is how every optional field is typed. With one variant to
# try, the ordering and collection below reduce to decoding it, so skip them.
candidate = nothing
candidates = 0
for variant in variants
variant in (Absent, Nothing) && continue
candidates += 1
candidate = variant
end
if candidates == 1
try
return _decode(candidate, value, validate)
catch error
error isa DecodeError || rethrow()
throw(
DecodeError(
"value does not match any variant of $T: " * error.message,
),
)
end
end
if !oneof
preferred = Any[
variant for variant in variants
Expand Down
96 changes: 96 additions & 0 deletions src/schema_engine/compiled.jl
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,64 @@ function _register_dialect_aliases!(compiler::Compiler, aliases::AbstractDict)
return compiler
end

"""
Evaluation shortcuts resolved once per compiled graph, indexed by `CompiledNode.index`.

`refs[i]` is node `i`'s static `\$ref` target (the reference table's `NodeId` and the node it
compiles to); `properties[i]` is node `i`'s `properties` children in schema order. Either is
`nothing` when the node has none, or when some part could not be resolved at compile time, in
which case evaluation takes the general path exactly as before. They replace a `NodeId`- or
string-tuple-keyed lookup per `\$ref` and per property, which dominated validation time.
"""
struct JumpTable
refs::Vector{Union{Nothing,Tuple{Resources.NodeId,CompiledNode}}}
properties::Vector{Union{Nothing,Vector{Tuple{String,CompiledNode}}}}
end

function JumpTable(
evaluation_nodes::AbstractDict,
transitions::AbstractDict,
references::AbstractDict,
registry,
)
count = maximum((node.index for node in values(evaluation_nodes)); init = 0)
refs = Vector{Union{Nothing,Tuple{Resources.NodeId,CompiledNode}}}(nothing, count)
properties = Vector{Union{Nothing,Vector{Tuple{String,CompiledNode}}}}(nothing, count)
for node in values(evaluation_nodes)
value = node.value
value isa AbstractDict || continue
if get(value, "\$ref", nothing) isa AbstractString
target = get(references, (node.id, "\$ref"), nothing)
if target !== nothing
compiled = get(evaluation_nodes, target, nothing)
if compiled === nothing
compiled = get(
evaluation_nodes,
Resources.canonical(registry, target),
nothing,
)
end
compiled === nothing || (refs[node.index] = (target, compiled))
end
end
declared = get(value, "properties", nothing)
declared isa AbstractDict || continue
children = Tuple{String,CompiledNode}[]
complete = true
for (name, subschema) in declared
(subschema isa AbstractDict || subschema isa Bool) || continue
child = get(transitions, (node.index, ("properties", String(name))), nothing)
if child === nothing
complete = false
break
end
push!(children, (String(name), child))
end
complete && (properties[node.index] = children)
end
return JumpTable(refs, properties)
end

"""A non-mutating, dialect-aware JSON Schema resource graph."""
struct CompiledSchema{R<:Resources.AbstractRetriever}
data::Union{Resources.FrozenObject,Bool}
Expand All @@ -122,6 +180,42 @@ struct CompiledSchema{R<:Resources.AbstractRetriever}
reference_failures::ReferenceFailures
regexes::Dict{String,Regex}
retriever::R
jumps::JumpTable
end

function CompiledSchema(
data,
dialect,
registry,
root,
dialects,
dialect_aliases,
evaluation_nodes,
transitions,
uses_annotations,
recursive_anchors,
references,
reference_failures,
regexes,
retriever,
)
return CompiledSchema(
data,
dialect,
registry,
root,
dialects,
dialect_aliases,
evaluation_nodes,
transitions,
uses_annotations,
recursive_anchors,
references,
reference_failures,
regexes,
retriever,
JumpTable(evaluation_nodes, transitions, references, registry),
)
end

"""A compiled graph with multiple JSON Schema roots embedded in JSON resources."""
Expand Down Expand Up @@ -1524,6 +1618,7 @@ function select(schemas::CompiledSchemas, requested::Resources.NodeId)
getfield(template, :reference_failures),
getfield(template, :regexes),
template.retriever,
getfield(template, :jumps),
)
end

Expand Down Expand Up @@ -1567,6 +1662,7 @@ function subschema(template::CompiledSchema, requested::Resources.NodeId)
getfield(template, :reference_failures),
getfield(template, :regexes),
template.retriever,
getfield(template, :jumps),
)
end

Expand Down
Loading
Loading