* 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