rom-factory 0.10.2 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.devtools/templates/changelog.erb +3 -0
  3. data/.devtools/templates/release.erb +36 -0
  4. data/.github/FUNDING.yml +1 -0
  5. data/.github/ISSUE_TEMPLATE/{---bug-report.md → bug-report.md} +6 -10
  6. data/.github/ISSUE_TEMPLATE/config.yml +5 -0
  7. data/.github/SUPPORT.md +3 -0
  8. data/.github/workflows/ci.yml +17 -25
  9. data/.github/workflows/docsite.yml +6 -6
  10. data/.github/workflows/rubocop.yml +46 -0
  11. data/.github/workflows/sync_configs.yml +7 -13
  12. data/.repobot.yml +24 -0
  13. data/.rubocop.yml +135 -16
  14. data/CHANGELOG.md +64 -0
  15. data/CODEOWNERS +1 -0
  16. data/CONTRIBUTING.md +3 -3
  17. data/Gemfile +20 -19
  18. data/Gemfile.devtools +6 -5
  19. data/LICENSE +1 -1
  20. data/README.md +3 -3
  21. data/Rakefile +1 -1
  22. data/benchmarks/basic.rb +10 -10
  23. data/changelog.yml +35 -0
  24. data/docsite/source/index.html.md +162 -1
  25. data/lib/rom/factory/attribute_registry.rb +6 -2
  26. data/lib/rom/factory/attributes/association.rb +127 -29
  27. data/lib/rom/factory/attributes/callable.rb +1 -1
  28. data/lib/rom/factory/attributes/value.rb +1 -1
  29. data/lib/rom/factory/attributes.rb +4 -6
  30. data/lib/rom/factory/builder/persistable.rb +21 -6
  31. data/lib/rom/factory/builder.rb +17 -19
  32. data/lib/rom/factory/constants.rb +1 -1
  33. data/lib/rom/factory/dsl.rb +43 -25
  34. data/lib/rom/factory/factories.rb +13 -15
  35. data/lib/rom/factory/registry.rb +2 -2
  36. data/lib/rom/factory/sequences.rb +4 -3
  37. data/lib/rom/factory/tuple_evaluator.rb +92 -43
  38. data/lib/rom/factory/version.rb +1 -1
  39. data/lib/rom/factory.rb +8 -5
  40. data/lib/rom-factory.rb +1 -1
  41. data/project.yml +1 -0
  42. data/rom-factory.gemspec +9 -10
  43. metadata +40 -26
  44. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
  45. data/.github/ISSUE_TEMPLATE/----please-don-t-report-feature-requests-via-issues.md +0 -10
  46. data/.github/ISSUE_TEMPLATE/---a-detailed-bug-report.md +0 -30
  47. data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
  48. data/.github/workflows/custom/ci.yml +0 -26
  49. data/Appraisals +0 -9
  50. data/LICENSE.txt +0 -21
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'delegate'
3
+ require "delegate"
4
4
 
5
5
  module ROM
6
6
  module Factory
@@ -21,11 +21,10 @@ module ROM
21
21
  end
22
22
 
23
23
  # @api private
24
- def create(*args)
25
- traits, attrs = builder.extract_tuple(args)
26
-
27
- tuple = tuple(*traits, attrs)
24
+ def create(*traits, **attrs)
28
25
  validate_keys(traits, attrs)
26
+
27
+ tuple = tuple(*traits, **attrs)
29
28
  persisted = persist(tuple)
30
29
 
31
30
  if tuple_evaluator.has_associations?(traits)
@@ -43,13 +42,29 @@ module ROM
43
42
 
44
43
  # @api private
45
44
  def persist(attrs)
