vorpal 1.0.1 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +72 -56
  3. data/lib/vorpal/aggregate_mapper.rb +11 -0
  4. data/lib/vorpal/config/class_config.rb +71 -0
  5. data/lib/vorpal/configs.rb +9 -66
  6. data/lib/vorpal/driver/postgresql.rb +39 -3
  7. data/lib/vorpal/dsl/config_builder.rb +12 -50
  8. data/lib/vorpal/dsl/configuration.rb +131 -42
  9. data/lib/vorpal/dsl/defaults_generator.rb +14 -4
  10. data/lib/vorpal/engine.rb +18 -5
  11. data/lib/vorpal/identity_map.rb +15 -14
  12. data/lib/vorpal/version.rb +1 -1
  13. data/vorpal.gemspec +9 -9
  14. metadata +36 -88
  15. data/.editorconfig +0 -13
  16. data/.envrc +0 -4
  17. data/.gitignore +0 -16
  18. data/.rspec +0 -1
  19. data/.yardopts +0 -1
  20. data/Appraisals +0 -34
  21. data/Gemfile +0 -4
  22. data/Rakefile +0 -10
  23. data/bin/appraisal +0 -29
  24. data/bin/rake +0 -29
  25. data/bin/rspec +0 -29
  26. data/gemfiles/rails_4_1.gemfile +0 -11
  27. data/gemfiles/rails_4_1.gemfile.lock +0 -92
  28. data/gemfiles/rails_4_2.gemfile +0 -11
  29. data/gemfiles/rails_4_2.gemfile.lock +0 -90
  30. data/gemfiles/rails_5_0.gemfile +0 -11
  31. data/gemfiles/rails_5_0.gemfile.lock +0 -88
  32. data/gemfiles/rails_5_1.gemfile +0 -11
  33. data/gemfiles/rails_5_1.gemfile.lock +0 -88
  34. data/gemfiles/rails_5_2.gemfile +0 -11
  35. data/gemfiles/rails_5_2.gemfile.lock +0 -88
  36. data/spec/helpers/db_helpers.rb +0 -66
  37. data/spec/helpers/profile_helpers.rb +0 -26
  38. data/spec/integration_spec_helper.rb +0 -36
  39. data/spec/unit_spec_helper.rb +0 -0
  40. data/spec/vorpal/acceptance/aggregate_mapper_spec.rb +0 -911
  41. data/spec/vorpal/integration/driver/postgresql_spec.rb +0 -42
  42. data/spec/vorpal/performance/performance_spec.rb +0 -235
  43. data/spec/vorpal/unit/configs_spec.rb +0 -117
  44. data/spec/vorpal/unit/db_loader_spec.rb +0 -103
  45. data/spec/vorpal/unit/dsl/config_builder_spec.rb +0 -18
  46. data/spec/vorpal/unit/dsl/defaults_generator_spec.rb +0 -75
  47. data/spec/vorpal/unit/identity_map_spec.rb +0 -62
  48. data/spec/vorpal/unit/loaded_objects_spec.rb +0 -22
  49. data/spec/vorpal/unit/util/string_utils_spec.rb +0 -25
@@ -16,59 +16,32 @@ module Vorpal
16
16
  @defaults_generator = DefaultsGenerator.new(clazz, db_driver)
17
17
  end
18
18
 
19
- # Maps the given attributes to and from the domain object and the DB. Not needed
20
- # if a serializer and deserializer were provided.
19
+ # @private
21
20
  def attributes(*attributes)
22
21
  @attributes.concat(attributes)
23
22
  end
24
23
 
25
- # Defines a one-to-many association with a list of objects of the same type.
26
- #
27
- # @param name [String] Name of the attribute that will refer to the other object.
28
- # @param options [Hash]
29
- # @option options [Boolean] :owned
30
- # @option options [String] :fk
31
- # @option options [String] :fk_type
32
- # @option options [Class] :child_class
24
+ # @private
33
25
  def has_many(name, options={})
34
- @has_manys << {name: name}.merge(options)
26
+ @has_manys << build_has_many({name: name}.merge(options))
35
27
  end
36
28
 
