--[[ Copyright (c) 2012-2016 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 M.Boolean = fld.Boolean M.Integer = fld.Integer M.Number = fld.Number 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') 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 M.Model = fld.Model --- collection field. -- @fcons Collection -- @param type ([<Field>](#Field_constructors) or -- [<Model>](#new)) field constructor, 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}. -- @fcons 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}. -- @fcons Set M.Set = class(M.Collection) function M.Set:init(params) if not params.widget and 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 function M.trigger(phase, addr, func) store:trigger(phase, addr, func) end function M.defer(addr) def_store:defer(addr) end --- Field constructor parameters. All field constructors accept one -- table argument containing field parameters as key–value -- pairs. The parameters listed below are valid for all -- subclasses. Subclasses may define additional parameters. -- @section Field --- back-end address for the field. This can be an -- absolute address or relative to the parent's 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. -- @tfield ?string addr --- 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 **string** or -- **{[string]=string,...}**) --- array of allowed values. Each value may be a primitive value or a -- tuple specifying the value used by the data model and a -- user-friendly value. -- @field choice (optional **{primitive** or -- **{primitive,string},...}**) --- 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 -- as an argument. If defined as a string, a method with the given -- name is invoked. -- @field compute (optional function(@{node.TreeNode}) -- or **string**) --- default value for the field. -- @tfield ?primitive default --- 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 function(@{node.TreeNode}, -- primitive) or **string**) --- 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