46
- relation.with(auto_struct: !tuple_evaluator.has_associations?).command(:create).call(attrs)
45
+ result = relation
46
+ .with(auto_struct: !tuple_evaluator.has_associations?)
47
+ .command(:create)
48
+ .call(attrs)
49
+
50
+ # Handle PK values generated by the factory
51
+ if pk? && (pks = attrs.values_at(*primary_key_names)).compact.size == primary_key_names.size
52
+ relation.by_pk(*pks).one!
53
+ elsif result
54
+ result
55
+ else
56
+ relation.where(attrs).one!
57
+ end
47
58
  end
48
59
 
49
60
  # @api private
50
61
  def primary_key_names
51
62
  relation.schema.primary_key.map(&:name)
52
63
  end
64
+
65
+ def pk?
66
+ primary_key_names.any?
67
+ end
53
68
  end
54
69
  end
55
70
  end
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/constants'
4
-
5
- require 'rom/struct'
6
- require 'rom/initializer'
7
- require 'rom/factory/tuple_evaluator'
8
- require 'rom/factory/builder/persistable'
3
+ require "rom/struct"
4
+ require "rom/initializer"
5
+ require "rom/factory/tuple_evaluator"
6
+ require "rom/factory/builder/persistable"
9
7
 
10
8
  module ROM::Factory
11
9
  # @api private
@@ -30,19 +28,20 @@ module ROM::Factory
30
28
  # @return [Module] Custom struct namespace
31
29
  option :struct_namespace, reader: false
32
30
 
33
- # @api private
34
- def tuple(*args)
35
- traits, attrs = extract_tuple(args)
31
+ # @!attribute [r] factories
32
+ # @return [Module] Factories with other builders
33
+ option :factories, reader: true, optional: true
36
34
 
35
+ # @api private
36
+ def tuple(*traits, **attrs)
37
37
  tuple_evaluator.defaults(traits, attrs)
38
38
  end
39
39
 
40
40
  # @api private
41
- def struct(*args)
42
- traits, attrs = extract_tuple(args)
41
+ def struct(*traits, **attrs)
43
42
  validate_keys(traits, attrs, allow_associations: true)
44
43
 
45
- tuple_evaluator.struct(*args)
44
+ tuple_evaluator.struct(*traits, **attrs)
46
45
  end
47
46
  alias_method :create, :struct
48
47
 
@@ -62,7 +61,11 @@ module ROM::Factory
62
61
 
63
62
  # @api private
64
63
  def tuple_evaluator
65
- @__tuple_evaluator__ ||= TupleEvaluator.new(attributes, tuple_evaluator_relation, traits)
64
+ @__tuple_evaluator__ ||= TupleEvaluator.new(
65
+ attributes,
66
+ tuple_evaluator_relation,
67
+ traits
68
+ )
66
69
  end
67
70
 
68
71
  # @api private
@@ -75,18 +78,13 @@ module ROM::Factory
75
78
  tuple_evaluator.relation
76
79
  end
77
80
 
78
- # @api private
79
- def extract_tuple(args)
80
- tuple_evaluator.extract_tuple(args)
81
- end
82
-
83
81
  # @api private
84
82
  def validate_keys(traits, tuple, allow_associations: false)
85
83
  schema_keys = relation.schema.attributes.map(&:name)
86
84
  assoc_keys = tuple_evaluator.assoc_names(traits)
87
85
  unknown_keys = tuple.keys - schema_keys - assoc_keys
88
86
 
89
- unknown_keys = unknown_keys - relation.schema.associations.to_h.keys if allow_associations
87
+ unknown_keys -= relation.schema.associations.to_h.keys if allow_associations
90
88
 
91
89
  raise UnknownFactoryAttributes, unknown_keys unless unknown_keys.empty?
92
90
  end
@@ -10,7 +10,7 @@ module ROM
10
10
 
11
11
  class UnknownFactoryAttributes < StandardError
12
12
  def initialize(attrs)
13
- super("Unknown attributes: #{attrs.join(', ')}")
13
+ super("Unknown attributes: #{attrs.join(", ")}")
14
14
  end
15
15
  end
16
16
  end
@@ -1,26 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'faker'
4
- require 'dry/core/inflector'
3
+ require "faker"
5
4
 
