store 0.0.1 → 0.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2fb59e3196b45ee40cb5849579674cbfa39f774b
4
- data.tar.gz: 7ca7cd693fd58a40ced7e5b8aa2b47fa13ba0aef
3
+ metadata.gz: 05c439b9355bde8a092b4bfea3d5d95630177d3b
4
+ data.tar.gz: 459439d12096f896eea821d5e57a6a57e7b28217
5
5
  SHA512:
6
- metadata.gz: dacc4c3b8bda6ec9d4cb7cc7276ff99f760c25e564a07d3ee1a0b82a032ff7d971ea199a8cb9264c8aefc4afbed8c70a36e0db17d621d13cd65c9e56ed17baa2
7
- data.tar.gz: 7b33a857f9250a3391a2873872448ad2cc112e562b57ad44d317717910972132b3ecfb8b9b4b670437e445b9146cc004461726519161255e8f98d2f9e43821c3
6
+ metadata.gz: 575737d172c70e5338cffe8c85d3b6361a14b775e06ae061f285746832a358f9e9c871c80b80ed1708fdfcd49cc1bca31e431bd5af8e6fe15b3c9f1ab168e9f3
7
+ data.tar.gz: c5e233707425ffd8c367c7f4beea0c12748820c893fe1bfb08597f182df19bad000efdd376fed4d47c87eaeaa69f569012ed974080d7f062a80c847e5849a9a8
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ doc
2
+ pkg
3
+ coverage
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Store
2
2
 
3
- TODO: Write a gem description
3
+ Implementations for using the Repository- and Data Mapper Pattern in ruby
4
4
 
5
5
  ## Installation
6
6
 
@@ -18,7 +18,7 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
- TODO: Write usage instructions here
21
+ See example.rb for capabilities of the current implementation.
22
22
 
23
23
  ## Contributing
24
24
 
data/example.rb ADDED
@@ -0,0 +1,90 @@
1
+ require 'store'
2
+
3
+ class Catalog
4
+ attr_accessor :name, :products
5
+
6
+ def products
7
+ @products ||= []
8
+ end
9
+
10
+ def add_product(product)
11
+ products << product
12
+ end
13
+ end
14
+
15
+ class Product
16
+ attr_accessor :name, :price
17
+ end
18
+
19
+ class CatalogEntityMapper < Struct.new(:store, :catalog)
20
+ def mapped
21
+ {
22
+ 'name' => catalog.name,
23
+ 'products' => store.save(*catalog.products)
24
+ }
25
+ end
26
+ end
27
+
28
+ class ProductEntityMapper < Struct.new(:store, :product)
29
+ def mapped
30
+ {
31
+ 'name' => product.name,
32
+ 'price' => product.price
33
+ }
34
+ end
35
+ end
36
+
37
+ class CatalogDataMapper
38
+ def insert(data)
39
+ reference = :"catalog_#{rand 100}"
40
+ puts "catalog_insert[#{reference.inspect}] => #{data.inspect}"
41
+ reference
42
+ end
43
+
44
+ def update(reference, data)
45
+ puts "catalog_update[#{reference.inspect}] => #{data.inspect}"
46
+ reference
47
+ end
48
+ end
49
+
50
+ class ProductDataMapper
51
+ def insert(data)
52
+ reference = :"product_#{rand 100}"
53
+ puts "product_insert[#{reference.inspect}] => #{data.inspect}"
54
+ reference
55
+ end
56
+
57
+ def update(reference, data)
58
+ puts "product_update[#{reference.inspect}] => #{data.inspect}"
59
+ reference
60
+ end
61
+ end
62
+
63
+
64
+ entities = {
65
+ 'Catalog' => Catalog,
66
+ 'Product' => Product
67
+ }
68
+
69
+ data_mappers = {
70
+ 'Catalog' => CatalogDataMapper.new,
71
+ 'Product' => ProductDataMapper.new
72
+ }
73
+
74
+ entity_mappers = {
75
+ 'Catalog' => CatalogEntityMapper,
76
+ 'Product' => ProductEntityMapper
77
+ }
78
+
79
+ store = Store.new(entities, data_mappers, entity_mappers)
80
+
81
+ catalog = store.build('Catalog', :name => 'Catalog 1')
82
+
83
+ catalog.add_product store.build('Product', :name => 'Pickaxe', :price => 100)
84
+ catalog.add_product store.build('Product', :name => 'Scredriver', :price => 400)
85
+
86
+ store.save(catalog)
87
+
88
+ catalog.products[0].price = 24
89
+
90
+ store.save(catalog)
data/lib/store.rb CHANGED
@@ -1,73 +1,107 @@
1
1
  require 'set'
