├── .busted ├── config.ld ├── README.md ├── .travis.yml ├── luarocks └── storefront-scm-0.rockspec ├── spec ├── backends_spec.lua ├── adapter_spec.lua ├── storetest.lua ├── path_spec.lua └── base_spec.lua └── storefront ├── backend └── inmem.lua ├── adapter └── lrucache.lua ├── base.lua └── path.lua /.busted: -------------------------------------------------------------------------------- 1 | -- vim:set ft=lua: 2 | return { 3 | _all = { 4 | verbose = true, 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /config.ld: -------------------------------------------------------------------------------- 1 | project = "StoreFront" 2 | package = "storefront" 3 | format = "markdown" 4 | file = "storefront" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lua-storefront 2 | ============== 3 | 4 | [![Build Status](https://travis-ci.org/aperezdc/lua-storefront.svg?branch=master)](https://travis-ci.org/aperezdc/lua-storefront) 5 | [![Coverage Status](https://coveralls.io/repos/github/aperezdc/lua-storefront/badge.svg?branch=master)](https://coveralls.io/github/aperezdc/lua-storefront?branch=master) 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | 4 | env: 5 | matrix: 6 | - LUA="lua 5.1" 7 | - LUA="lua 5.2" 8 | - LUA="lua 5.3" 9 | - LUA="luajit 2.0" 10 | - LUA="luajit 2.1" 11 | 12 | before_install: 13 | - pip install hererocks 14 | - hererocks here -r^ --$LUA 15 | - export PATH=$PATH:$PWD/here/bin 16 | - eval `luarocks path --bin` 17 | - lua -v 18 | 19 | install: 20 | - luarocks install --only-deps $(find luarocks -name '*-scm-*.rockspec' | sort -g | tail -1) 21 | - luarocks install luacov-coveralls 22 | - luarocks install cluacov 23 | - luarocks install busted 24 | 25 | script: 26 | - busted -c 27 | 28 | after_success: 29 | - luacov-coveralls -i storefront/ -e spec/ -e here/ 30 | -------------------------------------------------------------------------------- /luarocks/storefront-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "storefront" 2 | version = "scm-0" 3 | source = { 4 | url = "git://github.com/aperezdc/lua-storefront" 5 | } 6 | description = { 7 | maintainer = "Adrián Pérez de Castro ", 8 | summary = "Unified key-value store API with multiple backends", 9 | homepage = "https://github.com/aperezdc/lua-storefront", 10 | license = "MIT/X11" 11 | } 12 | dependencies = { 13 | "lua >= 5.1", 14 | "itertools >= 0.1", 15 | } 16 | build = { 17 | type = "builtin", 18 | modules = { 19 | ["storefront.base"] = "storefront/base.lua", 20 | ["storefront.path"] = "storefront/path.lua", 21 | 22 | ["storefront.adapter.lrucache"] = "storefront/adapter/lrucache.lua", 23 | 24 | ["storefront.backend.inmem"] = "storefront/backend/inmem.lua", 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spec/backends_spec.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- backends_spec.lua 3 | -- Copyright (C) 2016 Adrian Perez 4 | -- 5 | -- Distributed under terms of the MIT license. 6 | -- 7 | 8 | local storetest = require "spec.storetest" 9 | local path = require "storefront.path" 10 | local inmem = require "storefront.backend.inmem" 11 | 12 | describe("storefront.backend.inmem", function () 13 | it("has basic store methods", function () 14 | storetest.test_store_methods(inmem, true) 15 | storetest.test_store_methods(inmem()) 16 | end) 17 | 18 | it("stores and retrieves data", function () 19 | storetest.test_set_get(inmem(), path "foo.bar") 20 | end) 21 | 22 | it("deletes items", function () 23 | local s = inmem() 24 | s:set(path "foo", "foo value") 25 | storetest.test_del(s, path "foo.bar") 26 | assert.equal("foo value", s:get(path "foo")) 27 | end) 28 | end) 29 | -------------------------------------------------------------------------------- /spec/adapter_spec.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- adapter_spec.lua 3 | -- Copyright (C) 2016 Adrian Perez 4 | -- 5 | -- Distributed under terms of the MIT license. 6 | -- 7 | 8 | local storetest = require "spec.storetest" 9 | local inmem = require "storefront.backend.inmem" 10 | local lrucache = require "storefront.adapter.lrucache" 11 | local P = require "storefront.path" 12 | 13 | describe("storefront.adapter.lrucache", function () 14 | it("has basic store methods", function () 15 | storetest.test_store_methods(lrucache, true) 16 | storetest.test_store_methods(lrucache(inmem(), 10)) 17 | end) 18 | 19 | it("stores and retrieves data", function () 20 | storetest.test_set_get(lrucache(inmem(), 10), P"foo/bar") 21 | end) 22 | 23 | it("deletes items", function () 24 | local s = lrucache(inmem(), 10) 25 | s:set(P"foo", "foo value") 26 | storetest.test_del(s, P"foo/bar") 27 | assert.equal("foo value", s:get(P"foo")) 28 | end) 29 | end) 30 | 31 | -------------------------------------------------------------------------------- /storefront/backend/inmem.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- inmem.lua 3 | -- Copyright (C) 2016 Adrian Perez 4 | -- 5 | -- Distributed under terms of the MIT license. 6 | -- 7 | 8 | local keys = require "itertools" .keys 9 | local map = require "itertools" .map 10 | local base = require "storefront.base" 11 | local P = require "storefront.path" 12 | local check_path, query_iterable = P.check, base.query_iterable 13 | 14 | local _ENV = nil 15 | 16 | 17 | local inmem = base.store + "storefront.backend.inmem" 18 | 19 | function inmem:__init() 20 | base.store:__init() 21 | self._data = {} 22 | end 23 | 24 | function inmem:get(path) 25 | return self._data[check_path(path).string] 26 | end 27 | 28 | function inmem:set(path, value) 29 | self._data[check_path(path).string] = value 30 | end 31 | 32 | function inmem:del(path) 33 | self._data[check_path(path).string] = nil 34 | end 35 | 36 | function inmem:has(path) 37 | return self._data[check_path(path).string] ~= nil 38 | end 39 | 40 | function inmem:query(pattern, limit, offset) 41 | return query_iterable(map(P, keys(self._data)), pattern, limit, offset) 42 | end 43 | 44 | return inmem 45 | -------------------------------------------------------------------------------- /spec/storetest.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- storetest.lua 3 | -- Copyright (C) 2016 Adrian Perez 4 | -- 5 | -- Distributed under terms of the MIT license. 6 | -- 7 | 8 | local assert = require "luassert" 9 | local P = require "storefront.path" 10 | 11 | local function test_store_methods(s, nocall) 12 | assert.is_function(s.begin_transaction) 13 | assert.is_function(s.end_transaction) 14 | assert.is_function(s.transact) 15 | assert.is_function(s.get) 16 | assert.is_function(s.set) 17 | assert.is_function(s.del) 18 | assert.is_function(s.has) 19 | if not nocall then 20 | s:get(P"foo") 21 | s:set(P"foo", "42") 22 | s:del(P"foo", "42") 23 | s:has(P"foo") 24 | s:query("*") 25 | end 26 | end 27 | 28 | local function test_store_set_get(s, p) 29 | assert.not_truthy(s:has(p)) 30 | s:set(p, "random value") 31 | assert.truthy(s:has(p)) 32 | assert.equal("random value", s:get(p)) 33 | for path in s:query(tostring(p)) do 34 | assert.equal(p, path) 35 | break 36 | end 37 | end 38 | 39 | local function test_store_del (s, p) 40 | assert.is_false(s:has(p)) 41 | s:set(p, "the answer is 42") 42 | assert.is_true(s:has(p)) 43 | assert.equal("the answer is 42", s:get(p)) 44 | s:del(p) 45 | assert.is_false(s:has(p)) 46 | assert.is_nil(s:get(p)) 47 | for path in s:query(tostring(p)) do 48 | error("unreachable") 49 | end 50 | end 51 | 52 | return { 53 | test_store_methods = test_store_methods, 54 | test_set_get = test_store_set_get, 55 | test_del = test_store_del, 56 | } 57 | -------------------------------------------------------------------------------- /storefront/adapter/lrucache.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- lrucache.lua 3 | -- Copyright (C) 2016 Adrian Perez 4 | -- 5 | -- Distributed under terms of the MIT license. 6 | -- 7 | 8 | local tostring = tostring 9 | 10 | local keys = require "itertools" .keys 11 | local map = require "itertools" .map 12 | local base = require "storefront.base" 13 | local P = require "storefront.path" 14 | local check_path, query_iterable = P.check, base.query_iterable 15 | 16 | local _ENV = nil 17 | 18 | 19 | local store = base.store + "storefront.adapter.lrucache.store" 20 | 21 | function store:__init(size) 22 | base.store.__init(self) 23 | self._table = {} 24 | self._head = {} 25 | self._head._next = self._head 26 | self._head._prev = self._head 27 | self._list_size = 1 28 | self:size(size) 29 | end 30 | 31 | function store:__tostring() 32 | return "<" .. self.__name .. " size=" .. tostring(self.size) .. ">" 33 | end 34 | 35 | local function _add_tail_nodes(self, n) 36 | for _ = 1, n do 37 | local node = { _next = self._head, _prev = self._head._prev } 38 | self._head._prev._next = node 39 | self._head._prev = node 40 | end 41 | self._list_size = self._list_size + n 42 | end 43 | 44 | local function _del_tail_nodes(self, n) 45 | for _ = 1, n do 46 | local node = self._head._prev 47 | if node.key ~= nil then 48 | self._table[node.key] = nil 49 | end 50 | self._head._prev = node._prev 51 | node._prev._next = self._head 52 | node._prev = nil 53 | node._next = nil 54 | node.value = nil 55 | node.key = nil 56 | end 57 | self._list_size = self._list_size - n 58 | end 59 | 60 | local function _size(self, size) 61 | if size ~= nil and size > 0 then 62 | if size > self._list_size then 63 | _add_tail_nodes(self, size - self._list_size) 64 | elseif size < self._list_size then 65 | _del_tail_nodes(self, self._list_size - size) 66 | end 67 | end 68 | return self._list_size 69 | end 70 | 71 | store.__len = _size 72 | store.size = _size 73 | 74 | local function _move_to_front(self, node) 75 | node._prev._next = node._next 76 | node._next._prev = node._prev 77 | node._prev = self._head._prev 78 | node._next = self._head._prev._next 79 | node._next._prev = node 80 | node._prev._next = node 81 | end 82 | 83 | function store:get(path) 84 | local key = tostring(path) 85 | local node = self._table[key] 86 | if node then 87 | _move_to_front(self, node) 88 | self._head = node 89 | return node.value 90 | end 91 | return nil 92 | end 93 | 94 | function store:set(path, value) 95 | local key = tostring(path) 96 | local node = self._table[key] 97 | if node then 98 | -- Keep node, replace value. 99 | node.value = value 100 | _move_to_front(self, node) 101 | self._head = node 102 | else 103 | -- Choose a node for the new item. Evict the last item in the list when 104 | -- the cache is full, or pick an empty node. There are empty nodes are 105 | -- always at the end. 106 | local node = self._head._prev 107 | if node.key then 108 | self._table[node.key] = nil -- Evict. 109 | end 110 | node.key, node.value = key, value 111 | self._table[key] = node 112 | -- The list is circular; it's enough to move the head pointer. 113 | self._head = node 114 | end 115 | end 116 | 117 | function store:del(path) 118 | local key = tostring(path) 119 | local node = self._table[key] 120 | if node then 121 | self._table[key] = nil 122 | node.key, node.value = nil, nil 123 | -- Place the empty node at the end of the circular list, which is the 124 | -- same as moving it to the front and changing the head pointer. 125 | _move_to_front(self, node) 126 | self._head = node._next 127 | end 128 | end 129 | 130 | function store:has(path) 131 | return self._table[tostring(path)] ~= nil 132 | end 133 | 134 | function store:query(pattern, limit, offset) 135 | return query_iterable(map(P, keys(self._table)), pattern, limit, offset) 136 | end 137 | 138 | 139 | local lrucache = base.cache + "storefront.adapter.lrucache" 140 | 141 | function lrucache:__init(child, size) 142 | base.cache.__init(self, child, store(size)) 143 | end 144 | 145 | return lrucache 146 | -------------------------------------------------------------------------------- /storefront/base.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- base.lua 3 | -- Copyright (C) 2016 Adrian Perez 4 | -- 5 | -- Distributed under terms of the MIT license. 6 | -- 7 | 8 | local error, xpcall, setmetatable, pairs = error, xpcall, setmetatable, pairs 9 | local assert, type, select, d_traceback = assert, type, select, debug.traceback 10 | local tostring, s_gsub, s_match = tostring, string.gsub, string.match 11 | local unpack = table.unpack or unpack 12 | 13 | local islice = require "itertools" .islice 14 | local sorted = require "itertools" .sorted 15 | local filter = require "itertools" .filter 16 | 17 | local _ENV = nil 18 | 19 | -- 20 | -- In Lua 5.1 xpcall() does not pass additional argument down to the callback 21 | -- function, so we replace the function by a version which uses a closure to 22 | -- pass them. 23 | -- 24 | if not select(2, xpcall(function (x) return x ~= nil end, function() end, 1)) then 25 | local do_xpcall = xpcall 26 | xpcall = function (callback, error_handler, ...) 27 | local args, narg = { ... }, select("#", ...) 28 | return do_xpcall(function () 29 | return callback(unpack(args, 1, narg)) 30 | end, error_handler) 31 | end 32 | end 33 | 34 | local function error_add_traceback (message) 35 | local tp = type(message) 36 | if tp ~= "string" and tp ~= "number" then 37 | return message 38 | end 39 | return d_traceback(message, 2) 40 | end 41 | 42 | 43 | local class_meta = {} 44 | 45 | function class_meta:__call(...) 46 | local obj = {} 47 | setmetatable(obj, self) 48 | obj:__init(...) 49 | return obj 50 | end 51 | 52 | function class_meta:__tostring() 53 | return "" 54 | end 55 | 56 | function class_meta:__add(name) 57 | local cls = {} 58 | for k, v in pairs(self) do 59 | cls[k] = v 60 | end 61 | cls.__index = cls 62 | cls.__name = name 63 | return setmetatable(cls, class_meta) 64 | end 65 | 66 | 67 | local base = {} 68 | 69 | -- Construct the base class manually. 70 | base.class = class_meta.__add({ 71 | __tostring = function (self) 72 | return "<" .. self.__name .. ">" 73 | end, 74 | __init = function (self, properties) 75 | if properties then 76 | for k, v in pairs(properties) do 77 | self[k] = v 78 | end 79 | end 80 | end, 81 | }, "storefront.base.class") 82 | 83 | 84 | base.store = base.class + "storefront.base.store" 85 | 86 | function base.store:begin_transaction() 87 | return true -- Dummy implementation. 88 | end 89 | 90 | function base.store:end_transaction(token) 91 | return true -- Dummy implementation. 92 | end 93 | 94 | function base.store:transact(callable, ...) 95 | local token = assert(self:begin_transaction()) 96 | assert(xpcall(callable, error_add_traceback, self, token, ...)) 97 | assert(self:end_transaction(token)) 98 | end 99 | 100 | function base.store:get(path) 101 | error("store:get() unimplemented") 102 | end 103 | 104 | function base.store:set(path, value) 105 | error("store:set() unimplemented") 106 | end 107 | 108 | function base.store:del(path) 109 | error("store:del() unimplemented") 110 | end 111 | 112 | function base.store:has(path) 113 | return self:get(path) ~= nil 114 | end 115 | 116 | function base.store:query(pattern, limit, offset) 117 | error("store:query() unimplemented") 118 | end 119 | 120 | 121 | base.shim = base.store + "storefront.base.shim" 122 | 123 | function base.shim:__init(store) 124 | base.store.__init(self) 125 | self.child = store 126 | end 127 | 128 | function base.shim:__tostring() 129 | return "<" .. self.__name .. " " .. tostring(self.child) .. ">" 130 | end 131 | 132 | function base.shim:get(...) return self.child:get(...) end 133 | function base.shim:set(...) return self.child:set(...) end 134 | function base.shim:del(...) return self.child:del(...) end 135 | function base.shim:has(...) return self.child:has(...) end 136 | function base.shim:query(...) return self.child:query(...) end 137 | 138 | 139 | base.cache = base.shim + "storefront.base.cache" 140 | 141 | function base.cache:__init(store, cache) 142 | base.shim.__init(self, store) 143 | self.cache = cache 144 | end 145 | 146 | function base.cache:__tostring() 147 | return "<" .. self.__name .. " " .. tostring(self.child) 148 | .. " cache=" .. tostring(self.cache) .. ">" 149 | end 150 | 151 | function base.cache:get(path) 152 | local value = self.cache:get(path) 153 | if value == nil then 154 | value = self.child:get(path) 155 | if value ~= nil then 156 | self.cache:set(path, value) 157 | end 158 | end 159 | return value 160 | end 161 | 162 | function base.cache:set(path, value) 163 | self.cache:set(path, value) 164 | self.child:set(path, value) 165 | end 166 | 167 | function base.cache:del(path) 168 | self.cache:del(path) 169 | self.child:del(path) 170 | end 171 | 172 | function base.cache:has(path) 173 | return self.cache:has(path) or self.child:has(path) 174 | end 175 | 176 | 177 | local function make_pattern_matcher (pattern) 178 | pattern = (s_gsub(pattern, "[%-%.%+%[%]%(%)%$%^%%%?%*]", "%%%1")) 179 | pattern = (s_gsub(pattern, "%%%*%%%*", ".*")) 180 | pattern = (s_gsub(pattern, "%%%*", "[^/]*")) 181 | pattern = (s_gsub(pattern, "%%%?", ".")) 182 | pattern = "^" .. pattern .. "$" 183 | return function (s) 184 | return s_match(tostring(s), pattern) ~= nil 185 | end 186 | end 187 | 188 | function base.query_iterable(iterable, pattern, limit, offset) 189 | -- TODO: Canonicalize pattern first. 190 | 191 | -- Only apply the filtering when not iterating over all keys. 192 | if pattern ~= "**" then 193 | local matches_pattern = make_pattern_matcher(pattern) 194 | iterable = filter(matches_pattern, iterable) 195 | end 196 | 197 | if offset == nil then 198 | offset = 1 199 | end 200 | 201 | if limit == nil then 202 | if offset > 1 then 203 | iterable = islice(sorted(iterable), offset) 204 | end 205 | else 206 | iterable = islice(sorted(iterable), offset, offset + limit - 1) 207 | end 208 | 209 | return iterable 210 | end 211 | 212 | 213 | return base 214 | -------------------------------------------------------------------------------- /storefront/path.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- path.lua 3 | -- Copyright (C) 2016 Adrian Perez 4 | -- 5 | -- Distributed under terms of the MIT license. 6 | -- 7 | 8 | -- 9 | -- A "path" is a string separated by slashes. Each slash-separated component 10 | -- can only contain numbers, letters, dashes, underscores, and periods; and 11 | -- the first character of a component cannot be a period. Paths are always 12 | -- absolute. 13 | -- 14 | -- Path objects are interned. This means that: 15 | -- 16 | -- * The same path is represented always by the same table/object. 17 | -- * Instantiating multiple path objects for a path continues using 18 | -- the same unique path object. 19 | -- * Modifying a path objects can only be done using the modification 20 | -- functions. The path components are read-only (__newindex is used 21 | -- to ensure this). 22 | -- 23 | 24 | local setmetatable, getmetatable = setmetatable, getmetatable 25 | local type, error, ipairs, tostring = type, error, ipairs, tostring 26 | local s_match, s_gmatch, s_format = string.match, string.gmatch, string.format 27 | local t_insert, t_concat = table.insert, table.concat 28 | 29 | local _ENV = nil 30 | 31 | 32 | local path_component_pattern = "^[_%w%-][_%w%-%.]*$" 33 | local path_component_split_pattern = "[^/]+" 34 | 35 | 36 | -- Weak table of "live" path objects. 37 | local live_paths = setmetatable({}, { __mode = "v" }) 38 | 39 | 40 | -- Path object prototype. 41 | local path = {} 42 | path.__index = path 43 | 44 | function path:__newindex(key, value) 45 | error("path objects are immutable") 46 | end 47 | 48 | -- 49 | -- Creates a copy of a path without checking its components. This is used 50 | -- from the methods which return a new modified path based on another path, 51 | -- in order to avoid overhead. 52 | -- 53 | local function unsafe_copy(path) 54 | local result, num_components = {}, 0 55 | for i, component in ipairs(path) do 56 | result[i] = component 57 | num_components = num_components + 1 58 | end 59 | return result, num_components 60 | end 61 | 62 | local function check_path_component(s) 63 | if s_match(s, path_component_pattern) then return s end 64 | error(s_format("invalid path component: %q", s)) 65 | end 66 | 67 | local function canonicalize(path, copy) 68 | local result, num_components 69 | 70 | if type(path) == "string" then 71 | -- Split paths given as strings, in order to canonicalize them. 72 | result, num_components = {}, 0 73 | for component in s_gmatch(path, path_component_split_pattern) do 74 | t_insert(result, check_path_component(component)) 75 | num_components = num_components + 1 76 | end 77 | elseif copy then 78 | -- Copy and check components in a single go. 79 | result, num_components = {}, 0 80 | for i, component in ipairs(path) do 81 | result[i] = check_path_component(component) 82 | num_components = num_components + 1 83 | end 84 | else 85 | -- Check path components and count them. 86 | result, num_components = path, 0 87 | for _, component in ipairs(result) do 88 | if not s_match(component, path_component_pattern) then 89 | error(s_format("invalid path component: %q", component)) 90 | end 91 | num_components = num_components + 1 92 | end 93 | end 94 | 95 | if num_components == 0 then 96 | error(s_format("path is empty")) 97 | end 98 | 99 | return result, num_components 100 | end 101 | 102 | 103 | -- 104 | -- Accepts a table with a list of path components, and if the path is in the 105 | -- table of "live" path objects, return the existing object. Otherwise, create 106 | -- a new path object, convert the passed table into a path object, add it to 107 | -- the weak tale of "live" objects and return it. 108 | -- 109 | local function intern(path_components) 110 | local path_string = t_concat(path_components, "/") 111 | local path_object = live_paths[path_string] 112 | if not path_object then 113 | -- Convert into a path object: all modifications *must* be done before 114 | -- setting the metatable, because its __newindex produces an error. 115 | path_components.string = path_string 116 | path_object = setmetatable(path_components, path) 117 | live_paths[path_string] = path_object 118 | end 119 | return path_object 120 | end 121 | 122 | 123 | setmetatable(path, { __call = function (self, path, adopt) 124 | return intern(canonicalize(path, not adopt)) 125 | end }) 126 | 127 | function path:__tostring() 128 | return self.string 129 | end 130 | 131 | function path:child(component) 132 | check_path_component(component) 133 | local new_path, n = unsafe_copy(self) 134 | new_path[n + 1] = component 135 | return intern(new_path) 136 | end 137 | 138 | function path:parent() 139 | local new_path, n = unsafe_copy(self) 140 | new_path[n] = nil 141 | return intern(new_path) 142 | end 143 | 144 | function path:sibling(component) 145 | check_path_component(component) 146 | local new_path, n = unsafe_copy(self) 147 | new_path[n] = component 148 | return intern(new_path) 149 | end 150 | 151 | function path:__concat(other) 152 | local new_path, n = unsafe_copy(self) 153 | local other_type = type(other) 154 | if other_type == "table" then 155 | if getmetatable(other) == path then 156 | -- No need to check components. 157 | for i, component in ipairs(other) do 158 | new_path[n + i] = component 159 | end 160 | else 161 | -- Check path components. 162 | for i, component in ipairs(other) do 163 | new_path[n + i] = check_path_component(component) 164 | end 165 | end 166 | elseif other_type == "string" then 167 | -- Canonicalize the string into a new path. 168 | for i, component in ipairs(canonicalize(other)) do 169 | new_path[n + i] = component 170 | end 171 | else 172 | -- Append the stringization of the value. 173 | new_path[n + 1] = check_path_component(tostring(other)) 174 | end 175 | return intern(new_path) 176 | end 177 | 178 | 179 | function path.check(p, convert) 180 | if getmetatable(p) == path then 181 | return p 182 | elseif convert then 183 | return path(p) 184 | else 185 | error("argument is not a path object") 186 | end 187 | end 188 | 189 | 190 | return path 191 | -------------------------------------------------------------------------------- /spec/path_spec.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- path_spec.lua 3 | -- Copyright (C) 2016 Adrian Perez 4 | -- 5 | -- Distributed under terms of the MIT license. 6 | -- 7 | 8 | local P = require "storefront.path" 9 | 10 | describe("storefront.path", function () 11 | it("can be instantiated with valid path strings", function () 12 | for _, path_string in ipairs { 13 | "a", "a/b", "a/b/c", 14 | "a/b.c_d", "a/_b/c", "a/_b_/c", "a/b_/c", 15 | "a1", "0", "foo/n", "foo/1", 16 | } do 17 | assert.message(string.format("input: %q", path_string)) 18 | .not_has_error(function () P(path_string) end) 19 | end 20 | end) 21 | 22 | it("can be instantiated using tables", function () 23 | for _, path_table in ipairs { 24 | { "a" }, { "a", "b" }, { "a", "b", "c_d" } 25 | } do 26 | assert.message("input: " .. table.concat(path_table, ", ")) 27 | .not_has_error(function () P(path_table) end) 28 | end 29 | end) 30 | 31 | it("errors on invalid path strings", function () 32 | for _, path_string in ipairs { 33 | "", -- empty string 34 | "a:b", -- invalid component separator 35 | "a b c", -- spaces 36 | ".n", -- leading period 37 | "a/.n", 38 | } do 39 | assert.message(string.format("input: %q", path_string)) 40 | .has_error(function () P(path_string) end) 41 | end 42 | end) 43 | 44 | it("errors on invalid path tables", function () 45 | for _, path_table in ipairs { 46 | { }, -- no components 47 | { "" }, -- empty component 48 | { "a", "" }, -- ditto 49 | { "", "a" }, -- ditto 50 | { " foo" }, -- space in component 51 | { "a", "." }, -- period in component 52 | } do 53 | assert.message("input: [" .. table.concat(path_table, "/") .. "]") 54 | .has_error(function () P(path_table) end) 55 | end 56 | end) 57 | 58 | it("properly converts with tostring()", function () 59 | local items = { 60 | ["a/b/c/d"] = { 61 | P { "a", "b", "c", "d" }, 62 | P "a/b/c/d", 63 | }, 64 | ["foobar"] = { 65 | P { "foobar" }, 66 | P "foobar", 67 | }, 68 | ["a_b/c_d"] = { 69 | P { "a_b", "c_d" }, 70 | P "a_b/c_d", 71 | }, 72 | } 73 | for expected, paths in pairs(items) do 74 | for _, path in ipairs(paths) do 75 | assert.message("path <" .. table.concat(path, "/") .. ">") 76 | .equal(expected, path.string) 77 | assert.message("path <" .. table.concat(path, "/") .. ">") 78 | .equal(expected, tostring(path)) 79 | end 80 | end 81 | end) 82 | 83 | it("can be compared", function () 84 | assert.equal(P { "a","b" }, P { "a", "b" }) 85 | assert.equal(P "a.b.c.d", P "a.b.c.d") 86 | assert.equal(P { "a", "b" }, P "a/b") 87 | assert.equal(P "a/b", P { "a", "b" }) 88 | assert.not_equal(P "a/b", P "c/d") 89 | end) 90 | 91 | it("can be concatenated to other paths", function () 92 | local foo = P "foo" 93 | local bar = P "bar" 94 | local foobar = foo .. bar 95 | assert.equal(P, getmetatable(foobar)) 96 | assert.equal(P "foo/bar", foobar) 97 | local foobarbaz = foo .. P "bar/baz" 98 | assert.equal(P "foo/bar/baz", foobarbaz) 99 | end) 100 | 101 | it("can be concatenated to strings", function () 102 | local foo = P "foo" 103 | local foobar = foo .. "bar" 104 | assert.equal(P, getmetatable(foobar)) 105 | assert.equal(P "foo/bar", foobar) 106 | local foobarbaz = foo .. "bar/baz" 107 | assert.equal(P "foo/bar/baz", foobarbaz) 108 | end) 109 | 110 | it("can be concatenated to tables", function () 111 | local foo = P "foo" 112 | local foobar = foo .. { "bar" } 113 | assert.equal(P, getmetatable(foobar)) 114 | assert.equal(P "foo/bar", foobar) 115 | local foobarbaz = foo .. { "bar", "baz" } 116 | assert.equal(P "foo/bar/baz", foobarbaz) 117 | end) 118 | 119 | it("can be concatenated to stringizable values", function () 120 | local foo = P "foo" 121 | local foo1 = foo .. 1 122 | assert.equal(P, getmetatable(foo1)) 123 | assert.equal(P "foo/1", foo1) 124 | end) 125 | 126 | it("cannot be mutated", function () 127 | assert.has_error(function () 128 | local foo = P "foo" 129 | foo[#foo+1] = "bar" 130 | end) 131 | assert.has_error(function () 132 | local foo = P "foo" 133 | foo.random_attribute = true 134 | end) 135 | end) 136 | 137 | describe(":child()", function () 138 | it("creates new child paths", function () 139 | local foo = P "foo" 140 | local foobar = foo:child "bar" 141 | assert.not_same(foo, foobar) 142 | assert.equal(foobar, P "foo/bar") 143 | assert.equal(foobar, P {"foo", "bar"}) 144 | assert.equal(P, getmetatable(foobar)) 145 | end) 146 | 147 | it("can be chained", function () 148 | local foo = P "foo" 149 | 150 | local foobar = foo:child "bar" 151 | assert.not_same(foo, foobar) 152 | assert.equal(P, getmetatable(foobar)) 153 | assert.equal(P "foo/bar", foobar) 154 | 155 | local foobarbaz = foobar:child "baz" 156 | assert.not_same(foobar, foobarbaz) 157 | assert.equal(P, getmetatable(foobarbaz)) 158 | 159 | assert.equal(P "foo/bar/baz", foobarbaz) 160 | end) 161 | end) 162 | 163 | describe(":parent()", function () 164 | it("creates new parent paths", function () 165 | local foobar = P "foo/bar" 166 | local foo = foobar:parent() 167 | assert.not_same(foobar, foo) 168 | assert.equal(foo, P "foo") 169 | end) 170 | 171 | it("can be chained", function () 172 | local foobarbaz = P "foo/bar/baz" 173 | local foobar = foobarbaz:parent() 174 | assert.not_same(foobarbaz, foobar) 175 | assert.equal(P "foo/bar", foobar) 176 | assert.equal(P, getmetatable(foobar)) 177 | 178 | local foo = foobar:parent() 179 | assert.not_same(foobar, foo) 180 | assert.equal(P "foo", foo) 181 | assert.equal(P, getmetatable(foo)) 182 | end) 183 | end) 184 | 185 | describe(":sibling()", function () 186 | it("creates new sibling paths", function () 187 | local foobar = P "foo/bar" 188 | local foobaz = foobar:sibling "baz" 189 | assert.not_same(foobar, foobaz) 190 | assert.equal(P "foo/baz", foobaz) 191 | end) 192 | 193 | it("can be chained", function () 194 | local foobar = P "foo/bar" 195 | local foobaz = foobar:sibling "baz" 196 | local foofoo = foobar:sibling "foo" 197 | assert.equal(P "foo/baz", foobaz) 198 | assert.equal(P "foo/foo", foofoo) 199 | end) 200 | end) 201 | 202 | describe(".check", function () 203 | it("errors on non-path arguments", function () 204 | for _, value in ipairs { 205 | { "foo", "bar" }, 206 | "string/value", 207 | 12321, 208 | 3.14, 209 | } do 210 | local m = "value <" .. tostring(value) .. "> type: " .. type(value) 211 | assert.message(m).has_error(function () P.check(value) end) 212 | end 213 | end) 214 | 215 | it("accepts and returns path arguments", function () 216 | for _, value in ipairs { P "foo/bar", P { "foo", "bar" } } do 217 | assert.same(value, P.check(value)) 218 | assert.equal("foo/bar", value.string) 219 | end 220 | end) 221 | 222 | it("coerces with convert=true", function () 223 | for _, value in ipairs { "foo/bar", { "foo", "bar" } } do 224 | local path = P.check(value, true) 225 | assert.equal(P, getmetatable(path)) 226 | assert.equal(P "foo/bar", path) 227 | end 228 | end) 229 | end) 230 | end) 231 | 232 | -------------------------------------------------------------------------------- /spec/base_spec.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- spec_base.lua 3 | -- Copyright (C) 2016 Adrian Perez 4 | -- 5 | -- Distributed under terms of the MIT license. 6 | -- 7 | 8 | local base = require "storefront.base" 9 | local iter = require "itertools" 10 | local storetest = require "spec.storetest" 11 | 12 | describe("storefront.base.class", function () 13 | describe("__tostring", function () 14 | it("works in classes", function () 15 | assert.equal("", tostring(base.class)) 16 | end) 17 | it("works in instances", function () 18 | local obj = base.class() 19 | assert.equal("", tostring(obj)) 20 | end) 21 | it("works in derived classes", function () 22 | local derived = base.class + "derived" 23 | assert.equal("", tostring(derived)) 24 | end) 25 | it("works in derived class instances", function () 26 | local derived = base.class + "derived" 27 | local obj = derived() 28 | assert.equal("", tostring(obj)) 29 | end) 30 | end) 31 | 32 | describe("__init", function () 33 | it("sets properties by default", function () 34 | local obj = base.class { name = "Peter", surname = "Parker" } 35 | assert.equal("Peter", obj.name) 36 | assert.equal("Parker", obj.surname) 37 | end) 38 | it("can be overriden", function () 39 | local derived = base.class + "derived" 40 | stub(derived, "__init") 41 | local obj = derived("__init argument") 42 | assert.stub(derived.__init).called_with(obj, "__init argument") 43 | end) 44 | end) 45 | end) 46 | 47 | describe("storefront.base.store", function () 48 | it("has basic store methods", function () 49 | storetest.test_store_methods(base.store, true) 50 | end) 51 | 52 | it("can be subclassed", function () 53 | local derived_class = base.store + "derived" 54 | assert.is_table(derived_class) 55 | assert.not_same(derived_class, base.store) 56 | local instance = derived_class() 57 | assert.is_table(instance) 58 | storetest.test_store_methods(instance, true) 59 | assert.not_equal(derived_class, instance) 60 | end) 61 | 62 | describe(":get", function () 63 | it("raises an error", function () 64 | local store = base.store() 65 | assert.has_error(function () 66 | store:get("foo.bar") 67 | end) 68 | end) 69 | end) 70 | 71 | describe(":set", function () 72 | it("raises an error", function () 73 | local store = base.store() 74 | assert.has_error(function () 75 | store:set("foo.bar", "baz") 76 | end) 77 | end) 78 | end) 79 | 80 | describe(":del", function () 81 | it("raises an error", function () 82 | local store = base.store() 83 | assert.has_error(function () 84 | store:del("foo.bar") 85 | end) 86 | end) 87 | end) 88 | 89 | describe(":has", function () 90 | it("raises an error", function () 91 | local store = base.store() 92 | assert.has_error(function () 93 | store:has("foo.bar") 94 | end) 95 | end) 96 | end) 97 | 98 | describe(":query", function () 99 | it("raises an error", function () 100 | local store = base.store() 101 | assert.has_error(function () 102 | store:query("foo.*") 103 | end) 104 | end) 105 | end) 106 | 107 | describe(":transact", function () 108 | it("invokes the callback", function () 109 | local store = base.store() 110 | local s = spy.new(function () end) 111 | store:transact(s) 112 | assert.spy(s).called_with(store, match._) 113 | end) 114 | it("passes arguments to the callback", function () 115 | local store = base.store() 116 | local s = spy.new(function () end) 117 | store:transact(s, "Peter", "Parker") 118 | assert.spy(s).called_with(store, match._, "Peter", "Parker") 119 | end) 120 | it("wraps errors", function () 121 | local store = base.store() 122 | assert.has_error(function () 123 | store:transact(function () error("duh!") end) 124 | end) 125 | end) 126 | it("passes the transaction token", function () 127 | local derived = base.store + "derived" 128 | function derived:begin_transaction() 129 | return "transaction token" 130 | end 131 | local store = derived() 132 | local s = spy.new(function () end) 133 | store:transact(s) 134 | assert.spy(s).called_with(store, "transaction token") 135 | end) 136 | it("passes the transaction token to :end_transaction", function () 137 | local derived = base.store + "derived" 138 | function derived:begin_transaction() 139 | return "transaction token" 140 | end 141 | local store = derived() 142 | local s = spy.on(store, "end_transaction") 143 | store:transact(function () end) 144 | assert.spy(s).called_with(store, "transaction token") 145 | end) 146 | end) 147 | end) 148 | 149 | describe("storefront.base.query_iterable", function () 150 | 151 | local items = { 152 | "foo", 153 | "bar", 154 | "foo/foo", 155 | "foo/bar", 156 | "bar/foo", 157 | "bar/bar", 158 | "foo/bar/baz", 159 | "foo/baz/bar", 160 | } 161 | 162 | it("returns all items with '**'", function () 163 | local result = {} 164 | local count = 0 165 | for r in base.query_iterable(iter.each(items), "**") do 166 | result[r] = true 167 | count = count + 1 168 | end 169 | for _, item in ipairs(items) do 170 | assert.truthy(result[item]) 171 | end 172 | assert.equal(#items, count) 173 | end) 174 | 175 | it("does long-match prefix filtering", function () 176 | local resultset = { 177 | ["foo/foo"] = true, 178 | ["foo/bar"] = true, 179 | ["foo/bar/baz"] = true, 180 | ["foo/baz/bar"] = true, 181 | } 182 | local count = 0 183 | for item in base.query_iterable(iter.each(items), "foo/**") do 184 | assert.truthy(resultset[item]) 185 | count = count + 1 186 | end 187 | assert.equal(4, count) 188 | end) 189 | 190 | it("does short-match prefix filtering", function () 191 | local resultset = { ["foo/foo"] = true, ["foo/bar"] = true } 192 | local count = 0 193 | for item in base.query_iterable(iter.each(items), "foo/*") do 194 | assert.truthy(resultset[item]) 195 | count = count + 1 196 | end 197 | assert.equal(2, count) 198 | end) 199 | 200 | it("does long-match suffix filtering", function () 201 | local resultset = { 202 | ["foo/bar"] = true, 203 | ["bar/bar"] = true, 204 | ["foo/baz/bar"] = true, 205 | } 206 | local count = 0 207 | for item in base.query_iterable(iter.each(items), "**/bar") do 208 | assert.truthy(resultset[item]) 209 | count = count + 1 210 | end 211 | assert.equal(3, count) 212 | end) 213 | 214 | it("does short-match suffix filtering", function () 215 | local resultset = { ["foo/bar"] = true, ["bar/bar"] = true } 216 | local count = 0 217 | for item in base.query_iterable(iter.each(items), "*/bar") do 218 | assert.truthy(resultset[item]) 219 | count = count + 1 220 | end 221 | assert.equal(2, count) 222 | end) 223 | 224 | it("can paginate results", function () 225 | local s = iter.collect(iter.sorted(iter.each(items))) 226 | local r = iter.collect(base.query_iterable(iter.each(items), "**", 2)) 227 | assert.equal(2, #r) 228 | assert.equal(s[1], r[1]) 229 | assert.equal(s[2], r[2]) 230 | r = iter.collect(base.query_iterable(iter.each(items), "**", 2, 3)) 231 | assert.equal(2, #r) 232 | assert.equal(s[3], r[1]) 233 | assert.equal(s[4], r[2]) 234 | -- Now get the rest of items 235 | r = iter.collect(base.query_iterable(iter.each(items), "**", nil, 5)) 236 | assert.equal(#items - 4, #r) 237 | end) 238 | end) 239 | --------------------------------------------------------------------------------