6
- require 'rom/factory/builder'
7
- require 'rom/factory/attribute_registry'
8
- require 'rom/factory/attributes'
5
+ require "rom/factory/builder"
6
+ require "rom/factory/attribute_registry"
7
+ require "rom/factory/attributes"
9
8
 
10
9
  module ROM
11
10
  module Factory
11
+ extend ::Dry::Core::Cache
12
+
12
13
  class << self
13
14
  # @api private
14
- def fake(type, *args)
15
- api = ::Faker.const_get(::Dry::Core::Inflector.classify(type.to_s))
15
+ def fake(*args, **options)
16
+ api = fetch_or_store(:faker, *args) do
17
+ *ns, method_name = args
16
18
 
17
- if args[0].is_a?(Symbol)
18
- api.public_send(*args)
19
- else
20
- api.public_send(type, *args)
19
+ const = ns.reduce(::Faker) do |obj, name|
20
+ obj.const_get(::Dry::Core::Inflector.camelize(name))
21
+ end
22
+
23
+ const.method(method_name)
21
24
  end
25
+
26
+ api.(**options)
22
27
  end
23
- ruby2_keywords(:fake) if respond_to?(:ruby2_keywords, true)
24
28
  end
25
29
 
26
30
  # Factory builder DSL
@@ -33,7 +37,7 @@ module ROM
33
37
  attr_reader :_traits
34
38
 
35
39
  # @api private
36
- def initialize(name, attributes: AttributeRegistry.new, relation:, factories:, struct_namespace:)
40
+ def initialize(name, relation:, factories:, struct_namespace:, attributes: AttributeRegistry.new)
37
41
  @_name = name
38
42
  @_relation = relation
39
43
  @_factories = factories
@@ -46,7 +50,13 @@ module ROM
46
50
 
47
51
  # @api private
48
52
  def call
49
- ::ROM::Factory::Builder.new(_attributes, _traits, relation: _relation, struct_namespace: _struct_namespace)
53
+ ::ROM::Factory::Builder.new(
54
+ _attributes,
55
+ _traits,
56
+ relation: _relation,
57
+ struct_namespace: _struct_namespace,
58
+ factories: _factories
59
+ )
50
60
  end
51
61
 
52
62
  # Delegate to a builder and persist a struct
@@ -83,28 +93,36 @@ module ROM
83
93
  #
84
94
  # @param [Symbol] type The value type to generate
85
95
  #
86
- # @overload fake(api, type)
96
+ # @overload fake(genre, type)
87
97
  # @example
88
98
  # f.email { fake(:internet, :email) }
89
99
  #
90
- # @param [Symbol] api The faker API identifier ie. :internet, :product etc.
100
+ # @param [Symbol] genre The faker API identifier ie. :internet, :product etc.
91
101
  # @param [Symbol] type The value type to generate
92
102
  #
93
- # @overload fake(api, type, *args)
103
+ # @overload fake(genre, type, **options)
94
104
  # @example
95
- # f.email { fake(:number, :between, 10, 100) }
105
+ # f.email { fake(:number, :between, from: 10, to: 100) }
96
106
  #
97
- # @param [Symbol] api The faker API identifier ie. :internet, :product etc.
107
+ # @param [Symbol] genre The faker API identifier ie. :internet, :product etc.
98
108
  # @param [Symbol] type The value type to generate
99
- # @param [Array] args Additional arguments
109
+ # @param [Hash] options Additional arguments
110
+ #
111
+ # @overload fake(genre, subgenre, type, **options)
112
+ # @example
113
+ # f.quote { fake(:books, :dune, :quote, character: 'stilgar') }
114
+ #
115
+ # @param [Symbol] genre The Faker genre of API i.e. :books, :creature, :games etc
116
+ # @param [Symbol] subgenre The subgenre of API i.e. :dune, :bird, :myst etc
117
+ # @param [Symbol] type the value type to generate
118
+ # @param [Hash] options Additional arguments
100
119
  #
