summaryrefslogtreecommitdiffstats
path: root/aconf/model/init.lua
blob: b5a496f8583630cd834918aa2c8980b298c06a8a (plain)
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
--[[
Copyright (c) 2012-2019 Kaarle Ritvanen
See LICENSE file for license details
--]]

--- @module aconf.model
local M = {}

M.error = require('aconf.error')
local raise = M.error.raise
local relabel = M.error.relabel

M.binary = require('aconf.model.binary')

local combination = require('aconf.model.combination')
M.Union = combination.Union
M.Range = combination.Range

local fld = require('aconf.model.field')
local Field = fld.Field
--- boolean field.
-- @fclass Boolean
M.Boolean = fld.Boolean
--- integer field.
-- @fclass Integer
-- @tparam ?int max maximum allowed value
-- @tparam ?int min minimum allowed value
M.Integer = fld.Integer
M.Number = fld.Number
--- string field.
-- @fclass String
-- @tparam ?string pattern Lua pattern for acceptable values
M.String = fld.String

local model = require('aconf.model.model')
M.Action = model.Action
M.new = model.new
local to_field = model.to_field

M.net = require('aconf.model.net')

local node = require('aconf.model.node')
M.node = {}
for _, m in ipairs{
   'List',
   'Set',
   'TreeNode',
   'contains',
   'has_permission',
   'insert',
   'meta',
   'mmeta',
   'name',
   'parent',
   'path',
   'pairs'
} do M.node[m] = node[m] end

M.permission = require('aconf.model.permission')
M.register = require('aconf.model.root').register
M.service = require('aconf.model.service')
M.node.Set = require('aconf.model.set').Set
M.time = require('aconf.model.time')

M.object = require('aconf.object')
local class = M.object.class
local isinstance = M.object.isinstance
local super = M.object.super

M.path = require('aconf.path')
local store = require('aconf.persistence')
local def_store = require('aconf.persistence.defer')

local util = require('aconf.util')
local setdefault = util.setdefault
local update = util.update


local stringy = require('stringy')


