From 395da378214f4537901860b76c15ab1e405d1d3f Mon Sep 17 00:00:00 2001 From: Kaarle Ritvanen Date: Wed, 20 Mar 2013 16:53:56 +0200 Subject: new data type: set --- acf/model/field.lua | 4 +++- acf/model/init.lua | 9 ++++----- acf/model/model.lua | 35 ++++++++++++++++++----------------- acf/model/node.lua | 40 ++++++++++++++++++---------------------- acf/model/set.lua | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ acf/modules/aaa.lua | 6 +++--- acf/modules/awall.lua | 4 ++-- dev-shell | 11 ++++++++++- protocol.txt | 19 +++++++++++++------ server.lua | 27 ++++++++++++++++----------- 10 files changed, 136 insertions(+), 68 deletions(-) create mode 100644 acf/model/set.lua diff --git a/acf/model/field.lua b/acf/model/field.lua index 7e3aa1d..436ce2c 100644 --- a/acf/model/field.lua +++ b/acf/model/field.lua @@ -117,12 +117,14 @@ function TreeNode:save(context, value) local errors = err.ErrorDict() for k, v in pairs(value) do - errors:collect(function() new[k] = v end) + errors:collect(self.save_member, new, k, v) end errors:raise() end end +function TreeNode.save_member(node, k, v) node[k] = v end + Model = class(TreeNode) diff --git a/acf/model/init.lua b/acf/model/init.lua index bf4f7b4..2800ab6 100644 --- a/acf/model/init.lua +++ b/acf/model/init.lua @@ -17,6 +17,7 @@ new = model.new local to_field = model.to_field node = require('acf.model.node') +set = require('acf.model.set') local object = require('acf.object') local class = object.class @@ -196,11 +197,9 @@ function Collection:load(context) end -PrimitiveList = class(Collection) - -function PrimitiveList:init(params) - super(self, PrimitiveList):init(params, node.PrimitiveList) -end +Set = class(Collection) +function Set:init(params) super(self, Set):init(params, set.Set) end +function Set.save_member(node, k, v) set.add(node, v) end -- experimental diff --git a/acf/model/model.lua b/acf/model/model.lua index 5fd96a7..7c27b50 100644 --- a/acf/model/model.lua +++ b/acf/model/model.lua @@ -71,22 +71,7 @@ function Model:init(context) function mt.mmeta(name) return mt.field(name):meta() end - mt.meta = {type='model', - fields=util.map(function(f) return mt.mmeta(f) end, - self._fields)} - - function mt.members() - return util.map(function(f) return f.name end, mt.meta.fields) - end - - function mt.validate() - for _, name in ipairs(mt.members()) do - local field = mt.field(name) - if not field.compute then field:validate_saved() end - end - end - - function mt.__index(t, k) + function mt.get(k) local f = mt.field(k) if f then if f.compute then return f:compute() end @@ -95,9 +80,25 @@ function Model:init(context) return mt.class[k] end - function mt.__newindex(t, k, v) + function mt.set(k, v) local f = mt.field(k) if not f then raise(mt.path, 'Field named '..k..' does not exist') end f:save(v) end + + mt.meta = { + type='model', + fields=util.map(function(f) return mt.mmeta(f) end, self._fields) + } + + function mt.members() + return util.map(function(f) return f.name end, mt.meta.fields) + end + + function mt.validate() + for _, name in ipairs(mt.members()) do + local field = mt.field(name) + if not field.compute then field:validate_saved() end + end + end end diff --git a/acf/model/node.lua b/acf/model/node.lua index 7a818be..cf3111d 100644 --- a/acf/model/node.lua +++ b/acf/model/node.lua @@ -52,6 +52,11 @@ TreeNode = class() function TreeNode:init(context) local mt = getmetatable(self) update(mt, context) + + function mt.set(k, v) rawset(self, k, v) end + function mt.__index(t, k) return mt.get(k) end + function mt.__newindex(t, k, v) mt.set(k, v) end + mt.txn.validable[mt.path] = mt.addr end @@ -88,28 +93,8 @@ function Collection:init(context, params) end end - function mt.__index(t, k) return mt.field:load(k) end - function mt.__newindex(t, k, v) mt.field:save(k, v) end -end - - -PrimitiveList = class(Collection) - -function PrimitiveList:init(context, params) - super(self, PrimitiveList):init(context, params) - - local mt = getmetatable(self) - local index = mt.__index - - function mt.__index(t, k) - if type(k) == 'number' then return index(t, k) end - - for i, j in ipairs(mt.txn:get(mt.addr) or {}) do - assert(i == tonumber(j)) - if mt.field:load(i) == k then return k end - end - raise(mt.path, 'Value does not exist: '..k) - end + function mt.get(k) return mt.field:load(k) end + function mt.set(k, v) mt.field:save(k, v) end end @@ -138,3 +123,14 @@ mmeta = meta_func('mmeta') parent = meta_func('parent') path = meta_func('path') validate = meta_func('validate') + + +local rawpairs = pairs + +function pairs(tbl) + if not object.isinstance(tbl, TreeNode) then return rawpairs(tbl) end + local mt = getmetatable(tbl) + local res = {} + for _, member in ipairs(mt.members()) do res[member] = mt.get(member) end + return rawpairs(res) +end diff --git a/acf/model/set.lua b/acf/model/set.lua new file mode 100644 index 0000000..be6c2de --- /dev/null +++ b/acf/model/set.lua @@ -0,0 +1,49 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +module(..., package.seeall) + +local TreeNode = require('acf.model.field').TreeNode +local npairs = require('acf.model.node').pairs +local object = require('acf.object') + + +local function find(set, value) + for i, member in npairs(set) do + if member == value then return i end + end +end + + +Set = object.class(require('acf.model.node').Collection) + +function Set:init(context, params) + assert(not object.isinstance(params.field, TreeNode)) + object.super(self, Set):init(context, params) + + local mt = getmetatable(self) + mt.meta.type = 'set' + + function mt.__index(t, k) return find(self, k) and k end + + function mt.__newindex(t, k, v) + assert(v == nil) + local i = find(self, k) + if not i then return end + + local len = #mt.members() + while i < len do + mt.set(i, mt.get(i + 1)) + i = i + 1 + end + mt.set(len, nil) + end +end + + +function add(set, value) + local mt = getmetatable(set) + if not find(set, value) then mt.set(#mt.members() + 1, value) end +end diff --git a/acf/modules/aaa.lua b/acf/modules/aaa.lua index 23cb6df..e98e686 100644 --- a/acf/modules/aaa.lua +++ b/acf/modules/aaa.lua @@ -8,18 +8,18 @@ module(..., package.seeall) local M = require('acf.model') Role = M.new() -Role.permissions = M.Collection{type=M.Reference{scope='../../../permissions'}} +Role.permissions = M.Set{type=M.Reference{scope='../../../permissions'}} User = M.new() User.password = M.String User.real_name = M.String -User.roles = M.Collection{type=M.Reference{scope='../../../roles'}} +User.roles = M.Set{type=M.Reference{scope='../../../roles'}} function User:check_password(password) return password == self.password end Authentication = M.new() Authentication.users = M.Collection{type=User} Authentication.roles = M.Collection{type=Role} -Authentication.permissions = M.PrimitiveList{type=M.String} +Authentication.permissions = M.Set{type=M.String} M.register('auth', '/json'..require('lfs').currentdir()..'/config/aaa.json', diff --git a/acf/modules/awall.lua b/acf/modules/awall.lua index dbb335a..2920bdb 100644 --- a/acf/modules/awall.lua +++ b/acf/modules/awall.lua @@ -55,8 +55,8 @@ Service['icmp-type'] = M.String -- TODO fw zone Zone = M.new() -Zone.iface = M.PrimitiveList{type=M.String} -Zone.addr = M.PrimitiveList{type=M.String} +Zone.iface = M.Set{type=M.String} +Zone.addr = M.Set{type=M.String} Zone['route-back'] = M.Boolean{default=false} LogClass = M.new() diff --git a/dev-shell b/dev-shell index 397c7af..9e64500 100755 --- a/dev-shell +++ b/dev-shell @@ -45,6 +45,7 @@ EOF Available commands: Fetch object: get Create/update object: put + Add member to set: post Delete object: delete Start transaction: start @@ -65,12 +66,20 @@ EOF _acf_req "$path" "$@" } + function _acf_post_req { + _acf_obj_req "$2" -d "$3" -X $1 + } + function get { _acf_obj_req "$1" } function put { - _acf_obj_req "$1" -d "$2" -X PUT + _acf_post_req PUT "$@" + } + + function post { + _acf_post_req POST "$@" } function delete { diff --git a/protocol.txt b/protocol.txt index ffc50bf..a0ba4d4 100644 --- a/protocol.txt +++ b/protocol.txt @@ -36,16 +36,16 @@ resp: JSON object, with the following attributes: data: JSON serialization of object - primitive types as JSON primitives - references as path names (relative to scope) - - models and collections as JSON objects or arrays with - members as attributes: - - primitive members as JSON primitives - - reference, model, and collection members as path names + - models, collections, and sets as JSON objects or arrays + with members as attributes: + - primitive members as JSON primitives + - reference, model, and collection members as path names meta: JSON object, with the following attributes - name (last component of path name) - ui-name (shown to user) - description (optional help text) - - type (e.g. model, collection, reference, string, number, - boolean) + - type (e.g. model, collection, set, reference, string, + number, boolean) - widget (name of client-side JS module used to display the data) - required (boolean) @@ -70,6 +70,13 @@ req: PUT /config/ Delete object: req: DELETE /config/ +Add member to a set: +req: POST /config/ + - body shall contain a JSON primitive + +Delete set member: +req: DELETE /config// + Invoke object-specific action (not yet supported by server): req: POST /config// - arguments passed as a JSON array in body; serialization as in diff --git a/server.lua b/server.lua index 0bd5553..43a6a09 100644 --- a/server.lua +++ b/server.lua @@ -120,21 +120,18 @@ return function(env) if method == 'GET' then local obj = txn:search(path) - if obj == nil then return 404 end - local res if isinstance(obj, acf.model.node.TreeNode) then local node = {} - for _, k in ipairs(acf.model.node.members(obj)) do - local v = obj[k] - if isinstance(v, acf.model.node.TreeNode) then - v = acf.model.node.path(v) - end - node[k] = v + for k, v in acf.model.node.pairs(obj) do + node[k] = isinstance( + v, + acf.model.node.TreeNode + ) and acf.model.node.path(v) or v end res = {data=node, meta=acf.model.node.meta(obj)} - + else res = { data=obj, @@ -145,8 +142,16 @@ return function(env) return 200, nil, res end - -- TODO implement POST for invoking object-specific actions - if method == 'DELETE' then parent[name] = nil + if method == 'POST' then + local obj = txn:search(path) + if not acf.object.isinstance(obj, acf.model.set.Set) then + -- TODO invoke model-specific actions + return 405 + end + + acf.model.set.add(obj, data) + + elseif method == 'DELETE' then parent[name] = nil elseif method == 'PUT' then parent[name] = data else return 405 end -- cgit v1.2.3