* luci/libs: make uvl code usable
[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         -- helper function to construct identifiers for given elements
155         local function _id( c, t )
156                 if c == TYPE_SECTION then
157                         return string.format(
158                                 "section '%s.%s'",
159                                         scheme, t.name or '?' )
160                 elseif c == TYPE_VARIABLE then
161                         return string.format(
162                                 "variable '%s.%s.%s'",
163                                         scheme, t.section or '?.?', t.name or '?' )
164                 elseif c == TYPE_ENUM then
165                         return string.format(
166                                 "enum '%s.%s.%s'",
167                                         scheme, t.variable or '?.?.?', t.value or '?' )
168                 end
169         end
170
171         -- helper function to check for required fields
172         local function _req( c, t, r )
173                 for i, v in ipairs(r) do
174                         _assert( t[v], "Missing required field '%s' in %s", v, _id(c, t) )
175                 end
176         end
177
178         -- helper function to validate references
179         local function _ref( c, t )
180                 local k
181                 if c == TYPE_SECTION then
182                         k = "package"
183                 elseif c == TYPE_VARIABLE then
184                         k = "section"
185                 elseif c == TYPE_ENUM then
186                         k = "variable"
187                 end
188
189                 local r = luci.util.split( t[k], "." )
190                 r[1] = ( #r[1] > 0 and r[1] or scheme )
191
192                 _assert( #r == c, "Malformed %s reference in %s", k, _id(c, t) )
193
194                 return r
195         end
196
197         -- Step 1: get all sections
198         for i, conf in ipairs( schemes ) do
199                 for k, v in pairs( conf ) do
200                         if v['.type'] == 'section' then
201
202                                 _req( TYPE_SECTION, v, { "name", "package" } )
203
204                                 local r = _ref( TYPE_SECTION, v )
205
206                                 self.packages[r[1]] =
207                                         self.packages[r[1]] or {
208                                                 ["sections"]  = { };
209                                                 ["variables"] = { };
210                                         }
211
212                                 local p = self.packages[r[1]]
213                                           p.sections[v.name]  = p.sections[v.name]  or { }
214                                           p.variables[v.name] = p.variables[v.name] or { }
215
216                                 local s = p.sections[v.name]
217
218                                 for k, v2 in pairs(v) do
219                                         if k ~= "name" and k ~= "package" and k:sub(1,1) ~= "." then
220                                                 if k:match("^depends") then
221                                                         s["depends"] = _assert(
222                                                                 self:_read_depency( v2, s["depends"] ),
223                                                                 "Section '%s' in scheme '%s' has malformed " ..
224                                                                 "depency specification in '%s'",
225                                                                 v.name or '<nil>', scheme or '<nil>', k
226                                                         )
227                                                 else
228                                                         s[k] = v2
229                                                 end
230                                         end
231                                 end
232                         end
233                 end
234         end
235
236         -- Step 2: get all variables
237         for i, conf in ipairs( schemes ) do
238                 for k, v in pairs( conf ) do
239                         if v['.type'] == "variable" then
240
241                                 _req( TYPE_VARIABLE, v, { "name", "type", "section" } )
242
243                                 local r = _ref( TYPE_VARIABLE, v )
244
245                                 local p = _assert( self.packages[r[1]],
246                                         "Variable '%s' in scheme '%s' references unknown package '%s'",
247                                         v.name, scheme, r[1] )
248
249                                 local s = _assert( p.variables[r[2]],
250                                         "Variable '%s' in scheme '%s' references unknown section '%s'",
251                                         v.name, scheme, r[2] )
252
253                                 s[v.name] = s[v.name] or { }
254
255                                 local t = s[v.name]
256
257                                 for k, v in pairs(v) do
258                                         if k ~= "name" and k ~= "section" and k:sub(1,1) ~= "." then
259                                                 if k:match("^depends") then
260                                                         t["depends"] = _assert(
261                                                                 self:_read_depency( v, t["depends"] ),
262                                                                 "Variable '%s' in scheme '%s' has malformed " ..
263                                                                 "depency specification in '%s'",
264                                                                 v.name, scheme, k
265                                                         )
266                                                 elseif k:match("^validator") then
267                                                         t["validators"] = _assert(
268                                                                 self:_read_validator( v, t["validators"] ),
269                                                                 "Variable '%s' in scheme '%s' has malformed " ..
270                                                                 "validator specification in '%s'",
271                                                                 v.name, scheme, k
272                                                         )
273                                                 else
274                                                         t[k] = v
275                                                 end
276                                         end
277                                 end
278                         end
279                 end
280         end
281
282         -- Step 3: get all enums
283         for i, conf in ipairs( schemes ) do
284                 for k, v in pairs( conf ) do
285                         if v['.type'] == "enum" then
286
287                                 _req( TYPE_ENUM, v, { "value", "variable" } )
288
289                                 local r = _ref( TYPE_ENUM, v )
290
291                                 local p = _assert( self.packages[r[1]],
292                                         "Enum '%s' in scheme '%s' references unknown package '%s'",
293                                         v.value, scheme, r[1] )
294
295                                 local s = _assert( p.variables[r[2]],
296                                         "Enum '%s' in scheme '%s' references unknown section '%s'",
297                                         v.value, scheme, r[2] )
298
299                                 local t = _assert( s[r[3]],
300                                         "Enum '%s' in scheme '%s', section '%s' references " ..
301                                         "unknown variable '%s'",
302                                         v.value, scheme, r[2], r[3] )
303
304                                 _assert( t.type == "enum",
305                                         "Enum '%s' in scheme '%s', section '%s' references " ..
306                                         "variable '%s' with non enum type '%s'",
307                                         v.value, scheme, r[2], r[3], t.type )
308
309                                 if not t.values then
310                                         t.values = { [v.value] = v.title or v.value }
311                                 else
312                                         t.values[v.value] = v.title or v.value
313                                 end
314
315                                 if v.default then
316                                         _assert( not t.default,
317                                                 "Enum '%s' in scheme '%s', section '%s' redeclares " ..
318                                                 "the default value of variable '%s'",
319                                                 v.value, scheme, r[2], v.variable )
320
321                                         t.default = v.value
322                                 end
323                         end
324                 end
325         end
326
327         return self
328 end
329
330 -- Read a depency specification
331 function UVL._read_depency( self, value, deps )
332         local parts     = luci.util.split( value, "%s*,%s*", nil, true )
333         local condition = { }
334
335         for i, val in ipairs(parts) do
336                 local k, v = unpack(luci.util.split( val, "%s*=%s*", nil, true ))
337
338                 if k and (
339                         k:match("^%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+$") or
340                         k:match("^%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+$") or
341                         k:match("^%$?[a-zA-Z0-9_]+$")
342                 ) then
343                         condition[k] = v or true
344                 else
345                         return nil
346                 end
347         end
348
349         if not deps then
350                 deps = { condition }
351         else
352                 table.insert( deps, condition )
353         end
354
355         return deps
356 end
357
358 -- Read a validator specification
359 function UVL._read_validator( self, value, validators )
360         local validator
361
362         if value and value:match("/") and self.datatypes.file(value) then
363                 validator = value
364         else
365                 validator = self:_resolve_function( value )
366         end
367
368         if validator then
369                 if not validators then
370                         validators = { validator }
371                 else
372                         table.insert( validators, validator )
373                 end
374
375                 return validators
376         end
377 end
378
379 -- Resolve given path
380 function UVL._resolve_function( self, value )
381         local path = luci.util.split(value, ".")
382
383         for i=1, #path-1 do
384                 local stat, mod = pcall(require, table.concat(path, ".", 1, i))
385                 if stat and mod then
386                         for j=i+1, #path-1 do
387                                 if not type(mod) == "table" then
388                                         break;
389                                 end
390                                 mod = mod[path[j]]
391                                 if not mod then
392                                         break
393                                 end
394                         end
395                         mod = type(mod) == "table" and mod[path[#path]] or nil
396                         if type(mod) == "function" then
397                                 return mod
398                         end
399                 end
400         end
401 end