vorpal 1.0.3 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -26
  3. data/lib/vorpal/aggregate_mapper.rb +13 -2
  4. data/lib/vorpal/aggregate_traversal.rb +9 -8
  5. data/lib/vorpal/config/association_config.rb +84 -0
  6. data/lib/vorpal/config/belongs_to_config.rb +35 -0
  7. data/lib/vorpal/config/class_config.rb +71 -0
  8. data/lib/vorpal/config/configs.rb +54 -0
  9. data/lib/vorpal/config/foreign_key_info.rb +23 -0
  10. data/lib/vorpal/config/has_many_config.rb +38 -0
  11. data/lib/vorpal/config/has_one_config.rb +35 -0
  12. data/lib/vorpal/config/main_config.rb +68 -0
  13. data/lib/vorpal/db_loader.rb +25 -22
  14. data/lib/vorpal/driver/postgresql.rb +42 -6
  15. data/lib/vorpal/dsl/config_builder.rb +26 -73
  16. data/lib/vorpal/dsl/configuration.rb +139 -42
  17. data/lib/vorpal/dsl/defaults_generator.rb +1 -1
  18. data/lib/vorpal/engine.rb +27 -13
  19. data/lib/vorpal/exceptions.rb +4 -0
  20. data/lib/vorpal/loaded_objects.rb +57 -14
  21. data/lib/vorpal/util/array_hash.rb +22 -8
  22. data/lib/vorpal/util/hash_initialization.rb +1 -1
  23. data/lib/vorpal/version.rb +1 -1
  24. data/vorpal.gemspec +4 -4
  25. metadata +17 -74
  26. data/.editorconfig +0 -13
  27. data/.envrc +0 -4
  28. data/.gitignore +0 -16
  29. data/.rspec +0 -1
  30. data/.ruby-version +0 -1
  31. data/.travis.yml +0 -18
  32. data/.yardopts +0 -1
  33. data/Appraisals +0 -18
  34. data/Gemfile +0 -4
  35. data/Rakefile +0 -39
  36. data/bin/appraisal +0 -29
  37. data/bin/rake +0 -29
  38. data/bin/rspec +0 -29
  39. data/docker-compose.yml +0 -19
  40. data/gemfiles/rails_5_1.gemfile +0 -11
  41. data/gemfiles/rails_5_1.gemfile.lock +0 -101
  42. data/gemfiles/rails_5_2.gemfile +0 -11
  43. data/gemfiles/rails_5_2.gemfile.lock +0 -101
  44. data/gemfiles/rails_6_0.gemfile +0 -9
  45. data/gemfiles/rails_6_0.gemfile.lock +0 -101
  46. data/lib/vorpal/configs.rb +0 -296
  47. data/spec/acceptance/vorpal/aggregate_mapper_spec.rb +0 -910
  48. data/spec/helpers/codecov_helper.rb +0 -7
  49. data/spec/helpers/db_helpers.rb +0 -69
  50. data/spec/helpers/profile_helpers.rb +0 -26
  51. data/spec/integration/vorpal/driver/postgresql_spec.rb +0 -42
  52. data/spec/integration_spec_helper.rb +0 -29
  53. data/spec/performance/vorpal/performance_spec.rb +0 -305
  54. data/spec/unit/vorpal/configs_spec.rb +0 -117
  55. data/spec/unit/vorpal/db_loader_spec.rb +0 -103
  56. data/spec/unit/vorpal/dsl/config_builder_spec.rb +0 -18
  57. data/spec/unit/vorpal/dsl/defaults_generator_spec.rb +0 -75
  58. data/spec/unit/vorpal/identity_map_spec.rb +0 -62
  59. data/spec/unit/vorpal/loaded_objects_spec.rb +0 -22
  60. data/spec/unit/vorpal/util/string_utils_spec.rb +0 -25
  61. data/spec/unit_spec_helper.rb +0 -1
@@ -1,57 +1,154 @@
1
1
  require 'vorpal/engine'
2
2
  require 'vorpal/dsl/config_builder'
3
3
  require 'vorpal/driver/postgresql'