37
- # Defines a one-to-one association with another object where the foreign key
38
- # is stored on the other object.
39
- #
40
- # @param name [String] Name of the attribute that will refer to the other object.
41
- # @param options [Hash]
42
- # @option options [Boolean] :owned
43
- # @option options [String] :fk
44
- # @option options [String] :fk_type
45
- # @option options [Class] :child_class
29
+ # @private
46
30
  def has_one(name, options={})
47
- @has_ones << {name: name}.merge(options)
31
+ @has_ones << build_has_one({name: name}.merge(options))
48
32
  end
49
33
 
50
- # Defines a one-to-one association with another object where the foreign key
51
- # is stored on this object.
52
- #
53
- # This association can be polymorphic. i.e.
54
- #
55
- # @param name [String] Name of the attribute that will refer to the other object.
56
- # @param options [Hash]
57
- # @option options [Boolean] :owned
58
- # @option options [String] :fk
59
- # @option options [String] :fk_type
60
- # @option options [Class] :child_class
61
- # @option options [[Class]] :child_classes
34
+ # @private
62
35
  def belongs_to(name, options={})
63
- @belongs_tos << {name: name}.merge(options)
36
+ @belongs_tos << build_belongs_to({name: name}.merge(options))
64
37
  end
65
38
 
66
39
  # @private
67
40
  def build
68
41
  class_config = build_class_config
69
- class_config.has_manys = build_has_manys
70
- class_config.has_ones = build_has_ones
71
- class_config.belongs_tos = build_belongs_tos
42
+ class_config.has_manys = @has_manys
43
+ class_config.has_ones = @has_ones
44
+ class_config.belongs_tos = @belongs_tos
72
45
 
73
46
  class_config
74
47
  end
@@ -81,18 +54,15 @@ module Vorpal
81
54
  private
82
55
 
83
56
  def build_class_config
84
- Vorpal::ClassConfig.new(
57
+ Vorpal::Config::ClassConfig.new(
85
58
  domain_class: @domain_class,
86
59
  db_class: @class_options[:to] || @defaults_generator.build_db_class(@class_options[:table_name]),
87
60
  serializer: @class_options[:serializer] || @defaults_generator.serializer(attributes_with_id),
88
61
  deserializer: @class_options[:deserializer] || @defaults_generator.deserializer(attributes_with_id),
62
+ primary_key_type: @class_options[:primary_key_type] || @class_options[:id] || :serial,
89
63
  )
90
64
  end
91
65
 
92
- def build_has_manys
93
- @has_manys.map { |options| build_has_many(options) }
94
- end
95
-
96
66
  def build_has_many(options)
97
67
  options[:child_class] ||= @defaults_generator.child_class(options[:name])
98
68
  options[:fk] ||= @defaults_generator.foreign_key(@domain_class.name)
@@ -100,10 +70,6 @@ module Vorpal
100
70
  Vorpal::HasManyConfig.new(options)
101
71
  end
102
72
 
103
- def build_has_ones
104
- @has_ones.map { |options| build_has_one(options) }
105
- end
106
-
107
73
  def build_has_one(options)
108
74
  options[:child_class] ||= @defaults_generator.child_class(options[:name])
109
75
  options[:fk] ||= @defaults_generator.foreign_key(@domain_class.name)
@@ -111,10 +77,6 @@ module Vorpal
111
77
  Vorpal::HasOneConfig.new(options)
112
78
  end
113
79
 
114
- def build_belongs_tos
115
- @belongs_tos.map { |options| build_belongs_to(options) }
116
- end
117
-
118
80
  def build_belongs_to(options)
119
81
  child_class = options[:child_classes] || options[:child_class] || @defaults_generator.child_class(options[:name])
120
82
  options[:child_classes] = Array(child_class)
@@ -4,54 +4,143 @@ require 'vorpal/driver/postgresql'
4
4
 
5
5
  module Vorpal
6
6
  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}.
