Skip to content

Commit

Permalink
add types.params_map
Browse files Browse the repository at this point in the history
  • Loading branch information
leafo committed Oct 17, 2023
1 parent 0736b4f commit 1cbd6cc
Show file tree
Hide file tree
Showing 4 changed files with 432 additions and 2 deletions.
47 changes: 45 additions & 2 deletions docs/input_validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,16 +219,59 @@ $options_table{
},
{
name = "item_prefix",
description = "A string that prefixed before each collected error to identify the kind of object being checked",
description = "A string that is prefixed before each collected error to identify the kind of object being checked",
default = [["item"]]
},
{
name = "iter",
description = "Function used as iterator to visit each item in the table",
description = "Function used as an iterator to visit each item in the table",
default = "ipairs"
},
{
name = "join_error",
description = "A function that formats the error message. It takes the error, the index of the item, and the item itself as arguments and returns a formatted string",
}
}

#### `types.params_map(key_t, value_t, [opts])`

Creates a type checker that extracts key-value pairs from a table. Every key
must match type `key_t` and every value must match type `value_t`, otherwise
the checker will fail. If either `key_t` or `value_t` transform to `nil`, the
corresponding key-value pair is stripped from the final output. A new table is
always returned in the transformed output, even if no changes have been made to
any of the key or values.

Similar to `params_shape`, every pair is tested and all errors are accumulated
into an array object. `key_t` is tested before `value_t`, if the `key_t` type
fails then the `value_t` will not be tested, and only a single failure message
for the key is generated.

The `opts` argument is a table of options that control how the type checker
processes the input:

$options_table{
{
name = "join_error",
description = "A function that takes an error message, a key, a value, and an error type, and returns a string. This is used to construct the error message when a key-value pair fails to match the expected types"
},
{
name = "item_prefix",
description = "A string that is prefixed before each collected error to identify the kind of object being checked",
default = [["item"]]
},
{
name = "iter",
description = "Function used as iterator to visit each item in the table. This can be used to control the order in which items are visited",
default = "pairs"
}
}

> The `types.params_map.ordered_pairs` can be used for the `iter` option to
> ensure the key-value pairs are visited in a sorted order. This can be useful
> in cases where the order of error message output matters, such as in a test
> suite.
#### `types.assert_error(t)`

