summaryrefslogtreecommitdiffstats
path: root/aconf/model/field.lua
diff options
context:
space:
mode:
authorKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2014-03-10 22:45:18 +0200
committerKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2014-03-24 01:18:13 +0200
commit7d9c43916b0600ac4879dfe9793eab807a83ab2b (patch)
treeec54ed64c9a557b6ea4ad88d31138a02d3e0cd04 /aconf/model/field.lua
parentcb6c243dc356ef1d46d7ddb96e6ea6ae007c6cca (diff)
downloadaconf-7d9c43916b0600ac4879dfe9793eab807a83ab2b.tar.bz2
aconf-7d9c43916b0600ac4879dfe9793eab807a83ab2b.tar.xz
rename ACF2 to Alpine Configurator (aconf)
Diffstat (limited to 'aconf/model/field.lua')
-rw-r--r--aconf/model/field.lua382
1 files changed, 382 insertions, 0 deletions
diff --git a/aconf/model/field.lua b/aconf/model/field.lua
new file mode 100644
index 0000000..80f08c5
--- /dev/null
+++ b/aconf/model/field.lua
@@ -0,0 +1,382 @@
+--[[
+Copyright (c) 2012-2014 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+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
+
+
+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
+
+
+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 type(func) == 'string' then
+ self[param] = function(self, obj, ...) return obj[func](obj, ...) 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 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 util.update(
+ super(self, M.Field):meta(context),
+ {
+ type=self.dtype,
+ visible=self.visible,
+ editable=self:_editable(context),
+ 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, 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) return context.txn:get(context.addr) 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_editable(context)
+ if not self:_editable(context) then
+ raise(context.path, 'Is not editable')
+ end
+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)
+ self:check_editable(context)
+ 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) context.txn:set(context.addr, 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
+
+
+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
+
+
+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'})
+ )
+end
+
+function M.TreeNode:topology(context)
+ local res = super(self, M.TreeNode):topology(context)
+ 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
+ return self.itype(context, self.iparams)
+end
+
+function M.TreeNode:save(context, value)
+ local path = context.path
+
+ if value == path then return end
+ if type(value) == 'string' then value = context.txn:fetch(value) end
+ if object.isinstance(value, node.TreeNode) and node.path(value) == path then
+ return
+ end
+
+ local check = value ~= nil and next(value) ~= nil
+ if not check then
+ local old = self:_load(context)
+ if old and next(old) ~= nil then check = true end
+ end
+ if check then self:check_editable(context) 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 node.pairs(value) do
+ errors:collect(self.save_member, new, k, v)
+ end
+ errors:raise()
+ 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
+
+
+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
+
+
+return M