--[[ Copyright (c) 2012-2013 Kaarle Ritvanen See LICENSE file for license details --]] module(..., package.seeall) local raise = require('acf.error').raise local object = require('acf.object') local class = object.class local isinstance = object.isinstance local super = object.super local pth = require('acf.path') local util = require('acf.util') BoundMember = class() function BoundMember:init(parent, name, field) local pmt = getmetatable(parent) local mt = {} function mt.__index(t, k) local member = field[k] if type(member) ~= 'function' then return member end return function(self, ...) return member( field, { txn=pmt.txn, parent=parent, path=pth.join(pmt.path, name), addr=pth.to_absolute( field.addr or pth.escape(name), pmt.addr ) }, ... ) end end setmetatable(self, mt) end TreeNode = class() function TreeNode:init(context) local mt = getmetatable(self) util.update(mt, context) mt.meta = {} function mt.get(k, create) return mt.load(k, create) end if not mt.txn then return end if mt.parent then mt.meta['ui-name'] = getmetatable(mt.parent).mmeta( pth.name(mt.path) )['ui-name'] end function mt.save(k, v) rawset(self, k, v) end function mt.__index(t, k) return mt.get(k) end function mt.__newindex(t, k, v) mt.save(k, v) end function mt.has_permission(user, permission) local p = permission..mt.path if mt.txn:fetch('/auth/permissions')[p] then return user:check_permission(p) end if ({create=true, delete=true})[permission] then permission = 'modify' end return has_permission(mt.parent, user, permission) end mt.txn.validable[mt.path] = mt.addr end function TreeNode:fetch(path, create) if type(path) == 'string' then path = pth.split(path) end if #path == 0 then return self end local mt = getmetatable(self) local name = path[1] if not mt.member(name) then raise(mt.path, 'Member does not exist: '..name) end local next = mt.get(name, create) if next == nil and (not create or #path > 1) then raise(mt.path, 'Subordinate does not exist: '..name) end table.remove(path, 1) if #path > 0 and type(next) ~= 'table' then raise(pth.join(mt.path, name), 'Is a primitive value') end return TreeNode.fetch(next, path, create) end function TreeNode:search_refs(path) if type(path) == 'string' then path = pth.split(path) end if #path == 0 then return {} end local mt = getmetatable(self) local name = path[1] table.remove(path, 1) local function collect(name) local next = mt.load(name) if not next then return {} end local member = mt.member(name) if member.deleted then return {member} end return isinstance(next, TreeNode) and TreeNode.search_refs( next, path ) or {} end if name == pth.wildcard then local res = {} for _, member in ipairs(mt.members()) do util.extend(res, collect(member)) end return res end return collect(name) end Collection = class(TreeNode) function Collection:init(context, params) super(self, Collection):init(context) self.init = nil self.fetch = nil self.search_refs = nil local mt = getmetatable(self) local field = BoundMember(self, pth.wildcard, params.field) function mt.topology() return field:topology() end function mt.member(name) return BoundMember(self, name, params.field) end function mt.load(k, create) return mt.member(k):load(create) end if not mt.txn then return end mt.meta.type = 'collection' mt.meta.members = field:meta() mt.meta['ui-member'] = params.ui_member or string.gsub( mt.meta['ui-name'], 's$', '' ) function mt.mmeta(name) local res = util.copy(mt.meta.members) if name ~= pth.wildcard then res['ui-name'] = mt.meta['ui-member']..' '..name end return res end function mt.members() return mt.txn:get(mt.addr) or {} end function mt.validate() if #mt.members() > 0 then return end if params.required then raise(mt.path, 'Collection cannot be empty') end if params.destroy then mt.txn:set(mt.addr) validate(mt.parent) end end function mt.save(k, v) mt.member(k):save(v) end end List = class(Collection) function List:init(context, params) super(self, List):init(context, params) local mt = getmetatable(self) mt.meta.type = 'list' local save = mt.save function mt.save(k, v) assert(type(k) == 'number') if v == nil then local len = #mt.members() while k < len do mt.save(k, mt.load(k + 1)) k = k + 1 end end save(k, v) end function mt.insert(v, i) local len = #mt.members() if not i then i = len + 1 end for j = len,i,-1 do mt.save(j + 1, mt.load(j)) end mt.save(i, v) end end -- experimental Mixed = class(Collection) function Mixed:init(context, params) super(self, Mixed):init(context, params) -- TODO dynamic meta: list non-leaf children local mt = getmetatable(self) mt.meta = {type='mixed', ['ui-name']=mt.path} function mt.mmeta(name) return {type='mixed', ['ui-name']=pth.join(mt.path, name)} end end local function meta_func(attr) return function(node, ...) local res = getmetatable(node)[attr] if type(res) == 'function' then return res(...) end return res end end addr = meta_func('addr') has_permission = meta_func('has_permission') insert = meta_func('insert') members = meta_func('members') meta = meta_func('meta') mmeta = meta_func('mmeta') parent = meta_func('parent') path = meta_func('path') topology = meta_func('topology') validate = meta_func('validate') local rawpairs = pairs local rawipairs = ipairs function pairs(tbl) if not isinstance(tbl, TreeNode) then return rawpairs(tbl) end local mt = getmetatable(tbl) local res = {} for _, member in rawipairs(mt.members()) do res[member] = mt.load(member) end return rawpairs(res) end local function _ipairs(mt, i) i = i + 1 local v = mt.load(i) if v == nil then return end return i, v end function ipairs(tbl) if not isinstance(tbl, TreeNode) then return rawipairs(tbl) end return _ipairs, getmetatable(tbl), 0 end