hold 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ module Hold
2
+ # Interface extending Cell which offers some array-specific persistence
3
+ # methods for use only with Arrays.
4
+ # Default implementations are in terms of get, but it's expected that you'd
5
+ # override with more efficient implementations.
6
+ module ArrayCell
7
+ include Cell
8
+
9
+ def get_slice(start, length)
10
+ value = get() and value[start, length]
11
+ end
12
+
13
+ def get_length
14
+ value = get() and value.length
15
+ end
16
+
17
+ # returns an instance of ThinModels::LazyArray which lazily computes slices
18
+ # and length based on the get_length and get_slice methods you define.
19
+ def get_lazy_array
20
+ LazyArray.new(self)
21
+ end
22
+
23
+ def can_get_class?(klass); klass == Array; end
24
+ def can_set_class?(klass); klass <= Array; end
25
+
26
+ # Can override to indicate if you only support getting/setting arrays with
27
+ # items of a particular class or classes:
28
+ def can_get_item_class?(klass); true; end
29
+ def can_set_item_class?(klass); true; end
30
+
31
+ class LazyArray < ThinModels::LazyArray::Memoized
32
+ def initialize(array_cell)
33
+ @array_cell = array_cell
34
+ end
35
+
36
+ def _each(&b)
37
+ @array_cell.get.each(&b)
38
+ end
39
+
40
+ def slice_from_start_and_length(start, length)
41
+ @array_cell.get_slice(start, length)
42
+ end
43
+
44
+ def _length
45
+ @array_cell.get_length
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,61 @@
1
+ module Hold
2
+ # The most fundamental persistence interface. Just offers a storage slot
3
+ # which stores a single instance, supporting get/set
4
+ module Cell
5
+ def value
6
+ raise UnsupportedOperation
7
+ end
8
+
9
+ def value=(value)
10
+ raise UnsupportedOperation
11
+ end
12
+
13
+ # Cells may optionally be 'emptyable?', that is, admit a special state of
14
+ # 'empty' which is different to the state of storing an instance.
15
+ #
16
+ # empty and nil are distinct states.
17
+ #
18
+ # empty: undefined / uninitialized / unknown / not persisted / key not
19
+ # present in hash / missing nil: null / known to be nil / persisted
20
+ # explicitly as being nil / key present in hash with value of nil
21
+ #
22
+ # Annoying as this may seem this is useful in a bunch of contexts with the
23
+ # data models we're constrained to be using. Eg "row exists but value of
24
+ # column is NULL" vs "row doesn't exist" in SQL, or "property missing" vs
25
+ # "property present and equal to null" for JSON objects
26
+
27
+ def empty?
28
+ false
29
+ end
30
+
31
+ def clear
32
+ raise UnsupportedOperation
33
+ end
34
+
35
+ def set_if_empty(value)
36
+ raise EmptyConflict unless empty?
37
+ set(value)
38
+ end
39
+
40
+ def set_unless_empty(value)
41
+ raise EmptyConflict if empty?
42
+ set(value)
43
+ end
44
+
45
+ def get_unless_empty
46
+ raise EmptyConflict if empty?
47
+ get
48
+ end
49
+ alias :get! :get_unless_empty
50
+
51
+ # Can override to indicate if you only support getting/setting a particular
52
+ # class or classes:
53
+ def can_get_class?(_class)
54
+ true
55
+ end
56
+
57
+ def can_set_class?(_class)
58
+ true
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,63 @@
1
+ module Hold
2
+ # Persists values in a key/value store
3
+ module HashRepository
4
+ def set_with_key(key, value)
5
+ raise UnsupportedOperation
6
+ end
7
+
8
+ def get_with_key(key)
9
+ raise UnsupportedOperation
10
+ end
11
+
12
+ # Gets multiple entities at a time by a list of keys.
13
+ # May override with an efficient multi-get implementation.
14
+ def get_many_with_keys(keys)
15
+ keys.map {|key| get_with_key(key)}
16
+ end
17
+
18
+ def clear_key(key)
19
+ raise UnsupportedOperation
20
+ end
21
+
22
+ def has_key?(key)
23
+ raise UnsupportedOperation
24
+ end
25
+ alias_method :key?, :has_key?
26
+
27
+ def key_cell(key)
28
+ KeyCell.new(self, key)
29
+ end
30
+
31
+ # Can override to indicate if you only support getting/setting values of a
32
+ # particular class or classes:
33
+ def can_get_class?(klass); true; end
34
+ def can_set_class?(klass); true; end
35
+
36
+ class KeyCell
37
+ include Cell
38
+
39
+ def initialize(hash_repository, key)
40
+ @hash_repository, @key = hash_repository, key
41
+ end
42
+
43
+ def get
44
+ @hash_repository.get_with_key(@key)
45
+ end
46
+
47
+ def set(value)
48
+ @hash_repository.set_with_key(@key, value)
49
+ end
50
+
51
+ def clear
52
+ @hash_repository.clear_key(@key)
53
+ end
54
+
55
+ def empty?
56
+ @hash_repository.has_key?(@key)
57
+ end
58
+
59
+ def can_get_class?(klass); @hash_repository.can_get_class?(klass); end
60
+ def can_set_class?(klass); @hash_repository.can_set_class?(klass); end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,118 @@
1
+ module Hold
2
+ # A special kind of SetRepository which stores Objects whose identities are
3
+ # determined by an identity property, and supports indexed lookup by their
4
+ # id.
5
+ #
6
+ # May allocate the IDs itself, or not.
7
+ #
8
+ # Exposes a somewhat more familiar CRUD-style persistence interface as a
9
+ # result.
10
+ #
11
+ # Comes with default implementations for most of the extra interface
12
+ module IdentitySetRepository
13
+ include SetRepository
14
+
15
+ # Either the repository allocates IDs, and you don't (in which case any
16
+ # entity with an ID may be assumed to be already persisted in the repo), or
17
+ # the repository doesn't allocate IDs (in which case you must always supply
18
+ # one when persisting a new object).
19
+ #
20
+ # If you allocates_ids?, you should deal with an object without an identity
21
+ # as an argument to store and store_new, and you should set the id property
22
+ # on it before returning it.
23
+ #
24
+ # If you don't, you may raise MissingIdentity if passed an object without
25
+ # one.
26
+ def allocates_ids?
27
+ false
28
+ end
29
+
30
+ # Looks up a persisted object by the value of its identity property
31
+ def get_by_id(id)
32
+ raise UnsupportedOperation
33
+ end
34
+
35
+ # deletes the object with the given identity where it exists in the repo
36
+ def delete_id(id)
37
+ delete(get_by_id(id))
38
+ end
39
+
40
+ # Loads a fresh instance of the given object by its id
41
+ # Returns nil where the object is no longer present in the repository
42
+ def reload(object)
43
+ id = object.id or raise MissingIdentity
44
+ get_by_id(id)
45
+ end
46
+
47
+ # Like reload, but updates the given instance in-place with the updated
48
+ # data.
49
+ # Returns nil where the object is no longer present in the repository
50
+ def load(object)
51
+ raise UnsupportedOperation unless object.respond_to?(:merge!)
52
+ updated = reload(object) or return
53
+ object.merge!(updated)
54
+ object
55
+ end
56
+
57
+ # Applies an in-place update to the object, where it exists in the
58
+ # repository
59
+ def update(entity, update_entity)
60
+ raise UnsupportedOperation unless entity.respond_to?(:merge!)
61
+ load(entity) or return
62
+ entity.merge!(update_entity)
63
+ store(entity)
64
+ end
65
+
66
+ # Applies an in-place update to the object with the given identity, where
67
+ # it exists in the repository
68
+ def update_by_id(id, update_entity)
69
+ entity = get_by_id(id) or return
70
+ raise UnsupportedOperation unless entity.respond_to?(:merge!)
71
+ entity.merge!(update_entity)
72
+ store(entity)
73
+ end
74
+
75
+ def contains_id?(id)
76
+ !!get_by_id(id)
77
+ end
78
+
79
+
80
+ def get_many_by_ids(ids)
81
+ ids.map {|id| get_by_id(id)}
82
+ end
83
+
84
+ def id_cell(id)
85
+ IdCell.new(self, id)
86
+ end
87
+
88
+ def cell(object)
89
+ id = object.id or raise MissingIdentity
90
+ id_cell(id)
91
+ end
92
+
93
+ class IdCell
94
+ include ObjectCell
95
+
96
+ def initialize(id_set_repo, id)
97
+ @id_set_repo = id_set_repo
98
+ @id = id
99
+ end
100
+
101
+ def get
102
+ @id_set_repo.get_by_id(@id)
103
+ end
104
+
105
+ def set(value)
106
+ @id_set_repo.update_by_id(@id, value)
107
+ end
108
+
109
+ def empty?
110
+ !@id_set_repo.contains?(@id)
111
+ end
112
+
113
+ def clear
114
+ @id_set_repo.delete_id(@id)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,103 @@
1
+ module Hold
2
+ # Interface extending Cell which offers some object-property-specific
3
+ # persistence methods for use only with Structs/Objects.
4
+ # Default implementations are in terms of get and set, but it's expected that
5
+ # you'd override with more efficient implementations.
6
+ module ObjectCell
7
+ include Cell
8
+
9
+ # default implementation gets the entire object in order to get the
10
+ # property in question. you might want to override with something more
11
+ # efficient
12
+ def get_property(property_name)
13
+ value = get() and value[property_name]
14
+ end
15
+
16
+ # default implementation gets the entire object and replaces it with a
17
+ # version with the property in question changed.
18
+ # you might want to override with something more efficient.
19
+ def set_property(property_name, value)
20
+ object = get()
21
+ object[property_name] = value
22
+ set(object)
23
+ end
24
+
25
+ def clear_property(property_name)
26
+ value = get()
27
+ value.delete(property_name)
28
+ set(value)
29
+ end
30
+
31
+ def has_property?(property_name)
32
+ !get_property(property_name).nil?
33
+ end
34
+
35
+ def get_properties(*properties)
36
+ properties.map {|p| get_property(p)}
37
+ end
38
+
39
+ # May return a Cell which allows get / set / potentially other operations
40
+ # on a particular property of this object in the context of its parent
41
+ # object.
42
+ #
43
+ # Be careful about the semantics if exposing property cells which allow
44
+ # partial write operations (like set_property) on the property value in the
45
+ # context of the parent object. If you do this it should only update the
46
+ # property value in that context, not in all contexts.
47
+ #
48
+ # By analogy to normal ruby hashes, it should mean this:
49
+ # a[:foo] = a[:foo].merge(:bar => 3)
50
+ # rather than this:
51
+ # a[:foo][:bar] = 3
52
+ # which would have an effect visible to any other object holding a
53
+ # reference to a[:foo].
54
+ #
55
+ # If you want the latter, you probably want to be updating a[:foo] in some
56
+ # hold cell which is canonical for the identity of that object.
57
+ #
58
+ # If you don't want the former, don't return a PropertyCell which allows
59
+ # partial updates. For simplicity's sake this is the stance taken by the
60
+ # default PropertyCell implementation.
61
+ def property_cell(property_name)
62
+ PropertyCell.new(self, property_name)
63
+ end
64
+
65
+ # An implementation of the basic Cell interface designed to wrap a property
66
+ # of an ObjectCell as a Cell itself.
67
+ class PropertyCell
68
+ include Cell
69
+
70
+ def initialize(object_cell, property_name)
71
+ @object_cell = object_cell
72
+ @property_name = property_name
73
+ end
74
+
75
+ def get
76
+ @object_cell.get_property(@property_name)
77
+ end
78
+
79
+ def set(value)
80
+ @object_cell.set_property(@property_name, value)
81
+ end
82
+
83
+ def empty?
84
+ !@object_cell.has_property?(@property_name)
85
+ end
86
+
87
+ def clear
88
+ @object_cell.clear_property(@property_name)
89
+ end
90
+ end
91
+
92
+ # These are here for you to use if you want to use them for Array
93
+ # properties gotten via ObjectCells, although the default implementation of
94
+ # property_cell doesn't do this
95
+ class ArrayPropertyCell < PropertyCell
96
+ include ArrayCell
97
+ end
98
+
99
+ class ObjectPropertyCell < PropertyCell
100
+ include ObjectCell
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,37 @@
1
+ module Hold
2
+ module SetRepository
3
+ # Store the object in the persisted set. If the object is already in the
4
+ # set, it may stay there untouched (in the case where the object's identity
5
+ # is based on its entire contents), or get replaced by the newer version
6
+ # (where the object's identity is only based on, say, some identity
7
+ # property), but will never be duplicated (since this is a set)
8
+ def store(object)
9
+ raise UnsupportedOperation
10
+ end
11
+
12
+ # like store, but should raise IdentityConflict if the object (or one equal
13
+ # to it) already exists in the set
14
+ def store_new(object)
15
+ raise IdentityConflict if contains?(object)
16
+ store(object)
17
+ end
18
+
19
+ # Removes the object with this identity from the persisted set
20
+ def delete(object)
21
+ raise UnsupportedOperation
22
+ end
23
+
24
+ # Is this object in the persisted set?
25
+ def contains?(object)
26
+ raise UnsupportedOperation
27
+ end
28
+
29
+ # Returns an array of all persisted items in the set
30
+ def get_all
31
+ raise UnsupportedOperation
32
+ end
33
+
34
+ def can_get_class?(klass); true; end
35
+ def can_set_class?(klass); true; end
36
+ end
37
+ end
data/lib/hold/sequel.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'hold/interfaces'
2
+ require 'hold/error'
2
3
  require 'hold/sequel/identity_set_repository'
3
4
  require 'hold/sequel/polymorphic_repository'
4
5
  require 'hold/sequel/with_polymorphic_type_column'
@@ -22,20 +23,20 @@ require 'hold/sequel/property_mapper/custom_query_single_value'
22
23
  require 'sequel'
23
24
 
24
25
  module Hold
25
- # Module containing implementations of hold interfaces which persist in a relational database, using the Sequel
26
- # library, via some configurable mapping.
26
+ # Module containing implementations of hold interfaces which persist in a
27
+ # relational database, using the Sequel library, via some configurable
28
+ # mapping.
27
29
  module Sequel
28
-
29
30
  def self.translate_exceptions
30
- begin
31
- yield
32
- rescue ::Sequel::DatabaseError => e
33
- case e.message
34
- when /duplicate|unique/i then raise Hold::IdentityConflict.new(e)
35
- else raise Hold::Error.new("#{e.class}: #{e.message}")
36
- end
31
+ yield
32
+ rescue ::Sequel::DatabaseError => error
33
+ case error.message
34
+ when /duplicate|unique/i
35
+ raise Hold::IdentityConflict
36
+ else
37
+ error.extend(Hold::Error)
38
+ raise
37
39
  end
38
40
  end
39
-
40
41
  end
41
42
  end