diff options
author | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2014-03-10 22:45:18 +0200 |
---|---|---|
committer | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2014-03-24 01:18:13 +0200 |
commit | 7d9c43916b0600ac4879dfe9793eab807a83ab2b (patch) | |
tree | ec54ed64c9a557b6ea4ad88d31138a02d3e0cd04 /aconf/model/node.lua | |
parent | cb6c243dc356ef1d46d7ddb96e6ea6ae007c6cca (diff) | |
download | aconf-7d9c43916b0600ac4879dfe9793eab807a83ab2b.tar.bz2 aconf-7d9c43916b0600ac4879dfe9793eab807a83ab2b.tar.xz |
rename ACF2 to Alpine Configurator (aconf)
Diffstat (limited to 'aconf/model/node.lua')
-rw-r--r-- | aconf/model/node.lua | 379 |
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 |