--[[
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
--- 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 ([<Model>](#new)) 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
-- [<Field>](#Overview_of_field_classes)) primitive field
-- class or instance used to validate the collection keys.
-- @param type ([<Field>](#Overview_of_field_classes) or
-- [<Model>](#new)) 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 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–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–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 **string** or
-- **function(string)**)
--- 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
-- 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 **{primitive** or
-- **{primitive,string[,'be_value'=string][,'enabled'=boolean]},...}**)
--- 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
-- function(@{node.TreeNode},[<Transaction>](#Transaction_objects))
-- or **string**)
--- 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 **{[string]=string** or
-- **{string,...},...}**)
--- 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 **boolean**,
-- function(@{node.TreeNode}) or **string**)
--- 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