-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparams.lua
More file actions
465 lines (403 loc) · 13.7 KB
/
params.lua
File metadata and controls
465 lines (403 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
-- Parameter coercion and validation.
-- Handles path, query, and header parameters:
-- 1. Type coercion from string to schema-declared type
-- 2. Style/explode deserialization (form, simple, label, matrix, deepObject)
-- 3. JSON Schema validation via api7/jsonschema
local _M = {}
local type = type
local tonumber = tonumber
local pairs = pairs
local ipairs = ipairs
local pcall = pcall
local str_lower = string.lower
local str_find = string.find
local sub_str = string.sub
local tab_insert = table.insert
local str_gmatch = string.gmatch
local cjson
local has_cjson = pcall(function()
cjson = require("cjson.safe")
end)
local jsonschema
local has_jsonschema = pcall(function()
jsonschema = require("jsonschema")
end)
local errors = require("resty.openapi_validator.errors")
-- Schema validator cache: schema_table -> validator_function
local validator_cache = setmetatable({}, { __mode = "k" })
local function get_validator(schema)
if not has_jsonschema then
return nil
end
if validator_cache[schema] then
return validator_cache[schema]
end
local ok, v = pcall(jsonschema.generate_validator, schema)
if ok and v then
validator_cache[schema] = v
return v
end
return nil
end
-- Collect all possible types from a schema, including composite sub-schemas.
local function collect_types(schema, seen)
if not schema then return {} end
seen = seen or {}
if seen[schema] then return {} end
seen[schema] = true
local types = {}
local stype = schema.type
if stype then
if type(stype) == "table" then
for _, t in ipairs(stype) do
types[t] = true
end
else
types[stype] = true
end
end
for _, key in ipairs({"anyOf", "oneOf", "allOf"}) do
local composite = schema[key]
if composite then
for _, sub_schema in ipairs(composite) do
local sub_types = collect_types(sub_schema, seen)
for t in pairs(sub_types) do
types[t] = true
end
end
end
end
return types
end
-- Coerce a string value to the type declared in schema.
local function coerce_value(value, schema)
if value == nil then
return nil
end
local stype = schema.type
-- handle array type (e.g. ["integer", "null"] from nullable normalization)
if type(stype) == "table" then
for _, t in ipairs(stype) do
if t == "integer" or t == "number" then
local n = tonumber(value)
if n then return n end
elseif t == "boolean" then
if value == "true" or value == "1" then return true end
if value == "false" or value == "0" then return false end
end
end
return value
end
if stype == "integer" or stype == "number" then
local n = tonumber(value)
if n then
return n
end
return value
elseif stype == "boolean" then
if value == "true" or value == "1" then
return true
elseif value == "false" or value == "0" then
return false
end
return value
end
if not stype then
local possible = collect_types(schema)
if possible["boolean"] then
if value == "true" or value == "1" then return true end
if value == "false" or value == "0" then return false end
end
if possible["integer"] or possible["number"] then
local n = tonumber(value)
if n then return n end
end
end
return value
end
-- Coerce values within an object according to its schema properties.
local function coerce_object_values(obj, schema)
if type(obj) ~= "table" or type(schema) ~= "table" then
return obj
end
local props = schema.properties
if not props then
return obj
end
for k, v in pairs(obj) do
if type(v) == "string" and props[k] then
obj[k] = coerce_value(v, props[k])
end
end
return obj
end
-- Split a string by delimiter.
local function split(s, delim)
local result = {}
local from = 1
local pos
while true do
pos = str_find(s, delim, from, true)
if not pos then
tab_insert(result, sub_str(s, from))
break
end
tab_insert(result, sub_str(s, from, pos - 1))
from = pos + 1
end
return result
end
-- Parse deepObject style query parameters.
-- deepObject format: param[key]=value or param[key][subkey]=value
local function parse_deep_object(param_name, query_args, schema)
local obj = {}
local prefix = param_name .. "["
local found = false
for key, val in pairs(query_args) do
if sub_str(key, 1, #prefix) == prefix then
found = true
local path = {}
for bracket_key in str_gmatch(key, "%[([^%]]+)%]") do
tab_insert(path, bracket_key)
end
if #path > 0 then
local current = obj
for i = 1, #path - 1 do
local p = path[i]
if current[p] == nil then
current[p] = {}
end
current = current[p]
end
local v = type(val) == "table" and val[1] or val
current[path[#path]] = v
end
end
end
if not found then
return nil
end
coerce_object_values(obj, schema)
if schema.properties then
for pname, pschema in pairs(schema.properties) do
if type(obj[pname]) == "table" and pschema.type == "object" then
coerce_object_values(obj[pname], pschema)
end
end
end
return obj
end
-- Deserialize a parameter value according to its style and explode settings.
-- See: https://spec.openapis.org/oas/v3.1.0#style-values
local function deserialize_param(raw_value, param, query_args)
if param.content then
local json_content = param.content["application/json"]
if json_content and json_content.schema and has_cjson then
local decoded = cjson.decode(raw_value)
if decoded ~= nil then
return decoded
end
end
end
local schema = param.schema
if not schema then
return raw_value
end
local style = param.style
local explode = param.explode
local loc = param["in"]
if not style then
if loc == "query" then
style = "form"
else
style = "simple"
end
end
if explode == nil then
explode = (style == "form")
end
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
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
return nil
end
if stype == "array" then
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(coerce_to_string(","), ",")
elseif style == "form" then
if not explode then
values = split(coerce_to_string(","), ",")
else
if type(raw_value) == "table" then
values = raw_value
else
values = { raw_value }
end
end
elseif style == "pipeDelimited" then
values = split(coerce_to_string("|"), "|")
elseif style == "spaceDelimited" then
values = split(coerce_to_string(" "), " ")
else
values = { raw_value }
end
for i, v in ipairs(values) do
values[i] = coerce_value(v, items_schema)
end
return values
elseif stype == "object" then
if style == "deepObject" then
return parse_deep_object(param.name, query_args or {}, schema)
end
if style == "simple" and not explode then
local parts = split(raw_value, ",")
local obj = {}
for i = 1, #parts - 1, 2 do
obj[parts[i]] = parts[i + 1]
end
return coerce_object_values(obj, schema)
elseif style == "simple" and explode then
local parts = split(raw_value, ",")
local obj = {}
for _, part in ipairs(parts) do
local kv = split(part, "=")
if #kv == 2 then
obj[kv[1]] = kv[2]
end
end
return coerce_object_values(obj, schema)
elseif style == "form" and explode then
if type(raw_value) == "table" then
return coerce_object_values(raw_value, schema)
end
end
return raw_value
end
return coerce_value(raw_value, schema)
end
-- Validate parameters for a matched route.
function _M.validate(route, path_params, query_args, headers, skip)
skip = skip or {}
query_args = query_args or {}
headers = headers or {}
local errs = {}
local function validate_param_group(param_list, location, raw_values)
for _, param in ipairs(param_list) do
local name = param.name
local style = param.style
or (location == "query" and "form" or "simple")
local raw
if style == "deepObject" and location == "query" then
raw = "deepObject_placeholder"
else
raw = raw_values[name]
end
if location == "header" and raw == nil then
raw = raw_values[str_lower(name)]
end
if param.required and (raw == nil or raw == "") then
if style == "deepObject" and location == "query" then
local prefix = name .. "["
local found = false
for k in pairs(query_args) do
if sub_str(k, 1, #prefix) == prefix then
found = true
break
end
end
if not found then
tab_insert(errs, errors.new(location, name,
"required parameter is missing"))
goto continue
end
else
tab_insert(errs, errors.new(location, name,
"required parameter is missing"))
goto continue
end
end
if raw == nil then
goto continue
end
local schema = param.schema
if not schema and param.content then
local json_ct = param.content["application/json"]
if json_ct then
schema = json_ct.schema
end
end
if not schema then
goto continue
end
local value = deserialize_param(raw, param, query_args)
if value == nil and not param.required then
goto continue
end
local validator = get_validator(schema)
if validator then
local ok, err = validator(value)
if not ok then
local msg = type(err) == "string" and err
or "validation failed"
tab_insert(errs, errors.new(location, name, msg))
end
end
::continue::
end
end
if not skip.path and route.params.path then
validate_param_group(route.params.path, "path", path_params)
end
if not skip.query and route.params.query then
validate_param_group(route.params.query, "query", query_args)
end
if not skip.header and route.params.header then
validate_param_group(route.params.header, "header", headers)
end
if #errs > 0 then
return false, errs
end
return true, nil
end
return _M