diff options
Diffstat (limited to 'acf2/model/field.lua')
-rw-r--r-- | acf2/model/field.lua | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/acf2/model/field.lua b/acf2/model/field.lua new file mode 100644 index 0000000..6cbfa96 --- /dev/null +++ b/acf2/model/field.lua @@ -0,0 +1,241 @@ +--[[ +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 function contains(list, value) + for k, v in ipairs(list) do if v == value then return true end end + return false +end + +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 + return (name:sub(1, 1):upper()..name:sub(2)):gsub('-', ' ') +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 + + +M.Field = class(M.Member) + +function M.Field:init(params) + super(self, M.Field):init(params) + + if self.choice and not self['ui-choice'] then + self['ui-choice'] = util.map( + function(name) return self:auto_ui_name(name) end, + self.choice + ) + end + + if not self.widget then + self.widget = self.choice and 'combobox' or 'field' + end +end + +function M.Field:meta(context) + assert(self.dtype) + local res = super(self, M.Field):meta(context) + + res.type = self.dtype + res.required = self.required + res.default = self.default + res.choice = self.choice + res.widget = self.widget + res['ui-choice'] = self['ui-choice'] + + 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 = context.txn:get(context.addr) + if value == nil then return self.default end + return value +end + +function M.Field:_validate(context, value) + if self.required and value == nil then + raise(context.path, 'Required value not set') + end + if self.choice and value ~= nil and not contains(self.choice, value) then + raise(context.path, 'Invalid value') + end + if value ~= nil then self:validate(context, value) end + return value +end + +function M.Field:validate(context, value) end + +function M.Field:save(context, value) + context.txn:set(context.addr, self:_validate(context, value)) +end + +function M.Field:validate_saved(context) + self:save(context, self:load(context)) +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:_validate(context, value) + return super(self, M.Number):_validate( + context, + 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 context.txn:get(context.addr) + ) 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 + + context.txn:set(context.addr) + + if value then + if type(value) ~= 'table' then + raise(path, 'Cannot assign primitive value') + end + + context.txn:set(context.addr, {}) + 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 + + +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 |