diff options
-rw-r--r-- | acf/model/model.lua | 17 | ||||
-rw-r--r-- | acf/model/node.lua | 19 | ||||
-rw-r--r-- | acf/transaction/init.lua | 33 | ||||
-rw-r--r-- | acf/util.lua | 8 | ||||
-rwxr-xr-x | dev-shell | 14 | ||||
-rw-r--r-- | protocol.txt | 4 | ||||
-rw-r--r-- | server.lua | 24 |
7 files changed, 69 insertions, 50 deletions
diff --git a/acf/model/model.lua b/acf/model/model.lua index 6a9a843..aa27312 100644 --- a/acf/model/model.lua +++ b/acf/model/model.lua @@ -79,6 +79,13 @@ function Model:init(txn, path, addr) 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) local f = mt.field(k) if f then @@ -93,14 +100,4 @@ function Model:init(txn, path, addr) if not f then raise(mt.path, 'Field named '..k..' does not exist') end f:save(v) end - - txn.validate[mt.path] = function() self:validate() end -end - -function Model:validate() - local mt = getmetatable(self) - for _, name in ipairs(mt.members()) do - local field = mt.field(name) - if not field.compute then field:validate_saved() end - end end diff --git a/acf/model/node.lua b/acf/model/node.lua index 3459fce..f893d6a 100644 --- a/acf/model/node.lua +++ b/acf/model/node.lua @@ -48,6 +48,8 @@ function TreeNode:init(txn, path, addr) mt.txn = txn mt.path = path mt.addr = addr + + txn.validable[path] = true end function TreeNode:search(path) @@ -67,14 +69,6 @@ Collection = class(TreeNode) function Collection:init(txn, path, addr, field, required) super(self, Collection):init(txn, path, addr) - if required then - txn.validate[path] = function() - if #txn:get(addr) == 0 then - raise(path, 'Collection cannot be empty') - end - end - end - self.init = nil self.search = nil @@ -84,6 +78,14 @@ function Collection:init(txn, path, addr, field, required) mt.meta = {type='collection', members=mt.field:meta('$')} function mt.mmeta(name) return mt.meta.members end function mt.members() return txn:get(addr) or {} end + + function mt.validate() + if required then + if #txn:get(addr) == 0 then + raise(path, 'Collection cannot be empty') + end + 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 @@ -132,3 +134,4 @@ members = meta_func('members') meta = meta_func('meta') mmeta = meta_func('mmeta') path = meta_func('path') +validate = meta_func('validate') diff --git a/acf/transaction/init.lua b/acf/transaction/init.lua index eb92c34..e9b8bbc 100644 --- a/acf/transaction/init.lua +++ b/acf/transaction/init.lua @@ -11,7 +11,9 @@ local object = require('acf.object') local super = object.super local pth = require('acf.path') local be_mod = require('acf.transaction.backend') -local copy = require('acf.util').copy + +local util = require('acf.util') +local copy = util.copy local function remove_list_value(list, value) @@ -30,7 +32,7 @@ end local Transaction = object.class(be_mod.TransactionBackend) -function Transaction:init(backend) +function Transaction:init(backend, validate) super(self, Transaction):init() self.backend = backend @@ -42,7 +44,8 @@ function Transaction:init(backend) self.modified = {} self.deleted = {} - self.validate = {} + self.validate = validate + self.validable = {} self.root = RootModel(self) end @@ -122,11 +125,15 @@ function Transaction:search(path) return self.root:search(pth.split(path)) end function Transaction:commit() self:check() - local errors = ErrorDict() - for path, func in pairs(self.validate) do - if not self.deleted[path] then errors:collect(func) end + if self.validate then + local errors = ErrorDict() + for path, _ in pairs(copy(self.validable)) do + if not self.deleted[path] then + errors:collect(getmetatable(self:search(path)).validate) + end + end + errors:raise() end - errors:raise() local mods = {} local handled = {} @@ -174,8 +181,18 @@ function Transaction:commit() for path, _ in pairs(self.added) do insert_add(path) end self.backend:comp_and_setm(self.access_time, mods) + + + if not self.validate then + util.update(self.backend.validable, self.validable) + end + self.backend = nil end + local store = require('acf.persistence').DataStore() -function start(txn) return Transaction(txn or store) end + +function start(txn, defer_validation) + return Transaction(txn or store, not (txn and defer_validation)) +end diff --git a/acf/util.lua b/acf/util.lua index 9788c81..9ca287c 100644 --- a/acf/util.lua +++ b/acf/util.lua @@ -10,11 +10,15 @@ function setdefault(t, k, v) return t[k] end -function setdefaults(dst, src) - for k, v in pairs(src) do if dst[k] == nil then dst[k] = v end end +function update(dst, src, preserve) + for k, v in pairs(src) do + if not preserve or dst[k] == nil then dst[k] = v end + end return dst end +function setdefaults(dst, src) return update(dst, src, true) end + function copy(var) return type(var) == 'table' and setdefaults({}, var) or var end @@ -21,6 +21,7 @@ function _acf_start_req { local var=ACF_$3 shift 3 + local current=${!var} eval "export $var=\$(_acf_req \"\$url\" \"\$@\" \ -D /proc/self/fd/1 -o /proc/self/fd/3 | \ sed 's/^$hdr: \\(.\\+\\)'\$'\\r''\$/\\1/;ta;d;:a;q')" @@ -28,7 +29,7 @@ function _acf_start_req { bash --rcfile "$ACF_QD_CLI" && _acf_req $url -X DELETE - eval $var= + eval $var=$current } if [ "$ACF_AUTH_TOKEN" ]; then @@ -55,12 +56,7 @@ EOF PS1="$ACF_USER@acf2-dev-shell${ACF_TXN_ID:+($ACF_TXN_ID)}> " function start { - if [ "$ACF_TXN_ID" ]; then - echo "Nested transactions not yet supported" >&2 - return 1 - else - _acf_start_req / Transaction-ID TXN_ID -X POST - fi + _acf_start_req / Transaction-ID TXN_ID -X POST } function _acf_obj_req { @@ -83,7 +79,7 @@ EOF function commit { if [ "$ACF_TXN_ID" ]; then - if _acf_req / -X POST; then + if _acf_req / -X PUT; then echo Committed >&2 exit 1 fi @@ -145,6 +141,8 @@ EOF echo >&2 fi + ACF_TXN_ID= + _acf_start_req /login Auth-Token AUTH_TOKEN \ -d "{\"username\": \"$ACF_USER\", \"password\": \"$PASSWORD\"}" fi diff --git a/protocol.txt b/protocol.txt index 0d699f3..c869224 100644 --- a/protocol.txt +++ b/protocol.txt @@ -19,9 +19,11 @@ req: POST / resp: txn ID (in header as X-ACF-Transaction-ID) - use X-ACF-Transaction-ID in the header of any subsequent request to process it in the transaction's context + - nested transactions can be started by using it in a subsequent + start transaction request Commit transaction: -req: POST / +req: PUT / X-ACF-Transaction-ID: <txn_id> Abort transaction: @@ -64,13 +64,14 @@ return function(env) txn_id = tonumber(env.HTTP_X_ACF_TRANSACTION_ID) end - local txn + local parent_txn if txn_id then - txn = txns[txn_id] - if not txn then + parent_txn = txns[txn_id] + if not parent_txn then return wrap(400, nil, 'Invalid transaction ID') end - else txn = acf.transaction.start() end + end + local txn = acf.transaction.start(parent_txn, true) local function fetch_user(name) user = name and txn:search('/auth/users')[name] @@ -150,7 +151,7 @@ return function(env) elseif method == 'PUT' then parent[name] = data else return 405 end - if not txn_id then txn:commit() end + txn:commit() return 205 end @@ -159,21 +160,18 @@ return function(env) return 301, {['Location']='/browser/'} end - if not ({DELETE=true, POST=true})[method] then - return 405 - end - - if txn_id then - if method == 'POST' then txn:commit() end + if ({DELETE=true, PUT=true})[method] then + if not txn_id then return 405 end + if method == 'PUT' then parent_txn:commit() end txns[txn_id] = nil return 204 end - if method == 'DELETE' then return 405 end + if method ~= 'POST' then return 405 end last_txn_id = last_txn_id + 1 local txn_id = last_txn_id - txns[txn_id] = txn + txns[txn_id] = acf.transaction.start(parent_txn) return 204, {['X-ACF-Transaction-ID']=txn_id} end |