--[[
Copyright (c) 2012-2016 Kaarle Ritvanen
See LICENSE file for license details
--]]
--- @module aconf.model
local M = {}
local err = require('aconf.error')
local raise = err.raise
local node = require('aconf.model.node')
local object = require('aconf.object')
local class = object.class
local super = object.super
local util = require('aconf.util')
local map = util.map
local setdefaults = util.setdefaults
local update = util.update
M.Member = class()
function M.Member:init(params)
for k, v in pairs(params or {}) do
if self[k] == nil then self[k] = v end
end
end
function M.Member:auto_ui_name(name)
if not name then return end
if type(name) ~= 'string' then return tostring(name) end
local res = (name:sub(1, 1):upper()..name:sub(2)):gsub('-', ' ')
return res
end
function M.Member:meta(context)
return {
name=self.name,
description=self.description,
['ui-name']=self.ui_name or self:auto_ui_name(self.name)
}
end
function M.normalize_name(name) return name:gsub('_', '-') end
function M.conv_filter(filter)
if not filter then return end
local res = {}
for k, v in pairs(filter) do
res[M.normalize_name(k)] = type(v) == 'table' and v or {v}
end
return res
end
--- base class for fields. The constructor accepts a table argument
-- containing field parameters as key–value pairs. The parameters
-- listed below are valid for all subclasses. Subclasses may define
-- additional parameters.
-- @klass Field
-- @tparam ?string addr 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.
-- @param be_mode (optional **string** or
-- **{[string]=string,...}**) 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.
-- @param choice (optional **{primitive** or
-- **{primitive,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.
-- @param compute (optional function(@{node.TreeNode})
-- or **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.
-- @tparam ?primitive default default value for the field
-- @tparam ?boolean required field must be assigned a value if set,
-- defaults to false
-- @param store (optional function(@{node.TreeNode},
-- primitive) or **string**) 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.
-- @tparam ?string ui_name user-friendly name for the field.
-- @tparam ?boolean visible the field is visible in the user interface
-- if set, defaults to true
-- @tparam ?string widget 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.
M.Field = class(M.Member)
function M.Field:init(params)
if not params then params = {} end
setdefaults(
params, {addr=params.compute and node.null_addr or nil, visible=true}
)
super(self, M.Field):init(params)
for _, param in ipairs{'compute', 'store', 'editable'} do
local func = self[param]
if func then
if type(func) == 'string' then
local method = func
function func(obj, ...) return obj[method](obj, ...) end
end
if type(func) == 'function' then
self[param] = function(obj, ...)
return func(node.escalate(obj), ...)
end
end
end
end
self.condition = M.conv_filter(self.condition)
if self.choice then
self.choice = map(
function(choice)
if type(choice) ~= 'table' then choice = {choice} end
for i, k in ipairs{'value', 'ui-value'} do
if choice[i] then
assert(not choice[k])
choice[k] = choice[i]
choice[i] = nil
end
end
return setdefaults(
choice,
{
be_value=choice.value,
enabled=true,
['ui-value']=self:auto_ui_name(choice.value)
}
)
end,
self.choice
)
end
if not self.widget then
self.widget = self.choice and 'combobox' or 'field'
end
end
function M.Field:_editable(context)
if self.editable == nil then
if not self.visible then return false end
if self.store or not self.compute then return true end
if self.compute then return self:_compute(context) == nil end
return false
end
if type(self.editable) == 'function' then
return self.editable(context.parent) and true or false
end
return self.editable
end
function M.Field:_choice(context) return self.choice end
function M.Field:meta(context)
assert(self.dtype)
local choice = self:_choice(context)
return update(
super(self, M.Field):meta(context),
{
type=self.dtype,
visible=self.visible,
editable=self:_editable(context) and
node.has_permission(context.parent, 'modify'),
condition=self.condition,
required=self.required,
default=self.default,
choice=choice and map(
function(ch)
ch = util.copy(ch)
ch.be_value = nil
return ch
end,
choice
),
widget=self.widget,
detail=self.detail
}
)
end
function M.Field:topology(context)
return {
{
path=context.path,
addr=context.addr,
be_mode=self.be_mode,
type=self.dtype
}
}
end
function M.Field:load(context)
if not context.txn then return setmetatable({}, context) end
local value = self:_load(context)
if value == nil and self.compute then value = self:_compute(context) end
if value == nil then return self.default end
return value
end
function M.Field:_compute(context)
return self.compute(context.parent, context.txn)
end
function M.Field:_load(context)
local value = context.txn:get(context.addr)
if value ~= nil then return self:decode(context, value) end
end
function M.Field:decode(context, value) return value end
function M.Field:_validate(context, value)
if value == nil then
self:check_required(context)
return
end
value = self:normalize(context, value)
local committing = context.txn:committing()
local choice = self:_choice(context)
local be_value
if choice then
for _, ch in ipairs(choice) do
if ch.value == value and (committing or ch.enabled) then
be_value = ch.be_value
break
end
end
if be_value == nil then raise(context.path, 'Invalid value') end
end
self:validate(context, value)
if choice then return be_value end
return value
end
function M.Field:check_required(context)
if self.required then raise(context.path, 'Required value not set') end
end
function M.Field:normalize(context, value) return value end
function M.Field:validate(context, value) end
function M.Field:save(context, value)
if not (context.privileged or self:_editable(context)) then
raise(context.path, 'Is not editable')
end
if self.store then self.store(context.parent, value, context.txn)
else self:_save(context, self:_validate(context, value)) end
end
function M.Field:_save(context, value)
if value ~= nil then value = self:encode(context, value) end
context.txn:set(context.addr, value)
end
function M.Field:encode(context, value) return value end
function M.Field:validate_saved(context)
if self:_editable(context) then self:save(context, self:load(context)) end
end
local Primitive = class(M.Field)
function Primitive:_load(context)
local value = super(self, Primitive):_load(context)
if value == nil then return end
local choice = self:_choice(context)
if not choice then return value end
for _, ch in ipairs(choice) do
if ch.be_value == value then return ch.value end
end
assert(false)
end
function Primitive:validate(context, value)
local t = self.dtype
if type(value) ~= t then raise(context.path, 'Not a '..t) end
end
--- string field, inherits @{Field}.
-- @klass String
M.String = class(Primitive)
function M.String:init(params)
super(self, M.String):init(params)
self.dtype = 'string'
end
function M.String:validate(context, value)
super(self, M.String):validate(context, value)
if self['max-length'] and value:len() > self['max-length'] then
raise(context.path, 'Maximum length exceeded')
end
if self.pattern and not value:match('^'..self.pattern..'$') then
raise(context.path, 'Invalid value')
end
end
function M.String:meta(context)
local res = super(self, M.String):meta(context)
res['max-length'] = self['max-length']
return res
end
M.Number = class(Primitive)
function M.Number:init(params)
super(self, M.Number):init(params)
self.dtype = 'number'
end
function M.Number:normalize(context, value)
return value and tonumber(value) or value
end
function M.Number:validate(context, value)
super(self, M.Number):validate(context, value)
if self.min and value < self.min then
raise(context.path, 'Minimum value is '..self.min)
end
if self.max and value > self.max then
raise(context.path, 'Maximum value is '..self.max)
end
end
M.Integer = class(M.Number)
function M.Integer:validate(context, value)
super(self, M.Integer):validate(context, value)
if math.floor(value) ~= value then raise(context.path, 'Not an integer') end
end
--- boolean field, inherits @{Field}.
-- @klass Boolean
M.Boolean = class(Primitive)
function M.Boolean:init(params)
super(self, M.Boolean):init(params)
self.dtype = 'boolean'
self.widget = 'checkbox'
end
M.TreeNode = class(M.Field)
function M.TreeNode:init(params)
super(self, M.TreeNode):init(
setdefaults(params, {detail=true, widget='link'})
)
self.iparams = {}
end
function M.TreeNode:topology(context)
local res = super(self, M.TreeNode):topology(context)
res[1].subtype = res[1].type
res[1].type = 'table'
util.extend(res, node.topology(self:load(context, {create=true})))
return res
end
function M.TreeNode:load(context, options)
if context.txn and not (
util.setdefault(
options or {}, 'create', self.create
) or self:_load(context)
) then return end
local res = self.itype(
context, update({editable=self:_editable(context)}, self.iparams)
)
return node.has_permission(res, 'read') and res or nil
end
function M.TreeNode:save(context, value)
local path = context.path
if value == path then return end
if type(value) == 'string' then
value = node.fetch(context.parent, value)
end
if object.isinstance(value, node.TreeNode) and node.path(value) == path then
return
end
self:_save(context)
if value then
if type(value) ~= 'table' then
raise(path, 'Cannot assign primitive value')
end
self:_save(context, {})
local new = self:load(context, {create=true})
local errors = err.ErrorDict()
for k, v in pairs(value) do
errors:collect(self.save_member, new, k, v)
end
errors:raise()
return new
end
end
function M.TreeNode.save_member(node, k, v) node[k] = v end
function M.TreeNode:validate_saved(context)
if self:load(context) == nil then self:check_required(context) end
end
--- model field, inherits @{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}.
-- @klass Model
-- @param model ([<Model>](#new)) model describing the
-- structure of the model objects
M.Model = class(M.TreeNode)
function M.Model:init(params)
super(self, M.Model):init(params)
assert(self.model)
self.itype = self.model
self.dtype = 'model'
end
function M.Model:save(context, value)
local new = super(self, M.Model):save(context, value)
if new then node.check_permission(new, 'create') end
end
return M