-
Notifications
You must be signed in to change notification settings - Fork 9
/
schema.lua
331 lines (322 loc) · 9.46 KB
/
schema.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
-- Localize globals
local assert, error, ipairs, math, minetest, modlib, pairs, setmetatable, table, tonumber, tostring, type = assert, error, ipairs, math, minetest, modlib, pairs, setmetatable, table, tonumber, tostring, type
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
local metatable = {__index = _ENV}
function new(def)
-- TODO type inference, sanity checking etc.
return setmetatable(def, metatable)
end
local function field_name_to_title(name)
local title = modlib.text.split(name, "_")
title[1] = modlib.text.upper_first(title[1])
return table.concat(title, " ")
end
function generate_settingtypes(self)
local typ = self.type
local settingtype, type_args
self.title = self.title or field_name_to_title(self.name)
self._level = self._level or 0
local default = self.default
if typ == "boolean" then
settingtype = "bool"
default = default and "true" or "false"
elseif typ == "string" then
settingtype = "string"
if self.values then
local values = {}
for value in pairs(self.values) do
if value:find"," then
values = nil
break
end
table.insert(values, value)
end
if values then
settingtype = "enum"
type_args = table.concat(values, ",")
end
end
elseif typ == "number" then
settingtype = self.int and "int" or "float"
if self.range and (self.range.min or self.range.max) then
-- TODO handle exclusive min/max
type_args = (self.int and "%d %d" or "%f %f"):format(self.range.min or (2 ^ -30), self.range.max or (2 ^ 30))
end
elseif typ == "table" then
local settings = {}
if self._level > 0 then
-- HACK: Minetest automatically adds the modname
-- TODO simple names (not modname.field.other_field)
settings = {"[" .. ("*"):rep(self._level - 1) .. self.name .. "]"}
end
local function setting(key, value_scheme)
key = tostring(key)
assert(not key:find("[=%.%s]"))
value_scheme.name = self.name .. "." .. key
value_scheme.title = value_scheme.title or self.title .. " " .. field_name_to_title(key)
value_scheme._level = self._level + 1
table.insert(settings, generate_settingtypes(value_scheme))
end
local keys = {}
for key in pairs(self.entries or {}) do
table.insert(keys, key)
end
table.sort(keys, function(key, other_key)
-- Force leaves before subtrees to prevent them from being accidentally graphically treated as part of the subtree
local is_subtree = self.entries[key].type == "table"
local other_is_subtree = self.entries[other_key].type == "table"
if is_subtree ~= other_is_subtree then
return not is_subtree
end
return key < other_key
end)
for _, key in ipairs(keys) do
setting(key, self.entries[key])
end
return table.concat(settings, "\n\n")
end
if not typ then
return ""
end
local description = self.description
-- TODO extend description by range etc.?
-- TODO enum etc. support
if description then
if type(description) ~= "table" then
description = {description}
end
description = "# " .. table.concat(description, "\n# ") .. "\n"
else
description = ""
end
return description .. self.name .. " (" .. self.title .. ") " .. settingtype .. " " .. (default or "") .. (type_args and (" " .. type_args) or "")
end
function generate_markdown(self)
-- TODO address redundancies
local function description(lines)
local description = self.description
if description then
if type(description) ~= "table" then
table.insert(lines, description)
else
modlib.table.append(lines, description)
end
end
end
local typ = self.type
self.title = self.title or field_name_to_title(self._md_name)
self._md_level = self._md_level or 1
if typ == "table" then
local settings = {}
description(settings)
-- TODO generate Markdown for key/value-checks
local function setting(key, value_scheme)
value_scheme._md_name = key
value_scheme.title = value_scheme.title or self.title .. " " .. field_name_to_title(key)
value_scheme._md_level = self._md_level + 1
table.insert(settings, table.concat(modlib.table.repetition("#", self._md_level)) .. " `" .. key .. "`")
table.insert(settings, "")
table.insert(settings, generate_markdown(value_scheme))
table.insert(settings, "")
end
local keys = {}
for key in pairs(self.entries or {}) do
table.insert(keys, key)
end
table.sort(keys)
for _, key in ipairs(keys) do
setting(key, self.entries[key])
end
return table.concat(settings, "\n")
end
if not typ then
return ""
end
local lines = {}
description(lines)
local function line(text)
table.insert(lines, "* " .. text)
end
table.insert(lines, "")
line("Type: " .. self.type)
if self.default ~= nil then
line("Default: `" .. tostring(self.default) .. "`")
end
if self.int then
line"Integer"
elseif self.list then
line"List"
end
if self.infinity then
line"Infinities allowed"
end
if self.nan then
line"Not-a-Number (NaN) allowed"
end
if self.range then
if self.range.min then
line(">= `" .. self.range.min .. "`")
elseif self.range.min_exclusive then
line("> `" .. self.range.min_exclusive .. "`")
end
if self.range.max then
line("<= `" .. self.range.max .. "`")
elseif self.range.max_exclusive then
line("< `" .. self.range.max_exclusive .. "`")
end
end
if self.values then
line("Possible values:")
for value in pairs(self.values) do
table.insert(lines, " * " .. value)
end
end
return table.concat(lines, "\n")
end
function settingtypes(self)
self.settingtypes = self.settingtypes or generate_settingtypes(self)
return self.settingtypes
end
function load(self, override, params)
local converted
if params.convert_strings and type(override) == "string" then
converted = true
if self.type == "boolean" then
if override == "true" then
override = true
elseif override == "false" then
override = false
end
elseif self.type == "number" then
override = tonumber(override)
else
converted = false
end
end
if override == nil and not converted then
if self.type == "table" and self.default == nil then
override = {}
else
return self.default
end
end
local _error = error
local function format_error(typ, ...)
if typ == "type" then
return "mismatched type: expected " .. self.type ..", got " .. type(override) .. (converted and " (converted)" or "")
end
if typ == "range" then
local conditions = {}
local function push(condition, bound)
if self.range[bound] then
table.insert(conditions, " " .. condition .. " " .. minetest.write_json(self.range[bound]))
end
end
push(">", "min_exclusive")
push(">=", "min")
push("<", "max_exclusive")
push("<=", "max")
return "out of range: expected value" .. table.concat(conditions, " and")
end
if typ == "int" then
return "expected integer"
end
if typ == "infinity" then
return "expected no infinity"
end
if typ == "nan" then
return "expected no nan"
end
if typ == "required" then
local key = ...
return "required field " .. minetest.write_json(key) .. " missing"
end
if typ == "additional" then
local key = ...
return "superfluous field " .. minetest.write_json(key)
end
if typ == "list" then
return "not a list"
end
if typ == "values" then
return "expected one of " .. minetest.write_json(modlib.table.keys(self.values)) .. ", got " .. minetest.write_json(override)
end
_error("unknown error type")
end
local function error(type, ...)
if params.error_message then
local formatted = format_error(type, ...)
_error("Invalid value: " .. (self.name and (self.name .. ": ") or "") .. formatted)
end
_error{
type = type,
self = self,
override = override,
converted = converted
}
end
local function assert(value, ...)
if not value then
error(...)
end
return value
end
assert(self.type == type(override), "type")
if self.type == "number" or self.type == "string" then
if self.range then
if self.range.min then
assert(self.range.min <= override, "range")
elseif self.range.min_exclusive then
assert(self.range.min_exclusive < override, "range")
end
if self.range.max then
assert(self.range.max >= override, "range")
elseif self.range.max_exclusive then
assert(self.range.max_exclusive > override, "range")
end
end
if self.type == "number" then
assert((not self.int) or (override % 1 == 0), "int")
assert(self.infinity or math.abs(override) ~= math.huge, "infinity")
assert(self.nan or override == override, "nan")
end
elseif self.type == "table" then
if self.keys then
for key, value in pairs(override) do
override[load(self.keys, key, params)], override[key] = value, nil
end
end
if self.values then
for key, value in pairs(override) do
override[key] = load(self.values, value, params)
end
end
if self.entries then
for key, schema in pairs(self.entries) do
if schema.required and override[key] == nil then
error("required", key)
end
override[key] = load(schema, override[key], params)
end
if self.additional == false then
for key in pairs(override) do
if self.entries[key] == nil then
error("additional", key)
end
end
end
end
assert((not self.list) or modlib.table.count(override) == #override, "list")
end
-- Apply the values check only for primitive types where table indexing is by value;
-- the `values` field has a different meaning for tables (constraint all values must fulfill)
if self.type ~= "table" then
assert((not self.values) or self.values[override], "values")
end
if self.func then self.func(override) end
return override
end
-- Export environment
return _ENV