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
4 changes: 3 additions & 1 deletion docs/src/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ Implemented model behavior includes:

- objects, arrays, tuples, dictionaries, primitives, enums, and nullable types;
- required, optional, and explicit-null values;
- `allOf`, `oneOf`, `anyOf`, and discriminators;
- `allOf`, `oneOf`, `anyOf`, and discriminators, whose `mapping` and
`defaultMapping` values may be same-document, relative, or cross-file URI
references resolved like `$ref`;
- recursive models and recursive aliases;
- `additionalProperties`, `patternProperties`, `propertyNames`, and closed
objects;
Expand Down
4 changes: 4 additions & 0 deletions docs/src/pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ The default retriever has conservative access rules:
- HTTP redirects are not followed.
- Unsupported URI schemes are rejected.

Discriminator `mapping` and `defaultMapping` URI references are retrieved under
the same rules as `$ref`, relative to the document that holds the
`discriminator`.

Pass an `OpenAPI.SchemaEngine.Resources.AbstractRetriever` with `retriever=...`
when an application needs another retrieval policy or an in-memory resource
store. Resource size and count limits still apply.
Expand Down
41 changes: 41 additions & 0 deletions src/normalize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,46 @@ function _portable_schema_ids(context::NormalizationContext, schemas)
return output
end

# JSON Pointer, relative to a schema object, of a discriminator mapping value.
# Planning derives the same key to look up the binding the compiler recorded.
function _discriminator_mapping_pointer(tag::AbstractString)
return string(Resources.JSONPointer(("discriminator", "mapping", String(tag))))
end

const _DISCRIMINATOR_DEFAULT_POINTER = "/discriminator/defaultMapping"

# OAS 3.1.1 §4.8.25: a mapping value is either a schema name or a URI
# reference, and an ambiguous bare value such as "Cat" is a schema name; authors
# write "./Cat" to force a URI reference. Only URI-shaped values are references.
function _uri_reference_mapping_value(value::AbstractString)
return occursin('/', value) || occursin('#', value) || occursin(':', value)
end

# `discriminator.mapping` values are URI references that live outside the JSON
# Schema vocabulary, so the schema engine does not see them as references.
# Declare them as optional references so the compiler resolves them against the
# schema's real base URI, retrieving cross-file targets like `\$ref`, before the
# graph is rebased onto portable identifiers. Planning reads the bindings.
function _discriminator_references(schema::AbstractDict)
discriminator = get(schema, "discriminator", nothing)
discriminator isa AbstractDict || return ()
references = Tuple{String,String}[]
mapping = get(discriminator, "mapping", nothing)
if mapping isa AbstractDict
for tag in sort!(String[String(tag) for tag in keys(mapping)])
value = mapping[tag]
value isa AbstractString && _uri_reference_mapping_value(value) ||
continue
push!(references, (_discriminator_mapping_pointer(tag), String(value)))
end
end
default = get(discriminator, "defaultMapping", nothing)
if default isa AbstractString && _uri_reference_mapping_value(default)
push!(references, (_DISCRIMINATOR_DEFAULT_POINTER, String(default)))
end
return references
end

function _compile_schemas!(context::NormalizationContext)
isempty(context.schema_cache) && return
handles = sort(
Expand All @@ -678,6 +718,7 @@ function _compile_schemas!(context::NormalizationContext)
max_resources = context.resolver.max_resources,
max_nodes = context.resolver.max_nodes,
max_depth = context.resolver.max_depth,
extra_references = _discriminator_references,
)
catch error
location = error isa SchemaEngine.CompilationError ? error.location : first(roots)
Expand Down
106 changes: 64 additions & 42 deletions src/planning.jl
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,52 @@ function _reference_view(view::SchemaView, reference::AbstractString)
)
end

# Resolve a discriminator `mapping` or `defaultMapping` value. The schema
# compiler already resolved URI-shaped values (see `_discriminator_references`)
# against the pre-rebase base URI of the schema that owns the discriminator,
# retrieving cross-file targets, and recorded either the binding or a failure.
# Values the compiler was not asked about fall back to same-document lookup.
# Emit a diagnostic with `code` and return `nothing` when the value does not
# name a schema.
function _discriminator_target(
context,
owner::SchemaView,
pointer::String,
reference::AbstractString,
code::Symbol,
label::String,
)
compiled = owner.compiled
location = SourceLocation(owner.node.resource, owner.node.pointer)
if compiled !== nothing
target = SchemaEngine.reference_target(compiled, owner.node, pointer)
if target !== nothing
resource = Resources.resource(compiled.registry, target.resource)
value = Resources.resolve(resource.contents, target.pointer)
return SchemaView(value, target, owner.version, compiled)
end
failure = SchemaEngine.reference_failure(compiled, owner.node, pointer)
if failure !== nothing
_error!(context.bag, code, "cannot resolve $label: $failure", location)
return nothing
end
end
target = try
_reference_view(owner, reference)
catch error
_error!(
context.bag,
code,
"cannot resolve $label: $(sprint(showerror, error))",
location,
)
return nothing
end
target === nothing &&
_error!(context.bag, code, "$label does not resolve to a schema", location)
return target
end

function _plan_union!(context, view, suggested, mode, keyword)
resolved = _resolved_view(view)
union_owner = something(_keyword_owner(resolved, keyword), resolved)
Expand Down Expand Up @@ -1035,27 +1081,15 @@ function _plan_union!(context, view, suggested, mode, keyword)
get(discriminator, "mapping", nothing) isa AbstractDict
for (tag, reference) in discriminator["mapping"]
reference isa AbstractString || continue
target = try
_reference_view(resolved, reference)
catch error
_error!(
context.bag,
:invalid_discriminator_mapping,
"cannot resolve discriminator mapping $(repr(tag)): $(sprint(showerror, error))",
SourceLocation(resolved.node.resource, resolved.node.pointer),
)
missing
end
target === missing && continue
if target === nothing
_error!(
context.bag,
:invalid_discriminator_mapping,
"discriminator mapping $(repr(tag)) does not resolve to a schema",
SourceLocation(resolved.node.resource, resolved.node.pointer),
)
continue
end
target = _discriminator_target(
context,
discriminator_owner,
_discriminator_mapping_pointer(tag),
reference,
:invalid_discriminator_mapping,
"discriminator mapping $(repr(tag))",
)
target === nothing && continue
target_type = _type_for!(
context,
target,
Expand All @@ -1071,27 +1105,15 @@ function _plan_union!(context, view, suggested, mode, keyword)
default_mapping = nothing
if discriminator isa AbstractDict &&
get(discriminator, "defaultMapping", nothing) isa AbstractString
target = try
_reference_view(resolved, discriminator["defaultMapping"])
catch error
_error!(
context.bag,
:invalid_discriminator_default,
"cannot resolve discriminator defaultMapping: $(sprint(showerror, error))",
SourceLocation(resolved.node.resource, resolved.node.pointer),
)
missing
end
if target === missing
nothing
elseif target === nothing
_error!(
context.bag,
:invalid_discriminator_default,
"discriminator defaultMapping does not resolve to a schema",
SourceLocation(resolved.node.resource, resolved.node.pointer),
)
else
target = _discriminator_target(
context,
discriminator_owner,
_DISCRIMINATOR_DEFAULT_POINTER,
discriminator["defaultMapping"],
:invalid_discriminator_default,
"discriminator defaultMapping",
)
if target !== nothing
target_type = _type_for!(context, target, suggested * "Default", mode)
push!(types, target_type)
default_mapping = target.node => target_type
Expand Down
Loading
Loading