--- reference field. The value of this field refers to another node in
-- the data model. Reading the value of this field generally yields
-- the referred @{node.TreeNode} instance or primitive value. If the
-- reference is a @{node.Set} member, reading yields the relative path
-- of the referred node. The value can be set by assigning the
-- reference a @{node.TreeNode} instance or a data model path. The
-- path can be absolute or relative to the scope of the reference. If
-- a *compute* function is provided, the return value is supposed to
-- be a relative path.
-- @fclass Reference
-- @tparam ?{[string]=primitive,...} filter limit the reference to
-- [model objects](#Model_objects), the fields of which have specific
-- values. The keys of the table define the fields, and the
-- corresponding values define required field values. If a value is a
-- table, the field can have any of the value given in the table to
-- make the object eligible.
-- @tparam ?string on_delete specify behavior when the referenced node
-- is deleted. If set to *set-null*, the value of the reference is
-- cleared. If set to *cascade*, the parent model of this reference is
-- deleted. The default mode is *restrict*, which prevents the
-- deletion of the referenced node.
-- @tparam ?string scope limit the nodes that can be referenced to a
-- subtree defined by this path. The path can be absolute or relative
-- to the reference field. By default, the scope is the entire data
-- model.
M.Reference = class(Field)

function M.Reference:init(params)
   super(self, M.Reference):init(
      util.setdefaults(
	 params,
	 {on_delete='restrict', scope='/', search='*', widget='reference'}
      )
   )
   self.dtype = 'reference'
   self.dereference = true
   self.filter = fld.conv_filter(self.filter)
end

function M.Reference:topology(context)
   local res = super(self, M.Reference):topology(context)
   update(res[1], {scope=self.scope, search=self.search})
   return res
end

function M.Reference:abs_scope(context)
   return M.path.to_absolute(self.scope, node.path(context.parent))
end

function M.Reference:_choice(context)
   local res = {}
   local onelevel = self.search == '*'

   local obj = relabel('system', node.fetch, context.parent, self.scope)
   assert(isinstance(obj, node.TreeNode))

   for _, v in ipairs(node.search(obj, self.search)) do
      local ch = {enabled=true}

      if isinstance(v.value, node.TreeNode) then
	 ch.ref = node.path(v.value)
	 if M.path.is_subordinate(context.path, ch.ref) then ch = nil
	 else
	    update(
	       ch,
	       {
		  be_value=v.path,
		  value=self.dereference and ch.ref or v.path,
		  ['ui-value']=onelevel and M.path.name(v.path) or v.path
	       }
	    )
	    if self.filter then
	       assert(isinstance(v.value, model.Model))
	       if not v.value:match(self.filter) then ch.enabled = false end
	    end
	 end

      else
	 assert(onelevel)
	 local ep = M.path.escape(v.value)
	 update(ch, {be_value=ep, value=ep, ['ui-value']=v.value})
      end

      if ch then table.insert(res, ch) end
   end

   return res
end

function M.Reference:meta(context)
   return update(
      super(self, M.Reference):meta(context),
      {scope=self:abs_scope(context), dynamic=self.filter and true or false}
   )
end

function M.Reference:follow(context, value)
   return node.fetch(
      context.parent, M.path.rawjoin(self:abs_scope(context), value)
   )
end

function M.Reference:load(context, options)
   local ref = super(self, M.Reference):load(context)
   return (
      setdefault(
	 options or {}, 'dereference', self.dereference
      ) and context.txn and ref
   ) and self:follow(context, ref) or ref
end

function M.Reference:normalize(context, value)
   if isinstance(value, node.TreeNode) then value = node.path(value) end

   local path = context.path
   if type(value) ~= 'string' then raise(path, 'Path name must be string') end

   local rel = value
   local scope = self:abs_scope(context)

   if M.path.is_absolute(rel) then
      local prefix = scope..'/'
      if not stringy.startswith(rel, prefix) then
	 raise(path, 'Reference out of scope ('..scope..')')
      end
      rel = rel:sub(prefix:len() + 1, -1)
   end

   -- TODO check instance type
   relabel(path, self.follow, self, context, rel)

   return self.dereference and M.path.to_absolute(value, scope) or rel
end

function M.Reference:deleted(context, addr)
   local target = self:load(context, {dereference=true})

   if target and node.addr(target) == addr then
      local policy = self.on_delete

      if policy == 'restrict' then
	 -- TODO raise error for the target object
	 raise(context.path, 'Refers to '..addr)
      end

      local parent = context.parent
      local path = context.path

      if policy == 'cascade' then
	 path = node.path(parent)
	 parent = node.parent(parent)
      else assert(policy == 'set-null') end

      node.save(parent, M.path.name(path))
   end
end


--- model field. The value of this field is a [model
-- object](#Model_objects) conforming to the specified model. A model
-- field with default parameters is implicitly created when a model is
-- used in lieu of a field.
-- @fclass Model
-- @tparam ?boolean create specifies whether the model is created
-- automatically with the parent object, defaults to false
-- @param model (<i>[&lt;Model&gt;](#new)</i>) model describing the
-- structure of the model objects
M.Model = fld.Model


--- collection field. The value of this field is an instance of
-- @{node.Collection}.
-- @fclass Collection
-- @param key (optional
-- <i>[&lt;Field&gt;](#Overview_of_field_classes)</i>) primitive field
-- class or instance used to validate the collection keys.
-- @param type (<i>[&lt;Field&gt;](#Overview_of_field_classes)</i> or
-- <i>[&lt;Model&gt;](#new)</i>) field class, field instance, or model
-- specifying the type of the members.
-- @tparam ?string ui_member user-friendly noun for a member of this
-- collection
M.Collection = class(fld.TreeNode)

function M.Collection:init(params, itype)
   if params.create == nil then params.create = true end
   super(self, M.Collection):init(params)

   assert(self.type)
   self.itype = itype or node.Collection
   self.iparams = {
      destroy=self.destroy,
      key=self.key,
      layout=self.layout,
      required=self.required,
      ui_member=self.ui_member
   }

   self.dtype = 'collection'
end

function M.Collection:auto_ui_name(name)
   if not name then return end
   if name:sub(-1, -1) ~= 's' then name = name..'s' end
   return super(self, M.Collection):auto_ui_name(name)
end

function M.Collection:load(context, options)
   if not self.iparams.field then
      self.iparams.field = to_field(self.type)
      if isinstance(self.iparams.field, fld.Model) then
	 setdefault(self.iparams, 'layout', 'tabular')
      end
   end
   return super(self, M.Collection):load(context, options)
end


--- list field, inherits @{Collection}. The value of this field is an
-- instance of @{node.List}.
-- @fclass List
M.List = class(M.Collection)
function M.List:init(params) super(self, M.List):init(params, node.List) end


--- set field, inherits @{Collection}. The value of this field is an
-- instance of @{node.Set}.
-- @fclass Set
M.Set = class(M.Collection)
function M.Set:init(params)
   if not params.widget and (
      params.type.choice or isinstance(params.type, M.Reference)
   ) then
      params.widget = 'checkboxes'
   end
   super(self, M.Set):init(params, M.node.Set)
end
function M.Set.save_member(tn, k, v) node.insert(tn, v) end


-- experimental
M.Mixed = class(M.Collection)

function M.Mixed:init(params)
   super(self, M.Mixed):init(update(params, {type=M.Mixed}), node.Mixed)
   self.pfield = Field()
end

function M.Mixed:topology(context) return {} end

function M.Mixed:load(context)
   local value = self.pfield:load(context)
   if type(value) == 'table' then return super(self, M.Mixed):load(context) end
   return value
end

function M.Mixed:save(context, value)
   if type(value) == 'table' then super(self, M.Mixed):save(context, value)
   else self.pfield:save(context, value) end
end


--- install pre or post&ndash;commit hook, which is called whenever
-- the specified back-end subtree is affected.
-- @tparam string phase *pre* or *post*
-- @tparam string addr back-end address specifying the scope
-- @tparam function() func hook function
function M.trigger(phase, addr, func) store:trigger(phase, addr, func) end

--- defer committing a transaction affecting the specified back-end
-- subtree until the client socket has been closed. This is mainly
-- used with network configuration.
-- @tparam string addr back-end address specifying the scope
function M.defer(addr) def_store:defer(addr) end


--- Field constructor parameters. All field constructors accept one
-- table argument containing field parameters as key&ndash;value
-- pairs. The parameters listed below are valid for all field
-- classes. Depending on the class, there may be additional
-- parameters.
-- @section Field

--- back-end address for the field. This can be an
-- absolute address or relative to the parent's address. Can also be
-- defined as a function which receives the field's absolute path in
-- the data model as an argument and returns either absolute or
-- relative back-end address. The top-level component of a back-end
-- address specifies the back-end. The interpretation of the remaining
-- components is specific to the back-end. If not specified, the
-- address is formed by appending the field's name to the address of
-- the parent.
-- @field addr (optional <i>**string**</i> or
-- <i>**function(string)**</i>)

--- back-end mode. Controls how the Augeas back-end will map addresses
-- to Augeas paths. By default, each component of a back-end address
-- is directly mapped to an Augeas path component. This parameter is
-- an exception table applicable to the subtree defined by the field's
-- address. Each key is a relative address pattern, and the
-- corresponding value is a directive applied to matching address
-- components. The *enumerate* directive indicates there can be
-- several Augeas nodes matching the path and the next component is to
-- be interpreted as an index for such nodes. The *parent-value*
-- directive is applicable only to primitive fields and instructs the
-- back-end not to append the last address component at all, causing
-- the parent node's value to be accessed. If the *be\_mode* parameter
-- is defined as a string, it is assumed to be a directive applicable
-- to the field's own address.
-- @field be_mode (optional <i>**string**</i> or
-- <i>**{[string]=string,...}**</i>)

--- array of allowed values. Each value may be a primitive value or a
-- table specifying the value used by the data model and a
-- user-friendly value. The table may contain additional
-- choice-specific parameters. *be\_value* specifies the choice's
-- value in the back-end if different from the data model. *enabled*
-- specifies whether the choice is available for the user, defaulting
-- to true.
-- @field choice (optional <i>**{primitive**</i> or
-- <i>**{primitive,string[,'be_value'=string][,'enabled'=boolean]},...}**</i>)

--- function for computing the value of the field when not provided by
-- the back-end. The function gets a reference to the field's parent
-- and the ongoing transaction as arguments. If defined as a string, a
-- method with the given name is invoked.
-- @field compute (optional
-- <i><b>function(@{node.TreeNode},[&lt;Transaction&gt;](#Transaction_objects))</b></i>
-- or <i>**string**</i>)

--- indicates this field is relevant only when other fields of the
-- model have specific values. The keys of the table define the
-- condition fields, and the corresponding values define required
-- field values. If a value is a table, the condition field can have
-- any of the value given in the table for this field to be considered
-- relevant.
-- @field condition (optional <i>**{[string]=string**</i> or
-- <i>**{string,...},...}**</i>)

--- default value for the field.
-- @tfield ?primitive default

--- specifies whether the field shall be hidden in tabular user
-- interface layout. By default, only leaf objects are shown.
-- @tfield ?boolean detail specifies whether the field shall be hidden
-- in tabular user interface layout. By default, only leaf objects are
-- shown.

--- boolean or function specifying whether the value of the field can
-- be changed. If defined as a function, the return value determines
-- the behavior. The function gets a reference to the field's parent
-- as an argument. If defined as a string, a method with the given
-- name is invoked. When this parameter is not defined, the behavior
-- depends on the other parameters as follows: If *visible* is set to
-- false, the field is not editable. Otherwise, if *store* is defined
-- or *compute* is not, the field is editable. Otherwise, if the
-- *compute* function yields a value, the field is not
-- editable. Otherwise, the field is editable.
-- @field editable (optional <i>**boolean**</i>,
-- <i><b>function(@{node.TreeNode})</b></i> or <i>**string**</i>)

--- required field must be assigned a value if set. Defaults to false.
-- @tfield ?boolean required

--- function for storing the value of the field. If this parameter is
-- defined, the value of the field is not stored according to the
-- field's back-end address. Rather, the provided function is invoked
-- with a reference to the parent and the field value. If defined as a
-- string, a method with the given name is invoked.
-- @field store (optional <i><b>function(@{node.TreeNode},
-- primitive)</b></i> or <i>**string**</i>)

--- user-friendly name for the field.
-- @tfield ?string ui_name

--- visibility of the field in the user interface. Defaults to true.
-- @tfield ?boolean visible

--- widget for rendering the field in the user interface. The default
-- widget for non-leaf objects is *link*, which is a hyperlink to a
-- detailed view to the object. The *inline* widget renders a non-leaf
-- object embedded in the parent's view.
-- @tfield ?string widget


return M