-
Notifications
You must be signed in to change notification settings - Fork 1
/
type.lua
370 lines (314 loc) · 10 KB
/
type.lua
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
---@class Type
local Type = {
-- custom name for the defined type
---@type string|nil
name = nil,
-- list of assertions to perform on any given value
---@type { message: string, validate: fun(val: any): boolean }[]
conditions = nil
}
-- Execute an assertion for a given value
---@param val any Value to assert for
---@param message string? Optiona message to throw
function Type:assert(val, message)
for _, condition in ipairs(self.conditions) do
if not condition.validate(val) then
self:error(message or condition.message)
end
end
end
-- Add a custom condition/assertion to assert for
---@param message string Error message for the assertion
---@param assertion fun(val: any): boolean Custom assertion function that is asserted with the provided value
function Type:custom(message, assertion)
-- condition to add
local condition = {
message = message,
validate = assertion
}
-- new instance if there are no conditions yet
if self.conditions == nil then
local instance = {
conditions = {}
}
table.insert(instance.conditions, condition)
setmetatable(instance, self)
self.__index = self
return instance
end
table.insert(self.conditions, condition)
return self
end
-- Create a new instance of the existing assertions
-- that can be independently modified
function Type:extend()
local newInstance = {}
if self.name then
newInstance.name = self.name
end
newInstance.conditions = {}
for _, v in ipairs(self.conditions) do
table.insert(
newInstance.conditions,
v
)
end
setmetatable(newInstance, self)
self.__index = self
return newInstance
end
-- Add an assertion for built in types
---@param t "nil"|"number"|"string"|"boolean"|"table"|"function"|"thread"|"userdata" Type to assert for
---@param message string? Optional assertion error message
function Type:type(t, message)
return self:custom(
message or ("Not of type (" .. t .. ")"),
function (val) return type(val) == t end
)
end
-- Type must be userdata
---@param message string? Optional assertion error message
function Type:userdata(message)
return self:type("userdata", message)
end
-- Type must be thread
---@param message string? Optional assertion error message
function Type:thread(message)
return self:type("thread", message)
end
-- Type must be table
---@param message string? Optional assertion error message
function Type:table(message)
return self:type("table", message)
end
-- Table's keys must be of type t
---@param t Type Type to assert the keys for
---@param message string? Optional assertion error message
function Type:keys(t, message)
return self:custom(
message or "Invalid table keys",
function (val)
if type(val) ~= "table" then
return false
end
for key, _ in pairs(val) do
-- check if the assertion throws any errors
local success = pcall(function () return t:assert(key) end)
if not success then return false end
end
return true
end
)
end
-- Type must be array
---@param message string? Optional assertion error message
function Type:array(message)
return self:table():keys(Type:number(), message)
end
-- Table's values must be of type t
---@param t Type Type to assert the values for
---@param message string? Optional assertion error message
function Type:values(t, message)
return self:custom(
message or "Invalid table values",
function (val)
if type(val) ~= "table" then return false end
for _, v in pairs(val) do
-- check if the assertion throws any errors
local success = pcall(function () return t:assert(v) end)
if not success then return false end
end
return true
end
)
end
-- Type must be boolean
---@param message string? Optional assertion error message
function Type:boolean(message)
return self:type("boolean", message)
end
-- Type must be function
---@param message string? Optional assertion error message
function Type:_function(message)
return self:type("function", message)
end
-- Type must be nil
---@param message string? Optional assertion error message
function Type:_nil(message)
return self:type("nil", message)
end
-- Value must be the same
---@param val any The value the assertion must be made with
---@param message string? Optional assertion error message
function Type:is(val, message)
return self:custom(
message or "Value did not match expected value (Type:is(expected))",
function (v) return v == val end
)
end
-- Type must be string
---@param message string? Optional assertion error message
function Type:string(message)
return self:type("string", message)
end
-- String type must match pattern
---@param pattern string Pattern to match
---@param message string? Optional assertion error message
function Type:match(pattern, message)
return self:custom(
message or ("String did not match pattern \"" .. pattern .. "\""),
function (val) return string.match(val, pattern) ~= nil end
)
end
-- String type must be of defined length
---@param len number Required length
---@param match_type? "less"|"greater" String length should be "less" than or "greater" than the defined length. Leave empty for exact match.
---@param message string? Optional assertion error message
function Type:length(len, match_type, message)
local match_msgs = {
less = "String length is not less than " .. len,
greater = "String length is not greater than " .. len,
default = "String is not of length " .. len
}
return self:custom(
message or (match_msgs[match_type] or match_msgs.default),
function (val)
local strlen = string.len(val)
-- validate length
if match_type == "less" then return strlen < len
elseif match_type == "greater" then return strlen > len end
return strlen == len
end
)
end
-- Type must be a number
---@param message string? Optional assertion error message
function Type:number(message)
return self:type("number", message)
end
-- Number must be an integer (chain after "number()")
---@param message string? Optional assertion error message
function Type:integer(message)
return self:custom(
message or "Number is not an integer",
function (val) return val % 1 == 0 end
)
end
-- Number must be even (chain after "number()")
---@param message string? Optional assertion error message
function Type:even(message)
return self:custom(
message or "Number is not even",
function (val) return val % 2 == 0 end
)
end
-- Number must be odd (chain after "number()")
---@param message string? Optional assertion error message
function Type:odd(message)
return self:custom(
message or "Number is not odd",
function (val) return val % 2 == 1 end
)
end
-- Number must be less than the number "n" (chain after "number()")
---@param n number Number to compare with
---@param message string? Optional assertion error message
function Type:less_than(n, message)
return self:custom(
message or ("Number is not less than " .. n),
function (val) return val < n end
)
end
-- Number must be greater than the number "n" (chain after "number()")
---@param n number Number to compare with
---@param message string? Optional assertion error message
function Type:greater_than(n, message)
return self:custom(
message or ("Number is not greater than" .. n),
function (val) return val > n end
)
end
-- Make a type optional (allow them to be nil apart from the required type)
---@param t Type Type to assert for if the value is not nil
---@param message string? Optional assertion error message
function Type:optional(t, message)
return self:custom(
message or "Optional type did not match",
function (val)
if val == nil then return true end
t:assert(val)
return true
end
)
end
-- Table must be of object
---@param obj { [any]: Type }
---@param strict? boolean Only allow the defined keys from the object, throw error on other keys (false by default)
---@param message string? Optional assertion error message
function Type:object(obj, strict, message)
if type(obj) ~= "table" then
self:error("Invalid object structure provided for object assertion (has to be a table):\n" .. tostring(obj))
end
return self:custom(
message or ("Not of defined object (" .. tostring(obj) .. ")"),
function (val)
if type(val) ~= "table" then return false end
-- for each value, validate
for key, assertion in pairs(obj) do
-- check if the assertion throws any errors
local success = pcall(function () return assertion:assert(val[key]) end)
if not success then return false end
end
-- in strict mode, we do not allow any other keys
if strict then
for key, _ in pairs(val) do
if obj[key] == nil then return false end
end
end
return true
end
)
end
-- Type has to be either one of the defined assertions
---@param ... Type Type(s) to assert for
function Type:either(...)
---@type Type[]
local assertions = {...}
return self:custom(
"Neither types matched defined in (Type:either(...))",
function (val)
for _, assertion in ipairs(assertions) do
if pcall(function () return assertion:assert(val) end) then
return true
end
end
return false
end
)
end
-- Type cannot be the defined assertion (tip: for multiple negated assertions, use Type:either(...))
---@param t Type Type to NOT assert for
---@param message string? Optional assertion error message
function Type:is_not(t, message)
return self:custom(
message or "Value incorrectly matched with the assertion provided (Type:is_not())",
function (val)
local success = pcall(function () return t:assert(val) end)
return not success
end
)
end
-- Set the name of the custom type
-- This will be used with error logs
---@param name string Name of the type definition
function Type:set_name(name)
self.name = name
return self
end
-- Throw an error
---@param message any Message to log
---@private
function Type:error(message)
error("[Type " .. (self.name or tostring(self.__index)) .. "] " .. tostring(message))
end
return Type