Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 4 additions & 1 deletion lib/resty/openapi_validator/body.lua
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,10 @@ function _M.validate(route, body_str, content_type, opts)
end

-- check content-type is declared in the spec
if route.body_content and content_type then
-- guard against non-string content_type values (e.g. cjson.null sentinel,
-- which is userdata and truthy in Lua so the previous `and content_type`
-- check let it through and crashed in str_lower).
if route.body_content and type(content_type) == "string" then
local ct_lower = str_lower(content_type)
Comment thread
jarvis9443 marked this conversation as resolved.
Outdated
Comment thread
jarvis9443 marked this conversation as resolved.
Outdated
local found = false
for media_type in pairs(route.body_content) do
Expand Down
23 changes: 20 additions & 3 deletions lib/resty/openapi_validator/normalize.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

local _M = {}

local cjson = require("cjson.safe")
local type = type
local pairs = pairs
local ipairs = ipairs
Expand All @@ -28,16 +29,32 @@ local function normalize_30_schema(schema, warnings)

-- For nullable schemas with enum or const, we cannot simply inject
-- cjson.null into enum (jsonschema can't handle userdata in enum).
-- Use anyOf: [original_schema_without_nullable, {type: "null"}]
-- Use anyOf: [original_schema_without_nullable, {type: "null"}].
--
-- IMPORTANT: if the original enum already contains cjson.null (i.e.
-- the spec author wrote `enum: [..., null]`), the userdata sentinel
-- silently disables the entire enum check inside api7/jsonschema.
-- Strip null entries from the enum here — the {type: "null"} branch
-- of the anyOf wrapper already permits null.
if schema.enum or schema["const"] then
-- save and remove nullable-related fields, wrap in anyOf
local original = {}
for k, v in pairs(schema) do
if k ~= "nullable" then
original[k] = v
end
end
-- clear schema and replace with anyOf
if type(original.enum) == "table" then
local cleaned = {}
for _, val in ipairs(original.enum) do
if val ~= cjson.null then
tab_insert(cleaned, val)
end
end
original.enum = cleaned
end
if original["const"] == cjson.null then
original["const"] = nil
end
Comment thread
jarvis9443 marked this conversation as resolved.
Outdated
for k in pairs(schema) do
schema[k] = nil
end
Expand Down
25 changes: 21 additions & 4 deletions lib/resty/openapi_validator/params.lua
Original file line number Diff line number Diff line change
Expand Up @@ -287,11 +287,28 @@ local function deserialize_param(raw_value, param, query_args)
local items_schema = schema.items or {}
local values

-- Coerce table values into a single delimited string for the
-- delimiter-based styles. This happens in real callers when, e.g.,
-- ngx.req.get_uri_args returns `{"a","b"}` for `?fields=a&fields=b`
-- but the schema is declared `style:form, explode:false` (comma-
-- separated). Previously this crashed in split() with
-- "bad argument #1 to 'str_find' (string expected, got table)".
local function coerce_to_string(delim)
if type(raw_value) == "table" then
local out = {}
for _, v in ipairs(raw_value) do
tab_insert(out, tostring(v))
end
return table.concat(out, delim)
end
return raw_value
end

if style == "simple" then
values = split(raw_value, ",")
values = split(coerce_to_string(","), ",")
elseif style == "form" then
if not explode then
values = split(raw_value, ",")
values = split(coerce_to_string(","), ",")
else
if type(raw_value) == "table" then
values = raw_value
Expand All @@ -300,9 +317,9 @@ local function deserialize_param(raw_value, param, query_args)
end
end
elseif style == "pipeDelimited" then
values = split(raw_value, "|")
values = split(coerce_to_string("|"), "|")
elseif style == "spaceDelimited" then
values = split(raw_value, " ")
values = split(coerce_to_string(" "), " ")
else
values = { raw_value }
end
Expand Down
109 changes: 109 additions & 0 deletions lib/resty/openapi_validator/router.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,72 @@ local function convert_path(path_template)
end


-- Detect path templates whose segments mix a {var} with literal text or
-- multiple {var}s in the same segment, e.g. "/foo/{id}.json" or
-- "/baz/{name}.{ext}". radixtree's :param syntax cannot extract these
-- correctly because it consumes the entire `/`-bounded segment as one
-- variable; the literal suffix is silently dropped from both the captured
-- value and the param name. For such templates we fall back to a per-route
-- PCRE that re-extracts path params at match time.
local function has_mixed_segment(path_template)
for seg in str_gmatch(path_template, "/([^/]*)") do
local has_var = str_find(seg, "{", 1, true) ~= nil
if has_var then
-- a clean segment is exactly "{name}" with nothing else
if not (str_byte(seg, 1) == str_byte("{")
and str_byte(seg, #seg) == str_byte("}")
and (str_find(seg, "}", 2, true) == #seg)) then
return true
end
end
end
return false
end


local PCRE_META = {
["%"] = true, ["."] = true, ["*"] = true, ["+"] = true, ["?"] = true,
["("] = true, [")"] = true, ["["] = true, ["]"] = true, ["{"] = true,
["}"] = true, ["|"] = true, ["^"] = true, ["$"] = true, ["\\"] = true,
["/"] = true,
}

local function pcre_escape(s)
return (str_gsub(s, ".", function(c)
if PCRE_META[c] then return "\\" .. c end
end))
end


-- Build a PCRE pattern + ordered name list that extracts path params from
-- a request path matching `path_template`. Used as a fallback when
-- has_mixed_segment(path_template) is true.
local function build_param_pcre(path_template)
local names = {}
local out = {}
local i = 1
while i <= #path_template do
local lb = str_find(path_template, "{", i, true)
if not lb then
tab_insert(out, pcre_escape(sub_str(path_template, i)))
break
end
if lb > i then
tab_insert(out, pcre_escape(sub_str(path_template, i, lb - 1)))
end
local rb = str_find(path_template, "}", lb + 1, true)
if not rb then
tab_insert(out, pcre_escape(sub_str(path_template, lb)))
break
end
tab_insert(names, sub_str(path_template, lb + 1, rb - 1))
tab_insert(out, "([^/]+?)")
i = rb + 1
end
return "^" .. table.concat(out) .. "$", names
end


-- Extract param names from {param} in path template.
local function extract_param_names(path_template)
local names = {}
Expand Down Expand Up @@ -152,6 +218,11 @@ function _M.new(spec)
local route_id = 0
for path_template, path_item in pairs(paths) do
local param_names = extract_param_names(path_template)
local mixed = has_mixed_segment(path_template)
local param_pcre, pcre_names
if mixed then
param_pcre, pcre_names = build_param_pcre(path_template)
end

for method, operation in pairs(path_item) do
local m = str_upper(method)
Expand All @@ -166,6 +237,9 @@ function _M.new(spec)
route_metadata[id] = {
path_template = path_template,
param_names = param_names,
param_pcre = param_pcre,
pcre_names = pcre_names,
base_paths = base_paths,
method = m,
operation = operation,
params = params,
Expand Down Expand Up @@ -240,6 +314,41 @@ function _M.match(self, method, path)
path_params[name] = matched["_" .. name]
end

-- Fallback: when the template has mixed segments (e.g. "/foo/{id}.json"
-- or "/baz/{name}.{ext}"), radixtree can match the route but cannot
-- extract the variables (it consumes the whole `/`-bounded segment as
-- one param and silently drops the literal suffix). Re-extract the
-- params here using a per-route PCRE built from the template.
if route.param_pcre then
local ngx_re = require("ngx.re")
local re_match = ngx.re.match
Comment thread
jarvis9443 marked this conversation as resolved.
local m
local bases = route.base_paths or { "" }
for _, base in ipairs(bases) do
local rel = path
if base ~= "" then
if str_find(path, base, 1, true) == 1 then
rel = sub_str(path, #base + 1)
if rel == "" then rel = "/" end
else
rel = nil
end
end
if rel then
local mm, err = re_match(rel, route.param_pcre, "jo")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if mm then m = mm; break end
end
Comment thread
jarvis9443 marked this conversation as resolved.
end
if not m then
-- radixtree matched but our authoritative regex doesn't:
-- treat as no match so callers get a clean error.
return nil, nil
end
for i, name in ipairs(route.pcre_names or {}) do
path_params[name] = m[i]
end
end

return route, path_params
end

Expand Down
Loading
Loading