4
+ require 'vorpal/config/main_config'
4
5
 
5
6
  module Vorpal
6
7
  module Dsl
7
- module Configuration
8
-
9
- # Configures and creates a {Engine} instance.
10
- #
11
- # @param options [Hash] Global configuration options for the engine instance.
12
- # @option options [Object] :db_driver (Object that will be used to interact with the DB.)
13
- # Must be duck-type compatible with {Postgresql}.
8
+ # Implements the Vorpal DSL.
14
9
  #
15
- # @return [Engine] Instance of the mapping engine.
16
- def define(options={}, &block)
17
- master_config = build_config(&block)
18
- db_driver = options.fetch(:db_driver, Driver::Postgresql.new)
19
- Engine.new(db_driver, master_config)
20
- end
21
-
22
- # Maps a domain class to a relational table.
10
+ # ```ruby
11
+ # engine = Vorpal.define do
12
+ # map Tree do
13
+ # attributes :name
14
+ # belongs_to :trunk
15
+ # has_many :branches
16
+ # end
23
17
  #
24
- # @param domain_class [Class] Type of the domain model to be mapped
25
- # @param options [Hash] Configure how to map the domain model
26
- # @option options [String] :to
27
- # Class of the ActiveRecord object that will map this domain class to the DB.
28
- # Optional, if one is not specified, it will be generated.
29
- # @option options [Object] :serializer (map the {ConfigBuilder#attributes} directly)
30
- # Object that will convert the domain objects into a hash.
18
+ # map Trunk do
19
+ # attributes :length
20
+ # has_one :tree
21
+ # end
31
22
  #
32
- # Must have a `(Hash) serialize(Object)` method.
33
- # @option options [Object] :deserializer (map the {ConfigBuilder#attributes} directly)
34
- # Object that will set a hash of attribute_names->values onto a new domain
35
- # object.
23
+ # map Branch do
24
+ # attributes :length
25
+ # belongs_to :tree
26
+ # end
27
+ # end
36
28
  #
37
- # Must have a `(Object) deserialize(Object, Hash)` method.
38
- def map(domain_class, options={}, &block)
39
- @class_configs << build_class_config(domain_class, options, &block)
40
- end
29
+ # mapper = engine.mapper_for(Tree)
30
+ # ```
31
+ module Configuration
32
+ # Configures and creates a {Engine} instance.
33
+ #
34
+ # @param options [Hash] Global configuration options for the engine instance.
35
+ # @option options [Object] :db_driver (Object that will be used to interact with the DB.)
36
+ # Must be duck-type compatible with {Postgresql}.
37
+ #
38
+ # @return [Engine] Instance of the mapping engine.
39
+ def define(options={}, &block)
40
+ @main_config = Config::MainConfig.new
41
+ instance_exec(&block)
42
+ @main_config.initialize_association_configs
43
+ db_driver = options.fetch(:db_driver, Driver::Postgresql.new)
44
+ engine = Engine.new(db_driver, @main_config)
45
+ @main_config = nil # make sure this MainConfig is never re-used by accident.
46
+ engine
47
+ end
41
48
 
42
- # @private
43
- def build_class_config(domain_class, options={}, &block)
44
- builder = ConfigBuilder.new(domain_class, options, Driver::Postgresql.new)
45
- builder.instance_exec(&block) if block_given?
46
- builder.build
47
- end
49
+ # Maps a domain class to a relational table.
50
+ #
51
+ # @param domain_class [Class] Type of the domain model to be mapped
52
+ # @param options [Hash] Configure how to map the domain model
53
+ # @option options [String] :to
54
+ # Class of the ActiveRecord object that will map this domain class to the DB.
55
+ # Optional, if one is not specified, it will be generated.
56
+ # @option options [Object] :serializer (map the {ConfigBuilder#attributes} directly)
57
+ # Object that will convert the domain objects into a hash.
58
+ #
59
+ # Must have a `(Hash) serialize(Object)` method.
60
+ # @option options [Object] :deserializer (map the {ConfigBuilder#attributes} directly)
61
+ # Object that will set a hash of attribute_names->values onto a new domain
62
+ # object.
63
+ #
64
+ # Must have a `(Object) deserialize(Object, Hash)` method.
65
+ # @option options [Symbol] :primary_key_type [:serial, :uuid] (:serial)
66
+ # The type of primary key for the class. :serial for auto-incrementing integer, :uuid for a UUID
67
+ # @option options [Symbol] :id
68
+ # Same as :primary_key_type. Exists for compatibility with the Rails API.
69
+ def map(domain_class, options={}, &block)
70
+ class_config = build_class_config(domain_class, options, &block)
71
+ @main_config.add_class_config(class_config)
72
+ class_config
73
+ end
48
74
 
