store 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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)