* luci/libs: uvl: fix handling of boolean options, better error descriptions, impleme...
[project/luci.git] / libs / uvl / luasrc / uvl.lua
1 --[[
2
3 UCI Validation Layer - Main Library
4 (c) 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
5 (c) 2008 Steven Barth <steven@midlink.org>
6
7 Licensed under the Apache License, Version 2.0 (the "License");
8 you may not use this file except in compliance with the License.
9 You may obtain a copy of the License at
10
11 http://www.apache.org/licenses/LICENSE-2.0
12
13 $Id$
14
15 ]]--
16
17 module( "luci.uvl", package.seeall )
18
19 require("luci.fs")
20 require("luci.util")
21 require("luci.model.uci")
22 require("luci.uvl.loghelper")
23 require("luci.uvl.datatypes")
24 --require("luci.uvl.validation")
25 require("luci.uvl.dependencies")
26
27 TYPE_SECTION = 0x01
28 TYPE_VARIABLE = 0x02
29 TYPE_ENUM = 0x03
30
31 STRICT_UNKNOWN_SECTIONS = true
32 STRICT_UNKNOWN_OPTIONS = true
33
34
35 local default_schemedir = "/etc/scheme"
36
37 local function _assert( condition, fmt, ... )
38 if not condition then
39 return assert( nil, string.format( fmt, ... ) )
40 else
41 return condition
42 end
43 end
44
45 UVL = luci.util.class()
46
47 function UVL.__init__( self, schemedir )
48 self.schemedir = schemedir or default_schemedir
49 self.packages = { }
50 self.beenthere = { }
51 self.uci = luci.model.uci
52 self.log = luci.uvl.loghelper
53 self.datatypes = luci.uvl.datatypes
54 end
55
56
57 --- Validate given configuration.
58 -- @param config Name of the configuration to validate
59 -- @param scheme Scheme to validate against (optional)
60 -- @return Boolean indicating weather the given config validates
61 -- @return String containing the reason for errors (if any)
62 function UVL.validate( self, config )
63 self.uci.load_config( config )
64 self.beenthere = { }
65
66 local co = self.uci.get_all( config )
67 local sc = { }
68
69 local function _uci_foreach( type, func )
70 local ok, err
71 for k, v in pairs(co) do
72 if co[k]['.type'] == type then
73 sc[type] = sc[type] + 1
74 ok, err = func( k, v )
75 if not ok then
76 err = self.log.config_error( config, err )
77 break
78 end
79 end
80 end
81 return ok, err
82 end
83
84 for k, v in pairs( self.packages[config].sections ) do
85 sc[k] = 0
86 local ok, err = _uci_foreach( k,
87 function(s)
88 local sect = luci.uvl.section( self, co, k, config, s )
89 return self:_validate_section( sect )
90 end
91 )
92 if not ok then return false, err end
93 end
94
95 if STRICT_UNKNOWN_SECTIONS then
96 for k, v in pairs(co) do
97 if not self.beenthere[config..'.'..k] then
98 return false, self.log.config_error( config,
99 "Section '" .. config .. '.' .. co[k]['.type'] ..
100 "' not found in scheme" )
101 end
102 end
103 end
104
105 for _, k in ipairs(luci.util.keys(sc)) do
106 local s = self.packages[config].sections[k]
107
108 if s.required and sc[k] == 0 then
109 return false, self.log.config_error( config,
110 'Required section "' .. k .. '" not found in config' )
111 elseif s.unique and sc[k] > 1 then
112 return false, self.log.config_error( config,
113 'Unique section "' .. k .. '" occurs multiple times in config' )
114 end
115 end
116
117 return true, nil
118 end
119
120 function UVL.validate_section( self, config, section )
121 self.uci.load_config( config )
122 self.beenthere = { }
123
124 local co = self.uci.get_all( config )
125 if co[section] then
126 return self:_validate_section( luci.uvl.section(
127 self, co, co[section]['.type'], config, section
128 ) )
129 else
130 return false, "Section '" .. config .. '.' .. section ..
131 "' not found in config. Nothing to do."
132 end
133 end
134
135 function UVL.validate_option( self, config, section, option )
136 self.uci.load_config( config )
137 self.beenthere = { }
138
139 local co = self.uci.get_all( config )
140 if co[section] and co[section][option] then
141 return self:_validate_option( luci.uvl.option(
142 self, co, co[section]['.type'], config, section, option
143 ) )
144 else
145 return false, "Option '" ..
146 config .. '.' .. section .. '.' .. option ..
147 "' not found in config. Nothing to do."
148 end
149 end
150
151 --- Validate given section of given configuration.
152 -- @param config Name of the configuration to validate
153 -- @param section Key of the section to validate
154 -- @param scheme Scheme to validate against
155 -- @return Boolean indicating weather the given config validates
156 -- @return String containing the reason for errors (if any)
157 function UVL._validate_section( self, section )
158
159 if section:values() then
160
161 for _, v in ipairs(section:variables()) do
162 local ok, err = self:_validate_option( v )
163
164 if not ok then
165 return ok, self.log.section_error( section, err )
166 end
167 end
168
169 local ok, err = luci.uvl.dependencies.check( self, section )
170
171 if not ok then
172 return false, err
173 end
174 else
175 print( "Error, scheme section '" .. section:sid() .. "' not found in data" )
176 end
177
178 if STRICT_UNKNOWN_OPTIONS and not section:section().dynamic then
179 for k, v in pairs(section:values()) do
180 if k:sub(1,1) ~= "." and not self.beenthere[
181 section:cid() .. '.' .. k
182 ] then
183 return false, "Option '" .. section:sid() .. '.' .. k ..
184 "' not found in scheme"
185 end
186 end
187 end
188
189 return true, nil
190 end
191
192 --- Validate given option within section of given configuration.
193 -- @param config Name of the configuration to validate
194 -- @param section Key of the section to validate
195 -- @param option Name of the option to validate
196 -- @param scheme Scheme to validate against
197 -- @return Boolean indicating weather the given config validates
198 -- @return String containing the reason for errors (if any)
199 function UVL._validate_option( self, option, nodeps )
200
201 if not option:option() and
202 not ( option:section() and option:section().dynamic )
203 then
204 return false, "Option '" .. option:cid() ..
205 "' not found in scheme"
206 end
207
208 if option:option() then
209 if option:option().required and not option:value() then
210 return false, "Mandatory variable '" .. option:cid() ..
211 "' doesn't have a value"
212 end
213
214 if option:option().type == "enum" and option:value() then
215 if not option:option().values or
216 not option:option().values[option:value()]
217 then
218 return false, "Value '" .. ( option:value() or '<nil>' ) ..
219 "' of given option '" .. option:cid() ..
220 "' is not defined in enum { " ..
221 table.concat(luci.util.keys(option:option().values),", ") ..
222 " }"
223 end
224 end
225
226 if option:option().datatype and option:value() then
227 if self.datatypes[option:option().datatype] then
228 if not self.datatypes[option:option().datatype](
229 option:value()
230 ) then
231 return false, "Value '" .. ( option:value() or '<nil>' ) ..
232 "' of given option '" .. option:cid() ..
233 "' doesn't validate as datatype '" ..
234 option:option().datatype .. "'"
235 end
236 else
237 return false, "Unknown datatype '" ..
238 option:option().datatype .. "' encountered"
239 end
240 end
241
242 if not nodeps then
243 return luci.uvl.dependencies.check( self, option )
244 end
245 end
246
247 return true, nil
248 end
249
250 --- Find all parts of given scheme and construct validation tree
251 -- @param scheme Name of the scheme to parse
252 -- @return Parsed scheme
253 function UVL.read_scheme( self, scheme )
254 local schemes = { }
255
256 for i, file in ipairs( luci.fs.glob(self.schemedir .. '/*/' .. scheme) ) do
257 _assert( luci.fs.access(file), "Can't access file '%s'", file )
258
259 self.uci.set_confdir( luci.fs.dirname(file) )
260 self.uci.load( luci.fs.basename(file) )
261
262 table.insert( schemes, self.uci.get_all( luci.fs.basename(file) ) )
263 end
264
265 return self:_read_scheme_parts( scheme, schemes )
266 end
267
268 -- Process all given parts and construct validation tree
269 function UVL._read_scheme_parts( self, scheme, schemes )
270
271 -- helper function to construct identifiers for given elements
272 local function _id( c, t )
273 if c == TYPE_SECTION then
274 return string.format(
275 "section '%s.%s'",
276 scheme, t.name or '?' )
277 elseif c == TYPE_VARIABLE then
278 return string.format(
279 "variable '%s.%s.%s'",
280 scheme, t.section or '?.?', t.name or '?' )
281 elseif c == TYPE_ENUM then
282 return string.format(
283 "enum '%s.%s.%s'",
284 scheme, t.variable or '?.?.?', t.value or '?' )
285 end
286 end
287
288 -- helper function to check for required fields
289 local function _req( c, t, r )
290 for i, v in ipairs(r) do
291 _assert( t[v], "Missing required field '%s' in %s", v, _id(c, t) )
292 end
293 end
294
295 -- helper function to validate references
296 local function _ref( c, t )
297 local k
298 if c == TYPE_SECTION then
299 k = "package"
300 elseif c == TYPE_VARIABLE then
301 k = "section"
302 elseif c == TYPE_ENUM then
303 k = "variable"
304 end
305
306 local r = luci.util.split( t[k], "." )
307 r[1] = ( #r[1] > 0 and r[1] or scheme )
308
309 _assert( #r == c, "Malformed %s reference in %s", k, _id(c, t) )
310
311 return r
312 end
313
314 -- helper function to read bools
315 local function _bool( v )
316 return ( v == "true" or v == "yes" or v == "on" or v == "1" )
317 end
318
319 -- Step 1: get all sections
320 for i, conf in ipairs( schemes ) do
321 for k, v in pairs( conf ) do
322 if v['.type'] == 'section' then
323
324 _req( TYPE_SECTION, v, { "name", "package" } )
325
326 local r = _ref( TYPE_SECTION, v )
327
328 self.packages[r[1]] =
329 self.packages[r[1]] or {
330 ["sections"] = { };
331 ["variables"] = { };
332 }
333
334 local p = self.packages[r[1]]
335 p.sections[v.name] = p.sections[v.name] or { }
336 p.variables[v.name] = p.variables[v.name] or { }
337
338 local s = p.sections[v.name]
339
340 for k, v2 in pairs(v) do
341 if k ~= "name" and k ~= "package" and k:sub(1,1) ~= "." then
342 if k:match("^depends") then
343 s["depends"] = _assert(
344 self:_read_dependency( v2, s["depends"] ),
345 "Section '%s' in scheme '%s' has malformed " ..
346 "dependency specification in '%s'",
347 v.name or '<nil>', scheme or '<nil>', k
348 )
349 elseif k == "dynamic" or k == "unique" or k == "required" then
350 s[k] = _bool(v2)
351 else
352 s[k] = v2
353 end
354 end
355 end
356 end
357 end
358 end
359
360 -- Step 2: get all variables
361 for i, conf in ipairs( schemes ) do
362 for k, v in pairs( conf ) do
363 if v['.type'] == "variable" then
364
365 _req( TYPE_VARIABLE, v, { "name", "section" } )
366
367 local r = _ref( TYPE_VARIABLE, v )
368
369 local p = _assert( self.packages[r[1]],
370 "Variable '%s' in scheme '%s' references unknown package '%s'",
371 v.name, scheme, r[1] )
372
373 local s = _assert( p.variables[r[2]],
374 "Variable '%s' in scheme '%s' references unknown section '%s'",
375 v.name, scheme, r[2] )
376
377 s[v.name] = s[v.name] or { }
378
379 local t = s[v.name]
380
381 for k, v2 in pairs(v) do
382 if k ~= "name" and k ~= "section" and k:sub(1,1) ~= "." then
383 if k:match("^depends") then
384 t["depends"] = _assert(
385 self:_read_dependency( v2, t["depends"] ),
386 'Invalid reference "%s" in "%s.%s.%s"',
387 v2, v.name, scheme, k
388 )
389 elseif k:match("^validator") then
390 t["validators"] = _assert(
391 self:_read_validator( v2, t["validators"] ),
392 "Variable '%s' in scheme '%s' has malformed " ..
393 "validator specification in '%s'",
394 v.name, scheme, k
395 )
396 elseif k == "required" then
397 t[k] = _bool(v2)
398 else
399 t[k] = v2
400 end
401 end
402 end
403
404 t.type = t.type or "variable"
405 t.required = t.required or false
406 end
407 end
408 end
409
410 -- Step 3: get all enums
411 for i, conf in ipairs( schemes ) do
412 for k, v in pairs( conf ) do
413 if v['.type'] == "enum" then
414
415 _req( TYPE_ENUM, v, { "value", "variable" } )
416
417 local r = _ref( TYPE_ENUM, v )
418
419 local p = _assert( self.packages[r[1]],
420 "Enum '%s' in scheme '%s' references unknown package '%s'",
421 v.value, scheme, r[1] )
422
423 local s = _assert( p.variables[r[2]],
424 "Enum '%s' in scheme '%s' references unknown section '%s'",
425 v.value, scheme, r[2] )
426
427 local t = _assert( s[r[3]],
428 "Enum '%s' in scheme '%s', section '%s' references " ..
429 "unknown variable '%s'",
430 v.value, scheme, r[2], r[3] )
431
432 _assert( t.type == "enum",
433 "Enum '%s' in scheme '%s', section '%s' references " ..
434 "variable '%s' with non enum type '%s'",
435 v.value, scheme, r[2], r[3], t.type )
436
437 if not t.values then
438 t.values = { [v.value] = v.title or v.value }
439 else
440 t.values[v.value] = v.title or v.value
441 end
442
443 if v.default then
444 _assert( not t.default,
445 "Enum '%s' in scheme '%s', section '%s' redeclares " ..
446 "the default value of variable '%s'",
447 v.value, scheme, r[2], v.variable )
448
449 t.default = v.value
450 end
451 end
452 end
453 end
454
455 return self
456 end
457
458 -- Read a dependency specification
459 function UVL._read_dependency( self, value, deps )
460 local parts = luci.util.split( value, "%s*,%s*", nil, true )
461 local condition = { }
462
463 for i, val in ipairs(parts) do
464 local k, v = unpack(luci.util.split( val, "%s*=%s*", nil, true ))
465
466 if k and (
467 k:match("^%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+$") or
468 k:match("^%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+$") or
469 k:match("^%$?[a-zA-Z0-9_]+$")
470 ) then
471 condition[k] = v or true
472 else
473 return nil
474 end
475 end
476
477 if not deps then
478 deps = { condition }
479 else
480 table.insert( deps, condition )
481 end
482
483 return deps
484 end
485
486 -- Read a validator specification
487 function UVL._read_validator( self, value, validators )
488 local validator
489
490 if value and value:match("/") and self.datatypes.file(value) then
491 validator = value
492 else
493 validator = self:_resolve_function( value )
494 end
495
496 if validator then
497 if not validators then
498 validators = { validator }
499 else
500 table.insert( validators, validator )
501 end
502
503 return validators
504 end
505 end
506
507 -- Resolve given path
508 function UVL._resolve_function( self, value )
509 local path = luci.util.split(value, ".")
510
511 for i=1, #path-1 do
512 local stat, mod = pcall(require, table.concat(path, ".", 1, i))
513 if stat and mod then
514 for j=i+1, #path-1 do
515 if not type(mod) == "table" then
516 break
517 end
518 mod = mod[path[j]]
519 if not mod then
520 break
521 end
522 end
523 mod = type(mod) == "table" and mod[path[#path]] or nil
524 if type(mod) == "function" then
525 return mod
526 end
527 end
528 end
529 end
530
531
532 section = luci.util.class()
533
534 function section.__init__(self, scheme, co, st, c, s)
535 self.csection = co[s]
536 self.ssection = scheme.packages[c].sections[st]
537 self.cref = { c, s }
538 self.sref = { c, st }
539 self.scheme = scheme
540 self.config = co
541 self.type = luci.uvl.TYPE_SECTION
542 end
543
544 function section.cid(self)
545 return ( self.cref[1] or '?' ) .. '.' .. ( self.cref[2] or '?' )
546 end
547
548 function section.sid(self)
549 return ( self.sref[1] or '?' ) .. '.' .. ( self.sref[2] or '?' )
550 end
551
552 function section.values(self)
553 return self.csection
554 end
555
556 function section.section(self)
557 return self.ssection
558 end
559
560 function section.variables(self)
561 local v = { }
562 if self.scheme.packages[self.sref[1]].variables[self.sref[2]] then
563 for o, _ in pairs(
564 self.scheme.packages[self.sref[1]].variables[self.sref[2]]
565 ) do
566 table.insert( v, luci.uvl.option(
567 self.scheme, self.config, self.sref[2],
568 self.cref[1], self.cref[2], o
569 ) )
570 end
571 end
572 return v
573 end
574
575
576 option = luci.util.class()
577
578 function option.__init__(self, scheme, co, st, c, s, o)
579 self.coption = co[s] and co[s][o] or nil
580 self.soption = scheme.packages[c].variables[st][o]
581 self.cref = { c, s, o }
582 self.sref = { c, st, o }
583 self.scheme = scheme
584 self.config = co
585 self.type = luci.uvl.TYPE_OPTION
586 end
587
588 function option.cid(self)
589 return ( self.cref[1] or '?' ) .. '.' ..
590 ( self.cref[2] or '?' ) .. '.' ..
591 ( self.cref[3] or '?' )
592 end
593
594 function option.sid(self)
595 return ( self.sref[1] or '?' ) .. '.' ..
596 ( self.sref[2] or '?' ) .. '.' ..
597 ( self.sref[3] or '?' )
598 end
599
600 function option.value(self)
601 return self.coption
602 end
603
604 function option.option(self)
605 return self.soption
606 end
607
608 function option.section(self)
609 return self.scheme.packages[self.sref[1]].sections[self.sref[2]]
610 end