summaryrefslogtreecommitdiffstats
path: root/aconf/model/node.lua
diff options
context:
space:
mode:
Diffstat (limited to 'aconf/model/node.lua')
-rw-r--r--aconf/model/node.lua379
1 files changed, 379 insertions, 0 deletions
diff --git a/aconf/model/node.lua b/aconf/model/node.lua
new file mode 100644
index 0000000..49053f5
--- /dev/null
+++ b/aconf/model/node.lua
@@ -0,0 +1,379 @@
+--[[
+Copyright (c) 2012-2014 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 util = require('aconf.util')
+local copy = util.copy
+local update = util.update
+
+
+function M.null_addr(path, name) return '/null'..pth.join(path, name) end
+
+
+M.BoundMember = class()
+
+function M.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
+
+ local addr = field.addr or pth.escape(name)
+ if type(addr) == 'function' then addr = addr(pmt.path, name) end
+ return function(self, ...)
+ return member(
+ field,
+ {
+ txn=pmt.txn,
+ parent=parent,
+ path=pth.join(pmt.path, name),
+ addr=pth.to_absolute(addr, pmt.addr)
+ },
+ ...
+ )
+ 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, dtype, editable)
+ local mt = getmetatable(self)
+ update(mt, context)
+
+ mt.name = pth.name(mt.path)
+ mt.__eq = equal_tns
+
+ function mt.meta()
+ if not mt._meta then
+ mt._meta = {type=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
+ return mt._meta
+ end
+
+ function mt.get(k, options) return mt.load(k, options) end
+ function mt.removable() return true end
+
+ function mt.member_removable(k)
+ local v = mt.load(k, {dereference=false})
+ return editable and (
+ not isinstance(v, M.TreeNode) or getmetatable(v).removable()
+ )
+ end
+
+ function mt.check_removable(k, v)
+ if v == nil and not mt.member_removable(k) then
+ raise(pth.join(mt.path, k), 'Cannot be deleted')
+ end
+ 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) 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 M.has_permission(mt.parent, user, permission)
+ end
+
+ mt.txn.validable[mt.path] = mt.addr
+end
+
+function M.TreeNode:fetch(path, create)
+ local mt = getmetatable(self)
+
+ if type(path) == 'string' then
+ if pth.is_absolute(path) and mt.path > '/' then
+ assert(not create)
+ return mt.txn:fetch(path)
+ end
+ path = pth.split(path)
+ end
+
+ 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 M.TreeNode.fetch(mt.parent, path, create)
+ end
+
+ if not mt.member(name) then
+ raise(mt.path, 'Member does not exist: '..name)
+ 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 and type(next) ~= 'table' then
+ raise(pth.join(mt.path, name), 'Is a primitive value')
+ end
+
+ return M.TreeNode.fetch(next, path, create)
+end
+
+function M.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, {dereference=false})
+ if not next then return {} end
+
+ local member = mt.member(name)
+ if member.deleted then return {member} end
+
+ return isinstance(next, M.TreeNode) and M.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
+
+
+M.Collection = class(M.TreeNode)
+
+function M.Collection:init(context, params, dtype)
+ super(self, M.Collection):init(
+ context, dtype or 'collection', params.editable
+ )
+
+ self.init = nil
+ self.fetch = nil
+ self.search_refs = nil
+
+ local mt = getmetatable(self)
+ local field = M.BoundMember(self, pth.wildcard, params.field)
+
+ function mt.topology() return 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
+
+ if not mt.txn then return end
+
+ function mt.init_meta(meta)
+ update(
+ meta,
+ {
+ editable=params.editable,
+ members=field:meta(),
+ required=params.required,
+ ['ui-member']=params.ui_member or meta['ui-name']:gsub('s$', ''),
+ widget=params.layout
+ }
+ )
+ end
+
+ local meta = mt.meta
+ function mt.meta()
+ local res = copy(meta())
+ res.removable = {}
+ for _, member in ipairs(mt.members()) do
+ if mt.member_removable(member) then
+ table.insert(res.removable, member)
+ end
+ end
+ return res
+ 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 params.editable then
+ raise(mt.path, 'Collection is not editable')
+ end
+
+ mt.check_removable(k, v)
+
+ if params.key then
+ local kf = M.BoundMember(self, k, 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)
+
+function M.List:init(context, params, dtype)
+ super(self, M.List):init(context, params, dtype or 'list')
+
+ local mt = getmetatable(self)
+
+ 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, {dereference=false}))
+ 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, {dereference=false})) end
+ mt.save(i, v)
+ 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',
+ 'contains',
+ 'has_permission',
+ 'insert',
+ 'match',
+ 'meta',
+ 'mmeta',
+ 'name',
+ 'parent',
+ 'path',
+ 'save',
+ '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
+
+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.ipairs(tbl)
+ if not isinstance(tbl, M.TreeNode) then return ipairs(tbl) end
+ return _ipairs, getmetatable(tbl), 0
+end
+
+
+return M