diff options
Diffstat (limited to 'acf2/model')
-rw-r--r-- | acf2/model/aaa.lua | 87 | ||||
-rw-r--r-- | acf2/model/combination.lua | 50 | ||||
-rw-r--r-- | acf2/model/field.lua | 241 | ||||
-rw-r--r-- | acf2/model/init.lua | 205 | ||||
-rw-r--r-- | acf2/model/model.lua | 204 | ||||
-rw-r--r-- | acf2/model/net.lua | 96 | ||||
-rw-r--r-- | acf2/model/node.lua | 275 | ||||
-rw-r--r-- | acf2/model/permission.lua | 22 | ||||
-rw-r--r-- | acf2/model/root.lua | 91 | ||||
-rw-r--r-- | acf2/model/set.lua | 47 |
10 files changed, 1318 insertions, 0 deletions
diff --git a/acf2/model/aaa.lua b/acf2/model/aaa.lua new file mode 100644 index 0000000..c8a8328 --- /dev/null +++ b/acf2/model/aaa.lua @@ -0,0 +1,87 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = require('acf2.model') +local object = require('acf2.object') + +local digest = require('crypto').digest + + +Role = M.new() +Role.permissions = M.Set{type=M.Reference{scope='../../../permissions'}} + + +local function hash_password(algorithm, salt, password) + return algorithm..'$'..salt..'$'..digest(algorithm, salt..password) +end + +local hash_pattern = '^(%w+)%$(%w+)%$%x+$' + + +local Password = object.class(M.String) + +function Password:_validate(context, value) + value = object.super(self, M.String):_validate(context, value) + if not value or value:find(hash_pattern) then return value end + + local salt = '' + for i = 1,12 do + local c = math.random(48, 109) + if c > 57 then c = c + 7 end + if c > 90 then c = c + 6 end + salt = salt..string.char(c) + end + return hash_password('sha256', salt, value) +end + + +User = M.new() +User.password = Password +User['real-name'] = M.String +User.superuser = M.Boolean{default=false} +User.roles = M.Set{type=M.Reference{scope='../../../roles'}} + +function User:check_password(password) + if not self.password then return false end + local _, _, algorithm, salt = self.password:find(hash_pattern) + if not salt then return false end + return hash_password(algorithm, salt, password) == self.password +end + +function User:check_permission(permission) + -- TODO audit trail + print('check permission', permission) + + if self.superuser then return true end + + assert(getmetatable(self).txn:fetch('/auth/permissions')[permission]) + + for _, role in M.node.pairs(self.roles, true) do + for _, p in M.node.pairs(role.permissions, true) do + if p == permission then return true end + end + end + return false +end + + +Authentication = M.new() +Authentication.users = M.Collection{type=User} +Authentication.roles = M.Collection{type=Role} +Authentication.permissions = M.Set{ + type=M.String, + addr='/volatile/aaa/permissions' +} + +M.register( + 'auth', + Authentication, + { + addr='/json'..require('posix').getcwd()..'/config/aaa.json', + ui_name='Authentication' + } +) + +M.permission.defaults('/auth') diff --git a/acf2/model/combination.lua b/acf2/model/combination.lua new file mode 100644 index 0000000..28c1570 --- /dev/null +++ b/acf2/model/combination.lua @@ -0,0 +1,50 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local err = require('acf2.error') +local raise = err.raise + +local fld = require('acf2.model.field') +local String = fld.String + +local to_field = require('acf2.model.model').to_field + +local object = require('acf2.object') +local class = object.class +local super = object.super + + +local stringy = require('stringy') + + +M.Range = class(String) + +function M.Range:init(params) + super(self, M.Range):init(params) + if not self.type then self.type = fld.Integer end +end + +function M.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 + + +M.Union = class(String) + +function M.Union:validate(context, value) + super(self, M.Union):validate(context, value) + for _, tpe in ipairs(self.types) do + local field = to_field(tpe) + if err.call(field.validate, field, context, value) then return end + end + raise(context.path, self.error or 'Invalid value') +end + + +return M diff --git a/acf2/model/field.lua b/acf2/model/field.lua new file mode 100644 index 0000000..6cbfa96 --- /dev/null +++ b/acf2/model/field.lua @@ -0,0 +1,241 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local err = require('acf2.error') +local raise = err.raise + +local node = require('acf2.model.node') + +local object = require('acf2.object') +local class = object.class +local super = object.super + +local util = require('acf2.util') + + +local function contains(list, value) + for k, v in ipairs(list) do if v == value then return true end end + return false +end + +M.Member = class() + +function M.Member:init(params) + for k, v in pairs(params or {}) do + if self[k] == nil then self[k] = v end + end +end + +function M.Member:auto_ui_name(name) + if not name then return end + return (name:sub(1, 1):upper()..name:sub(2)):gsub('-', ' ') +end + +function M.Member:meta(context) + return { + name=self.name, + description=self.description, + ['ui-name']=self.ui_name or self:auto_ui_name(self.name) + } +end + + +M.Field = class(M.Member) + +function M.Field:init(params) + super(self, M.Field):init(params) + + if self.choice and not self['ui-choice'] then + self['ui-choice'] = util.map( + function(name) return self:auto_ui_name(name) end, + self.choice + ) + end + + if not self.widget then + self.widget = self.choice and 'combobox' or 'field' + end +end + +function M.Field:meta(context) + assert(self.dtype) + local res = super(self, M.Field):meta(context) + + res.type = self.dtype + res.required = self.required + res.default = self.default + res.choice = self.choice + res.widget = self.widget + res['ui-choice'] = self['ui-choice'] + + return res +end + +function M.Field:topology(context) + return { + {path=context.path, addr=context.addr, type=self.dtype} + } +end + +function M.Field:load(context) + if not context.txn then return setmetatable({}, context) end + local value = context.txn:get(context.addr) + if value == nil then return self.default end + return value +end + +function M.Field:_validate(context, value) + if self.required and value == nil then + raise(context.path, 'Required value not set') + end + if self.choice and value ~= nil and not contains(self.choice, value) then + raise(context.path, 'Invalid value') + end + if value ~= nil then self:validate(context, value) end + return value +end + +function M.Field:validate(context, value) end + +function M.Field:save(context, value) + context.txn:set(context.addr, self:_validate(context, value)) +end + +function M.Field:validate_saved(context) + self:save(context, self:load(context)) +end + + +local Primitive = class(M.Field) + +function Primitive:validate(context, value) + local t = self.dtype + if type(value) ~= t then raise(context.path, 'Not a '..t) end +end + + +M.String = class(Primitive) + +function M.String:init(params) + super(self, M.String):init(params) + self.dtype = 'string' +end + +function M.String:validate(context, value) + super(self, M.String):validate(context, value) + if self['max-length'] and value:len() > self['max-length'] then + raise(context.path, 'Maximum length exceeded') + end + if self.pattern and not value:match('^'..self.pattern..'$') then + raise(context.path, 'Invalid value') + end +end + +function M.String:meta(context) + local res = super(self, M.String):meta(context) + res['max-length'] = self['max-length'] + return res +end + + +M.Number = class(Primitive) + +function M.Number:init(params) + super(self, M.Number):init(params) + self.dtype = 'number' +end + +function M.Number:_validate(context, value) + return super(self, M.Number):_validate( + context, + value and tonumber(value) or value + ) +end + + +M.Integer = class(M.Number) + +function M.Integer:validate(context, value) + super(self, M.Integer):validate(context, value) + if math.floor(value) ~= value then raise(context.path, 'Not an integer') end +end + + +M.Boolean = class(Primitive) + +function M.Boolean:init(params) + super(self, M.Boolean):init(params) + self.dtype = 'boolean' + self.widget = self.dtype +end + + +M.TreeNode = class(M.Field) + +function M.TreeNode:init(params) + if not params.widget then params.widget = 'link' end + super(self, M.TreeNode):init(params) +end + +function M.TreeNode:topology(context) + local res = super(self, M.TreeNode):topology(context) + res[1].type = 'table' + util.extend(res, node.topology(self:load(context, {create=true}))) + return res +end + +function M.TreeNode:load(context, options) + if context.txn and not ( + ( + options and options.create + ) or self.create or context.txn:get(context.addr) + ) then return end + return self.itype(context, self.iparams) +end + +function M.TreeNode:save(context, value) + local path = context.path + + if value == path then return end + if type(value) == 'string' then value = context.txn:fetch(value) end + if object.isinstance(value, node.TreeNode) and node.path(value) == path then + return + end + + context.txn:set(context.addr) + + if value then + if type(value) ~= 'table' then + raise(path, 'Cannot assign primitive value') + end + + context.txn:set(context.addr, {}) + local new = self:load(context, {create=true}) + + local errors = err.ErrorDict() + for k, v in node.pairs(value) do + errors:collect(self.save_member, new, k, v) + end + errors:raise() + end +end + +function M.TreeNode.save_member(node, k, v) node[k] = v end + + +M.Model = class(M.TreeNode) + +function M.Model:init(params) + super(self, M.Model):init(params) + + assert(self.model) + self.itype = self.model + self.dtype = 'model' +end + + +return M diff --git a/acf2/model/init.lua b/acf2/model/init.lua new file mode 100644 index 0000000..51cdbe8 --- /dev/null +++ b/acf2/model/init.lua @@ -0,0 +1,205 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +M.error = require('acf2.error') +local raise = M.error.raise + +local combination = require('acf2.model.combination') +M.Union = combination.Union +M.Range = combination.Range + +local fld = require('acf2.model.field') +local Field = fld.Field +M.Boolean = fld.Boolean +M.Integer = fld.Integer +M.Number = fld.Number +M.String = fld.String + +local model = require('acf2.model.model') +M.Action = model.Action +M.new = model.new +local to_field = model.to_field + +M.net = require('acf2.model.net') + +local node = require('acf2.model.node') +M.node = {} +for _, m in ipairs{ + 'List', + 'Set', + 'TreeNode', + 'has_permission', + 'insert', + 'meta', + 'mmeta', + 'path', + 'pairs', + 'ipairs' +} do M.node[m] = node[m] end + +M.permission = require('acf2.model.permission') +M.register = require('acf2.model.root').register +M.node.Set = require('acf2.model.set').Set + +local object = require('acf2.object') +local class = object.class +local isinstance = object.isinstance +local super = object.super + +local pth = require('acf2.path') +local map = require('acf2.util').map + + +local stringy = require('stringy') + + +M.Reference = class(Field) + +function M.Reference:init(params) + if not params.widget then params.widget = 'reference' end + super(self, M.Reference):init(params) + self.dtype = 'reference' + if not self.scope then self.scope = '/' end +end + +function M.Reference:topology(context) + local res = super(self, M.Reference):topology(context) + res[1].scope = self.scope + return res +end + +function M.Reference:abs_scope(context) + return pth.to_absolute(self.scope, node.path(context.parent)) +end + +function M.Reference:meta(context) + local res = super(self, M.Reference):meta(context) + res.scope = self:abs_scope(context) + return res +end + +function M.Reference:follow(context, value) + return context.txn:fetch(pth.rawjoin(self:abs_scope(context), value)) +end + +function M.Reference:load(context, options) + local ref = super(self, M.Reference):load(context) + return ( + (not options or options.dereference ~= false) and context.txn and ref + ) and self:follow(context, ref) or ref +end + +function M.Reference:_validate(context, value) + super(self, M.Reference):_validate(context, value) + + if value == nil then return end + + if isinstance(value, node.TreeNode) then value = node.path(value) end + + local path = context.path + if type(value) ~= 'string' then raise(path, 'Path name must be string') end + + 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 = value:sub(prefix:len() + 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 + M.error.relabel(path, self.follow, self, context, value) + + return value +end + +function M.Reference:deleted(context, addr) + local target = self:load(context) + if target and node.addr(target) == addr then + -- TODO raise error for the target object + raise(context.path, 'Refers to '..addr) + end +end + + +M.Model = fld.Model + + +M.Collection = class(fld.TreeNode) + +function M.Collection:init(params, itype) + if params.create == nil then params.create = true end + super(self, M.Collection):init(params) + + assert(self.type) + self.itype = itype or node.Collection + self.iparams = { + destroy=self.destroy, + layout=self.layout, + required=self.required, + ui_member=self.ui_member + } + + self.dtype = 'collection' +end + +function M.Collection:auto_ui_name(name) + if not name then return end + if name:sub(-1, -1) ~= 's' then name = name..'s' end + return super(self, M.Collection):auto_ui_name(name) +end + +function M.Collection:load(context, options) + if not self.iparams.field then self.iparams.field = to_field(self.type) end + return super(self, M.Collection):load(context, options) +end + + +M.List = class(M.Collection) +function M.List:init(params) super(self, M.List):init(params, node.List) end + + +M.Set = class(M.Collection) +function M.Set:init(params) + if not params.widget and isinstance(params.type, M.Reference) then + params.widget = 'checkboxes' + end + super(self, M.Set):init(params, M.node.Set) +end +function M.Set.save_member(tn, k, v) node.insert(tn, v) end + + +-- experimental +M.Mixed = class(M.Collection) + +function M.Mixed:init(params) + params.type = M.Mixed + super(self, M.Mixed):init(params, node.Mixed) + self.pfield = Field() +end + +function M.Mixed:topology(context) return {} end + +function M.Mixed:load(context) + local value = self.pfield:load(context) + if type(value) == 'table' then return super(self, M.Mixed):load(context) end + return value +end + +function M.Mixed:save(context, value) + if type(value) == 'table' then super(self, M.Mixed):save(context, value) + else self.pfield:save(context, value) end +end + + +return M diff --git a/acf2/model/model.lua b/acf2/model/model.lua new file mode 100644 index 0000000..dc506b3 --- /dev/null +++ b/acf2/model/model.lua @@ -0,0 +1,204 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local raise = require('acf2.error').raise + +local fld = require('acf2.model.field') +local Field = fld.Field +local Member = fld.Member + +local node = require('acf2.model.node') +local BoundMember = node.BoundMember + +local object = require('acf2.object') +local class = object.class +local super = object.super +local isinstance = object.isinstance + +local pth = require('acf2.path') +local util = require('acf2.util') + + +local function to_member(obj, params) + if not params then params = {} end + if object.issubclass(obj, M.Model) then + params.model = obj + return fld.Model(params) + end + local res = getmetatable(obj).class and obj or obj(params) + assert(isinstance(res, Member)) + return res +end + +function M.to_field(obj, params) + local res = to_member(obj, params) + assert(isinstance(res, Field)) + return res +end + + +M.Action = class(Member) + +function M.Action:init(params) + super(self, M.Action):init(params) + + if not self.func then error('Function not defined for action') end + + if self.field then + assert(type(self.field) == 'table') + self.field = M.to_field(self.field) + self.field.addr = '/null/action' + end + + getmetatable(self).__newindex = function(t, k, v) + assert(k == 'name') + rawset(t, k, v) + if t.field then t.field.name = v end + end +end + +function M.Action:meta(context) + local res = super(self, M.Action):meta(context) + if self.field then res.arg = self.field:meta(context) end + return res +end + + +function M.new(base) + if not base then base = M.Model end + + local res = class(base) + res.members = base == node.TreeNode and {} or util.copy(base.members) + + local mt = util.copy(getmetatable(res)) + + function mt.__index(t, k) return base[k] end + + function mt.__newindex(t, k, v) + assert(v) + + local override = t[k] + if type(v) == 'table' then v = to_member(v) end + + rawset(t, k, v) + + if isinstance(v, Member) then + v.name = k + if not override then table.insert(t.members, k) end + end + end + + setmetatable(res, mt) + + if isinstance(base, M.Model) then util.setdefaults(res, base) end + + return res +end + +M.Model = M.new(node.TreeNode) + +function M.Model:init(context) + super(self, M.Model):init(context) + + local mt = getmetatable(self) + + function mt.member(name, loose, tpe) + local m = mt.class[name] + + if not tpe then tpe = Member end + if not isinstance(m, tpe) then m = nil end + + if m == nil then + if loose then return end + raise(mt.path, 'Does not exist: '..name) + end + + return BoundMember(self, name, m) + end + + local function _members(tpe) + local res = {} + for _, name in ipairs(self.members) do + local m = mt.member(name, true, tpe) + if m then table.insert(res, m) end + end + return res + end + + function mt.topology() + local res = {} + for _, f in ipairs(_members(Field)) do util.extend(res, f:topology()) end + return res + end + + function mt.load(k, options) + local v = mt.class[k] + local create = options and options.create + + if isinstance(v, Field) then + v = BoundMember(self, k, v) + if v.compute then return v:compute() end + return v:load{create=create} + end + + assert(mt.txn) + + if isinstance(v, M.Action) then + local f = v.field and BoundMember(self, k, v.field) + if create then return f and f:load{create=true} end + + return function(var) + if f then f:save(var) + elseif var ~= nil then + raise( + pth.join(mt.path, v.name), + 'Action does not accept an input argument' + ) + end + local res = v.func(self, f and f:load()) + if f then mt.txn:set(v.field.addr) end + return res + end + end + + return v + end + + if not mt.txn then return end + + + function mt.mmeta(name) return mt.member(name):meta() end + + function mt.save(k, v) return mt.member(k, false, Field):save(v) end + + local function tmeta(tpe) + return util.map(function(m) return m:meta() end, _members(tpe)) + end + + mt.meta.type = 'model' + mt.meta.fields = tmeta(Field) + mt.meta.actions = tmeta(M.Action) + + function mt.members() + return util.map(function(f) return f.name end, mt.meta.fields) + end + + function mt.validate() + for _, f in ipairs(_members(Field)) do + if not f.compute then f:validate_saved() end + end + end + + if self.has_permission then + function mt.has_permission(user, permission) + return self:has_permission(user, permission) + end + end +end + + +return M diff --git a/acf2/model/net.lua b/acf2/model/net.lua new file mode 100644 index 0000000..27ab262 --- /dev/null +++ b/acf2/model/net.lua @@ -0,0 +1,96 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local raise = require('acf2.error').raise +local Union = require('acf2.model.combination').Union + +local fld = require('acf2.model.field') +local String = fld.String + +local object = require('acf2.object') +local class = object.class +local super = object.super + +local update = require('acf2.util').update + + +local stringy = require('stringy') + + +M.IPv4Address = class(String) + +function M.IPv4Address:validate(context, value) + super(self, M.IPv4Address):validate(context, value) + local function test(...) + if #{...} ~= 4 then return true end + for _, octet in ipairs{...} do + if tonumber(octet) > 255 then return true end + end + end + if test(value:match('^(%d+)%.(%d+)%.(%d+)%.(%d+)$')) then + raise(context.path, 'Invalid IPv4 address') + end +end + + +M.IPv6Address = class(String) + +function M.IPv6Address:validate(context, value) + super(self, M.IPv6Address):validate(context, value) + + local function invalid() raise(context.path, 'Invalid IPv6 address') end + + if value == '' then invalid() end + + local comps = stringy.split(value, ':') + if #comps < 3 then invalid() end + + local function collapse(i, ofs) + if comps[i] > '' then return end + if comps[i + ofs] > '' then invalid() end + table.remove(comps, i) + end + collapse(1, 1) + collapse(#comps, -1) + if #comps > 8 then invalid() end + + local short = false + for _, comp in ipairs(comps) do + if comp == '' then + if short then invalid() end + short = true + elseif not comp:match('^%x%x?%x?%x?$') then invalid() end + end + if ( + short and #comps == 3 and comps[2] == '' + ) or (not short and #comps < 8) then + invalid() + end +end + + +M.IPAddress = class(Union) + +function M.IPAddress:init(params) + super(self, M.IPAddress):init( + update( + params, + {types={M.IPv4Address, M.IPv6Address}, error='Invalid IP address'} + ) + ) +end + + +M.Port = class(fld.Integer) + +function M.Port:validate(context, value) + super(self, M.Port):validate(context, value) + if value < 0 or value > 65535 then raise(context.path, 'Invalid port') end +end + + +return M diff --git a/acf2/model/node.lua b/acf2/model/node.lua new file mode 100644 index 0000000..85fc416 --- /dev/null +++ b/acf2/model/node.lua @@ -0,0 +1,275 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local raise = require('acf2.error').raise + +local object = require('acf2.object') +local class = object.class +local isinstance = object.isinstance +local super = object.super + +local pth = require('acf2.path') +local util = require('acf2.util') + + +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 + 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 + + +M.TreeNode = class() + +function M.TreeNode:init(context) + local mt = getmetatable(self) + util.update(mt, context) + + mt.dereference = true + mt.meta = {} + function mt.get(k, create) return mt.load(k, {create=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 M.has_permission(mt.parent, user, permission) + end + + mt.txn.validable[mt.path] = mt.addr +end + +function M.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 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) + 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) + super(self, M.Collection):init(context) + + 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 + + mt.meta.type = 'collection' + mt.meta.members = field:meta() + mt.meta['ui-member'] = params.ui_member or mt.meta['ui-name']:gsub('s$', '') + mt.meta.widget = params.layout + + 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 + + +M.List = class(M.Collection) + +function M.List:init(context, params) + super(self, M.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, {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', 'has_permission', 'insert', 'meta', 'mmeta', 'path', '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) + if dereference == nil then dereference = mt.dereference end + + 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) + 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 diff --git a/acf2/model/permission.lua b/acf2/model/permission.lua new file mode 100644 index 0000000..71fa1a5 --- /dev/null +++ b/acf2/model/permission.lua @@ -0,0 +1,22 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local insert = require('acf2.model.node').insert +local start_txn = require('acf2.transaction') + +function M.define(path, ...) + local txn = start_txn() + local db = txn:fetch('/auth/permissions') + for _, permission in ipairs{...} do insert(db, permission..path) end + txn:commit() +end + +function M.defaults(path) + M.define(path, 'read', 'create', 'modify', 'delete') +end + +return M diff --git a/acf2/model/root.lua b/acf2/model/root.lua new file mode 100644 index 0000000..61debec --- /dev/null +++ b/acf2/model/root.lua @@ -0,0 +1,91 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local model = require('acf2.model.model') +local node = require('acf2.model.node') +local object = require('acf2.object') +local pth = require('acf2.path') + +local util = require('acf2.util') +local setdefault = util.setdefault + + +M.RootModel = model.new() + +function M.RootModel:init(txn) + object.super(self, M.RootModel):init{txn=txn, path='/', addr='/null/root'} +end + +function M.RootModel:has_permission(user, permission) + return permission == 'read' +end + +function M.RootModel:meta(path) + local obj = self:fetch(path, true) + if object.isinstance(obj, node.TreeNode) then return node.meta(obj) end + return node.mmeta(self:fetch(pth.parent(path), true), pth.name(path)) +end + + +local _topology = {} +local order = 0 + +function M.topology(addr, create) + local top = _topology + if type(addr) == 'table' then addr = util.copy(addr) + else addr = pth.split(addr) end + + local function defaults(top) + return util.setdefaults(top, {members={}, paths={}, referrers={}}) + end + + while #addr > 0 do + if create then + top = setdefault(defaults(top).members, addr[1], {}) + else + top = top.members[addr[1]] or top.members[pth.wildcard] + if not top then return end + end + table.remove(addr, 1) + end + + return defaults(top) +end + +function M.register(name, field, params) + if not params then params = {} end + params.create = true + M.RootModel[name] = model.to_field(field, params) + + local root = M.RootModel() + + for _, record in ipairs(node.topology(root:fetch(name))) do + local top = M.topology(record.addr, true) + + setdefault(top, 'order', order) + order = order + 1 + + local function set(k, v) + setdefault(top, k, v) + assert(top[k] == v) + end + + set('type', record.type) + table.insert(top.paths, record.path) + + if record.scope then + local scope = node.addr( + root:fetch(pth.to_absolute(record.scope, pth.parent(record.path))) + ) + set('scope', scope) + table.insert(M.topology(scope, true).referrers, record.path) + end + end +end + + +return M diff --git a/acf2/model/set.lua b/acf2/model/set.lua new file mode 100644 index 0000000..215056e --- /dev/null +++ b/acf2/model/set.lua @@ -0,0 +1,47 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local TreeNode = require('acf2.model.field').TreeNode +local npairs = require('acf2.model.node').pairs +local object = require('acf2.object') + + +M.Set = object.class(require('acf2.model.node').List) + +function M.Set:init(context, params) + assert(not object.isinstance(params.field, TreeNode)) + object.super(self, M.Set):init(context, params) + + local function find(value) + for i, member in npairs(self) do + if member == value then return i end + end + end + + local mt = getmetatable(self) + mt.dereference = false + mt.meta.type = 'set' + + function mt.get(k, create) + local i = find(k) + if i then return mt.load(i) end + if create then return k end + end + + function mt.__newindex(t, k, v) + assert(v == nil) + local i = find(k) + if not i then return end + mt.save(i, nil) + end + + local insert = mt.insert + function mt.insert(v) if not find(v) then insert(v) end end +end + + +return M |