49
- # @private
50
- def build_config(&block)
51
- @class_configs = []
52
- self.instance_exec(&block)
53
- MasterConfig.new(@class_configs)
75
+ # @private
76
+ def build_class_config(domain_class, options, &block)
77
+ @builder = ConfigBuilder.new(domain_class, options, Driver::Postgresql.new)
78
+ instance_exec(&block) if block_given?
79
+ class_config = @builder.build
80
+ @builder = nil # make sure this ConfigBuilder is never re-used by accident.
81
+ class_config
82
+ end
83
+
84
+ # Maps the given attributes to and from the domain object and the DB. Not needed
85
+ # if a serializer and deserializer were provided.
86
+ def attributes(*attributes)
87
+ @builder.attributes(*attributes)
88
+ end
89
+
90
+ # Defines a one-to-many association to another type where the foreign key is stored on the associated table.
91
+ #
92
+ # In Object-Oriented programming, associations are *directed*. This means that they can only be
93
+ # traversed in one direction: from the type that defines the association (the one with the
94
+ # getter) to the type that is associated.
95
+ #
96
+ # @param name [String] Name of the association getter.
97
+ # @param options [Hash]
98
+ # @option options [Boolean] :owned (True) True if the associated type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted.
99
+ # @option options [String] :fk (Association-owning class name converted to snakecase and appended with a '_id') The name of the DB column on the associated table that contains the foreign key reference to the association owner.
100
+ # @option options [String] :fk_type The name of the DB column on the associated table that contains the association-owning class name. Only needed when the associated end is polymorphic.
101
+ # @option options [String] :unique_key_name ("id") The name of the column on the owning table that the foreign key points to. Normally the primary key column.
102
+ # @option options [String] :primary_key Same as :unique_key_name. Exists for compatibility with Rails API.
103
+ # @option options [Class] :child_class DEPRECATED. Use `associated_class` instead. The associated class.
104
+ # @option options [Class] :associated_class (Name of the association converted to a Class) The associated class.
105
+ def has_many(name, options={})
106
+ @builder.has_many(name, options)
107
+ end
108
+
109
+ # Defines a one-to-one association to another type where the foreign key
110
+ # is stored on the associated table.
111
+ #
112
+ # In Object-Oriented programming, associations are *directed*. This means that they can only be
113
+ # traversed in one direction: from the type that defines the association (the one with the
114
+ # getter) to the type that is associated.
115
+ #
116
+ # @param name [String] Name of the association getter.
117
+ # @param options [Hash]
118
+ # @option options [Boolean] :owned (True) True if the associated type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted.
119
+ # @option options [String] :fk (Association-owning class name converted to snakecase and appended with a '_id') The name of the DB column on the associated table that contains the foreign key reference to the association owner.
120
+ # @option options [String] :fk_type The name of the DB column on the associated table that contains the association-owning class name. Only needed when the associated end is polymorphic.
121
+ # @option options [String] :unique_key_name ("id") The name of the column on the owning table that the foreign key points to. Normally the primary key column.
122
+ # @option options [String] :primary_key Same as :unique_key_name. Exists for compatibility with Rails API.
123
+ # @option options [Class] :child_class DEPRECATED. Use `associated_class` instead. The associated class.
124
+ # @option options [Class] :associated_class (Name of the association converted to a Class) The associated class.
125
+ def has_one(name, options={})
126
+ @builder.has_one(name, options)
127
+ end
128
+
129
+ # Defines a one-to-one association with another type where the foreign key
130
+ # is stored on the table of the entity declaring the association.
131
+ #
132
+ # This association can be polymorphic. I.E. associates can be of different types.
133
+ #
134
+ # In Object-Oriented programming, associations are *directed*. This means that they can only be
135
+ # traversed in one direction: from the type that defines the association (the one with the
136
+ # getter) to the type that is associated.
137
+ #
138
+ # @param name [String] Name of the association getter.
139
+ # @param options [Hash]
140
+ # @option options [Boolean] :owned (True) True if the associated type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted.
141
+ # @option options [String] :fk (Associated class name converted to snakecase and appended with a '_id') The name of the DB column on the association-owning table that contains the foreign key reference to the associated table.
142
+ # @option options [String] :fk_type The name of the DB column on the association-owning table that contains the associated class name. Only needed when the association is polymorphic.
143
+ # @option options [String] :unique_key_name ("id") The name of the column on the associated table that the foreign key points to. Normally the primary key column.
144
+ # @option options [String] :primary_key Same as :unique_key_name. Exists for compatibility with Rails API.
145
+ # @option options [Class] :child_class DEPRECATED. Use `associated_class` instead. The associated class.
146
+ # @option options [Class] :associated_class (Name of the association converted to a Class) The associated class.
147
+ # @option options [[Class]] :child_classes DEPRECATED. Use `associated_classes` instead. The list of possible classes that can be associated. This is for polymorphic associations. Takes precedence over `:associated_class`.
148
+ # @option options [[Class]] :associated_classes (Name of the association converted to a Class) The list of possible classes that can be associated. This is for polymorphic associations. Takes precedence over `:associated_class`.
149
+ def belongs_to(name, options={})
150
+ @builder.belongs_to(name, options)
151
+ end
54
152
  end