101
- # @see https://github.com/stympy/faker/tree/master/doc
120
+ # @see https://github.com/faker-ruby/faker/tree/master/doc
102
121
  #
103
122
  # @api public
104
- def fake(*args)
105
- ::ROM::Factory.fake(*args)
123
+ def fake(type, *args, **options)
124
+ ::ROM::Factory.fake(type, *args, **options)
106
125
  end
107
- ruby2_keywords(:fake) if respond_to?(:ruby2_keywords, true)
108
126
 
109
127
  def trait(name, parents = [], &block)
110
128
  _traits[name] = DSL.new(
@@ -136,7 +154,7 @@ module ROM
136
154
  assoc = _relation.associations[name]
137
155
 
138
156
  if assoc.is_a?(::ROM::SQL::Associations::OneToOne) && options.fetch(:count, 1) > 1
139
- ::Kernel.raise ::ArgumentError, 'count cannot be greater than 1 on a OneToOne'
157
+ ::Kernel.raise ::ArgumentError, "count cannot be greater than 1 on a OneToOne"
140
158
  end
141
159
 
142
160
  builder = -> { _factories.for_relation(assoc.target) }
@@ -1,12 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/configurable'
4
- require 'dry/core/inflector'
5
-
6
- require 'rom/initializer'
7
- require 'rom/struct'
8
- require 'rom/factory/dsl'
9
- require 'rom/factory/registry'
3
+ require "rom/initializer"
4
+ require "rom/struct"
5
+ require "rom/factory/dsl"
6
+ require "rom/factory/registry"
10
7
 
11
8
  module ROM::Factory
12
9
  # In-memory builder API
@@ -42,7 +39,7 @@ module ROM::Factory
42
39
  #
43
40
  # @api public
44
41
  def [](name, *traits, **attrs)
45
- registry[name].struct_namespace(struct_namespace).create(*traits, attrs)
42
+ registry[name].struct_namespace(struct_namespace).create(*traits, **attrs)
46
43
  end
47
44
  end
48
45
 
@@ -147,7 +144,7 @@ module ROM::Factory
147
144
  name,
148
145
  relation: relation,
149
146
  factories: self,
150
- struct_namespace: builder_sturct_namespace(namespace),
147
+ struct_namespace: builder_struct_namespace(namespace),
151
148
  &block
152
149
  ).call
153
150
  end
@@ -164,13 +161,14 @@ module ROM::Factory
164
161
  # MyFactory[:user, name: "Jane"]
165
162
  #
166
163
  # @param [Symbol] name The name of the registered factory
167
- # @param [Hash] attrs An optional hash with attributes
164
+ # @param [Array<Symbol>] traits List of traits to apply
165
+ # @param [Hash] attrs optional attributes to override the defaults
168
166
  #
169
167
  # @return [ROM::Struct]
170
168
  #
171
169
  # @api public
172
- def [](name, *args)
173
- registry[name].struct_namespace(struct_namespace).persistable.create(*args)
170
+ def [](name, *traits, **attrs)
171
+ registry[name].struct_namespace(struct_namespace).persistable.create(*traits, **attrs)
174
172
  end
175
173
 
176
174
  # Return in-memory struct builder
@@ -204,8 +202,8 @@ module ROM::Factory
204
202
  end
205
203
 
206
204
  # @api private
207
- def builder_sturct_namespace(ns)
208
- ns ? { namespace: ns, overridable: false } : { namespace: struct_namespace, overridable: true }
205
+ def builder_struct_namespace(ns)
206
+ ns ? {namespace: ns, overridable: false} : {namespace: struct_namespace, overridable: true}
209
207
  end
210
208
 
211
209
  # @api private
@@ -226,7 +224,7 @@ module ROM::Factory
226
224
  # @api private
227
225
  def extend_builder(name, parent, relation_name, ns, &block)
228
226
  namespace = parent.options[:struct_namespace]
