summaryrefslogtreecommitdiffstats
path: root/acf/model/init.lua
diff options
context:
space:
mode:
Diffstat (limited to 'acf/model/init.lua')
-rw-r--r--acf/model/init.lua206
1 files changed, 206 insertions, 0 deletions
diff --git a/acf/model/init.lua b/acf/model/init.lua
new file mode 100644
index 0000000..6934796
--- /dev/null
+++ b/acf/model/init.lua
@@ -0,0 +1,206 @@
+--[[
+Copyright (c) 2012 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+module(..., package.seeall)
+
+local fld = require('acf.model.field')
+local Field = fld.Field
+
+local model = require('acf.model.model')
+new = model.new
+to_field = model.to_field
+
+node = require('acf.model.node')
+
+local object = require('acf.object')
+local class = object.class
+local super = object.super
+
+local pth = require('acf.path')
+local map = require('acf.util').map
+
+
+-- TODO object-specific actions
+
+-- TODO access control
+
+
+local Primitive = class(Field)
+
+function Primitive:validate(txn, path, value)
+ local t = self.dtype
+ if type(value) ~= t then error('Not a '..t..': '..tostring(value)) end
+end
+
+
+String = class(Primitive)
+
+function String:init(params)
+ super(self, String):init(params)
+ self.dtype = 'string'
+end
+
+function String:validate(txn, path, value)
+ if self['max-length'] and string.len(value) > self['max-length'] then
+ error('Maximum length exceeded: '..value)
+ end
+end
+
+function String:meta(txn, path, addr)
+ local res = super(self, String):meta(txn, path, addr)
+ res['max-length'] = self['max-length']
+ return res
+end
+
+
+Number = class(Primitive)
+
+function Number:init(params)
+ super(self, Number):init(params)
+ self.dtype = 'number'
+end
+
+function Number:_validate(txn, path, value)
+ return tonumber(super(self, Number):_validate(txn, path, value))
+end
+
+
+Integer = class(Number)
+
+function Integer:validate(txn, path, value)
+ if math.floor(value) ~= value then error('Not an integer: '..value) end
+end
+
+
+Boolean = class(Primitive)
+
+function Boolean:init(params)
+ super(self, Boolean):init(params)
+ self.dtype = 'boolean'
+ self.widget = self.dtype
+end
+
+
+Reference = class(Field)
+
+function Reference:init(params)
+ super(self, Reference):init(params)
+ self.dtype = 'reference'
+ if not self.scope then self.scope = '/' end
+end
+
+function Reference:abs_scope(path)
+ return pth.to_absolute(self.scope, pth.parent(pth.parent(path)))
+end
+
+function Reference:meta(txn, path, addr)
+ local res = super(self, Reference):meta(txn, path, addr)
+ res.scope = self:abs_scope(path)
+
+ local base = txn:search(res.scope)
+ local objs = base and txn:get(getmetatable(base).addr) or {}
+ res.choice = map(function(p) return pth.join(res.scope, p) end, objs)
+ res['ui-choice'] = objs
+
+ return res
+end
+
+function Reference:follow(txn, path, value)
+ return txn:search(pth.join(self:abs_scope(path), value))
+end
+
+function Reference:load(txn, path, addr)
+ local ref = super(self, Reference):load(txn, path, addr)
+ return ref and self:follow(txn, path, ref) or nil
+end
+
+function Reference:_validate(txn, path, value)
+ super(self, Reference):_validate(txn, path, value)
+
+ if value == nil then return end
+
+ if object.isinstance(value, node.TreeNode) then
+ value = getmetatable(value).path
+ end
+ if value and pth.is_absolute(value) then
+ local scope = self:abs_scope(path)
+ local prefix = scope..'/'
+ if not stringy.startswith(value, prefix) then
+ error('Reference out of scope ('..scope..')')
+ end
+ value = string.sub(value, string.len(prefix) + 1, -1)
+ end
+
+ -- assume one-level ref for now
+ assert(not string.find(value, '/'))
+ if not self:follow(txn, path, value) then
+ error('Does not exist: '..path)
+ end
+ -- TODO check instance type
+
+ return value
+end
+
+
+Model = fld.Model
+
+
+Collection = class(fld.TreeNode)
+
+function Collection:init(params, ctype)
+ super(self, Collection):init(params)
+ assert(self.type)
+ self.ctype = ctype or node.Collection
+ self.dtype = 'collection'
+ self.widget = self.dtype
+end
+
+function Collection:load(txn, path, addr)
+ -- automatically create missing collection (TODO: make this configurable?)
+ return self.ctype(txn, path, addr, to_field(self.type), self.required)
+end
+
+
+PrimitiveList = class(Collection)
+
+function PrimitiveList:init(params)
+ super(self, PrimitiveList):init(params, node.PrimitiveList)
+end
+
+
+-- experimental
+Mixed = class(Collection)
+
+function Mixed:init()
+ super(self, Mixed):init({type=Mixed}, node.Mixed)
+end
+
+function Mixed:load(txn, path, addr)
+ local value = Primitive.load(self, txn, path, addr)
+ if type(value) == 'table' then
+ return super(self, Mixed):load(txn, path, addr)
+ end
+ return value
+end
+
+function Mixed:save(txn, path, addr, value)
+ if type(value) == 'table' then
+ super(self, Mixed):save(txn, path, addr, value)
+ else Primitive.save(self, txn, path, addr, value) end
+end
+
+
+
+RootModel = new()
+
+function RootModel:init(txn) super(self, RootModel):init(txn, '/') end
+
+function register(name, path, field)
+ local field = to_field(field)
+ function field:compute(txn, pth, addr)
+ return self:load(txn, '/'..name, path)
+ end
+ RootModel[name] = field
+end