summaryrefslogtreecommitdiffstats
path: root/acf2/model/field.lua
diff options
context:
space:
mode:
Diffstat (limited to 'acf2/model/field.lua')
-rw-r--r--acf2/model/field.lua241
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