2
2
 
3
3
  class Store
4
- def initialize(mapping)
5
- @mapping = mapping
4
+ def initialize(entity_classes, data_mapping, entity_mapping = {})
5
+ @entity_classes = entity_classes
6
+ @data_mapping = data_mapping
7
+ @entity_mapping = entity_mapping
6
8
  @existing_entities = Set.new
9
+ @entity_references = Hash.new
7
10
  end
8
11
 
9
- def save(entity)
10
- mapper = mapper_for_entity(entity)
12
+ def build(entity_class_name, attributes = {})
13
+ entity = @entity_classes[entity_class_name].new
11
14
 
12
- if new_entity?(entity)
13
- mapper.insert(entity)
14
- mark_entities_as_existing([entity])
15
- else
16
- mapper.update(entity)
15
+ attributes.each do |attribute, value|
16
+ entity.public_send :"#{attribute}=", value
17
+ end
18
+
19
+ entity
20
+ end
21
+
22
+ def save(*entities)
23
+ entities.map do |entity|
24
+ data_mapper = data_mapper_for_entity(entity)
25
+ raise_unless_data_mapper_found(entity, data_mapper)
26
+
27
+ reference = find_entity_reference(entity)
28
+ if reference.nil?
29
+ reference = data_mapper.insert(map_entity entity)
30
+ save_entity_reference(reference, entity)
31
+ else
32
+ data_mapper.update(reference, map_entity(entity))
33
+ end
17
34
  end
18
35
  end
19
36
 
20
37
  def remove(entity)
21
- mapper = mapper_for_entity(entity)
38
+ data_mapper = data_mapper_for_entity(entity)
39
+
40
+ reference = find_entity_reference(entity)
41
+ data_mapper.delete(reference)
22
42
 
23
- mapper.delete(entity)
24
- mark_entity_as_remove(entity)
43
+ remove_entity_reference entity
25
44
  end
26
45
 
27
46
  def query(query)
28
- mapper = mapper_for_query(query)
47
+ data_mapper = data_mapper_for_query(query)
29
48
 
30
- entities = mapper.select(query) || []
31
- mark_entities_as_existing(entities)
49
+ entities_with_references = data_mapper.select(query) || {}
32
50
 
33
- entities
51
+ entities_with_references.map do |reference, entity|
52
+ save_entity_reference reference, entity
53
+ entity
54
+ end
34
55
  end
35
56
 
36
57
  private
37
- def mapper_for_entity(entity)
58
+ def raise_unless_data_mapper_found(entity, data_mapper)
59
+ raise "No data_mapper for #{entity.inspect} found" unless data_mapper
60
+ end
61
+
62
+ def map_entity(entity)
63
+ entity_mapper = entity_mapper_for_entity(entity)
64
+ entity_mapper.new(self, entity).mapped
65
+ end
66
+
67
+ def entity_mapper_for_entity(entity)
38
68
  type = entity_type(entity)
39
- mapper_for_type type
69
+ entity_mapper_for_type(type)
70
+ end
71
+
72
+ def entity_mapper_for_type(type)
73
+ @entity_mapping[type]
40
74
  end
41
75
 
42
76
  def entity_type(entity)
43
77
  entity_class = entity.class
44
- entity_class.entity_type
78
+ entity_class.name
45
79
  end
46
80
 
47
- def mapper_for_query(query)
48
- type = query.target
49
- mapper_for_type type
81
+ def data_mapper_for_entity(entity)
82
+ type = entity_type(entity)
83
+ data_mapper_for_type type
50
84
  end
51
85
 
52
- def mapper_for_type(type)
53
- @mapping[type]
86
+ def data_mapper_for_query(query)
87
+ type = query.target
88
+ data_mapper_for_type type
54
89
  end
55
90
 
56
- def new_entity?(entity)
57
- !existing_entity?(entity)
91
+ def data_mapper_for_type(type)
92
+ @data_mapping[type]
58
93
  end
59
94
 
60
- def existing_entity?(entity)
61
- @existing_entities.include? entity.object_id
95
+ def find_entity_reference(entity)
96
+ @entity_references[entity.object_id]
62
97
  end
63
98
 
64
- def mark_entities_as_existing(entities)
65
- entities.each do |entity|
66
- @existing_entities << entity.object_id
67
- end
99
+ def save_entity_reference(reference, entity)
100
+ @entity_references[entity.object_id] = reference
68
101
  end
69
102
 