7
+ # Implements the Vorpal DSL.
14
8
  #
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.
9
+ # ```ruby
10
+ # engine = Vorpal.define do
11
+ # map Tree do
12
+ # attributes :name
13
+ # belongs_to :trunk
14
+ # has_many :branches
15
+ # end
23
16
  #
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.
17
+ # map Trunk do
18
+ # attributes :length
19
+ # has_one :tree
20
+ # end
31
21
  #
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.
22
+ # map Branch do
23
+ # attributes :length
24
+ # belongs_to :tree
25
+ # end
26
+ # end
36
27
  #
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
28
+ # mapper = engine.mapper_for(Tree)
29
+ # ```
30
+ module Configuration
31
+ # Configures and creates a {Engine} instance.
32
+ #
33
+ # @param options [Hash] Global configuration options for the engine instance.
34
+ # @option options [Object] :db_driver (Object that will be used to interact with the DB.)
35
+ # Must be duck-type compatible with {Postgresql}.
36
+ #
37
+ # @return [Engine] Instance of the mapping engine.
38
+ def define(options={}, &block)
39
+ @main_config = MainConfig.new
40
+ instance_exec(&block)
41
+ @main_config.initialize_association_configs
42
+ db_driver = options.fetch(:db_driver, Driver::Postgresql.new)
43
+ engine = Engine.new(db_driver, @main_config)
44
+ @main_config = nil # make sure this MainConfig is never re-used by accident.
45
+ engine
46
+ end
41
47
 
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
48
+ # Maps a domain class to a relational table.
49
+ #
50
+ # @param domain_class [Class] Type of the domain model to be mapped
51
+ # @param options [Hash] Configure how to map the domain model
52
+ # @option options [String] :to
53
+ # Class of the ActiveRecord object that will map this domain class to the DB.
54
+ # Optional, if one is not specified, it will be generated.
55
+ # @option options [Object] :serializer (map the {ConfigBuilder#attributes} directly)
56
+ # Object that will convert the domain objects into a hash.
57
+ #
58
+ # Must have a `(Hash) serialize(Object)` method.
59
+ # @option options [Object] :deserializer (map the {ConfigBuilder#attributes} directly)
60
+ # Object that will set a hash of attribute_names->values onto a new domain
61
+ # object.
62
+ #
63
+ # Must have a `(Object) deserialize(Object, Hash)` method.
64
+ # @option options [Symbol] :primary_key_type [:serial, :uuid] (:serial)
65
+ # The type of primary key for the class. :serial for auto-incrementing integer, :uuid for a UUID
66
+ # @option options [Symbol] :id
67
+ # Same as :primary_key_type. Exists for compatibility with the Rails API.
68
+ def map(domain_class, options={}, &block)
69
+ class_config = build_class_config(domain_class, options, &block)
70
+ @main_config.add_class_config(class_config)
71
+ class_config
72
+ end
48
73
 
