rom-factory 0.11.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,10 +3,11 @@
3
3
  module ROM::Factory
4
4
  module Attributes
5
5
  # @api private
6
+ # rubocop:disable Style/OptionalArguments
6
7
  module Association
7
8
  class << self
8
- def new(assoc, builder, *traits, **options)
9
- const_get(assoc.definition.type).new(assoc, builder, *traits, **options)
9
+ def new(assoc, ...)
10
+ const_get(assoc.definition.type).new(assoc, ...)
10
11
  end
11
12
  end
12
13
 
@@ -23,9 +24,7 @@ module ROM::Factory
23
24
  end
24
25
 
25
26
  # @api private
26
- def through?
27
- false
28
- end
27
+ def through? = false
29
28
 
30
29
  # @api private
31
30
  def builder
@@ -33,41 +32,64 @@ module ROM::Factory
33
32
  end
34
33
 
35
34
  # @api private
36
- def name
37
- assoc.key
38
- end
35
+ def name = assoc.key
39
36
 
40
37
  # @api private
41
- def dependency?(*)
42
- false
43
- end
38
+ def dependency?(*) = false
44
39
 
45
40
  # @api private
46
- def value?
47
- false
48
- end
41
+ def value? = false
42
+
43
+ # @api private
44
+ def factories = builder.factories
45
+
46
+ # @api private
47
+ def foreign_key = assoc.foreign_key
48
+
49
+ # @api private
50
+ def count = options.fetch(:count, 1)
49
51
  end
50
52
 
51
53
  # @api private
52
54
  class ManyToOne < Core
53
55
  # @api private
56
+ # rubocop:disable Metrics/AbcSize
54
57
  def call(attrs, persist: true)
55
- if attrs.key?(name) && !attrs[foreign_key]
58
+ return if attrs.key?(name) && attrs[name].nil?
59
+
60
+ assoc_data = attrs.fetch(name, EMPTY_HASH)
61
+
62
+ if assoc_data.is_a?(::Hash) && assoc_data[assoc.target.primary_key] && !attrs[foreign_key]
56
63
  assoc.associate(attrs, attrs[name])
57
- elsif !attrs[foreign_key]
58
- struct = if persist
59
- builder.persistable.create(*traits)
60
- else
61
- builder.struct(*traits)
62
- end
63
- tuple = {name => struct}
64
- assoc.associate(tuple, struct)
64
+ elsif assoc_data.is_a?(::ROM::Struct)
65
+ assoc.associate(attrs, assoc_data)
66
+ else
67
+ parent =
68
+ if persist && !attrs[foreign_key]
69
+ builder.persistable.create(*parent_traits, **assoc_data)
70
+ else
71
+ builder.struct(
72
+ *parent_traits,
73
+ **assoc_data, assoc.target.primary_key => attrs[foreign_key]
74
+ )
75
+ end
76
+
77
+ tuple = {name => parent}
78
+
79
+ assoc.associate(tuple, parent)
65
80
  end
66
81
  end
82
+ # rubocop:enable Metrics/AbcSize
67
83
 
68
- # @api private
69
- def foreign_key
70
- assoc.foreign_key
84
+ private
85
+
86
+ def parent_traits
87
+ @parent_traits ||=
88
+ if assoc.target.associations.key?(assoc.source.name)
89
+ traits + [assoc.target.associations[assoc.source.name].key => false]
90
+ else
91
+ traits
92
+ end
71
93
  end
72
94
  end
73
95
 
@@ -77,7 +99,7 @@ module ROM::Factory
77
99
  def call(attrs = EMPTY_HASH, parent, persist: true)
78
100
  return if attrs.key?(name)
79
101
 
80
- structs = Array.new(count).map do
102
+ structs = ::Array.new(count).map do
81
103
  # hash which contains the foreign key info, i.e: { user_id: 1 }
82
104
  association_hash = assoc.associate(attrs, parent)
83
105
 
@@ -92,14 +114,7 @@ module ROM::Factory
92
114
  end
93
115
 
94
116
  # @api private
95
- def dependency?(rel)
96
- assoc.source == rel
97
- end
98
-
99
- # @api private
100
- def count
101
- options.fetch(:count)
102
- end
117
+ def dependency?(rel) = assoc.source == rel
103
118
  end
104
119
 
105
120
  # @api private
