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
28 changes: 28 additions & 0 deletions lib/resty/openapi_validator/params.lua
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,34 @@ local function deserialize_param(raw_value, param, query_args)

local stype = schema.type

-- deepObject style with anyOf/oneOf: try the object branch via parse_deep_object;
-- if no param[...] keys are present, coerce the bare param value against the
-- full schema (collect_types handles anyOf/oneOf branches) and let the
-- downstream jsonschema validator pick the matching branch.
if style == "deepObject" and stype ~= "object"
and (schema.anyOf or schema.oneOf) then
local branches = schema.anyOf or schema.oneOf
local function branch_has_object(b)
return collect_types(b)["object"] == true
end
for _, branch in ipairs(branches) do
if branch_has_object(branch) then
Comment thread
coderabbitai[bot] marked this conversation as resolved.
local obj = parse_deep_object(param.name, query_args or {}, branch)
if obj ~= nil then
return obj
end
end
end
local scalar_raw = (query_args or {})[param.name]
if scalar_raw ~= nil then
if type(scalar_raw) == "table" then
scalar_raw = scalar_raw[1]
end
return coerce_value(scalar_raw, schema)
end
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return nil
end

if stype == "array" then
local items_schema = schema.items or {}
local values
Expand Down
153 changes: 153 additions & 0 deletions t/unit/test_params.lua
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,157 @@ T.describe("params: error format", function()
T.like(formatted, "path parameter 'id'", "format includes path param")
end)

-- Test: deepObject query param with anyOf {object, integer} schema.
-- Common in real-world specs (e.g. Stripe range_query_specs: created, etc.)
-- where a filter accepts either a Unix timestamp or {gt, gte, lt, lte} object.
T.describe("params: deepObject anyOf optional missing", function()
local route = make_route({
{ name = "created", ["in"] = "query", required = false,
style = "deepObject", explode = true,
schema = { anyOf = {
{ type = "object", properties = {
gt = { type = "integer" }, gte = { type = "integer" },
lt = { type = "integer" }, lte = { type = "integer" },
} },
{ type = "integer" },
} } },
}, "query")

local ok, errs = params_mod.validate(route, {}, {}, {})
T.ok(ok, "missing optional deepObject anyOf accepted")
T.ok(not errs or #errs == 0, "no errors")
end)

T.describe("params: deepObject anyOf object branch", function()
local route = make_route({
{ name = "created", ["in"] = "query", required = false,
style = "deepObject", explode = true,
schema = { anyOf = {
{ type = "object", properties = {
gt = { type = "integer" }, lte = { type = "integer" },
} },
{ type = "integer" },
} } },
}, "query")

local ok, errs = params_mod.validate(route,
{}, { ["created[gt]"] = "1700000000", ["created[lte]"] = "1800000000" }, {})
T.ok(ok, "deepObject object form accepted")
T.ok(not errs or #errs == 0, "no errors")
end)

T.describe("params: deepObject anyOf integer branch", function()
local route = make_route({
{ name = "created", ["in"] = "query", required = false,
style = "deepObject", explode = true,
schema = { anyOf = {
{ type = "object", properties = {
gt = { type = "integer" },
} },
{ type = "integer" },
} } },
}, "query")

local ok, errs = params_mod.validate(route,
{}, { ["created"] = "1700000000" }, {})
T.ok(ok, "deepObject scalar (integer branch) accepted")
T.ok(not errs or #errs == 0, "no errors")
end)

-- Same shape as the anyOf cases but using oneOf, to lock in both branches
-- of the runtime path.
T.describe("params: deepObject oneOf object branch", function()
local route = make_route({
{ name = "created", ["in"] = "query", required = false,
style = "deepObject", explode = true,
schema = { oneOf = {
{ type = "object", properties = {
gt = { type = "integer" }, lte = { type = "integer" },
} },
{ type = "integer" },
} } },
}, "query")

local ok, errs = params_mod.validate(route,
{}, { ["created[gt]"] = "1700000000" }, {})
T.ok(ok, "deepObject oneOf object form accepted")
T.ok(not errs or #errs == 0, "no errors")
end)

T.describe("params: deepObject oneOf integer branch", function()
local route = make_route({
{ name = "created", ["in"] = "query", required = false,
style = "deepObject", explode = true,
schema = { oneOf = {
{ type = "object", properties = { gt = { type = "integer" } } },
{ type = "integer" },
} } },
}, "query")

local ok, errs = params_mod.validate(route,
{}, { ["created"] = "1700000000" }, {})
T.ok(ok, "deepObject oneOf scalar (integer branch) accepted")
T.ok(not errs or #errs == 0, "no errors")
end)

-- Negative: a value that matches none of the anyOf branches must be rejected.
T.describe("params: deepObject anyOf rejects unmatched scalar", function()
local route = make_route({
{ name = "created", ["in"] = "query", required = false,
style = "deepObject", explode = true,
schema = { anyOf = {
{ type = "object", properties = { gt = { type = "integer" } } },
{ type = "integer" },
} } },
}, "query")

local ok, errs = params_mod.validate(route,
{}, { ["created"] = "not-a-number" }, {})
T.ok(not ok, "non-integer scalar rejected")
T.ok(errs and #errs >= 1, "error reported")
end)

-- Union type arrays from nullable normalization, e.g. type = {"object","null"},
-- must still be recognised as an object branch.
T.describe("params: deepObject anyOf nullable object branch", function()
local route = make_route({
{ name = "created", ["in"] = "query", required = false,
style = "deepObject", explode = true,
schema = { anyOf = {
{ type = { "object", "null" }, properties = {
gt = { type = "integer" },
} },
{ type = "integer" },
} } },
}, "query")

local ok, errs = params_mod.validate(route,
{}, { ["created[gt]"] = "1700000000" }, {})
T.ok(ok, "nullable object branch parsed")
T.ok(not errs or #errs == 0, "no errors")
end)

-- Object branch expressed via composition (allOf) must also be detected, so
-- parse_deep_object is invoked and the value reaches the union validator
-- (instead of being dropped or coerced as a scalar).
T.describe("params: deepObject anyOf composed (allOf) object branch", function()
local route = make_route({
{ name = "created", ["in"] = "query", required = false,
style = "deepObject", explode = true,
schema = { anyOf = {
{ allOf = {
{ type = "object", properties = {
gt = { type = "string" },
} },
} },
{ type = "integer" },
} } },
}, "query")

local ok, errs = params_mod.validate(route,
{}, { ["created[gt]"] = "abc" }, {})
T.ok(ok, "composed object branch parsed")
T.ok(not errs or #errs == 0, "no errors")
end)

T.done()
Loading