49
- # @private
50
- def build_config(&block)
51
- @class_configs = []
52
- self.instance_exec(&block)
53
- MasterConfig.new(@class_configs)
74
+ # @private
75
+ def build_class_config(domain_class, options, &block)
76
+ @builder = ConfigBuilder.new(domain_class, options, Driver::Postgresql.new)
77
+ instance_exec(&block) if block_given?
78
+ class_config = @builder.build
79
+ @builder = nil # make sure this ConfigBuilder is never re-used by accident.
80
+ class_config
81
+ end
82
+
83
+ # Maps the given attributes to and from the domain object and the DB. Not needed
84
+ # if a serializer and deserializer were provided.
85
+ def attributes(*attributes)
86
+ @builder.attributes(*attributes)
87
+ end
88
+
89
+ # Defines a one-to-many association to another type where the foreign key is stored on the child.
90
+ #
91
+ # In Object-Oriented programming, associations are *directed*. This means that they can only be
92
+ # traversed in one direction: from the type that defines the association (the one with the
93
+ # getter) to the type that is associated. They end that defines the association is called the
94
+ # 'Parent' and the end that is associated is called the 'Child'.
95
+ #
96
+ # @param name [String] Name of the association getter.
97
+ # @param options [Hash]
98
+ # @option options [Boolean] :owned (True) True if the child 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 (Parent class name converted to snakecase and appended with a '_id') The name of the DB column on the child that contains the foreign key reference to the parent.
100
+ # @option options [String] :fk_type The name of the DB column on the child that contains the parent class name. Only needed when there is an association from the child side that is polymorphic.
101
+ # @option options [Class] :child_class (name converted to a Class) The child class.
102
+ def has_many(name, options={})
103
+ @builder.has_many(name, options)
104
+ end
105
+
106
+ # Defines a one-to-one association to another type where the foreign key
107
+ # is stored on the child.
108
+ #
109
+ # In Object-Oriented programming, associations are *directed*. This means that they can only be
110
+ # traversed in one direction: from the type that defines the association (the one with the
111
+ # getter) to the type that is associated. They end that defines the association is called the
112
+ # 'Parent' and the end that is associated is called the 'Child'.
113
+ #
114
+ # @param name [String] Name of the association getter.
115
+ # @param options [Hash]
116
+ # @option options [Boolean] :owned (True) True if the child type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted.
117
+ # @option options [String] :fk (Parent class name converted to snakecase and appended with a '_id') The name of the DB column on the child that contains the foreign key reference to the parent.
118
+ # @option options [String] :fk_type The name of the DB column on the child that contains the parent class name. Only needed when there is an association from the child side that is polymorphic.
119
+ # @option options [Class] :child_class (name converted to a Class) The child class.
120
+ def has_one(name, options={})
121
+ @builder.has_one(name, options)
122
+ end
123
+
124
+ # Defines a one-to-one association with another type where the foreign key
125
+ # is stored on the parent.
126
+ #
127
+ # This association can be polymorphic. I.E. children can be of different types.
128
+ #
129
+ # In Object-Oriented programming, associations are *directed*. This means that they can only be
130
+ # traversed in one direction: from the type that defines the association (the one with the
131
+ # getter) to the type that is associated. They end that defines the association is called the
132
+ # 'Parent' and the end that is associated is called the 'Child'.
133
+ #
134
+ # @param name [String] Name of the association getter.
135
+ # @param options [Hash]
136
+ # @option options [Boolean] :owned (True) True if the child type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted.
137
+ # @option options [String] :fk (Child class name converted to snakecase and appended with a '_id') The name of the DB column on the parent that contains the foreign key reference to the child.
138
+ # @option options [String] :fk_type The name of the DB column on the parent that contains the child class name. Only needed when the association is polymorphic.
139
+ # @option options [Class] :child_class (name converted to a Class) The child class.
140
+ # @option options [[Class]] :child_classes The list of possible classes that can be children. This is for polymorphic associations. Takes precedence over `:child_class`.
141
+ def belongs_to(name, options={})
142
+ @builder.belongs_to(name, options)
143
+ end
54
144
  end
55
145
  end
56
- end
57
146
  end
@@ -1,6 +1,6 @@
1
1
  require 'simple_serializer/serializer'
2
2
  require 'simple_serializer/deserializer'
3
- require 'active_support/inflector/methods'
3
+ require 'active_support'
4
4
  require 'active_support/core_ext/module/introspection'
5
5
 
6
6
  module Vorpal
@@ -36,9 +36,19 @@ module Vorpal
36
36
  end
37
37
 
38
38
  def child_class(association_name)
39
- # Module#parent comes from 'active_support/core_ext/module/introspection'
40
- parent_module = @domain_class.parent
41
- parent_module.const_get(ActiveSupport::Inflector.classify(association_name.to_s))
39
+ module_parent.const_get(ActiveSupport::Inflector.classify(association_name.to_s))
40
+ end
41
+
42
+ private
43
+
44
+ def module_parent
45
+ if (ActiveSupport::VERSION::MAJOR == 5)
46
+ # Module#parent comes from 'active_support/core_ext/module/introspection'
47
+ @domain_class.parent
48
+ else
49
+ # Module#module_parent comes from 'active_support/core_ext/module/introspection'
50
+ @domain_class.module_parent
51
+ end
42
52
  end
43
53
  end
44
54
  end
@@ -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)
@@ -150,10 +159,10 @@ module Vorpal
150
159
  def serialize_object(object, config, loaded_db_objects)
151
160
  if config.serialization_required?
152
161
  attributes = config.serialize(object)
153
- if object.id.nil?
162
+ db_object = loaded_db_objects.find_by_id(config, object.id)
163
+ if object.id.nil? || db_object.nil? # object doesn't exist in the DB
154
164
  config.build_db_object(attributes)
155
165
  else
