vorpal 0.0.1

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,445 @@
1
+ require 'vorpal/identity_map'
2
+
3
+ module Vorpal
4
+ class AggregateRepository
5
+ # @private
6
+ def initialize(class_configs)
7
+ configure(class_configs)
8
+ end
9
+
10
+ # Saves an aggregate to the DB. Inserts objects that are new to the
11
+ # aggregate, updates existing objects and deletes objects that are no longer
12
+ # present.
13
+ #
14
+ # Objects that are on the boundary of the aggregate (owned: false) will not
15
+ # be inserted, updated, or deleted. However, the relationships to these
16
+ # objects (provided they are stored within the aggregate) will be saved.
17
+ #
18
+ # @param object [Object] Root of the aggregate to be saved.
19
+ # @return [Object] Root of the aggregate.
20
+ def persist(object)
21
+ mapping = {}
22
+ serialize(object, mapping)
23
+ new_objects = get_unsaved_objects(mapping.keys)
24
+ set_primary_keys(object, mapping)
25
+ set_foreign_keys(object, mapping)
26
+ remove_orphans(object, mapping)
27
+ save(object, mapping)
28
+ object
29
+ rescue
30
+ nil_out_object_ids(new_objects)
31
+ raise
32
+ end
33
+
34
+ # Like {#persist} but operates on multiple aggregates. Roots do not need to
35
+ # be of the same type.
36
+ #
37
+ # @param objects [[Object]] array of aggregate roots to be saved.
38
+ # @return [[Object]] array of aggregate roots.
39
+ #
40
+ # TODO: Nil out object ids if one of the objects can't be saved?
41
+ def persist_all(objects)
42
+ objects.map(&method(:persist))
43
+ end
44
+
45
+ # Loads an aggregate from the DB. Will eagerly load all objects in the
46
+ # aggregate and on the boundary (owned: false).
47
+ #
48
+ # @param id [Integer] Primary key value of the root of the aggregate to be
49
+ # loaded.
50
+ # @param domain_class [Class] Type of the root of the aggregate to
51
+ # be loaded.
52
+ # @return [Object] Entity with the given primary key value and type.
53
+ def load(id, domain_class, identity_map=IdentityMap.new)
54
+ db_object = @configs.config_for(domain_class).load_by_id(id)
55
+ hydrate(db_object, identity_map)
56
+ end
57
+
58
+ # Like {#load} but operates on multiple ids.
59
+ #
60
+ # @param ids [[Integer]] Array of primary key values of the roots of the
61
+ # aggregates to be loaded.
62
+ # @param domain_class [Class] Type of the roots of the aggregate to be loaded.
63
+ # @return [[Object]] Entities with the given primary key values and type.
64
+ def load_all(ids, domain_class)
65
+ identity_map = IdentityMap.new
66
+ ids.map { |id| load(id, domain_class, identity_map) }
67
+ end
68
+
69
+ # Removes an aggregate from the DB. Even if the aggregate contains unsaved
70
+ # changes this method will correctly remove everything.
71
+ #
72
+ # @param object [Object] Root of the aggregate to be destroyed.
73
+ # @return [Object] Root that was passed in.
74
+ def destroy(object)
75
+ config = @configs.config_for(object.class)
76
+ db_object = config.find_in_db(object)
77
+ @traversal.accept_for_db(db_object, DestroyVisitor.new())
78
+ object
79
+ end
80
+
81
+ # Like {#destroy} but operates on multiple aggregates. Roots do not need to
82
+ # be of the same type.
83
+ #
84
+ # @param objects [[Object]] Array of roots of the aggregates to be destroyed.
85
+ # @return [[Object]] Roots that were passed in.
86
+ def destroy_all(objects)
87
+ objects.each(&method(:destroy))
88
+ objects
89
+ end
90
+
91
+ private
92
+
93
+ def configure(class_configs)
94
+ @configs = MasterConfig.new(class_configs)
95
+ @traversal = Traversal.new(@configs)
96
+ end
97
+
98
+ def hydrate(db_object, identity_map)
99
+ deserialize(db_object, identity_map)
100
+ set_associations(db_object, identity_map)
101
+ identity_map.get(db_object)
102
+ end
103
+
104
+ def serialize(object, mapping)
105
+ @traversal.accept_for_domain(object, SerializeVisitor.new(mapping))
106
+ end
107
+
108
+ def set_primary_keys(object, mapping)
109
+ @traversal.accept_for_domain(object, IdentityVisitor.new(mapping))
110
+ mapping.rehash # needs to happen because setting the id on an AR::Base model changes its hash value
111
+ end
112
+
113
+ def set_foreign_keys(object, mapping)
114
+ @traversal.accept_for_domain(object, PersistenceAssociationVisitor.new(mapping))
115
+ end
116
+
117
+ def save(object, mapping)
118
+ @traversal.accept_for_domain(object, SaveVisitor.new(mapping))
119
+ end
120
+
121
+ def remove_orphans(object, mapping)
122
+ diff_visitor = AggregateDiffVisitor.new(mapping.values)
123
+ @traversal.accept_for_db(mapping[object], diff_visitor)
124
+
125
+ orphans = diff_visitor.orphans
126
+ orphans.each { |o| @configs.config_for_db(o.class).destroy(o) }
127
+ end
128
+
129
+ def deserialize(db_object, identity_map)
130
+ @traversal.accept_for_db(db_object, DeserializeVisitor.new(identity_map))
131
+ end
132
+
133
+ def set_associations(db_object, identity_map)
134
+ @traversal.accept_for_db(db_object, DomainAssociationVisitor.new(identity_map))
135
+ end
136
+
137
+ def get_unsaved_objects(objects)
138
+ objects.find_all { |object| object.id.nil? }
139
+ end
140
+
141
+ def nil_out_object_ids(objects)
142
+ objects ||= []
143
+ objects.each { |object| object.id = nil }
144
+ end
145
+ end
146
+
147
+ # @private
148
+ class Traversal
149
+ def initialize(configs)
150
+ @configs = configs
151
+ end
152
+
153
+ def accept_for_domain(object, visitor, already_visited=[])
154
+ return if object.nil?
155
+
156
+ config = @configs.config_for(object.class)
157
+ return if config.nil?
158
+
159
+ return if already_visited.include?(object)
160
+ already_visited << object
161
+
162
+ visitor.visit_object(object, config)
163
+
164
+ config.belongs_tos.each do |belongs_to_config|
165
+ child = belongs_to_config.get_child(object)
166
+ accept_for_domain(child, visitor, already_visited) if visitor.continue_traversal?(belongs_to_config)
167
+
168
+ visitor.visit_belongs_to(object, child, belongs_to_config)
169
+ end
170
+
171
+ config.has_ones.each do |has_one_config|
172
+ child = has_one_config.get_child(object)
173
+ accept_for_domain(child, visitor, already_visited) if visitor.continue_traversal?(has_one_config)
174
+
175
+ visitor.visit_has_one(object, child, has_one_config)
176
+ end
177
+
178
+ config.has_manys.each do |has_many_config|
179
+ children = has_many_config.get_children(object)
180
+ children.each do |child|
181
+ accept_for_domain(child, visitor, already_visited) if visitor.continue_traversal?(has_many_config)
182
+ end
183
+ visitor.visit_has_many(object, children, has_many_config)
184
+ end
185
+ end
186
+
187
+ def accept_for_db(db_object, visitor, already_visited=[])
188
+ return if db_object.nil?
189
+
190
+ config = @configs.config_for_db(db_object.class)
191
+ return if config.nil?
192
+
193
+ return if already_visited.include?(db_object)
194
+ already_visited << db_object
195
+
196
+ visitor.visit_object(db_object, config)
197
+
198
+ config.belongs_tos.each do |belongs_to_config|
199
+ child = belongs_to_config.load_child(db_object)
200
+ accept_for_db(child, visitor, already_visited) if visitor.continue_traversal?(belongs_to_config)
201
+
202
+ visitor.visit_belongs_to(db_object, child, belongs_to_config)
203
+ end
204
+
205
+ config.has_ones.each do |has_one_config|
206
+ child = has_one_config.load_child(db_object)
207
+ accept_for_db(child, visitor, already_visited) if visitor.continue_traversal?(has_one_config)
208
+
209
+ visitor.visit_belongs_to(db_object, child, has_one_config)
210
+ end
211
+
212
+ config.has_manys.each do |has_many_config|
213
+ children = has_many_config.load_children(db_object)
214
+ children.each do |child|
215
+ accept_for_db(child, visitor, already_visited) if visitor.continue_traversal?(has_many_config)
216
+ end
217
+ visitor.visit_has_many(db_object, children, has_many_config)
218
+ end
219
+ end
220
+ end
221
+
222
+ # @private
223
+ module AggregateVisitorTemplate
224
+ def visit_object(object, config)
225
+ # override me!
226
+ end
227
+
228
+ def visit_belongs_to(parent, child, belongs_to_config)
229
+ # override me!
230
+ end
231
+
232
+ def visit_has_one(parent, child, has_one_config)
233
+ # override me!
234
+ end
235
+
236
+ def visit_has_many(parent, children, has_many_config)
237
+ # override me!
238
+ end
239
+
240
+ def continue_traversal?(association_config)
241
+ true
242
+ end
243
+ end
244
+
245
+ # @private
246
+ class SerializeVisitor
247
+ include AggregateVisitorTemplate
248
+
249
+ def initialize(mapping)
250
+ @mapping = mapping
251
+ end
252
+
253
+ def visit_object(object, config)
254
+ serialize(object, config)
255
+ end
256
+
257
+ def continue_traversal?(association_config)
258
+ association_config.owned
259
+ end
260
+
261
+ def serialize(object, config)
262
+ db_object = serialize_object(object, config)
263
+ @mapping[object] = db_object
264
+ end
265
+
266
+ def serialize_object(object, config)
267
+ if config.serialization_required?
268
+ attributes = config.serialize(object)
269
+ if object.id.nil?
270
+ config.build_db_object(attributes)
271
+ else
272
+ db_object = config.find_in_db(object)
273
+ config.set_db_object_attributes(db_object, attributes)
274
+ db_object
275
+ end
276
+ else
277
+ object
278
+ end
279
+ end
280
+ end
281
+
282
+ # @private
283
+ class IdentityVisitor
284
+ include AggregateVisitorTemplate
285
+
286
+ def initialize(mapping)
287
+ @mapping = mapping
288
+ end
289
+
290
+ def visit_object(object, config)
291
+ set_primary_key(object, config)
292
+ end
293
+
294
+ def continue_traversal?(association_config)
295
+ association_config.owned
296
+ end
297
+
298
+ private
299
+
300
+ def set_primary_key(object, config)
301
+ return unless object.id.nil?
302
+
303
+ primary_key = config.get_primary_keys(1).first
304
+
305
+ @mapping[object].id = primary_key
306
+ object.id = primary_key
307
+ end
308
+ end
309
+
310
+ # @private
311
+ class PersistenceAssociationVisitor
312
+ include AggregateVisitorTemplate
313
+
314
+ def initialize(mapping)
315
+ @mapping = mapping
316
+ end
317
+
318
+ def visit_belongs_to(parent, child, belongs_to_config)
319
+ belongs_to_config.set_foreign_key(@mapping[parent], child)
320
+ end
321
+
322
+ def visit_has_one(parent, child, has_one_config)
323
+ return unless has_one_config.owned
324
+ has_one_config.set_foreign_key(@mapping[child], parent)
325
+ end
326
+
327
+ def visit_has_many(parent, children, has_many_config)
328
+ return unless has_many_config.owned
329
+ children.each do |child|
330
+ has_many_config.set_foreign_key(@mapping[child], parent)
331
+ end
332
+ end
333
+
334
+ def continue_traversal?(association_config)
335
+ association_config.owned
336
+ end
337
+ end
338
+
339
+ # @private
340
+ class SaveVisitor
341
+ include AggregateVisitorTemplate
342
+
343
+ def initialize(mapping)
344
+ @mapping = mapping
345
+ end
346
+
347
+ def visit_object(object, config)
348
+ config.save(@mapping[object])
349
+ end
350
+
351
+ def continue_traversal?(association_config)
352
+ association_config.owned
353
+ end
354
+ end
355
+
356
+ # @private
357
+ class AggregateDiffVisitor
358
+ include AggregateVisitorTemplate
359
+
360
+ def initialize(db_objects_in_aggregate)
361
+ @db_objects_in_aggregate = db_objects_in_aggregate
362
+ @db_objects_in_db = []
363
+ end
364
+
365
+ def visit_object(db_object, config)
366
+ @db_objects_in_db << db_object
367
+ end
368
+
369
+ def continue_traversal?(association_config)
370
+ association_config.owned
371
+ end
372
+
373
+ def orphans
374
+ @db_objects_in_db - @db_objects_in_aggregate
375
+ end
376
+ end
377
+
378
+ # @private
379
+ class DeserializeVisitor
380
+ include AggregateVisitorTemplate
381
+
382
+ def initialize(identity_map)
383
+ @identity_map = identity_map
384
+ end
385
+
386
+ def visit_object(db_object, config)
387
+ deserialize_db_object(db_object, config)
388
+ end
389
+
390
+ private
391
+
392
+ def deserialize_db_object(db_object, config)
393
+ @identity_map.get_and_set(db_object) { config.deserialize(db_object) }
394
+ end
395
+ end
396
+
397
+ # @private
398
+ class DomainAssociationVisitor
399
+ include AggregateVisitorTemplate
400
+
401
+ def initialize(identity_map)
402
+ @identity_map = identity_map
403
+ end
404
+
405
+ def visit_belongs_to(db_object, db_child, belongs_to_config)
406
+ associate_one_to_one(db_object, db_child, belongs_to_config)
407
+ end
408
+
409
+ def visit_has_one(db_parent, db_child, has_one_config)
410
+ associate_one_to_one(db_parent, db_child, has_one_config)
411
+ end
412
+
413
+ def visit_has_many(db_object, db_children, has_many_config)
414
+ associate_one_to_many(db_object, db_children, has_many_config)
415
+ end
416
+
417
+ private
418
+
419
+ def associate_one_to_many(db_object, db_children, has_many_config)
420
+ parent = @identity_map.get(db_object)
421
+ children = @identity_map.map(db_children)
422
+ has_many_config.set_children(parent, children)
423
+ end
424
+
425
+ def associate_one_to_one(db_parent, db_child, belongs_to_config)
426
+ parent = @identity_map.get(db_parent)
427
+ child = @identity_map.get(db_child)
428
+ belongs_to_config.set_child(parent, child)
429
+ end
430
+ end
431
+
432
+ # @private
433
+ class DestroyVisitor
434
+ include AggregateVisitorTemplate
435
+
436
+ def visit_object(object, config)
437
+ config.destroy(object)
438
+ end
439
+
440
+ def continue_traversal?(association_config)
441
+ association_config.owned
442
+ end
443
+ end
444
+
445
+ end
@@ -0,0 +1,148 @@
1
+ require 'simple_serializer/simple_serializer'
2
+ require 'simple_serializer/simple_deserializer'
3
+ require 'vorpal/configs'
4
+
5
+ module Vorpal
6
+ class ConfigBuilder
7
+
8
+ # @private
9
+ def initialize(clazz, options)
10
+ @domain_class = clazz
11
+ @class_options = options
12
+ @has_manys = []
13
+ @has_ones = []
14
+ @belongs_tos = []
15
+ @fields = []
16
+ end
17
+
18
+ # Maps the given fields to and from the domain object and the DB. Not needed
19
+ # if a serializer and deserializer were provided.
20
+ def fields(*fields)
21
+ @fields = fields
22
+ end
23
+
24
+ # Defines a one-to-many association with a list of objects of the same type.
25
+ #
26
+ # @param name [String] Name of the field that will refer to the other object.
27
+ # @param options [Hash]
28
+ # @option options [Boolean] :owned
29
+ # @option options [String] :fk
30
+ # @option options [String] :fk_type
31
+ # @option options [Class] :child_class
32
+ def has_many(name, options={})
33
+ @has_manys << {name: name}.merge(options)
34
+ end
35
+
36
+ # Defines a one-to-one association with another object where the foreign key
37
+ # is stored on the other object.
38
+ #
39
+ # @param name [String] Name of the field that will refer to the other object.
40
+ # @param options [Hash]
41
+ # @option options [Boolean] :owned
42
+ # @option options [String] :fk
43
+ # @option options [String] :fk_type
44
+ # @option options [Class] :child_class
45
+ def has_one(name, options={})
46
+ @has_ones << {name: name}.merge(options)
47
+ end
48
+
49
+ # Defines a one-to-one association with another object where the foreign key
50
+ # is stored on this object.
51
+ #
52
+ # This association can be polymorphic. i.e.
53
+ #
54
+ # @param name [String] Name of the field that will refer to the other object.
55
+ # @param options [Hash]
56
+ # @option options [Boolean] :owned
57
+ # @option options [String] :fk
58
+ # @option options [String] :fk_type
59
+ # @option options [Class] :child_class
60
+ # @option options [[Class]] :child_classes
61
+ def belongs_to(name, options={})
62
+ @belongs_tos << {name: name}.merge(options)
63
+ end
64
+
65
+ # @private
66
+ def build
67
+ class_config = build_class_config
68
+ class_config.has_manys = build_has_manys
69
+ class_config.has_ones = build_has_ones
70
+ class_config.belongs_tos = build_belongs_tos
71
+
72
+ class_config
73
+ end
74
+
75
+ private
76
+
77
+ def build_class_config
78
+ Vorpal::ClassConfig.new(
79
+ domain_class: @domain_class,
80
+ table_name: @class_options[:table_name] || table_name,
81
+ serializer: @class_options[:serializer] || serializer(fields_with_id),
82
+ deserializer: @class_options[:deserializer] || deserializer(fields_with_id),
83
+ )
84
+ end
85
+
86
+ def fields_with_id
87
+ [:id].concat @fields
88
+ end
89
+
90
+ def table_name
91
+ @domain_class.name.tableize
92
+ end
93
+
94
+ def build_has_manys
95
+ @has_manys.map { |options| build_has_many(options) }
96
+ end
97
+
98
+ def build_has_many(options)
99
+ options[:child_class] ||= child_class(options[:name])
100
+ options[:fk] ||= foreign_key(@domain_class.name)
101
+ options[:owned] = options.fetch(:owned, true)
102
+ Vorpal::HasManyConfig.new(options)
103
+ end
104
+
105
+ def foreign_key(name)
106
+ name.to_s.underscore + '_id'
107
+ end
108
+
109
+ def child_class(association_name)
110
+ association_name.to_s.classify.constantize
111
+ end
112
+
113
+ def build_has_ones
114
+ @has_ones.map { |options| build_has_one(options) }
115
+ end
116
+
117
+ def build_has_one(options)
118
+ options[:child_class] ||= child_class(options[:name])
119
+ options[:fk] ||= foreign_key(@domain_class.name)
120
+ options[:owned] = options.fetch(:owned, true)
121
+ Vorpal::HasOneConfig.new(options)
122
+ end
123
+
124
+ def build_belongs_tos
125
+ @belongs_tos.map { |options| build_belongs_to(options) }
126
+ end
127
+
128
+ def build_belongs_to(options)
129
+ child_class = options[:child_classes] || options[:child_class] || child_class(options[:name])
130
+ options[:child_classes] = Array(child_class)
131
+ options[:fk] ||= foreign_key(options[:name])
132
+ options[:owned] = options.fetch(:owned, true)
133
+ Vorpal::BelongsToConfig.new(options)
134
+ end
135
+
136
+ def serializer(attrs)
137
+ Class.new(SimpleSerializer) do
138
+ attributes *attrs
139
+ end
140
+ end
141
+
142
+ def deserializer(attrs)
143
+ Class.new(SimpleDeserializer) do
144
+ data_attributes *attrs
145
+ end
146
+ end
147
+ end
148
+ end