* luci/libs: add preliminary uvl code
[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.datatypes")
23
24 TYPE_SECTION  = 0x01
25 TYPE_VARIABLE = 0x02
26 TYPE_ENUM     = 0x03
27
28
29 local default_schemedir = "/etc/scheme"
30 local function _assert( condition, fmt, ... )
31         if not condition then
32                 return assert( nil, string.format( fmt, ... ) )
33         else
34                 return condition
35         end
36 end
37
38 UVL = luci.util.class()
39
40 function UVL.__init__( self, schemedir )
41
42         self.schemedir  = schemedir or default_schemedir
43         self.packages   = { }
44         self.uci                = luci.model.uci
45         self.datatypes  = luci.uvl.datatypes
46 end
47
48 --- Validate given configuration.
49 -- @param config        Name of the configuration to validate
50 -- @param scheme        Scheme to validate against (optional)
51 -- @return                      Boolean indicating weather the given config validates
52 -- @return                      String containing the reason for errors (if any)
53 function UVL.validate( self, config, scheme )
54
55         if not scheme then
56                 return false, "No scheme found"
57         end
58
59         for k, v in pairs( config ) do
60                 local ok, err = self:validate_section( config, k, scheme )
61
62                 if not ok then
63                         return ok, err
64                 end
65         end
66
67         return true, nil
68 end
69
70 --- Validate given section of given configuration.
71 -- @param config        Name of the configuration to validate
72 -- @param section       Key of the section to validate
73 -- @param scheme        Scheme to validate against
74 -- @return                      Boolean indicating weather the given config validates
75 -- @return                      String containing the reason for errors (if any)
76 function UVL.validate_section( self, config, section, scheme )
77
78         if not scheme then
79                 return false, "No scheme found"
80         end
81
82         for k, v in pairs( config[section] ) do
83                 local ok, err = self:validate_option( config, section, k, scheme )
84
85                 if not ok then
86                         return ok, err
87                 end
88         end
89
90         return true, nil
91 end
92
93 --- Validate given option within section of given configuration.
94 -- @param config        Name of the configuration to validate
95 -- @param section       Key of the section to validate
96 -- @param option        Name of the option to validate
97 -- @param scheme        Scheme to validate against
98 -- @return                      Boolean indicating weather the given config validates
99 -- @return                      String containing the reason for errors (if any)
100 function UVL.validate_option( self, config, section, option, scheme )
101
102         if type(config) == "string" then
103                 config = { ["variables"] = { [section] = { [option] = config } } }
104         end
105
106         if not scheme then
107                 return false, "No scheme found"
108         end
109
110         local sv = scheme.variables[section]
111         if not sv then return false, "Requested section not found in scheme" end
112
113         sv = sv[option]
114         if not sv then return false, "Requested option not found in scheme" end
115
116         if not ( config[section] and config[section][option] ) and sv.required then
117                 return false, "Mandatory variable doesn't have a value"
118         end
119
120         if sv.type then
121                 if self.datatypes[sv.type] then
122                         if not self.datatypes[sv.type]( config[section][option] ) then
123                                 return false, "Value of given option doesn't validate"
124                         end
125                 else
126                         return false, "Unknown datatype '" .. sv.type .. "' encountered"
127                 end
128         end
129
130         return true, nil
131 end
132
133 --- Find all parts of given scheme and construct validation tree
134 -- @param scheme        Name of the scheme to parse
135 -- @return                      Parsed scheme
136 function UVL.read_scheme( self, scheme )
137         local schemes = { }
138
139         for i, file in ipairs( luci.fs.glob(self.schemedir .. '/*/' .. scheme) ) do
140                 _assert( luci.fs.access(file), "Can't access file '%s'", file )
141
142                 self.uci.set_confdir( luci.fs.dirname(file) )
143                 self.uci.load( luci.fs.basename(file) )
144
145                 table.insert( schemes, self.uci.get_all( luci.fs.basename(file) ) )
146         end
147
148         return self:_read_scheme_parts( scheme, schemes )
149 end
150
151 -- Process all given parts and construct validation tree
152 function UVL._read_scheme_parts( self, scheme, schemes )
153
154         local stbl = { }
155
156         -- helper function to construct identifiers for given elements
157         local function _id( c, t )
158                 if c == TYPE_SECTION then
159                         return string.format(
160                                 "section '%s.%s'",
161                                         scheme, t.name or '?' )
162                 elseif c == TYPE_VARIABLE then
163                         return string.format(
164                                 "variable '%s.%s.%s'",
165                                         scheme, t.section or '?.?', t.name or '?' )
166                 elseif c == TYPE_ENUM then
167                         return string.format(
168                                 "enum '%s.%s.%s'",
169                                         scheme, t.variable or '?.?.?', t.value or '?' )
170                 end
171         end
172
173         -- helper function to check for required fields
174         local function _req( c, t, r )
175                 for i, v in ipairs(r) do
176                         _assert( t[v], "Missing required field '%s' in %s", v, _id(c, t) )
177                 end
178         end
179
180         -- helper function to validate references
181         local function _ref( c, t )
182                 local k
183                 if c == TYPE_SECTION then
184                         k = "package"
185                 elseif c == TYPE_VARIABLE then
186                         k = "section"
187                 elseif c == TYPE_ENUM then
188                         k = "variable"
189                 end
190
191                 local r = luci.util.split( t[k], "." )
192                 r[1] = ( #r[1] > 0 and r[1] or scheme )
193
194                 _assert( #r == c, "Malformed %s reference in %s", k, _id(c, t) )
195
196                 return r
197         end
198
199         -- Step 1: get all sections
200         for i, conf in ipairs( schemes ) do
201                 for k, v in pairs( conf ) do
202                         if v['.type'] == 'section' then
203
204                                 _req( TYPE_SECTION, v, { "name", "package" } )
205
206                                 local r = _ref( TYPE_SECTION, v )
207
208                                 stbl.packages[r[1]] =
209                                         stbl.packages[r[1]] or {
210                                                 ["sections"]  = { };
211                                                 ["variables"] = { };
212                                         }
213
214                                 local p = stbl.packages[r[1]]
215                                           p.sections[v.name]  = p.sections[v.name]  or { }
216                                           p.variables[v.name] = p.variables[v.name] or { }
217
218                                 local s = p.sections[v.name]
219
220                                 for k, v in pairs(v) do
221                                         if k ~= "name" and k ~= "package" and k:sub(1,1) ~= "." then
222                                                 if k:match("^depends") then
223                                                         s["depends"] = _assert(
224                                                                 self:_read_depency( v, s["depends"] ),
225                                                                 "Section '%s' in scheme '%s' has malformed " ..
226                                                                 "depency specification in '%s'",
227                                                                 v.name, scheme, k
228                                                         )
229                                                 else
230                                                         s[k] = v
231                                                 end
232                                         end
233                                 end
234                         end
235                 end
236         end
237
238         -- Step 2: get all variables
239         for i, conf in ipairs( schemes ) do
240                 for k, v in pairs( conf ) do
241                         if v['.type'] == "variable" then
242
243                                 _req( TYPE_VARIABLE, v, { "name", "type", "section" } )
244
245                                 local r = _ref( TYPE_VARIABLE, v )
246
247                                 local p = _assert( stbl.packages[r[1]],
248                                         "Variable '%s' in scheme '%s' references unknown package '%s'",
249                                         v.name, scheme, r[1] )
250
251                                 local s = _assert( p.variables[r[2]],
252                                         "Variable '%s' in scheme '%s' references unknown section '%s'",
253                                         v.name, scheme, r[2] )
254
255                                 s[v.name] = s[v.name] or { }
256
257                                 local t = s[v.name]
258
259                                 for k, v in pairs(v) do
260                                         if k ~= "name" and k ~= "section" and k:sub(1,1) ~= "." then
261                                                 if k:match("^depends") then
262                                                         t["depends"] = _assert(
263                                                                 self:_read_depency( v, t["depends"] ),
264                                                                 "Variable '%s' in scheme '%s' has malformed " ..
265                                                                 "depency specification in '%s'",
266                                                                 v.name, scheme, k
267                                                         )
268                                                 elseif k:match("^validator") then
269                                                         t["validators"] = _assert(
270                                                                 self:_read_validator( v, t["validators"] ),
271                                                                 "Variable '%s' in scheme '%s' has malformed " ..
272                                                                 "validator specification in '%s'",
273                                                                 v.name, scheme, k
274                                                         )
275                                                 else
276                                                         t[k] = v
277                                                 end
278                                         end
279                                 end
280                         end
281                 end
282         end
283
284         -- Step 3: get all enums
285         for i, conf in ipairs( schemes ) do
286                 for k, v in pairs( conf ) do
287                         if v['.type'] == "enum" then
288
289                                 _req( TYPE_ENUM, v, { "value", "variable" } )
290
291                                 local r = _ref( TYPE_ENUM, v )
292
293                                 local p = _assert( stbl.packages[r[1]],
294                                         "Enum '%s' in scheme '%s' references unknown package '%s'",
295                                         v.value, scheme, r[1] )
296
297                                 local s = _assert( p.variables[r[2]],
298                                         "Enum '%s' in scheme '%s' references unknown section '%s'",
299                                         v.value, scheme, r[2] )
300
301                                 local t = _assert( s[r[3]],
302                                         "Enum '%s' in scheme '%s', section '%s' references " ..
303                                         "unknown variable '%s'",
304                                         v.value, scheme, r[2], r[3] )
305
306                                 _assert( t.type == "enum",
307                                         "Enum '%s' in scheme '%s', section '%s' references " ..
308                                         "variable '%s' with non enum type '%s'",
309                                         v.value, scheme, r[2], r[3], t.type )
310
311                                 if not t.values then
312                                         t.values = { [v.value] = v.title or v.value }
313                                 else
314                                         t.values[v.value] = v.title or v.value
315                                 end
316
317                                 if v.default then
318                                         _assert( not t.default,
319                                                 "Enum '%s' in scheme '%s', section '%s' redeclares " ..
320                                                 "the default value of variable '%s'",
321                                                 v.value, scheme, r[2], v.variable )
322
323                                         t.default = v.value
324                                 end
325                         end
326                 end
327         end
328
329         return stbl
330 end
331
332 -- Read a depency specification
333 function UVL._read_depency( self, value, deps )
334         local parts     = luci.util.split( value, "%s*;%s*" )
335         local condition = { }
336
337         for i, val in ipairs(parts) do
338                 local k, v = unpack(luci.util.split( val, "%s*=%s*" ))
339
340                 if k and (
341                         k:match("^%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+$")
342                         k:match("^%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+$") or
343                         k:match("^%$?[a-zA-Z0-9_]+$") or
344                 ) then
345                         condition[k] = v or true
346                 else
347                         return nil
348                 end
349         end
350
351         if not deps then
352                 deps = { condition }
353         else
354                 table.insert( deps, condition )
355         end
356
357         return deps
358 end
359
360 -- Read a validator specification
361 function UVL._read_validator( self, value, validators )
362         local validator
363
364         if value and value:match("/") and self.datatypes.file(value) then
365                 validator = value
366         else
367                 validator = self:_resolve_function( value )
368         end
369
370         if validator then
371                 if not validators then
372                         validators = { validator }
373                 else
374                         table.insert( validators, validator )
375                 end
376
377                 return validators
378         end
379 end
380
381 -- Resolve given path
382 function UVL._resolve_function( self, value )
383         local path = luci.util.split(value, ".")
384
385         for i=1, #path-1 do
386                 local stat, mod = pcall(require, table.concat(path, ".", 1, i))
387                 if stat and mod then
388                         for j=i+1, #path-1 do
389                                 if not type(mod) == "table" then
390                                         break;
391                                 end
392                                 mod = mod[path[j]]
393                                 if not mod then
394                                         break
395                                 end
396                         end
397                         mod = type(mod) == "table" and mod[path[#path]] or nil
398                         if type(mod) == "function" then
399                                 return mod
400                         end
401                 end
402         end
403 end