rom-factory 0.11.0 → 0.13.0

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.
@@ -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