Wraps a Tableshape type checker, `t`, to yield an error when the checking or
Expand Down
129 changes: 129 additions & 0 deletions lapis/validate/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,134 @@ do
end
ParamsShapeType = _class_0
end
local ParamsMapType
do
local _class_0
local test_input_type
local _parent_0 = BaseType
local _base_0 = {
iter = pairs,
item_prefix = "item",
join_error = function(self, err, key, value, error_type)
local _exp_0 = error_type
if "key" == _exp_0 then
return tostring(self.item_prefix) .. " key: " .. tostring(err)
else
return tostring(self.item_prefix) .. " " .. tostring(key) .. ": " .. tostring(err)
end
end,
_transform = function(self, input_value, state)
local pass, err = test_input_type(input_value)
if not (pass) then
return FailedTransform, {
"params map: " .. tostring(err)
}
end
local errors
local push_error
push_error = function(err, ...)
errors = errors or { }
local _exp_0 = type(err)
if "table" == _exp_0 then
for _index_0 = 1, #err do
local e = err[_index_0]
table.insert(errors, self:join_error(e, ...))
end
elseif "string" == _exp_0 then
return table.insert(errors, self:join_error(err, ...))
end
end
local out = { }
for key, value in self.iter(input_value) do
local _continue_0 = false
repeat
local pair_state = state
local new_key, state_or_err = self.key_type:_transform(key, pair_state)
if new_key == FailedTransform then
push_error(state_or_err, key, value, "key")
_continue_0 = true
break
else
pair_state = state_or_err
end
local new_value
new_value, state_or_err = self.value_type:_transform(value, pair_state)
if new_value == FailedTransform then
push_error(state_or_err, key, value, "value")
_continue_0 = true
break
else
pair_state = state_or_err
end
if new_key ~= nil and new_value ~= nil then
out[new_key] = new_value
end
state = pair_state
_continue_0 = true
until true
if not _continue_0 then
break
end
end
if errors then
return FailedTransform, errors
end
return out, state
end
}
_base_0.__index = _base_0
setmetatable(_base_0, _parent_0.__base)
_class_0 = setmetatable({
__init = function(self, key_type, value_type, opts)
self.key_type, self.value_type = key_type, value_type
if opts then
self.item_prefix = opts.item_prefix
self.iter = opts.iter
self.join_error = opts.join_error
end
end,
__base = _base_0,
__name = "ParamsMapType",
__parent = _parent_0
}, {
__index = function(cls, name)
local val = rawget(_base_0, name)
if val == nil then
local parent = rawget(cls, "__parent")
if parent then
return parent[name]
end
else
return val
end
end,
__call = function(cls, ...)
local _self_0 = setmetatable({}, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
local self = _class_0
self.ordered_pairs = function(obj)
return coroutine.wrap(function()
local keys = { }
for k in pairs(obj) do
table.insert(keys, k)
end
table.sort(keys)
for _index_0 = 1, #keys do
local k = keys[_index_0]
coroutine.yield(k, obj[k])
end
end)
end
test_input_type = types.table
if _parent_0.__inherited then
_parent_0.__inherited(_parent_0, _class_0)
end
ParamsMapType = _class_0
end
local ParamsArrayType
do
local _class_0
Expand Down Expand Up @@ -565,6 +693,7 @@ local file_upload = types.partial({
return setmetatable({
params_shape = ParamsShapeType,
params_array = ParamsArrayType,
params_map = ParamsMapType,
flatten_errors = FlattenErrors,
multi_params = MultiParamsType,
assert_error = AssertErrorType,
Expand Down
82 changes: 82 additions & 0 deletions lapis/validate/types.moon
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,87 @@ class ParamsShapeType extends BaseType
"params type {\n #{table.concat rows, "\n "}\n}"


-- tests every key, value pair in the table
-- types.params_map(types.db_id, types.params_shape {...})
class ParamsMapType extends BaseType
@ordered_pairs: (obj) ->
coroutine.wrap ->
keys = {}
for k in pairs obj
table.insert keys, k

table.sort keys

for k in *keys
coroutine.yield k, obj[k]

test_input_type = types.table

iter: pairs
item_prefix: "item"

new: (@key_type, @value_type, opts) =>
if opts
@item_prefix = opts.item_prefix
@iter = opts.iter
@join_error = opts.join_error

join_error: (err, key, value, error_type) =>
switch error_type
when "key"
"#{@item_prefix} key: #{err}"
else
"#{@item_prefix} #{key}: #{err}"

_transform: (input_value, state) =>
pass, err = test_input_type input_value
unless pass
return FailedTransform, {"params map: #{err}"}

local errors

push_error = (err, ...) ->
errors or= {}

switch type(err)
-- append all errors
when "table"
for e in *err
table.insert errors, @join_error e, ...
when "string"
table.insert errors, @join_error err, ...

out = {}

for key, value in @.iter input_value
pair_state = state

-- test if key validates
new_key, state_or_err = @key_type\_transform key, pair_state
if new_key == FailedTransform
push_error state_or_err, key, value, "key"
-- Note that if the key fails, we bypass the value test
continue
else
pair_state = state_or_err

-- test if value validates
new_value, state_or_err = @value_type\_transform value, pair_state
if new_value == FailedTransform
push_error state_or_err, key, value, "value"
continue
else
pair_state = state_or_err

if new_key != nil and new_value != nil
out[new_key] = new_value

state = pair_state

if errors
return FailedTransform, errors

out, state

-- applies a params_shape to each item of array. This is necessary because
-- params_shape returns a special errors object
Expand Down Expand Up @@ -307,6 +388,7 @@ file_upload = types.partial({
setmetatable {
params_shape: ParamsShapeType
params_array: ParamsArrayType
params_map: ParamsMapType
flatten_errors: FlattenErrors

multi_params: MultiParamsType
Expand Down
Loading

0 comments on commit 1cbd6cc

Please sign in to comment.