70
- def mark_entity_as_remove(entity)
71
- @existing_entities.delete entity.object_id
103
+ def remove_entity_reference(entity)
104
+ @entity_references.delete entity.object_id
72
105
  end
106
+
73
107
  end
data/lib/store/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Store
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/spec/helper.rb CHANGED
@@ -1,6 +1,12 @@
1
+ if ENV['coverage']
2
+ require 'simplecov'
3
+ SimpleCov.start
4
+ end
5
+
1
6
  require 'minitest/autorun'
2
7
  require 'minitest/spec'
3
8
 
4
9
  require 'ostruct'
5
10
 
6
11
  require 'store'
12
+
data/spec/store_spec.rb CHANGED
@@ -1,107 +1,235 @@
1
1
  require 'helper'
2
2
 
3
3
  describe 'store' do
4
- let(:store) { Store.new(mapping) }
5
- let(:mapping) {
4
+ let(:store) { Store.new({}, data_mapping, entity_mapping) }
5
+ let(:data_mapping) {
6
6
  {
7
- :entity1 => entity1_mapper
7
+ entity_class.name => data_mapper
8
8
  }
9
9
  }
10
- let(:entity1_mapper) { MiniTest::Mock.new }
11
-
12
- let(:entity_class) { Struct.new(:email, :gender) do
13
- def self.entity_type
14
- :entity1
10
+ let(:entity_mapping) {
11
+ {
12
+ entity_class.name => entity_mapper
13
+ }
14
+ }
15
+ let(:data_mapper) { MiniTest::Mock.new }
16
+ let(:entity_mapper) { Struct.new(:store, :entity) do
17
+ def mapped
18
+ {
19
+ 'email' => entity.email,
20
+ 'gender' => entity.gender
21
+ }
15
22
  end
16
23
  end }
17
24
 
18
- let(:entity) { entity_class.new(nil, nil) }
25
+ let(:entity_class) { Struct.new(:email, :gender) }
26
+ let(:entity) { entity_class.new("me@foo.bar", "w") }
27
+ let(:entity_hash) { {'email' => entity.email, 'gender' => entity.gender} }
19
28
 
20
- let(:query) { OpenStruct.new(:target => :entity1) }
29
+ let(:query) { OpenStruct.new(:target => entity_class.name) }
21
30
 
22
31
  it 'delegates save to insert' do
23
- entity1_mapper.expect(:insert, nil, [entity])
32
+ data_mapper.expect(:insert, nil, [entity_hash])
24
33
  store.save(entity)
25
34
 
26
- assert entity1_mapper.verify
35
+ assert data_mapper.verify
27
36
  end
28
37
 
29
38
  it 'delegates second save to update' do
30
- entity1_mapper.expect(:insert, nil, [entity])
39
+ data_mapper.expect(:insert, :ref1, [entity_hash])
31
40
 
32
41
  store.save(entity)
33
42
 
34
- entity1_mapper.expect(:update, nil, [entity])
43
+ data_mapper.expect(:update, nil, [:ref1, entity_hash])
35
44
 
36
45
  store.save(entity)
37
46
 
38
- assert entity1_mapper.verify
47
+ assert data_mapper.verify
48
+ end
49
+
50
+ it 'fails save if data_mapper not found' do
51
+ entity = OpenStruct.new(:test => 42)
52
+ error = assert_raises RuntimeError do
53
+ store.save(entity)
54
+ end
55
+
56
+ assert_match(/no data_mapper .* found/i, error.message)
39
57
  end
40
58
 
41
59
  it 'delegates query to select' do
42
- entity1_mapper.expect(:select, nil, [query])
60
+ data_mapper.expect(:select, nil, [query])
43
61
 
44
62
  store.query(query)
45
63
 
46
- assert entity1_mapper.verify
64
+ assert data_mapper.verify
47
65
  end
48
66
 
49
67
  it 'returns query result' do
50
- entity1_mapper.expect(:select, [:e1, :e2], [query])
68
+ data_mapper.expect(:select, {:ref1 => :e1, :ref2 => :e2}, [query])
51
69
  assert_equal [:e1, :e2], store.query(query)
52
70
  end
53
71
 
54
72
  it 'maps nil query result to empty collection' do
55
- entity1_mapper.expect(:select, nil, [query])
73
+ data_mapper.expect(:select, nil, [query])
56
74
  assert_equal [], store.query(query)
57
75
  end
58
76
 
59
77
  it 'only calls update for queried results' do
60
78
  e1 = entity_class.new('e1', 'e1')
79
+ e1_hash = {'email' => 'e1', 'gender' => 'e1'}
61
80
  e2 = entity_class.new('e2', 'e2')
81
+ e2_hash = {'email' => 'e2', 'gender' => 'e2'}
62
82
  e3 = entity_class.new('e3', 'e3')
83
+ e3_hash = {'email' => 'e3', 'gender' => 'e3'}
63
84
 
64
- entity1_mapper.expect(:select, [e1, e2], [query])
85
+ data_mapper.expect(:select, {:ref1 => e1, :ref2 => e2}, [query])
65
86
  store.query(query)
66
87
 
67
- entity1_mapper.expect(:update, nil, [e1])
88
+ data_mapper.expect(:update, nil, [:ref1, e1_hash])
68
89
  store.save(e1)
69
90
 
70
- entity1_mapper.expect(:update, nil, [e2])
91
+ data_mapper.expect(:update, nil, [:ref2, e2_hash])
71
92
  store.save(e2)
72
93
 
73
- entity1_mapper.expect(:insert, nil, [e3])
94
+ data_mapper.expect(:insert, :ref1, [e3_hash])
74
95
  store.save(e3)
75
96
 
76
- assert entity1_mapper.verify
97
+ assert data_mapper.verify
77
98
  end
78
99
 
79
100
  it 'identifies entity as existent if changed' do
80
101
  entity.gender = 'm'
81
102
 
82
- entity1_mapper.expect(:insert, nil, [entity])
103
+ data_mapper.expect(:insert, :ref1, [entity_hash])
83
104
  store.save(entity)
84
105
 
85
106
  entity.gender = 'w'
86
107
 
87
- entity1_mapper.expect(:update, nil, [entity])
108
+ data_mapper.expect(:update, nil, [:ref1, entity_hash.merge('gender' => 'w')])
88
109
  store.save(entity)
89
110
  end
90
111
 
91
112
  it 'delegates remove to delete' do
92
- entity1_mapper.expect(:delete, nil, [entity])
113
+ data_mapper.expect(:insert, :ref1, [entity_hash])
114
+ store.save(entity)
115
+
116
+ data_mapper.expect(:delete, nil, [:ref1])
93
117
  store.remove(entity)
94
118
  end
95
119
 
96
120
  it 'treats entity as new after delete' do
97
- entity1_mapper.expect(:insert, nil, [entity])
121
+ data_mapper.expect(:insert, :ref22, [entity_hash])
98
122
  store.save(entity)
99
123
 
100
- entity1_mapper.expect(:delete, nil, [entity])
124
+ data_mapper.expect(:delete, nil, [:ref22])
101
125
  store.remove(entity)
102
126
 
103
- entity1_mapper.expect(:insert, nil, [entity])
127
+ data_mapper.expect(:insert, :ref23, [entity_hash])
104
128
  store.save(entity)
105
129
  end
106
130
 
131
+ describe 'building' do
132
+ Project = Struct.new(:name, :due_date)
133
+
134
+ let(:store) { Store.new({'Project' => Project}, {}, {}) }
135
+
136
+ it 'should instanciate an empty object' do
137
+ project = store.build 'Project'
138
+
139
+ assert_nil project.name
140
+ assert_nil project.due_date
141
+ end
142
+
143
+ it 'should prepopulate attributes' do
144
+ due_date = Time.now
145
+ project = store.build 'Project', :name => 'foo', :due_date => due_date
146
+
147
+ assert_equal 'foo', project.name
148
+ assert_equal due_date, project.due_date
149
+ end
150
+ end
151
+
152
+ describe 'nesting' do
153
+ User = Struct.new(:name)
154
+ Group = Struct.new(:name, :users)
155
+
156
+ let(:store) { Store.new({}, data_mapping, entity_mapping) }
157
+ let(:data_mapping) do
158
+ {
159
+ 'User' => user_data_mapper,
160
+ 'Group' => group_data_mapper
161
+ }
162
+ end
163
+ let(:entity_mapping) do
164
+ {
165
+ 'User' => user_entity_mapper,
166
+ 'Group' => group_entity_mapper
167
+ }
168
+ end
169
+ let(:user_data_mapper) { MiniTest::Mock.new }
170
+ let(:group_data_mapper) { MiniTest::Mock.new }
171
+ let(:user_entity_mapper) { Class.new do
172
+ def initialize(store, entity)
173
+ @entity = entity
174
+ end
175
+
176
+ def mapped
177
+ {
178
+ 'name' => @entity.name
179
+ }
180
+ end
181
+ end
182
+ }
183
+ let(:group_entity_mapper) { Class.new do
184
+ def initialize(store, entity)
185
+ @store = store
186
+ @entity = entity
187
+ end
188
+
189
+ def mapped
190
+ {
191
+ 'name' => @entity.name,
192
+ 'users' => @store.save(*@entity.users)
193
+ }
194
+ end
195
+ end
196
+ }
197
+
198
+ it 'should map saves for nested users' do
199
+ users = [
200
+ User.new('u1'),
201
+ User.new('u2')
202
+ ]
203
+ group = Group.new('g1', users)
204
+
205
+ group_data_mapper.expect :insert, nil, [{'name' => 'g1', 'users' => [:ref1, :ref2]}]
206
+ user_data_mapper.expect :insert, :ref1, [{'name' => 'u1'}]
207
+ user_data_mapper.expect :insert, :ref2, [{'name' => 'u2'}]
208
+
209
+ store.save(group)
210
+
211
+ assert group_data_mapper.verify
212
+ assert user_data_mapper.verify
213
+ end
214
+
215
+ it 'should map insert and update separated' do
216
+ users = [
217
+ User.new('u1'),
218
+ User.new('u2')
219
+ ]
220
+ group = Group.new('g1', users)
221
+
222
+ user_data_mapper.expect :insert, :uref1, [{'name' => 'u1'}]
223
+ store.save(users[0])
224
+
225
+ group_data_mapper.expect :insert, nil, [{'name' => 'g1', 'users' => [:ref1, :ref2]}]
226
+ user_data_mapper.expect :update, :ref1, [:uref1, {'name' => 'u1'}]
227
+ user_data_mapper.expect :insert, :ref2, [{'name' => 'u2'}]
228
+
229
+ store.save(group)
230
+
231
+ assert group_data_mapper.verify
232
+ assert user_data_mapper.verify
233
+ end
234
+ end
107
235
  end
data/store.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Jakob Holderbaum"]
10
10
  spec.email = ["jakob@featurefabrik.de"]
11
11
  spec.summary = %q{Repository Pattern and Data-Mapper Pattern}
12
- spec.homepage = ""
12
+ spec.homepage = "https://github.com/holderbaum/store"
13
13
  spec.license = "MIT"
14
14
 
15
15
  spec.files = `git ls-files`.split($/)
@@ -19,4 +19,5 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  spec.add_development_dependency "bundler", "~> 1.3"
21
21
  spec.add_development_dependency "rake"
22
+ spec.add_development_dependency "simplecov"
22
23
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakob Holderbaum
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: simplecov
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  description:
42
56
  email:
43
57
  - jakob@featurefabrik.de
@@ -45,16 +59,17 @@ executables: []
45
59
  extensions: []
46
60
  extra_rdoc_files: []
47
61
  files:
62
+ - .gitignore
48
63
  - Gemfile
49
64
  - README.md
50
65
  - Rakefile
66
+ - example.rb
51
67
  - lib/store.rb
52
68
  - lib/store/version.rb
53
- - spec.rb
54
69
  - spec/helper.rb
55
70
  - spec/store_spec.rb
56
71
  - store.gemspec
57
- homepage: ''
72
+ homepage: https://github.com/holderbaum/store
58
73
  licenses:
59
74
  - MIT
60
75
  metadata: {}
data/spec.rb DELETED
@@ -1,59 +0,0 @@
1
- class Store
2
- def catalog
3
- @catalog ||= CatalogStore.new
4
- end
5
-
6
- def product
7
- @product ||= ProductStore.new
8
- end
9
- end
10
-
11
- class CatalogStore
12
- def build(attrs = {})
13
- Catalog.new(attrs)
14
- end
15
-
16
- def save(catalog)
17
- end
18
- end
19
-
20
- class ProductStore
21
- def build(attrs = {})
22
- Product.new(attrs)
23
- end
24
- end
25
-
26
-
27
- class Entity
28
- def initialize(attrs = {})
29
- attrs.each do |attr, value|
30
- self.public_send :"#{attr}=", value
31
- end
32
- end
33
- end
34
-
35
- class Catalog < Entity
36
- attr_accessor :name, :products
37
-
38
- def products
39
- @products ||= []
40
- end
41
-
42
- def add_product(product)
43
- products << product
44
- end
45
- end
46
-
47
- class Product < Entity
48
- attr_accessor :name, :price
49
- end
50
-
51
-
52
- catalog = store.catalog.build(:name => 'Catalog 1')
53
-
54
- catalog.add_product store.product.build(:name => 'Pickaxe', :price => 100)
55
- catalog.add_product store.product.build(:name => 'Scredriver', :price => 400)
56
-
57
- p catalog
58
-
59
- store.save(catalog)