@@ -113,59 +128,78 @@ module ROM::Factory
113
128
 
114
129
  association_hash = assoc.associate(attrs, parent)
115
130
 
116
- struct = if persist
117
- builder.persistable.create(*traits, **association_hash)
118
- else
119
- belongs_to_name = Dry::Core::Inflector.singularize(assoc.source_alias)
120
- belongs_to_associations = {belongs_to_name.to_sym => parent}
121
- final_attrs = attrs.merge(association_hash).merge(belongs_to_associations)
122
- builder.struct(*traits, **final_attrs)
123
- end
131
+ struct =
132
+ if persist
133
+ builder.persistable.create(*traits, **association_hash)
134
+ else
135
+ belongs_to_name = ::ROM::Inflector.singularize(assoc.source_alias)
136
+ belongs_to_associations = {belongs_to_name.to_sym => parent}
137
+ final_attrs = attrs.merge(association_hash).merge(belongs_to_associations)
138
+ builder.struct(*traits, **final_attrs)
139
+ end
124
140
 
125
141
  {name => struct}
126
142
  end
127
-
128
- # @api private
129
- def count
130
- options.fetch(:count, 1)
131
- end
132
143
  end
133
144
 
134
- class OneToOneThrough < Core
145
+ class ManyToMany < Core
135
146
  def call(attrs = EMPTY_HASH, parent, persist: true)
136
147
  return if attrs.key?(name)
137
148
 
138
- struct = if persist && attrs[tpk]
139
- attrs
140
- elsif persist
141
- builder.persistable.create(*traits, **attrs)
142
- else
143
- builder.struct(*traits, **attrs)
144
- end
149
+ structs = count.times.map do
150
+ if persist && attrs[tpk]
151
+ attrs
152
+ elsif persist
153
+ builder.persistable.create(*traits, **attrs)
154
+ else
155
+ builder.struct(*traits, **attrs)
156
+ end
157
+ end
145
158
 
146
- assoc.persist([parent], struct) if persist
159
+ # Delegate to through factory if it exists
160
+ if persist
161
+ if through_factory?
162
+ structs.each do |child|
163
+ through_attrs = {
164
+ ::ROM::Inflector.singularize(assoc.source.name.key).to_sym => parent,
165
+ assoc.through.assoc_name => child
166
+ }
167
+
168
+ factories[through_factory_name, **through_attrs]
169
+ end
170
+ else
171
+ assoc.persist([parent], structs)
172
+ end
147
173
 
148
- {name => struct}
174
+ {name => result(structs)}
175
+ else
176
+ result(structs)
177
+ end
149
178
  end
150
179
 
151
- def dependency?(rel)
152
- assoc.source == rel
180
+ def result(structs) = {name => structs}
181
+
182
+ def dependency?(rel) = assoc.source == rel
183
+
184
+ def through? = true
185
+
186
+ def through_factory?
187
+ factories.registry.key?(through_factory_name)
153
188
  end
154
189
 
155
- def through?
156
- true
190
+ def through_factory_name
191
+ ::ROM::Inflector.singularize(assoc.definition.through.source).to_sym
157
192
  end
158
193
 
159
194
  private
160
195
 
161
- def count
162
- options.fetch(:count, 1)
163
- end
196
+ def tpk = assoc.target.primary_key
197
+ end
164
198
 
165
- def tpk
166
- assoc.target.primary_key
167
- end
199
+ class OneToOneThrough < ManyToMany
200
+ def result(structs) = {name => structs[0]}
168
201
  end
169
202
  end
170
203
  end
204
+ # rubocop:enable Style/OptionalArguments
171
205
  end
@@ -28,6 +28,12 @@ module ROM::Factory
28
28
  def dependency_names
29
29
  block.parameters.map(&:last)
30
30
  end
31
+
32
+ # @api private
33
+ def inspect
34
+ "#<#{self.class.name} #{name} at #{block.source_location.join(":")}>"
35
+ end
36
+ alias_method :to_s, :inspect
31
37
  end
32
38
  end
33
39
  end
@@ -22,8 +22,9 @@ module ROM
22
22
 
23
23
  # @api private
24
24
  def create(*traits, **attrs)
25
- tuple = tuple(*traits, **attrs)
26
25
  validate_keys(traits, attrs)