55
153
  end
56
- end
57
154
  end
@@ -35,7 +35,7 @@ module Vorpal
35
35
  ActiveSupport::Inflector.foreign_key(name.to_s)
36
36
  end
37
37
 
38
- def child_class(association_name)
38
+ def associated_class(association_name)
39
39
  module_parent.const_get(ActiveSupport::Inflector.classify(association_name.to_s))
40
40
  end
41
41
 
data/lib/vorpal/engine.rb CHANGED
@@ -7,9 +7,9 @@ require 'vorpal/exceptions'
7
7
  module Vorpal
8
8
  class Engine
9
9
  # @private
10
- def initialize(db_driver, master_config)
10
+ def initialize(db_driver, main_config)
11
11
  @db_driver = db_driver
12
- @configs = master_config
12
+ @configs = main_config
13
13
  end
14
14
 
15
15
  # Creates a mapper for saving/updating/loading/destroying an aggregate to/from
@@ -34,6 +34,9 @@ module Vorpal
34
34
  serialize(all_owned_objects, mapping, loaded_db_objects)
35
35
  new_objects = get_unsaved_objects(mapping.keys)
36
36
  begin
37
+ # Primary keys are set eagerly (instead of waiting for them to be set by ActiveRecord upon create)
38
+ # because we want to support non-null FK constraints without needing to figure the correct
39
+ # order to save entities in.
37
40
  set_primary_keys(all_owned_objects, mapping)
38
41
  set_foreign_keys(all_owned_objects, mapping)
39
42
  remove_orphans(mapping, loaded_db_objects)
@@ -89,10 +92,16 @@ module Vorpal
89
92
  @configs.config_for(domain_class).db_class
90
93
  end
91
94
 
95
+ # Try to use {AggregateMapper#query} instead.
92
96
  def query(domain_class)
93
97
  @db_driver.query(@configs.config_for(domain_class).db_class, mapper_for(domain_class))
94
98
  end
95
99
 
100
+ # @private
101
+ def class_config(domain_class)
102
+ @configs.config_for(domain_class)
103
+ end
104
+
96
105
  private
97
106
 
98
107
  def wrap(collection_or_not)
@@ -128,8 +137,9 @@ module Vorpal
128
137
  loaded_db_objects.each do |config, db_objects|