156
- db_object = loaded_db_objects.find_by_id(config, object.id)
157
166
  config.set_db_object_attributes(db_object, attributes)
158
167
  db_object
159
168
  end
@@ -165,7 +174,11 @@ module Vorpal
165
174
  def set_primary_keys(owned_objects, mapping)
166
175
  owned_objects.each do |config, objects|
167
176
  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)
177
+ if config.primary_key_type == :uuid
178
+ primary_keys = Array.new(in_need_of_primary_keys.length) { SecureRandom.uuid }
179
+ elsif config.primary_key_type == :serial
180
+ primary_keys = @db_driver.get_primary_keys(config.db_class, in_need_of_primary_keys.length)
181
+ end
169
182
  in_need_of_primary_keys.zip(primary_keys).each do |object, primary_key|
170
183
  mapping[object].id = primary_key
171
184
  object.id = primary_key
@@ -1,22 +1,23 @@
1
1
  module Vorpal
2
+ # Maps DB rows to Entities
2
3
  class IdentityMap
3
4
  def initialize
4
5
  @entities = {}
5
6
  end
6
7
 
7
- def get(key_object)
8
- @entities[key(key_object)]
8
+ def get(db_row)
9
+ @entities[key(db_row)]
9
10
  end
10
11
 
11
- def set(key_object, object)
12
- @entities[key(key_object)] = object
12
+ def set(db_row, entity)
13
+ @entities[key(db_row)] = entity
13
14
  end
14
15
 
15
- def get_and_set(key_object)
16
- object = get(key_object)
17
- object = yield if object.nil?
18
- set(key_object, object)
19
- object
16
+ def get_and_set(db_row)
17
+ entity = get(db_row)
18
+ entity = yield if entity.nil?
19
+ set(db_row, entity)
20
+ entity
20
21
  end
21
22
 
22
23
  def map(key_objects)
@@ -25,11 +26,11 @@ module Vorpal
25
26
 
26
27
  private
27
28
 
28
- def key(key_object)
29
- return nil unless key_object
30
- raise "Cannot put entity '#{key_object.inspect}' into IdentityMap without an id." if key_object.id.nil?
31
- raise "Cannot put entity '#{key_object.inspect}' into IdentityMap without a Class with a name." if key_object.class.name.nil?
32
- [key_object.id, key_object.class.name]
29
+ def key(db_row)
30
+ return nil unless db_row
31
+ raise "Cannot map a DB row without an id '#{db_row.inspect}' to an entity." if db_row.id.nil?
32
+ raise "Cannot map a DB row without a Class with a name '#{db_row.inspect}' to an entity." if db_row.class.name.nil?
33
+ [db_row.id, db_row.class.name]
33
34
  end
34
35
  end
35
36
  end
@@ -1,3 +1,3 @@
1
1
  module Vorpal
2
- VERSION = "1.0.1"
2
+ VERSION = "1.2.1"
3
3
  end
@@ -13,22 +13,22 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = "https://github.com/nulogy/vorpal"
14
14
  spec.license = "MIT"
15
15
 
16
- spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.files = Dir["CHANGELOG.md", "LICENSE.txt", "README.md", "vorpal.gemspec", "lib/**/*"]
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_runtime_dependency "simple_serializer", "~> 1.0"
22
22
  spec.add_runtime_dependency "equalizer"
23
23
  spec.add_runtime_dependency "activesupport"
24
24
 
25
- spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rake", "~> 13"
26
26
  spec.add_development_dependency "rspec", "~> 3.0"
27
- spec.add_development_dependency "virtus", "~> 1.0"
28
27
  spec.add_development_dependency "appraisal", "~> 2.2"
29
28
 
30
- spec.required_ruby_version = ">= 2.1.6"
31
- spec.add_development_dependency "activerecord", "~> 4.1.0"
32
- spec.add_development_dependency "pg", "~> 0.17.0"
33
- spec.add_development_dependency "activerecord-import", "~> 0.10.0"
29
+ spec.required_ruby_version = ">= 2.5.7"
30
+ spec.add_development_dependency "activerecord"
31
+ spec.add_development_dependency "pg"
32
+ spec.add_development_dependency "activerecord-import"
33
+ spec.add_development_dependency "codecov"
34
34
  end