26
+
27
+ tuple = tuple(*traits, **attrs)
27
28
  persisted = persist(tuple)
28
29
 
29
30
  if tuple_evaluator.has_associations?(traits)
@@ -41,13 +42,29 @@ module ROM
41
42
 
42
43
  # @api private
43
44
  def persist(attrs)
44
- 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
45
58
  end
46
59
 
47
60
  # @api private
48
61
  def primary_key_names
49
62
  relation.schema.primary_key.map(&:name)
50
63
  end
64
+
65
+ def pk?
66
+ primary_key_names.any?
67
+ end
51
68
  end
52
69
  end
53
70
  end
@@ -28,6 +28,10 @@ module ROM::Factory
28
28
  # @return [Module] Custom struct namespace
29
29
  option :struct_namespace, reader: false
30
30
 
31
+ # @!attribute [r] factories
32
+ # @return [Module] Factories with other builders
33
+ option :factories, reader: true, optional: true
34
+
31
35
  # @api private
32
36
  def tuple(*traits, **attrs)
33
37
  tuple_evaluator.defaults(traits, attrs)
@@ -57,7 +61,11 @@ module ROM::Factory
57
61
 
58
62
  # @api private
59
63
  def tuple_evaluator
60
- @__tuple_evaluator__ ||= TupleEvaluator.new(attributes, tuple_evaluator_relation, traits)
64
+ @__tuple_evaluator__ ||= TupleEvaluator.new(
65
+ attributes,
66
+ tuple_evaluator_relation,
67
+ traits
68
+ )
61
69
  end
62
70
 
63
71
  # @api private
@@ -12,26 +12,39 @@ module ROM
12
12
 
13
13
  class << self
14
14
  # @api private
15
- def fake(*args, **options)
16
- api = fetch_or_store(:faker, *args) do
15
+ def fake(*args, unique: false, **options)
16
+ factory, produce = fetch_or_store(:faker, unique, *args) do
17
17
  *ns, method_name = args
18
18
 
19
19
  const = ns.reduce(::Faker) do |obj, name|
20
- obj.const_get(::Dry::Core::Inflector.camelize(name))
20
+ obj.const_get(::ROM::Inflector.camelize(name))
21
21
  end
22
22
 
23
- const.method(method_name)
23
+ if unique
24
+ [const.unique, method_name]
25
+ else
26
+ [const, method_name]
27
+ end
24
28
  end
25
29
 
26
- api.(**options)
30
+ factory.public_send(produce, **options)
27
31
  end
28
32
  end
29
33
 
30
34
  # Factory builder DSL
31
35
  #
32
36
  # @api public
33
- class DSL < BasicObject
34
- define_method(:rand, ::Kernel.instance_method(:rand))
37
+ class DSL < ::BasicObject
38
+ # @api private
39
+ module Kernel
40
+ %i[binding class instance_of? is_a? rand respond_to_missing? singleton_class].each do |meth|
41
+ define_method(meth, ::Kernel.instance_method(meth))
42
+ end
43
+
44
+ private :respond_to_missing?, :rand, :binding
45
+ end
46
+
47
+ include Kernel
35
48
 
36
49
  attr_reader :_name, :_relation, :_attributes, :_factories, :_struct_namespace, :_valid_names
37
50
  attr_reader :_traits
@@ -50,7 +63,13 @@ module ROM
50
63
 
51
64
  # @api private
52
65
  def call
53
- ::ROM::Factory::Builder.new(_attributes, _traits, relation: _relation, struct_namespace: _struct_namespace)
66
+ ::ROM::Factory::Builder.new(
67
+ _attributes,
68
+ _traits,
69
+ relation: _relation,
70
+ struct_namespace: _struct_namespace,
71
+ factories: _factories
72
+ )
54
73
  end
55
74
 
56
75
  # Delegate to a builder and persist a struct
@@ -58,17 +77,15 @@ module ROM
58
77
  # @param [Symbol] The name of the registered builder
59
78
  #
60
79
  # @api public
61
- def create(name, *args)
62
- _factories[name, *args]
63
- end
80
+ def create(name, *args) = _factories[name, *args]
64
81
 
65
82
  # Create a sequence attribute
66
83
  #
67
84
  # @param [Symbol] name The attribute name
68
85
  #
69
86
  # @api private
