--[[ Copyright (c) 2012-2013 Kaarle Ritvanen See LICENSE file for license details --]] module(..., package.seeall) error = require('acf.error') local raise = error.raise local relabel = error.relabel local fld = require('acf.model.field') local Field = fld.Field local model = require('acf.model.model') Action = model.Action new = model.new local to_field = model.to_field node = require('acf.model.node') permission = require('acf.model.permission') register = require('acf.model.root').register set = require('acf.model.set') local object = require('acf.object') local class = object.class local super = object.super local pth = require('acf.path') local map = require('acf.util').map require 'stringy' -- TODO object-specific actions local Primitive = class(Field) function Primitive:validate(context, value) local t = self.dtype if type(value) ~= t then raise(context.path, 'Not a '..t) end end String = class(Primitive) function String:init(params) super(self, String):init(params) self.dtype = 'string' end function String:validate(context, value) super(self, String):validate(context, value) if self['max-length'] and string.len(value) > self['max-length'] then raise(context.path, 'Maximum length exceeded') end end function String:meta(context) local res = super(self, String):meta(context) 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(context, value) return super(self, Number):_validate( context, value and tonumber(value) or value ) end Integer = class(Number) function Integer:validate(context, value) super(self, Integer):validate(context, value) if math.floor(value) ~= value then raise(context.path, 'Not an integer') end end Boolean = class(Primitive) function Boolean:init(params) super(self, Boolean):init(params) self.dtype = 'boolean' self.widget = self.dtype end Range = class(String) function Range:init(params) super(self, Range):init(params) if not self.type then self.type = Integer end end function Range:validate(context, value) local comps = stringy.split(value, '-') if #comps > 2 then raise(context.path, 'Invalid range') end for _, v in ipairs(comps) do to_field(self.type):_validate(context, v) end 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(context) return pth.to_absolute(self.scope, node.path(context.parent)) end function Reference:meta(context) local res = super(self, Reference):meta(context) res.scope = self:abs_scope(context) local txn = context.txn local objs = txn:get( node.addr(relabel('system', txn.search, txn, res.scope)) ) or {} res.choice = map(function(p) return pth.join(res.scope, p) end, objs) res['ui-choice'] = objs return res end function Reference:follow(context, value) return context.txn:search(pth.mjoin(self:abs_scope(context), value)) end function Reference:load(context) local ref = super(self, Reference):load(context) return ref and self:follow(context, ref) or nil end function Reference:_validate(context, value) super(self, Reference):_validate(context, value) if value == nil then return end if object.isinstance(value, node.TreeNode) then value = node.path(value) end local path = context.path if pth.is_absolute(value) then local scope = self:abs_scope(context) local prefix = scope..'/' if not stringy.startswith(value, prefix) then raise(path, 'Reference out of scope ('..scope..')') end value = string.sub(value, string.len(prefix) + 1, -1) end -- assume one-level ref for now if #pth.split(value) > 1 then raise(path, 'Subtree references not yet supported') end -- TODO check instance type relabel(path, self.follow, self, context, value) 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(context) -- automatically create missing collection (TODO: make this configurable?) return self.ctype( context, {field=to_field(self.type), required=self.required} ) end Set = class(Collection) function Set:init(params) super(self, Set):init(params, set.Set) end function Set.save_member(node, k, v) set.add(node, v) end -- experimental Mixed = class(Collection) function Mixed:init(params) params.type = Mixed super(self, Mixed):init(params, node.Mixed) end function Mixed:load(context) local value = Primitive.load(self, context) if type(value) == 'table' then return super(self, Mixed):load(context) end return value end function Mixed:save(context, value) if type(value) == 'table' then super(self, Mixed):save(context, value) else Primitive.save(self, context, value) end end