diff options
author | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2014-03-10 22:45:18 +0200 |
---|---|---|
committer | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2014-03-24 01:18:13 +0200 |
commit | 7d9c43916b0600ac4879dfe9793eab807a83ab2b (patch) | |
tree | ec54ed64c9a557b6ea4ad88d31138a02d3e0cd04 /aconf/model/field.lua | |
parent | cb6c243dc356ef1d46d7ddb96e6ea6ae007c6cca (diff) | |
download | aconf-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.lua | 382 |
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 |