70
- def sequence(meth, &block)
71
- define_sequence(meth, block) if _valid_names.include?(meth)
87
+ def sequence(meth, &)
88
+ define_sequence(meth, &) if _valid_names.include?(meth)
72
89
  end
73
90
 
74
91
  # Set timestamp attributes
@@ -98,9 +115,13 @@ module ROM
98
115
  # @example
99
116
  # f.email { fake(:number, :between, from: 10, to: 100) }
100
117
  #
118
+ # @example
119
+ # f.email { fake(:internet, :email, unique: true) }
120
+ #
101
121
  # @param [Symbol] genre The faker API identifier ie. :internet, :product etc.
102
122
  # @param [Symbol] type The value type to generate
103
- # @param [Hash] options Additional arguments
123
+ # @param [Hash] options Additional arguments, including unique: true will generate unique values
124
+ #
104
125
  #
105
126
  # @overload fake(genre, subgenre, type, **options)
106
127
  # @example
@@ -114,11 +135,9 @@ module ROM
114
135
  # @see https://github.com/faker-ruby/faker/tree/master/doc
115
136
  #
116
137
  # @api public
117
- def fake(type, *args, **options)
118
- ::ROM::Factory.fake(type, *args, **options)
119
- end
138
+ def fake(...) = ::ROM::Factory.fake(...)
120
139
 
