--[[ Copyright (c) 2012-2013 Kaarle Ritvanen See LICENSE file for license details --]] local M = {} local err = require('acf2.error') local raise = err.raise local node = require('acf2.model.node') local object = require('acf2.object') local class = object.class local super = object.super local util = require('acf2.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 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.conv_filter(filter) return filter and map( function(values) return type(values) == 'table' and values or {values} end, filter ) or nil end M.Field = class(M.Member) function M.Field:init(params) if not params then params = {} end params = setdefaults( params, { addr=params.compute and node.null_addr or nil, editable=not params.compute } ) super(self, M.Field):init(params) if type(self.editable) ~= 'function' then function self.editable(context) return params.editable 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, {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:_choice(context) return self.choice end function M.Field:meta(context) assert(self.dtype) local res = super(self, M.Field):meta(context) res.type = self.dtype res.editable = self:editable(context) res.condition = self.condition res.required = self.required res.default = self.default res.choice = self:_choice(context) res.widget = self.widget return res 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 if self.compute then value = self:compute(context) end if value == nil then value = self:_load(context) end if value == nil then return self.default end return value 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 local save value, save = self:normalize(context, value) local committing = context.txn:committing() local choice = self:_choice(context) if choice and not util.contains( map( function(ch) return ch.value end, util.filter(function(ch) return committing or ch.enabled end, choice) ), value ) then raise(context.path, 'Invalid value') end self:validate(context, value) if save == nil then save = value end return save 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) self:_save(context, self:_validate(context, value)) 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: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 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 = self.dtype end M.TreeNode = class(M.Field) function M.TreeNode:init(params) if not params.widget then params.widget = 'link' end super(self, M.TreeNode):init(params) 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 ( ( options and options.create ) or self.create or self:_load(context) ) then return end return self.itype(context, self.iparams) end function M.TreeNode:save(context, value) self:check_editable(context) 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 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