--[[ Copyright (c) 2012-2015 Kaarle Ritvanen See LICENSE file for license details --]] local M = {} local raise = require('aconf.error').raise local object = require('aconf.object') local class = object.class local isinstance = object.isinstance local super = object.super local pth = require('aconf.path') local address = require('aconf.path.address') local util = require('aconf.util') local copy = util.copy local setdefaults = util.setdefaults local update = util.update function M.null_addr(path, name) local comps = pth.split(path) table.insert(comps, pth.escape(name)) return address.join('/null', table.unpack(comps)) end M.BoundMember = class() function M.BoundMember:init(parent, name, field) local pmt = getmetatable(parent) if pmt.maddr and name ~= address.wildcard then self.addr = pmt.maddr(name) else self.addr = field.addr or address.escape(name) if type(self.addr) == 'function' then self.addr = self.addr(pmt.path, name) end self.addr = address.to_absolute(self.addr, pmt.addr) end local context = { txn=pmt.txn, privileged=pmt.privileged, parent=parent, path=pth.join(pmt.path, name), addr=self.addr } 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, context, ...) end end setmetatable(self, mt) end M.TreeNode = class() local function equal_tns(tn1, tn2) return getmetatable(tn1).path == getmetatable(tn2).path end function M.TreeNode:init(context, params) local mt = getmetatable(self) update(mt, context) mt.name = pth.name(mt.path) mt.__eq = equal_tns mt.__pairs = M.pairs if not (mt.txn and mt.txn.user) then mt.privileged = true end mt.escalate = mt.privileged and self or mt.class( setdefaults( { parent=mt.parent and getmetatable(mt.parent).escalate, privileged=true }, context ), params ) function mt.get(k, options) return mt.load(k, options) end function mt._fetch(path, create) if #path == 0 then return self end local name = path[1] table.remove(path, 1) if name == pth.up then if not mt.parent then raise(mt.path, 'Root object does not have parent') end return getmetatable(mt.parent)._fetch(path, create) end local options = {} if create then options.create = true if #path == 0 then options.dereference = false end end local next = mt.get(name, options) if next == nil and (not create or #path > 0) then raise(mt.path, 'Subordinate does not exist: '..name) end if #path == 0 then return next end if type(next) ~= 'table' then raise(pth.join(mt.path, name), 'Is a primitive value') end return getmetatable(next)._fetch(path, create) end function mt.fetch(path, create) if pth.is_absolute(path) and mt.path > '/' then assert(not create) return mt.txn:fetch(path, mt.privileged) end return mt._fetch(pth.split(path), create) end function mt._search(path, prefix) if #path == 0 then return {{path=prefix, value=self}} end local mt = getmetatable(self) local name = path[1] table.remove(path, 1) local function collect(name) local next = mt.load(name, {dereference=false}) if not next then return {} end if isinstance(next, M.TreeNode) then return getmetatable(next)._search( copy(path), pth.join(prefix, name) ) end assert(#path == 0) return { { value=next, deleted=function(path) mt.member(name):deleted(path) end } } 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 function mt.search(path) return mt._search(pth.split(path), '') end local permissions = {} function mt._has_permission(permission) end -- TODO audit trail function mt.has_permission(permission) if mt.privileged then return true end local name = permission..mt.path local res = permissions[name] if res ~= nil then return res end local user = mt.txn.user res = user.superuser or mt._has_permission(permission) if res == nil then if getmetatable(mt.escalate).fetch('/aaa/permissions')[name] then res = user:check_permission(name) else if ({create=true, delete=true})[permission] then permission = 'modify' end res = getmetatable(mt.parent).has_permission(permission) end end permissions[name] = res return res end function mt.check_permission(permission) if not mt.has_permission(permission) then raise('forbidden', permission..mt.path) end end function mt.removable() end function mt.value_removable(v) if isinstance(v, M.TreeNode) then return getmetatable(v).removable() end end local function key_removable(k) if not mt.removing_permitted() then return false end local res = mt.value_removable(mt.load(k, {dereference=false})) if res == nil then return params.editable end return res end function mt.check_removable(k) if not (mt.privileged or key_removable(k)) then raise(pth.join(mt.path, k), 'Cannot be deleted') end end function mt.meta() if not mt._meta then mt._meta = {type=params.dtype} if mt.txn then if mt.parent then mt._meta['ui-name'] = getmetatable(mt.parent).mmeta(mt.name)['ui-name'] end mt.init_meta(mt._meta) end end local res = copy(mt._meta) res.removable = {} for _, key in ipairs(mt.members()) do if key_removable(key) then table.insert(res.removable, key) end end return res end if not mt.txn then return end function mt.save(k, v) rawset(self, k, v) end function mt.__index(t, k) return mt.get(k, {private=true}) end function mt.__newindex(t, k, v) mt.save(k, v) end mt.txn.validable[mt.path] = mt.addr end M.Collection = class(M.TreeNode) function M.Collection:init(context, params) super(self, M.Collection):init( context, setdefaults(params, {dtype='collection'}) ) self.init = nil local mt = getmetatable(self) mt.field = M.BoundMember(self, pth.wildcard, params.field) function mt.topology() return mt.field:topology() end function mt.member(name) return M.BoundMember(self, name, params.field) end function mt.load(k, options) return mt.member(k):load(options) end function mt.removing_permitted() return mt.has_permission('delete') end if not mt.txn then return end function mt.init_meta(meta) update( meta, { editable=params.editable and mt.has_permission('create'), members=mt.field:meta(), required=params.required, ['ui-member']=params.ui_member or meta['ui-name']:gsub('s$', ''), widget=params.layout } ) end function mt.mmeta(name) local meta = mt.meta() local res = copy(meta.members) if name ~= pth.wildcard then res['ui-name'] = meta['ui-member']..' '..name end return res end function mt.members() return mt.txn:get(mt.addr) or {} end function mt.__len() return #mt.members() 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) if not mt.privileged then local delete = v == nil local old = mt.load(k, {dereference=false}) if old == nil then if delete then return end if not params.editable then raise(mt.path, 'Collection is not editable') end mt.check_permission('create') elseif delete then mt.check_removable(k) elseif type(old) == 'table' then mt.check_removable(k) mt.check_permission('create') else mt.check_permission('modify') end end if params.key then local kf = M.BoundMember(self, k, object.toinstance(params.key)) if kf:normalize(k) ~= k then raise(mt.path, 'Invalid member name: '..k) end kf:validate(k) end mt.member(k):save(v) end end M.List = class(M.Collection) local function _ipairs(mt, i) i = i + 1 local v = mt.load(i, {create=false}) if v == nil then return end return i, v end function M.List:init(context, params) super(self, M.List):init(context, setdefaults(params, {dtype='list'})) local mt = getmetatable(self) if not mt.txn then return end local function expand() return mt.txn:expand(mt.field.addr) end function mt.maddr(i) local addrs = expand() if not addrs[1] then addrs[1] = address.join( '/', table.unpack( util.map( function(c) return c == address.wildcard and 1 or c end, address.split(mt.field.addr) ) ) ) end if addrs[i] then return addrs[i] end local comps = address.split(addrs[#addrs]) comps[#comps] = comps[#comps] + i - #addrs return address.join('/', table.unpack(comps)) end function mt.members() return util.keys(expand()) end local tmt = getmetatable(mt.escalate) mt._save = mt.save local function check_index(i, max) if type(i) ~= 'number' or math.floor(i) ~= i or i < 1 or i > max then raise(mt.path, 'Invalid list index: '..i) end end function mt.save(k, v) local len = #mt.members() if v == nil then check_index(k, len) mt.check_removable(k) while k < len do tmt._save(k, tmt.load(k + 1, {dereference=false})) k = k + 1 end tmt._save(len) else check_index(k, len + 1) mt._save(k, v) end end function mt.insert(v, i) assert(v ~= nil) local len = #mt.members() local max = len + 1 if i then check_index(i, max) else i = max end mt.check_permission('create') for j = len,i,-1 do tmt._save(j + 1, tmt.load(j, {dereference=false})) end tmt._save(i, v) end function mt.__ipairs(t) return _ipairs, mt, 0 end end -- experimental M.Mixed = class(M.Collection) function M.Mixed:init(context, params) super(self, M.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 for _, mf in ipairs{ 'addr', 'check_permission', 'contains', 'escalate', 'fetch', 'has_permission', 'insert', 'meta', 'mmeta', 'name', 'parent', 'path', 'save', 'search', 'topology' } do M[mf] = meta_func(mf) end function M.pairs(tbl, dereference) if not isinstance(tbl, M.TreeNode) then return pairs(tbl) end local mt = getmetatable(tbl) local res = {} for _, member in ipairs(mt.members()) do res[member] = mt.load(member, {dereference=dereference}) end return pairs(res) end return M