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
8 changes: 5 additions & 3 deletions src/normalize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -587,9 +587,11 @@ function _portable_schema_ids(context::NormalizationContext, schemas)
registry,
context.resolver.root.resource.retrieval,
)
labels = Dict{Resources.ResourceId,String}(
primary.id => "root-" * first(_content_digest(primary.contents), 20),
)
# The primary label is constant. Labels of referenced resources derive from
# their parent label and reference location, so an unrelated root edit must
# not rotate every generated schema ID. IDs are scoped to one generated
# module, and the "external-" and "resource-" prefixes cannot collide with it.
labels = Dict{Resources.ResourceId,String}(primary.id => "root")

references = collect(getfield(template, :references))
while true
Expand Down
98 changes: 98 additions & 0 deletions test/references.jl
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,104 @@
@test !occursin("token=secret", credentialed)
end

@testset "generated schema IDs survive unrelated root edits" begin
schema_ids(source) = [
match.captures[1] for match in eachmatch(
r"^ \(id = \"([^\"]+)\", retrieval = "m,
source,
)
]
response(schema) = OpenAPI.obj(
"200" => OpenAPI.obj(
"description" => "value",
"content" => OpenAPI.obj(
"application/json" => OpenAPI.obj("schema" => schema),
),
),
)
document = minimal_openapi(
"3.1.1",
OpenAPI.obj(
"/local" => OpenAPI.obj(
"get" => OpenAPI.obj(
"operationId" => "getLocal",
"responses" => response(
OpenAPI.obj("\$ref" => "#/components/schemas/Local"),
),
),
),
"/value" => OpenAPI.obj(
"get" => OpenAPI.obj(
"operationId" => "getValue",
"responses" => response(
OpenAPI.obj("\$ref" => "./common.json#/\$defs/Value"),
),
),
),
),
)
document["components"] = OpenAPI.obj(
"schemas" => OpenAPI.obj(
"Local" => OpenAPI.obj(
"type" => "object",
"required" => ["name"],
"properties" => OpenAPI.obj(
"name" => OpenAPI.obj("type" => "string"),
),
),
),
)
common = OpenAPI.obj(
"\$defs" => OpenAPI.obj(
"Value" => OpenAPI.obj(
"type" => "object",
"required" => ["id", "nested"],
"properties" => OpenAPI.obj(
"id" => OpenAPI.obj("type" => "integer"),
"nested" => OpenAPI.obj("\$ref" => "./nested.json"),
),
),
),
)
nested = OpenAPI.obj(
"type" => "object",
"properties" => OpenAPI.obj("flag" => OpenAPI.obj("type" => "boolean")),
)
directory = mktempdir()
write(joinpath(directory, "common.json"), JSON.json(common))
write(joinpath(directory, "nested.json"), JSON.json(nested))
path = joinpath(directory, "openapi.json")
write(path, JSON.json(document))
before = OpenAPI.client(path; name = "StableIdClient")

edited = deepcopy(document)
edited["info"]["version"] = "2.0.0"
edited["info"]["description"] = "An unrelated root edit."
write(path, JSON.json(edited))
after = OpenAPI.client(path; name = "StableIdClient")

ids = schema_ids(before)
@test length(ids) == 3
@test "https://openapi.invalid/schema/root.json" in ids
@test count(id -> occursin("/schema/external-", id), ids) == 2
@test schema_ids(after) == ids

# Only the header and the embedded root document change.
before_lines = split(before, '\n')
after_lines = split(after, '\n')
@test length(before_lines) == length(after_lines)
changed = [
index for index in eachindex(before_lines)
if before_lines[index] != after_lines[index]
]
@test length(changed) == 2
@test startswith(after_lines[changed[1]], "# Generated by OpenAPI.jl")
@test startswith(
after_lines[changed[2]],
" (id = \"https://openapi.invalid/schema/root.json\"",
)
end

@testset "HTTP origin policy and redirects" begin
external_schema = JSON.json(
OpenAPI.obj(
Expand Down
Loading