229
- namespace = builder_sturct_namespace(ns) if ns
227
+ namespace = builder_struct_namespace(ns) if ns
230
228
  relation = rom.relations.fetch(relation_name) { parent.relation }
231
229
  DSL.new(
232
230
  name,
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rom/factory/constants'
3
+ require "rom/factory/constants"
4
4
 
5
5
  module ROM
6
6
  module Factory
@@ -28,7 +28,7 @@ module ROM
28
28
  # @api private
29
29
  def [](name)
30
30
  elements.fetch(name) do
31
- raise FactoryNotDefinedError.new(name)
31
+ raise FactoryNotDefinedError, name
32
32
  end
33
33
  end
34
34
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
3
+ require "concurrent/map"
4
+ require "singleton"
4
5
 
5
6
  module ROM
6
7
  module Factory
@@ -24,12 +25,12 @@ module ROM
24
25
 
25
26
  # @api private
26
27
  def next(key)
27
- registry[key] += 1
28
+ registry.compute(key) { |v| (v || 0).succ }
28
29
  end
29
30
 
30
31
  # @api private
31
32
  def reset
32
- @registry = Concurrent::Map.new { |h, k| h[k] = 0 }
33
+ @registry = Concurrent::Map.new
33
34
  self
34
35
  end
35
36
  end
@@ -1,11 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rom/factory/sequences'
3
+ require "rom/factory/sequences"
4
4
 
5
5
  module ROM
6
6
  module Factory
7
7
  # @api private
8
8
  class TupleEvaluator
9
+ class TupleEvaluatorError < StandardError
10
+ attr_reader :original_exception
11
+
12
+ def initialize(relation, original_exception, attrs, traits, assoc_attrs)
13
+ super(<<~STR)
14
+ Failed to build attributes for #{relation.name}
15
+
16
+ Attributes:
17
+ #{attrs.inspect}
18
+
19
+ Associations:
20
+ #{assoc_attrs}
21
+
22
+ Traits:
23
+ #{traits.inspect}
24
+
25
+ Original exception: #{original_exception.message}
26
+ STR
27
+
28
+ set_backtrace(original_exception.backtrace)
29
+ end
30
+ end
31
+
9
32
  # @api private
10
33
  attr_reader :attributes
11
34
 
@@ -15,9 +38,6 @@ module ROM
15
38
  # @api private
16
39
  attr_reader :traits
17
40
 
18
- # @api private
19
- attr_reader :model
20
-
21
41
  # @api private
22
42
  attr_reader :sequence
23
43
 
@@ -26,18 +46,21 @@ module ROM
26
46
  @attributes = attributes
27
47
  @relation = relation.with(auto_struct: true)
28
48
  @traits = traits
29
- @model = @relation.combine(*assoc_names).mapper.model
30
49
  @sequence = Sequences[relation]
31
50
  end
32
51
 
52
+ def model(traits, combine: assoc_names(traits))
53
+ @relation.combine(*combine).mapper.model
54
+ end
55
+
33
56
  # @api private
34
- def defaults(traits, attrs, opts = EMPTY_HASH)
35
- evaluate(traits, attrs, opts).merge(attrs)
57
+ def defaults(traits, attrs, **opts)
58
+ mergeable_attrs = select_mergeable_attrs(traits, attrs)
59
+ evaluate(traits, attrs, opts).merge(mergeable_attrs)
36
60
  end
37
61
 
38
62
  # @api private
39
- def struct(*args)
40
- traits, attrs = extract_tuple(args)
63
+ def struct(*traits, **attrs)
41
64
  merged_attrs = struct_attrs.merge(defaults(traits, attrs, persist: false))
42
65
  is_callable = proc { |_name, value| value.respond_to?(:call) }
43
66
 
@@ -45,21 +68,41 @@ module ROM
45
68
  attributes = merged_attrs.reject(&is_callable)
46
69
 
47
70
  materialized_callables = {}
48
- callables.each do |_name, callable|
71
+ callables.each_value do |callable|
49
72
  materialized_callables.merge!(callable.call(attributes, persist: false))
50
73
  end
51
74
 
52
75
  attributes.merge!(materialized_callables)
53
76
 
54
- associations = assoc_names
55
- .map { |key| [key, attributes[key]] if attributes.key?(key) }
56
- .compact
77
+ assoc_attrs = attributes.slice(*assoc_names(traits)).merge(
78
+ assoc_names(traits)
79
+ .select { |key|
80
+ build_assoc?(key, attributes)
81
+ }
82
+ .map { |key|
83
+ [key, build_assoc_attrs(key, attributes[relation.primary_key], attributes[key])]
84
+ }
57
85
  .to_h
86
+ )
87
+
88
+ model_attrs = relation.output_schema[attributes]
89
+ model_attrs.update(assoc_attrs)
90
+
91
+ model(traits).new(**model_attrs)
92
+ rescue StandardError => e
93
+ raise TupleEvaluatorError.new(relation, e, attrs, traits, assoc_attrs)
94
+ end
58
95
 
59
- attributes = relation.output_schema[attributes]
60
- attributes.update(associations)
96
+ def build_assoc?(name, attributes)
97
+ attributes.key?(name) && attributes[name] != [] && !attributes[name].nil?
98
+ end
61
99
 
62
- model.new(attributes)
100
+ def build_assoc_attrs(key, fk, value)
101
+ if value.is_a?(Array)
102
+ value.map { |el| build_assoc_attrs(key, fk, el) }
103
+ else
104
+ {attributes[key].foreign_key => fk}.merge(value.to_h)
105
+ end
63
106
  end
64
107
 
65
108
  # @api private
@@ -76,10 +119,15 @@ module ROM
76
119
  end
77
120
 
78
121
  def assocs(traits_names = [])
79
- traits
122
+ found_assocs = traits
80
123
  .values_at(*traits_names)
124
+ .compact
81
125
  .map(&:associations).flat_map(&:elements)
82
126
  .inject(AttributeRegistry.new(attributes.associations.elements), :<<)
127
+
128
+ exclude = traits_names.select { |t| t.is_a?(Hash) }.reduce(:merge) || EMPTY_HASH
129
+
130
+ found_assocs.reject { |a| exclude[a.name] == false }
83
131
  end
84
132
 
85
133
  # @api private
@@ -92,28 +140,17 @@ module ROM
92
140
  relation.primary_key
93
141
  end
94
142
 
95
- # @api private
96
- def extract_tuple(args)
97
- if !args.empty? && args.last.is_a?(::Hash)
98
- *traits, attrs = args
99
-
100
- [traits, attrs]
101
- else
102
- [args, EMPTY_HASH]
103
- end
104
- end
105
-
106
143
  private
107
144
 
108
145
  # @api private
109
146
  def evaluate(traits, attrs, opts)
110
- evaluate_values(attrs, opts)
111
- .merge(evaluate_associations(attrs, opts))
147
+ evaluate_values(attrs)
148
+ .merge(evaluate_associations(traits, attrs, opts))
112
149
  .merge(evaluate_traits(traits, attrs, opts))
113
150
  end
114
151
 
115
152
  # @api private
116
- def evaluate_values(attrs, opts)
153
+ def evaluate_values(attrs)
117
154
  attributes.values.tsort.each_with_object({}) do |attr, h|
118
155
  deps = attr.dependency_names.map { |k| h[k] }.compact
119
156
  result = attr.(attrs, *deps)
@@ -124,33 +161,38 @@ module ROM
124
161
  end
125
162
  end
126
163
 
127
- def evaluate_traits(traits, attrs, opts)
128
- return {} if traits.empty?
164
+ def evaluate_traits(trait_list, attrs, opts)
165
+ return {} if trait_list.empty?
166
+
167
+ traits = trait_list.map { |v| v.is_a?(Hash) ? v : {v => true} }.reduce(:merge)
129
168
 
130
- traits_attrs = self.traits.values_at(*traits).flat_map(&:elements)
169
+ traits_attrs = self.traits.select { |key, _value| traits[key] }.values.flat_map(&:elements)
131
170
  registry = AttributeRegistry.new(traits_attrs)
132
- self.class.new(registry, relation).defaults([], attrs, opts)
171
+
172
+ self.class.new(registry, relation).defaults([], attrs, **opts)
133
173
  end
134
174
 
135
175
  # @api private
136
- def evaluate_associations(attrs, opts)
137
- attributes.associations.each_with_object({}) do |assoc, h|
138
- if assoc.dependency?(relation)
139
- h[assoc.name] = ->(parent, call_opts) do
176
+ def evaluate_associations(traits, attrs, opts)
177
+ assocs(traits).associations.each_with_object({}) do |assoc, memo|
178
+ if attrs.key?(assoc.name) && attrs[assoc.name].nil?
179
+ memo
180
+ elsif assoc.dependency?(relation)
181
+ memo[assoc.name] = ->(parent, call_opts) do
140
182
  assoc.call(parent, **opts, **call_opts)
141
183
  end
142
184
  else
143
185
  result = assoc.(attrs, **opts)
144
- h.update(result) if result
186
+ memo.update(result) if result
145
187
  end
146
188
  end
147
189
  end
148
190
 
149
191
  # @api private
150
192
  def struct_attrs
151
- struct_attrs = relation.schema.
152
- reject(&:primary_key?).
153
- map { |attr| [attr.name, nil] }.to_h
193
+ struct_attrs = relation.schema
194
+ .reject(&:primary_key?)
195
+ .map { |attr| [attr.name, nil] }.to_h
154
196
 
155
197
  if primary_key
156
198
  struct_attrs.merge(primary_key => next_id)
@@ -163,6 +205,13 @@ module ROM
163
205
  def next_id
164
206
  sequence.()
165
207
  end
208
+
209
+ def select_mergeable_attrs(traits, attrs)
210
+ unmergeable = assocs(traits).select(&:through?).map do |a|
211
+ Dry::Core::Inflector.singularize(a.assoc.target.name.to_sym).to_sym
212
+ end
213
+ attrs.dup.delete_if { |key, _| unmergeable.include?(key) }
214
+ end
166
215
  end
167
216
  end
168
217
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ROM
4
4
  module Factory
5
- VERSION = '0.10.2'
5
+ VERSION = "0.12.0"
6
6
  end
7
7
  end
data/lib/rom/factory.rb CHANGED
@@ -1,14 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/class_builder'
4
- require 'rom/factory/factories'
3
+ require "dry/core"
4
+ require "dry/configurable"
5
+ require "dry/struct"
6
+
7
+ require "rom/factory/factories"
5
8
 
6
9
  module ROM
7
10
  # Main ROM::Factory API
8
11
  #
9
12
  # @api public
10
13
  module Factory
11
- DEFAULT_NAME = 'Factories'.freeze
14
+ DEFAULT_NAME = "Factories"
12
15
 
13
16
  # Configure a new factory
14
17
  #
@@ -23,8 +26,8 @@ module ROM
23
26
  #
24
27
  # @api public
25
28
  def self.configure(name = DEFAULT_NAME, &block)
26
- klass = Dry::Core::ClassBuilder.new(name: name, parent: Factories).call do |klass|
27
- klass.configure(&block)
29
+ klass = Dry::Core::ClassBuilder.new(name: name, parent: Factories).call do |c|
30
+ c.configure(&block)
28
31
  end
29
32
 
30
33
  klass.new(klass.config.rom)
data/lib/rom-factory.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rom/factory'
3
+ require "rom/factory"
data/project.yml CHANGED
@@ -1,2 +1,3 @@
1
1
  name: rom-factory
2
+ custom_ci: true
2
3
  codacy_id: 5fd26fae687549218458879b1a607e18