129
138
  db_objects.each do |db_object|
130
139
  config.local_association_configs.each do |association_config|
131
- db_remote = loaded_db_objects.find_by_id(
140
+ db_remote = loaded_db_objects.find_by_unique_key(
132
141
  association_config.remote_class_config(db_object),
142
+ association_config.unique_key_name,
133
143
  association_config.fk_value(db_object)
134
144
  )
135
145
  association_config.associate(identity_map.get(db_object), identity_map.get(db_remote))
@@ -150,10 +160,10 @@ module Vorpal
150
160
  def serialize_object(object, config, loaded_db_objects)
151
161
  if config.serialization_required?
152
162
  attributes = config.serialize(object)
153
- if object.id.nil?
163
+ db_object = loaded_db_objects.find_by_primary_key(config, object)
164
+ if object.id.nil? || db_object.nil? # object doesn't exist in the DB
154
165
  config.build_db_object(attributes)
155
166
  else
156
- db_object = loaded_db_objects.find_by_id(config, object.id)
157
167
  config.set_db_object_attributes(db_object, attributes)
158
168
  db_object
159
169
  end
@@ -165,7 +175,11 @@ module Vorpal
165
175
  def set_primary_keys(owned_objects, mapping)
166
176
  owned_objects.each do |config, objects|
167
177
  in_need_of_primary_keys = objects.find_all { |obj| obj.id.nil? }
168
- primary_keys = @db_driver.get_primary_keys(config.db_class, in_need_of_primary_keys.length)
178
+ if config.primary_key_type == :uuid
179
+ primary_keys = Array.new(in_need_of_primary_keys.length) { SecureRandom.uuid }
180
+ elsif config.primary_key_type == :serial
181
+ primary_keys = @db_driver.get_primary_keys(config.db_class, in_need_of_primary_keys.length)
182
+ end
169
183
  in_need_of_primary_keys.zip(primary_keys).each do |object, primary_key|
170
184
  mapping[object].id = primary_key
171
185
  object.id = primary_key
@@ -179,23 +193,23 @@ module Vorpal
179
193
  objects.each do |object|
180
194
  config.has_manys.each do |has_many_config|
181
195
  if has_many_config.owned
182
- children = has_many_config.get_children(object)
183
- children.each do |child|
184
- has_many_config.set_foreign_key(mapping[child], object)
196
+ associates = has_many_config.get_associated(object)
197
+ associates.each do |associate|
198
+ has_many_config.set_foreign_key(mapping[associate], object)
185
199
  end
186
200
  end
187
201
  end
188
202
 
189
203
  config.has_ones.each do |has_one_config|
190
204
  if has_one_config.owned
191
- child = has_one_config.get_child(object)
192
- has_one_config.set_foreign_key(mapping[child], object)
205
+ associate = has_one_config.get_associated(object)
206
+ has_one_config.set_foreign_key(mapping[associate], object) if associate
193
207
  end
194
208
  end
195
209
 
196
210
  config.belongs_tos.each do |belongs_to_config|
197
- child = belongs_to_config.get_child(object)
198
- belongs_to_config.set_foreign_key(mapping[object], child)
211
+ associate = belongs_to_config.get_associated(object)
212
+ belongs_to_config.set_foreign_key(mapping[object], associate)
199
213
  end
200
214
  end
201
215
  end
@@ -4,4 +4,8 @@ module Vorpal
4
4
  class InvalidAggregateRoot < StandardError; end
5
5
 
6
6
  class ConfigurationNotFound < StandardError; end
7
+
8
+ class ConfigurationError < StandardError; end
9
+
10
+ class InvariantViolated < StandardError; end
7
11
  end
@@ -5,37 +5,80 @@ module Vorpal
5
5
 
6
6
  # @private
7
7
  class LoadedObjects
8
- include Util::ArrayHash
9
8
  extend Forwardable
10
9
  include Enumerable
11
10
 
12
- attr_reader :objects
13
- def_delegators :objects, :each
11
+ def_delegators :@objects, :each
14
12
 
15
13
  def initialize
16
- @objects = Hash.new([])
17
- @objects_by_id = Hash.new
14
+ @objects = Util::ArrayHash.new
15
+ @cache = {}
18
16
  end
19
17
 
20
18
  def add(config, objects)
21
19
  objects_to_add = objects.map do |object|
22
- if !already_loaded?(config, object.id)
23
- @objects_by_id[[config.domain_class.name, object.id]] = object
20
+ if !already_loaded?(config, object)
21
+ add_to_cache(config, object)
24
22
  end
25
- end
26
- add_to_hash(@objects, config, objects_to_add.compact)
23
+ end.compact
24
+ @objects.append(config, objects_to_add)
25
+ objects_to_add
26
+ end
27
+
28
+ def find_by_primary_key(config, object)
29
+ find_by_unique_key(config, "id", object.id)
27
30
  end
28
31
 
29
- def find_by_id(config, id)
30
- @objects_by_id[[config.domain_class.name, id]]
32
+ def find_by_unique_key(config, column_name, value)
33
+ get_from_cache(config, column_name, value)
31
34
  end
32
35
 
33
36
  def all_objects
34
- @objects_by_id.values
37
+ @objects.values
38
+ end
39
+
40
+ def already_loaded_by_unique_key?(config, column_name, id)
41
+ !find_by_unique_key(config, column_name, id).nil?
42
+ end
43
+
44
+ private
45
+
46
+ def already_loaded?(config, object)
47
+ !find_by_primary_key(config, object).nil?
35
48
  end
36
49
 
37
- def already_loaded?(config, id)
38
- !find_by_id(config, id).nil?
50
+ # TODO: Do we have to worry about symbols vs strings for the column_name?
51
+ def add_to_cache(config, object)
52
+ # we take a shortcut here assuming that the cache has already been primed with the primary key column
53
+ # because this method should always be guarded by #already_loaded?
54
+ column_cache = @cache[config]
55
+ column_cache.each do |column_name, unique_key_cache|
56
+ unique_key_cache[object.send(column_name)] = object
57
+ end
58
+ object
59
+ end
60
+
61
+ def get_from_cache(config, column_name, value)
62
+ lookup_hash(config, column_name)[value]
63
+ end
64
+
65
+ # lazily primes the cache
66
+ # TODO: Do we have to worry about symbols vs strings for the column_name?
67
+ def lookup_hash(config, column_name)
68
+ column_cache = @cache[config]
69
+ if column_cache.nil?
70
+ column_cache = {}
71
+ @cache[config] = column_cache
72
+ end
73
+ unique_key_cache = column_cache[column_name]
74
+ if unique_key_cache.nil?
75
+ unique_key_cache = {}
76
+ column_cache[column_name] = unique_key_cache
77
+ @objects[config].each do |object|
78
+ unique_key_cache[object.send(column_name)] = object
79
+ end
80
+ end
81
+ unique_key_cache
39
82
  end
40
83
  end
41
84
  end
@@ -1,19 +1,33 @@
1
+ require 'forwardable'
2
+
1
3
  module Vorpal
2
4
  module Util
3
5
  # @private
4
- module ArrayHash
5
- def add_to_hash(array_hash, key, values)
6
- if array_hash[key].nil? || array_hash[key].empty?
7
- array_hash[key] = []
6
+ class ArrayHash
7
+ extend Forwardable
8
+
9
+ def_delegators :@hash, :each, :empty?, :[]
10
+
11
+ def initialize
12
+ @hash = Hash.new([])
13
+ end
14
+
15
+ def append(key, values)
16
+ if @hash[key].nil? || @hash[key].empty?
17
+ @hash[key] = []
8
18
  end
9
- array_hash[key].concat(Array(values))
19
+ @hash[key].concat(Array(values))
10
20
  end
11
21
 
12
- def pop(array_hash)
13
- key = array_hash.first.first
14
- values = array_hash.delete(key)
22
+ def pop
23
+ key = @hash.first.first
24
+ values = @hash.delete(key)
15
25
  [key, values]
16
26
  end
27
+
28
+ def values
29
+ @hash.values.flatten
30
+ end
17
31
  end
18
32
  end
19
33
  end