diff options
author | Lars Wirzenius <liw@liw.fi> | 2018-10-27 12:58:00 +0300 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2018-10-28 10:10:47 +0200 |
commit | 8f5c7b0fc2b273e55b2116ddece21ac2c19fa863 (patch) | |
tree | 7f12c091b7ec63b2953695cee3f9be3685d4373d /muck | |
parent | cc2d1b21e67643e237d968793d31b7b9437a1640 (diff) | |
download | muck-poc-8f5c7b0fc2b273e55b2116ddece21ac2c19fa863.tar.gz |
Add: HTTP API, with scenario tests
Diffstat (limited to 'muck')
-rw-r--r-- | muck/__init__.py | 1 | ||||
-rw-r--r-- | muck/idgen.py | 22 | ||||
-rw-r--r-- | muck/idgen_tests.py | 27 | ||||
-rw-r--r-- | muck/mem.py | 57 | ||||
-rw-r--r-- | muck/mem_tests.py | 226 |
5 files changed, 248 insertions, 85 deletions
diff --git a/muck/__init__.py b/muck/__init__.py index b286cf0..d78e998 100644 --- a/muck/__init__.py +++ b/muck/__init__.py @@ -14,6 +14,7 @@ from .exc import Error +from .idgen import IdGenerator from .change import ( Change, CreateChange, diff --git a/muck/idgen.py b/muck/idgen.py new file mode 100644 index 0000000..a884b8d --- /dev/null +++ b/muck/idgen.py @@ -0,0 +1,22 @@ +# Copyright (C) 2018 Lars Wirzenius +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import uuid + + +class IdGenerator: + + def new_id(self): + return str(uuid.uuid4()) diff --git a/muck/idgen_tests.py b/muck/idgen_tests.py new file mode 100644 index 0000000..ecab4dd --- /dev/null +++ b/muck/idgen_tests.py @@ -0,0 +1,27 @@ +# Copyright (C) 2018 Lars Wirzenius +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import unittest + +import muck + + +class IdGeneratorTests(unittest.TestCase): + + def test_returns_different_value_each_time(self): + gen = muck.IdGenerator() + id1 = gen.new_id() + id2 = gen.new_id() + self.assertNotEqual(id1, id2) diff --git a/muck/mem.py b/muck/mem.py index 437085d..3100a62 100644 --- a/muck/mem.py +++ b/muck/mem.py @@ -13,6 +13,8 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. +import copy + import muck @@ -27,6 +29,61 @@ class MemoryStore: def as_dict(self): return self._dict + def __contains__(self, key): + return key in self._dict + + def __getitem__(self, key): + meta, res = self._dict[key] + meta = copy.deepcopy(meta) + res = copy.deepcopy(res) + return meta, res + + def search(self, conds): + hits = [] + for rid in self._dict: + meta, res = self._dict[rid] + if any(self._matches(meta, res, cond) for cond in conds): + hits.append(rid) + return hits + + def _matches(self, meta, res, cond): + if cond['where'] not in ('meta', 'data'): # pragma: no cover + return False + + if cond['where'] == 'meta': + thing = meta + else: + thing = res + + field = cond['field'] + pattern = cond['pattern'] + + if field not in thing: # pragma: no cover + return False + value = thing[field] + + funcs = { + '==': self._equal, + '>=': self._ge, + '<=': self._le, + } + if cond['op'] not in funcs: # pragma: no cover + return False + is_match = funcs[cond['op']] + + if isinstance(value, list): + return any(is_match(pattern, item) for item in value) + return is_match(pattern, value) + + def _equal(self, expected, actual): + return expected.lower() == actual.lower() + + def _ge(self, expected, actual): + return expected.lower() == actual.lower() + + def _le(self, expected, actual): + return expected.lower() == actual.lower() + def change(self, chg): funcs = { 'create': self._create, diff --git a/muck/mem_tests.py b/muck/mem_tests.py index e858a33..6b7a069 100644 --- a/muck/mem_tests.py +++ b/muck/mem_tests.py @@ -20,119 +20,175 @@ import muck class MemoryStoreTests(unittest.TestCase): - def test_is_initially_empty(self): - ms = muck.MemoryStore() - self.assertEqual(len(ms), 0) - self.assertEqual(ms.as_dict(), {}) - - def test_creates_resource(self): - meta = { + def setUp(self): + self.ms = muck.MemoryStore() + self.meta1 = { 'id': 'id-1', 'rev': 'rev-1', } - - res = { + self.res1 = { 'foo': 'bar', } - chg = muck.CreateChange(meta=meta, res=res) - ms = muck.MemoryStore() - ms.change(chg) - self.assertEqual(len(ms), 1) + def test_is_initially_empty(self): + self.assertEqual(len(self.ms), 0) + self.assertEqual(self.ms.as_dict(), {}) + self.assertFalse('foo' in self.ms) + + def test_creates_resource(self): + chg = muck.CreateChange(meta=self.meta1, res=self.res1) + self.ms.change(chg) + + rid = self.meta1['id'] + self.assertEqual(len(self.ms), 1) + self.assertTrue(rid in self.ms) + self.assertEqual(self.ms[rid], (self.meta1, self.res1)) self.assertEqual( - ms.as_dict(), + self.ms.as_dict(), { - 'id-1': (meta, res), + rid: (self.meta1, self.res1), }) def test_wont_create_resource_with_conflicting_id(self): - meta = { - 'id': 'id-1', - 'rev': 'rev-1', - } - - res = { - 'foo': 'bar', - } - - chg = muck.CreateChange(meta=meta, res=res) - ms = muck.MemoryStore() - ms.change(chg) + chg = muck.CreateChange(meta=self.meta1, res=self.res1) + self.ms.change(chg) with self.assertRaises(muck.Error): - ms.change(chg) + self.ms.change(chg) def test_updates_resource(self): - meta_1 = { - 'id': 'id-1', - 'rev': 'rev-1', - } + meta2 = dict(self.meta1) + meta2['rev'] = 'rev-2' - res_v1 = { - 'foo': 'bar', - } - - meta_2 = dict(meta_1) - meta_2['rev'] = 'rev-2' + res2 = dict(self.res1) + res2['foo'] = 'yo' - res_v2 = dict(res_v1) - res_v2['foo'] = 'yo' + create = muck.CreateChange(meta=self.meta1, res=self.res1) + update = muck.UpdateChange(meta=meta2, res=res2) - create = muck.CreateChange(meta=meta_1, res=res_v1) - update = muck.UpdateChange(meta=meta_2, res=res_v2) + rid = self.meta1['id'] - ms = muck.MemoryStore() - ms.change(create) - ms.change(update) - self.assertEqual(len(ms), 1) + self.ms.change(create) + self.ms.change(update) + self.assertEqual(len(self.ms), 1) self.assertEqual( - ms.as_dict(), + self.ms.as_dict(), { - 'id-1': (meta_2, res_v2), + rid: (meta2, res2), }) def test_refuses_to_update_resource_that_didnt_exist(self): - meta = { - 'id': 'id-1', - 'rev': 'rev-1', - } - - res = { - 'foo': 'bar', - } - - update = muck.UpdateChange(meta=meta, res=res) - - ms = muck.MemoryStore() + update = muck.UpdateChange(meta=self.meta1, res=self.res1) with self.assertRaises(muck.Error): - ms.change(update) + self.ms.change(update) def test_deletes_resource(self): - meta = { - 'id': 'id-1', - 'rev': 'rev-1', - } - - res = { - 'foo': 'bar', - } - - create = muck.CreateChange(meta=meta, res=res) - delete = muck.DeleteChange(meta=meta) + create = muck.CreateChange(meta=self.meta1, res=self.res1) + delete = muck.DeleteChange(meta=self.meta1) - ms = muck.MemoryStore() - ms.change(create) - ms.change(delete) - self.assertEqual(len(ms), 0) - self.assertEqual(ms.as_dict(), {}) + self.ms.change(create) + self.ms.change(delete) + self.assertEqual(len(self.ms), 0) + self.assertEqual(self.ms.as_dict(), {}) def test_refuses_to_delete_resource_that_doesnt_exist(self): - meta = { - 'id': 'id-1', - 'rev': 'rev-1', - } + delete = muck.DeleteChange(meta=self.meta1) + with self.assertRaises(muck.Error): + self.ms.change(delete) - delete = muck.DeleteChange(meta=meta) + def test_finds_nothing_for_impossible_condition(self): + chg = muck.CreateChange(meta=self.meta1, res=self.res1) + self.ms.change(chg) - ms = muck.MemoryStore() - with self.assertRaises(muck.Error): - ms.change(delete) + cond = [ + { + 'where': 'meta', + 'field': 'id', + 'pattern': 'does-not-exist', + 'op': '==', + }, + ] + hits = self.ms.search(cond) + self.assertEqual(hits, []) + + def test_finds_matching_resources_in_meta(self): + chg = muck.CreateChange(meta=self.meta1, res=self.res1) + self.ms.change(chg) + + rid = self.meta1['id'] + cond = [ + { + 'where': 'meta', + 'field': 'id', + 'pattern': rid, + 'op': '==', + }, + ] + hits = self.ms.search(cond) + self.assertEqual(hits, [rid]) + + def test_finds_matching_resources_in_data(self): + chg = muck.CreateChange(meta=self.meta1, res=self.res1) + self.ms.change(chg) + + rid = self.meta1['id'] + cond = [ + { + 'where': 'data', + 'field': 'foo', + 'pattern': self.res1['foo'], + 'op': '==', + }, + ] + hits = self.ms.search(cond) + self.assertEqual(hits, [rid]) + + def test_finds_matches_in_list(self): + self.res1['foo'] = ['bar', 'yo'] + chg = muck.CreateChange(meta=self.meta1, res=self.res1) + self.ms.change(chg) + + rid = self.meta1['id'] + cond = [ + { + 'where': 'data', + 'field': 'foo', + 'pattern': 'yo', + 'op': '==', + }, + ] + hits = self.ms.search(cond) + self.assertEqual(hits, [rid]) + + def test_finds_ge(self): + self.res1['foo'] = ['bar', 'yo'] + chg = muck.CreateChange(meta=self.meta1, res=self.res1) + self.ms.change(chg) + + rid = self.meta1['id'] + cond = [ + { + 'where': 'data', + 'field': 'foo', + 'pattern': 'yo', + 'op': '>=', + }, + ] + hits = self.ms.search(cond) + self.assertEqual(hits, [rid]) + + def test_finds_le(self): + self.res1['foo'] = ['bar', 'yo'] + chg = muck.CreateChange(meta=self.meta1, res=self.res1) + self.ms.change(chg) + + rid = self.meta1['id'] + cond = [ + { + 'where': 'data', + 'field': 'foo', + 'pattern': 'bar', + 'op': '<=', + }, + ] + hits = self.ms.search(cond) + self.assertEqual(hits, [rid]) |