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
16 changes: 14 additions & 2 deletions lib/permittable/json_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,16 @@ def nullify!(schema, field)
schema
end

# deep_dup, not dup: .freeze is shallow, so the Array nested in an entry
# like :decimal ("type" => %w[string number]) stays live inside the frozen
# top-level Hash, and a shallow .dup would hand every :decimal field's
# exported schema that SAME Array. Nothing in this file mutates it in
# place (nullify! rebinds "type" to a new Array), but the exported
# document is caller-owned data, and a caller appending to it would
# otherwise silently rewrite the constant for every schema exported
# afterward in the process.
def scalar_schema(field)
schema = SCALAR_SCHEMAS.fetch(field[:type]).dup
schema = SCALAR_SCHEMAS.fetch(field[:type]).deep_dup
apply_format_name!(schema, field)
apply_in!(schema, field)
apply_string_bounds!(schema, field)
Expand Down Expand Up @@ -151,7 +159,11 @@ def array_schema(field, unknown:)
min, max = length_bounds(field[:length])
schema["minItems"] = min if min
schema["maxItems"] = max if max
schema["items"] = field[:fields] ? object(field[:fields], unknown: unknown) : SCALAR_SCHEMAS.fetch(field[:of]).dup
# deep_dup here for the same reason as scalar_schema: a scalar `of:`
# otherwise hands every array field the SAME nested Array/Hash from
# SCALAR_SCHEMAS.
schema["items"] =
field[:fields] ? object(field[:fields], unknown: unknown) : SCALAR_SCHEMAS.fetch(field[:of]).deep_dup
schema
end

Expand Down
26 changes: 26 additions & 0 deletions spec/json_schema_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ def quietly
expect(props["day"]).to eq("type" => "string", "format" => "date")
expect(props["at"]).to eq("type" => "string", "format" => "date-time")
end

it "deep-dups a SCALAR_SCHEMAS entry, so mutating one field's exported type array never leaks " \
"into the frozen constant or any other field's schema" do
type_array = property("d") { optional :d, :decimal }["type"]
expect(type_array).not_to equal(Permittable::JsonSchema::SCALAR_SCHEMAS[:decimal]["type"])

type_array << "null"

expect(Permittable::JsonSchema::SCALAR_SCHEMAS[:decimal]["type"]).to eq(%w[string number])
expect(property("d2") { optional :d2, :decimal }["type"]).to eq(%w[string number])
ensure
Permittable::JsonSchema::SCALAR_SCHEMAS[:decimal]["type"].delete("null")
end
end

describe "required and absence semantics" do
Expand Down Expand Up @@ -649,6 +662,19 @@ def server_accepts?(field_rule, value)
expect(items["required"]).to eq(%w[sku quantity])
expect(items["properties"]["quantity"]).to include("minimum" => 1, "maximum" => 99)
end

it "deep-dups a SCALAR_SCHEMAS entry for `of:`, so mutating one array field's items type never leaks " \
"into the frozen constant or any other field's schema" do
items_type = property("ds") { array :ds, of: :decimal }["items"]["type"]
expect(items_type).not_to equal(Permittable::JsonSchema::SCALAR_SCHEMAS[:decimal]["type"])

items_type << "null"

expect(Permittable::JsonSchema::SCALAR_SCHEMAS[:decimal]["type"]).to eq(%w[string number])
expect(property("ds2") { array :ds2, of: :decimal }["items"]["type"]).to eq(%w[string number])
ensure
Permittable::JsonSchema::SCALAR_SCHEMAS[:decimal]["type"].delete("null")
end
end

describe "root:" do
Expand Down
Loading