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