121
- def trait(name, parents = [], &block)
140
+ def trait(name, parents = [], &)
122
141
  _traits[name] = DSL.new(
123
142
  "#{_name}_#{name}",
124
143
  attributes: _traits.values_at(*parents).flat_map(&:elements).inject(
@@ -127,7 +146,7 @@ module ROM
127
146
  relation: _relation,
128
147
  factories: _factories,
129
148
  struct_namespace: _struct_namespace,
130
- &block
149
+ &
131
150
  )._attributes
132
151
  end
133
152
 
@@ -139,12 +158,16 @@ module ROM
139
158
  # @example has-many
140
159
  # f.association(:posts, count: 2)
141
160
  #
161
+ # @example adding traits
162
+ # f.association(:posts, traits: [:published])
163
+ #
142
164
  # @param [Symbol] name The name of the configured association
143
165
  # @param [Hash] options Additional options
144
166
  # @option options [Integer] count Number of objects to generate
167
+ # @option options [Array<Symbol>] traits Traits to apply to the association
145
168
  #
146
169
  # @api public
147
- def association(name, *traits, **options)
170
+ def association(name, *seq_traits, traits: EMPTY_ARRAY, **options)
148
171
  assoc = _relation.associations[name]
149
172
 
150
173
  if assoc.is_a?(::ROM::SQL::Associations::OneToOne) && options.fetch(:count, 1) > 1
@@ -153,15 +176,25 @@ module ROM
153
176
 
154
177
  builder = -> { _factories.for_relation(assoc.target) }
155
178
 
156
- _attributes << attributes::Association.new(assoc, builder, *traits, **options)
179
+ _attributes << attributes::Association.new(
180
+ assoc,
181
+ builder,
182
+ *seq_traits,
183
+ *traits,
184
+ **options
185
+ )
157
186
  end
158
187
 
188
+ # @api private
189
+ def inspect = "#<#{self.class} name=#{_name}>"
190
+ alias_method :to_s, :inspect
191
+
159
192
  private
160
193
 
161
194
  # @api private
162
- def method_missing(meth, *args, &block)
195
+ def method_missing(meth, ...)
163
196
  if _valid_names.include?(meth)
164
- define_attr(meth, *args, &block)
197
+ define_attr(meth, ...)
165
198
  else
166
199
  super
167
200
  end
@@ -169,27 +202,26 @@ module ROM
169
202
 
170
203
  # @api private
171
204
  def respond_to_missing?(method_name, include_private = false)
172
- _valid_names.include?(meth) || super
205
+ _valid_names.include?(method_name) || super
173
206
  end
174
207
 
175
208
  # @api private
176
- def define_sequence(name, block)
177
- _attributes << attributes::Callable.new(name, self, attributes::Sequence.new(name, &block))
209
+ def define_sequence(name, &)
210
+ _attributes << attributes::Callable.new(name, self, attributes::Sequence.new(name, &))
178
211
  end
179
212
 
180
213
  # @api private
181
214
  def define_attr(name, *args, &block)
182
- _attributes << if block
183
- attributes::Callable.new(name, self, block)
184
- else
185
- attributes::Value.new(name, *args)
186
- end
215
+ _attributes <<
216
+ if block
217
+ attributes::Callable.new(name, self, block)
218
+ else
219
+ attributes::Value.new(name, *args)
220
+ end
187
221
  end
188
222
 
189
223
  # @api private
190
- def attributes
191
- ::ROM::Factory::Attributes
192
- end
224
+ def attributes = ::ROM::Factory::Attributes
193
225
  end
194
226
  end
195
227
  end
@@ -126,7 +126,7 @@ module ROM::Factory
126
126
  # @return [ROM::Factory::Builder]
127
127
  #
128
128
  # @api public
129
- def define(spec, opts = EMPTY_HASH, &block)
129
+ def define(spec, opts = EMPTY_HASH, &)
130
130
  name, parent = spec.is_a?(Hash) ? spec.flatten(1) : spec
131
131
  namespace = opts[:struct_namespace]
132
132
  relation_name = opts.fetch(:relation) { infer_relation(name) }
@@ -137,7 +137,7 @@ module ROM::Factory
137
137
 
138
138
  builder =
139
139
  if parent
140
- extend_builder(name, registry[parent], relation_name, namespace, &block)
140
+ extend_builder(name, registry[parent], relation_name, namespace, &)
141
141
  else
142
142
  relation = rom.relations[relation_name]
143
143
  DSL.new(
@@ -145,7 +145,7 @@ module ROM::Factory
145
145
  relation: relation,
146
146
  factories: self,
147
147
  struct_namespace: builder_struct_namespace(namespace),
148
- &block
148
+ &
149
149
  ).call
150
150
  end
151
151
 
@@ -170,6 +170,7 @@ module ROM::Factory
170
170
  def [](name, *traits, **attrs)
171
171
  registry[name].struct_namespace(struct_namespace).persistable.create(*traits, **attrs)
172
172
  end
173
+ alias_method :create, :[]
173
174
 
174
175
  # Return in-memory struct builder
175
176
  #
@@ -180,6 +181,25 @@ module ROM::Factory
180
181
  @__structs__ ||= Structs.new(registry, struct_namespace)
181
182
  end
182
183
 
184
+ # Return a new, non-persisted struct
185
+ #
186
+ # @example create a struct with default attributes
187
+ # MyFactory.build(:user)
188
+ #
189
+ # @example create a struct with some attributes overridden
190
+ # MyFactory.build(:uesr, name: "Jane")
191
+ #
192
+ # @param [Symbol] name The name of the registered factory
193
+ # @param [Array<Symbol>] traits List of traits to apply
194
+ # @param [Hash] attrs optional attributes to override the defaults
195
+ #
196
+ # @return [ROM::Struct]
197
+ #
198
+ # @api public
199
+ def build(name, *traits, **attrs)
200
+ structs[name, *traits, **attrs]
201
+ end
202
+
183
203
  # Get factories with a custom struct namespace
184
204
  #
185
205
  # @example
@@ -213,16 +233,16 @@ module ROM::Factory
213
233
 
214
234
  # @api private
215
235
  def infer_factory_name(name)
216
- ::Dry::Core::Inflector.singularize(name).to_sym
236
+ ::ROM::Inflector.singularize(name).to_sym
217
237
  end
218
238
 
219
239
  # @api private
220
240
  def infer_relation(name)
221
- ::Dry::Core::Inflector.pluralize(name).to_sym
241
+ ::ROM::Inflector.pluralize(name).to_sym
222
242
  end
223
243
 
224
244
  # @api private
225
- def extend_builder(name, parent, relation_name, ns, &block)
245
+ def extend_builder(name, parent, relation_name, ns, &)
226
246
  namespace = parent.options[:struct_namespace]
227
247
  namespace = builder_struct_namespace(ns) if ns
228
248
  relation = rom.relations.fetch(relation_name) { parent.relation }
@@ -232,7 +252,7 @@ module ROM::Factory
232
252
  relation: relation,
233
253
  factories: self,
234
254
  struct_namespace: namespace,
235
- &block
255
+ &
236
256
  ).call
237
257
  end
238
258
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/map"
3
4
  require "singleton"
4
5
 
5
6
  module ROM
@@ -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