--[[ 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