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.
- checksums.yaml +15 -0
- data/.gitignore +15 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +167 -0
- data/Rakefile +2 -0
- data/lib/simple_serializer/simple_deserializer.rb +67 -0
- data/lib/simple_serializer/simple_serializer.rb +72 -0
- data/lib/vorpal/aggregate_repository.rb +445 -0
- data/lib/vorpal/config_builder.rb +148 -0
- data/lib/vorpal/configs.rb +274 -0
- data/lib/vorpal/configuration.rb +61 -0
- data/lib/vorpal/hash_initialization.rb +10 -0
- data/lib/vorpal/identity_map.rb +26 -0
- data/lib/vorpal/version.rb +3 -0
- data/lib/vorpal.rb +5 -0
- data/spec/integration_spec_helper.rb +41 -0
- data/spec/vorpal/aggregate_repository_spec.rb +759 -0
- data/spec/vorpal/class_config_builder_spec.rb +11 -0
- data/vorpal.gemspec +28 -0
- metadata +153 -0
@@ -0,0 +1,274 @@
|
|
1
|
+
require 'vorpal/hash_initialization'
|
2
|
+
|
3
|
+
module Vorpal
|
4
|
+
|
5
|
+
# @private
|
6
|
+
class MasterConfig
|
7
|
+
def initialize(class_configs)
|
8
|
+
@class_configs = class_configs
|
9
|
+
initialize_association_configs
|
10
|
+
end
|
11
|
+
|
12
|
+
def config_for(clazz)
|
13
|
+
@class_configs.detect { |conf| conf.domain_class == clazz }
|
14
|
+
end
|
15
|
+
|
16
|
+
def config_for_db(clazz)
|
17
|
+
@class_configs.detect { |conf| conf.db_class == clazz }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def initialize_association_configs
|
23
|
+
@class_configs.each do |config|
|
24
|
+
(config.has_ones + config.has_manys).each do |association_config|
|
25
|
+
association_config.init_relational_association(
|
26
|
+
config_for(association_config.child_class),
|
27
|
+
config
|
28
|
+
)
|
29
|
+
end
|
30
|
+
config.belongs_tos.each do |association_config|
|
31
|
+
association_config.init_relational_association(
|
32
|
+
association_config.child_classes.map(&method(:config_for)),
|
33
|
+
config
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @private
|
41
|
+
class ClassConfig
|
42
|
+
attr_reader :serializer, :deserializer, :domain_class, :table_name
|
43
|
+
attr_accessor :has_manys, :belongs_tos, :has_ones
|
44
|
+
|
45
|
+
def initialize(attrs)
|
46
|
+
@has_manys = []
|
47
|
+
@belongs_tos = []
|
48
|
+
@has_ones = []
|
49
|
+
|
50
|
+
attrs.each do |k,v|
|
51
|
+
instance_variable_set("@#{k}", v)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_primary_keys(count)
|
56
|
+
result = ActiveRecord::Base.connection.execute("select nextval('#{sequence_name}') from generate_series(1,#{count});")
|
57
|
+
result.column_values(0).map(&:to_i)
|
58
|
+
end
|
59
|
+
|
60
|
+
def db_class
|
61
|
+
@db_class ||= ActiveRecord::Base.descendants.detect { |ar| ar.table_name == table_name }
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_in_db(object)
|
65
|
+
db_class.find(object.id)
|
66
|
+
end
|
67
|
+
|
68
|
+
def load_by_id(id)
|
69
|
+
db_class.where(id: id).first
|
70
|
+
end
|
71
|
+
|
72
|
+
def load_by_foreign_key(id, foreign_key_info)
|
73
|
+
arel = db_class.where(foreign_key_info.fk_column => id)
|
74
|
+
arel = arel.where(foreign_key_info.fk_type_column => foreign_key_info.fk_type) if foreign_key_info.polymorphic?
|
75
|
+
arel.order(:id).all
|
76
|
+
end
|
77
|
+
|
78
|
+
def destroy(db_object)
|
79
|
+
db_object.destroy
|
80
|
+
end
|
81
|
+
|
82
|
+
def save(db_object)
|
83
|
+
db_object.save!
|
84
|
+
end
|
85
|
+
|
86
|
+
def build_db_object(attributes)
|
87
|
+
db_class.new(attributes)
|
88
|
+
end
|
89
|
+
|
90
|
+
def set_db_object_attributes(db_object, attributes)
|
91
|
+
db_object.attributes = attributes
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_db_object_attributes(db_object)
|
95
|
+
db_object.attributes.symbolize_keys
|
96
|
+
end
|
97
|
+
|
98
|
+
def serialization_required?
|
99
|
+
!(domain_class < ActiveRecord::Base)
|
100
|
+
end
|
101
|
+
|
102
|
+
def serialize(object)
|
103
|
+
serializer.serialize(object)
|
104
|
+
end
|
105
|
+
|
106
|
+
def deserialize(db_object)
|
107
|
+
attributes = get_db_object_attributes(db_object)
|
108
|
+
serialization_required? ? deserializer.deserialize(domain_class.new, attributes) : db_object
|
109
|
+
end
|
110
|
+
|
111
|
+
def set_field(db_object, field, value)
|
112
|
+
db_object.send("#{field}=", value)
|
113
|
+
end
|
114
|
+
|
115
|
+
def get_field(db_object, field)
|
116
|
+
db_object.send(field)
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def sequence_name
|
122
|
+
"#{table_name}_id_seq"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# @private
|
127
|
+
class ForeignKeyInfo
|
128
|
+
attr_reader :fk_column, :fk_type_column, :fk_type, :polymorphic
|
129
|
+
|
130
|
+
def initialize(fk_column, fk_type_column, fk_type, polymorphic)
|
131
|
+
@fk_column = fk_column
|
132
|
+
@fk_type_column = fk_type_column
|
133
|
+
@fk_type = fk_type
|
134
|
+
@polymorphic = polymorphic
|
135
|
+
end
|
136
|
+
|
137
|
+
def polymorphic?
|
138
|
+
@polymorphic
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# @private
|
143
|
+
# Object associations:
|
144
|
+
# - All object associations are uni-directional
|
145
|
+
# - The end that holds the association is the 'Parent' and the end that
|
146
|
+
# is refered to is the 'Child' or 'Children'
|
147
|
+
#
|
148
|
+
# Relational associations:
|
149
|
+
# - Local end: has FK
|
150
|
+
# - Remote end: has no FK
|
151
|
+
#
|
152
|
+
class RelationalAssociation
|
153
|
+
include HashInitialization
|
154
|
+
attr_reader :fk, :fk_type, :local_config, :remote_configs
|
155
|
+
|
156
|
+
# Can't pass in a remote db model for last param because when saving we only have
|
157
|
+
# a db model if the model is part of the aggregate and not just referenced by the
|
158
|
+
# aggregate
|
159
|
+
def set_foreign_key(local_db_model, remote_model)
|
160
|
+
local_config.set_field(local_db_model, fk, remote_model.try(:id))
|
161
|
+
local_config.set_field(local_db_model, fk_type, remote_model.class.name) if polymorphic?
|
162
|
+
end
|
163
|
+
|
164
|
+
def load_locals(remote_db_model)
|
165
|
+
id = remote_db_model.id
|
166
|
+
# TODO: this method should probably be able to determine the config for the remote models
|
167
|
+
raise "Only supports having one remote configuration when navigating from the remote side to the local side of an association." if remote_configs.size != 1
|
168
|
+
remote_config = remote_configs.first
|
169
|
+
local_config.load_by_foreign_key(id, foreign_key_info(remote_config))
|
170
|
+
end
|
171
|
+
|
172
|
+
def load_remote(local_db_model)
|
173
|
+
remote_config = polymorphic? ? remote_config_for_local_db_object(local_db_model) : remote_configs.first
|
174
|
+
remote_config.load_by_id(get_foreign_key(local_db_model))
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def polymorphic?
|
180
|
+
!fk_type.nil?
|
181
|
+
end
|
182
|
+
|
183
|
+
def foreign_key_info(class_config)
|
184
|
+
ForeignKeyInfo.new(fk, fk_type, class_config.domain_class.name, polymorphic?)
|
185
|
+
end
|
186
|
+
|
187
|
+
def remote_config_for_local_db_object(local_db_model)
|
188
|
+
class_name = local_config.get_field(local_db_model, fk_type)
|
189
|
+
remote_configs.detect { |config| config.domain_class.name == class_name }
|
190
|
+
end
|
191
|
+
|
192
|
+
def get_foreign_key(local_db_model)
|
193
|
+
local_config.get_field(local_db_model, fk)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# @private
|
198
|
+
class HasManyConfig
|
199
|
+
include HashInitialization
|
200
|
+
attr_reader :name, :owned, :fk, :fk_type, :child_class
|
201
|
+
|
202
|
+
def init_relational_association(child_config, parent_config)
|
203
|
+
@relational_association = RelationalAssociation.new(fk: fk, fk_type: fk_type, local_config: child_config, remote_configs: [parent_config])
|
204
|
+
end
|
205
|
+
|
206
|
+
def get_children(parent)
|
207
|
+
parent.send(name)
|
208
|
+
end
|
209
|
+
|
210
|
+
def set_children(parent, children)
|
211
|
+
parent.send("#{name}=", children)
|
212
|
+
end
|
213
|
+
|
214
|
+
def set_foreign_key(db_child, parent)
|
215
|
+
@relational_association.set_foreign_key(db_child, parent)
|
216
|
+
end
|
217
|
+
|
218
|
+
def load_children(db_parent)
|
219
|
+
@relational_association.load_locals(db_parent)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# @private
|
224
|
+
class BelongsToConfig
|
225
|
+
include HashInitialization
|
226
|
+
attr_reader :name, :owned, :fk, :fk_type, :child_classes
|
227
|
+
|
228
|
+
def init_relational_association(child_configs, parent_config)
|
229
|
+
@relational_association = RelationalAssociation.new(fk: fk, fk_type: fk_type, local_config: parent_config, remote_configs: child_configs)
|
230
|
+
end
|
231
|
+
|
232
|
+
def get_child(parent)
|
233
|
+
parent.send(name)
|
234
|
+
end
|
235
|
+
|
236
|
+
def set_child(parent, child)
|
237
|
+
parent.send("#{name}=", child)
|
238
|
+
end
|
239
|
+
|
240
|
+
def set_foreign_key(db_parent, child)
|
241
|
+
@relational_association.set_foreign_key(db_parent, child)
|
242
|
+
end
|
243
|
+
|
244
|
+
def load_child(db_parent)
|
245
|
+
@relational_association.load_remote(db_parent)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# @private
|
250
|
+
class HasOneConfig
|
251
|
+
include HashInitialization
|
252
|
+
attr_reader :name, :owned, :fk, :fk_type, :child_class
|
253
|
+
|
254
|
+
def init_relational_association(child_config, parent_config)
|
255
|
+
@relational_association = RelationalAssociation.new(fk: fk, fk_type: fk_type, local_config: child_config, remote_configs: [parent_config])
|
256
|
+
end
|
257
|
+
|
258
|
+
def get_child(parent)
|
259
|
+
parent.send(name)
|
260
|
+
end
|
261
|
+
|
262
|
+
def set_child(parent, child)
|
263
|
+
parent.send("#{name}=", child)
|
264
|
+
end
|
265
|
+
|
266
|
+
def set_foreign_key(db_child, parent)
|
267
|
+
@relational_association.set_foreign_key(db_child, parent)
|
268
|
+
end
|
269
|
+
|
270
|
+
def load_child(db_parent)
|
271
|
+
@relational_association.load_locals(db_parent).first
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'vorpal/aggregate_repository'
|
2
|
+
require 'vorpal/config_builder'
|
3
|
+
|
4
|
+
module Vorpal
|
5
|
+
# Allows easy creation of {Vorpal::AggregateRepository}
|
6
|
+
# instances.
|
7
|
+
#
|
8
|
+
# ```ruby
|
9
|
+
# repository = Vorpal::Configuration.define do
|
10
|
+
# map Tree do
|
11
|
+
# fields :name
|
12
|
+
# belongs_to :trunk
|
13
|
+
# has_many :branches
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# map Trunk do
|
17
|
+
# fields :length
|
18
|
+
# has_one :tree
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# map Branch do
|
22
|
+
# fields :length
|
23
|
+
# belongs_to :tree
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# ```
|
27
|
+
module Configuration
|
28
|
+
extend self
|
29
|
+
|
30
|
+
# Configures and creates a {Vorpal::AggregateRepository} instance.
|
31
|
+
#
|
32
|
+
# @return [Vorpal::AggregateRepository] Repository instance.
|
33
|
+
def define(&block)
|
34
|
+
@class_configs = []
|
35
|
+
self.instance_exec(&block)
|
36
|
+
AggregateRepository.new(@class_configs)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Maps a domain class to a relational table.
|
40
|
+
#
|
41
|
+
# @param domain_class [Class] Type of the domain model to be mapped
|
42
|
+
# @param options [Hash] Configure how to map the domain model
|
43
|
+
# @option options [String] :table_name (Name of the domain class snake-cased and pluralized.)
|
44
|
+
# Name of the relational DB table.
|
45
|
+
# @option options [Object] :serializer (map the {ConfigBuilder#fields} directly)
|
46
|
+
# Object that will convert the domain objects into a hash.
|
47
|
+
#
|
48
|
+
# Must have a `(Hash) serialize(Object)` method.
|
49
|
+
# @option options [Object] :deserializer (map the {ConfigBuilder#fields} directly)
|
50
|
+
# Object that will set a hash of attribute_names->values onto a new domain
|
51
|
+
# object.
|
52
|
+
#
|
53
|
+
# Must have a `(Object) deserialize(Object, Hash)` method.
|
54
|
+
def map(domain_class, options={}, &block)
|
55
|
+
builder = ConfigBuilder.new(domain_class, options)
|
56
|
+
builder.instance_exec(&block) if block_given?
|
57
|
+
|
58
|
+
@class_configs << builder.build
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Vorpal
|
2
|
+
class IdentityMap
|
3
|
+
def initialize
|
4
|
+
@entities = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def get(key_object)
|
8
|
+
@entities[key_object]
|
9
|
+
end
|
10
|
+
|
11
|
+
def set(key_object, object)
|
12
|
+
@entities[key_object] = object
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_and_set(key_object)
|
16
|
+
object = get(key_object)
|
17
|
+
object = yield(key_object) if object.nil?
|
18
|
+
set(key_object, object)
|
19
|
+
object
|
20
|
+
end
|
21
|
+
|
22
|
+
def map(key_objects)
|
23
|
+
key_objects.map { |k| @entities[k] }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/vorpal.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'pg' # or 'mysql2' or 'sqlite3'
|
3
|
+
|
4
|
+
# Change the following to reflect your database settings
|
5
|
+
ActiveRecord::Base.establish_connection(
|
6
|
+
adapter: 'postgresql', # or 'mysql2' or 'sqlite3'
|
7
|
+
host: 'localhost',
|
8
|
+
database: 'vorpal_test',
|
9
|
+
username: 'vorpal',
|
10
|
+
password: 'pass',
|
11
|
+
min_messages: 'error'
|
12
|
+
)
|
13
|
+
|
14
|
+
RSpec.configure do |config|
|
15
|
+
# ## Mock Framework
|
16
|
+
#
|
17
|
+
# If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
|
18
|
+
#
|
19
|
+
# config.mock_with :mocha
|
20
|
+
# config.mock_with :flexmock
|
21
|
+
# config.mock_with :rr
|
22
|
+
|
23
|
+
# implements use_transactional_fixtures = true
|
24
|
+
# from lib/active_record/fixtures.rb
|
25
|
+
# works with Rails 3.2. Probably not with Rails 4
|
26
|
+
config.before(:each) do
|
27
|
+
connection = ActiveRecord::Base.connection
|
28
|
+
connection.increment_open_transactions
|
29
|
+
connection.transaction_joinable = false
|
30
|
+
connection.begin_db_transaction
|
31
|
+
end
|
32
|
+
|
33
|
+
config.after(:each) do
|
34
|
+
connection = ActiveRecord::Base.connection
|
35
|
+
if connection.open_transactions != 0
|
36
|
+
connection.rollback_db_transaction
|
37
|
+
connection.decrement_open_transactions
|
38
|
+
end
|
39
|
+
ActiveRecord::Base.clear_active_connections!
|
40
|
+
end
|
41
|
+
end
|