faker_maker 3.0.0 → 4.0.0.beta1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6d677fff1380c1eddf6c0305b978d8b161d0f5fbb5e1752bc587a0dc10e8554
4
- data.tar.gz: 7df7f5f9279c2e745842999117672e608b9d1e2302d63bcae9914429f796491a
3
+ metadata.gz: 2010123a071b807d1400f16c16cbaf58b4db86507224c72f50847affcfbc1bd4
4
+ data.tar.gz: 8e72393016c7a54106bc1aca8ecccf46d35c94ee1b9fb4b95c6b49ba5bbd2dfc
5
5
  SHA512:
6
- metadata.gz: fe8ae2a8550b5eaf9a78eaf2bda1ab8f7f1eac1f1f557223e744111bf45ee417c5a9215aa1cd2dae5e23d90a1b3b9aa347a2bba52a5d97d7d8f53f91fd629196
7
- data.tar.gz: 19290557db759839023a0a7b80b9cd2bf9f01e652a7e610f5f5cfd9f037459f2be04cc51a34eed271dead74289f9001fd15aebbf8913355e55c5004a7b466f31
6
+ metadata.gz: f8f73cb6e888ef7f49abd66959c64916df617f0254a98b0ca022c19dbc45aef6e259d92d9186d57128f9180c094c46af4776fb3c087ec3fce94eff1cead58eb0
7
+ data.tar.gz: b7baa192ddf36b18f6a696d4cec08b8372e57fd95a44d0397a8a6f94f298938e449553aec1d6b03c32a63544589c421afb46c080f67a699eaace38d5d9f15eaf
data/.gitignore CHANGED
@@ -8,6 +8,10 @@
8
8
  /tmp/
9
9
 
10
10
  Gemfile.lock
11
+
12
+ /hack/
13
+ TODO.md
14
+
11
15
  # rspec failure tracking
12
16
  .rspec_status
13
17
 
data/.rubocop.yml CHANGED
@@ -52,4 +52,5 @@ Metrics/PerceivedComplexity:
52
52
  Max: 10
53
53
 
54
54
  AllCops:
55
+ TargetRubyVersion: 3.4
55
56
  NewCops: enable
data/Gemfile CHANGED
@@ -10,3 +10,5 @@ gemspec
10
10
  gem 'bundler-audit', '~> 0.9.2'
11
11
  gem 'irb', '~> 1.15'
12
12
  gem 'rdoc', '~> 6.13'
13
+
14
+ gem "awesome_print", "~> 1.9"
@@ -3,7 +3,7 @@
3
3
  module FakerMaker
4
4
  # Attributes describe the fields of classes
5
5
  class Attribute
6
- attr_reader :name, :block, :translation, :required, :optional, :optional_weighting, :embedded_factories
6
+ attr_reader :name, :block, :translation, :required, :optional, :optional_weighting
7
7
 
8
8
  DEFAULT_OPTIONAL_WEIGHTING = 0.5
9
9
 
@@ -25,6 +25,15 @@ module FakerMaker
25
25
  end
26
26
  end
27
27
 
28
+ # Return an array of factory instances
29
+ def embedded_factories
30
+ @embedded_factories.map { |name| FakerMaker[name] }
31
+ end
32
+
33
+ def embedded_factories?
34
+ @embedded_factories.any?
35
+ end
36
+
28
37
  def array?
29
38
  forced_array? || @array
30
39
  end
@@ -5,8 +5,26 @@ module FakerMaker
5
5
  # Factories construct instances of a fake
6
6
  class Factory
7
7
  include Auditable
8
+
8
9
  attr_reader :name, :class_name, :parent, :chaos_selected_attributes
9
10
 
11
+ # Create a new +Factory+ object.
12
+ #
13
+ # This method does not automatically register the factory,see
14
+ # FakerMaker#register_factory
15
+ #
16
+ # Options:
17
+ # - +:class_name+ - override the default class name that FakerMaker will generate.
18
+ # This is useful in the case of collisions with existing classes or keywords.
19
+ # - +:parent+ - the parent factory from which this factory inherits attributes.
20
+ # Instances built by this factory will have a class which inherits from the parent's
21
+ # class.
22
+ # - +:naming+ - one of:
23
+ # - +nil+ (default) - use field names as the method name and in JSON conversion
24
+ # - +:json+ - use field names as the method name but convert when rendering JSON, e.g.
25
+ # +hello_world+ becomes +helloWorld+
26
+ # - +:json_capitalised+ (or +:json_capitalized+) - as +:json+ but with the first letter
27
+ # captialised, e.g. +hello_world+ becomes +HelloWorld+
10
28
  def initialize( name, options = {} )
11
29
  assert_valid_options options
12
30
  @name = name.respond_to?(:to_sym) ? name.to_sym : name.to_s.underscore.to_sym
@@ -26,6 +44,7 @@ module FakerMaker
26
44
  @parent = options[:parent]
27
45
  end
28
46
 
47
+ # Get the Class of the parent for this factory
29
48
  def parent_class
30
49
  if @parent
31
50
  FakerMaker::Factory.const_get( FakerMaker[@parent].class_name )
@@ -34,6 +53,7 @@ module FakerMaker
34
53
  end
35
54
  end
36
55
 
56
+ # Attach a FakerMaker::Attribute to this Factory
37
57
  def attach_attribute( attribute )
38
58
  @attributes << attribute
39
59
  end
@@ -42,14 +62,11 @@ module FakerMaker
42
62
  @instance ||= instantiate
43
63
  end
44
64
 
45
- def build( attributes: {}, chaos: false, **kwargs )
46
- if kwargs.present?
47
- validate_deprecated_build(kwargs)
48
- attributes = kwargs
49
- end
50
-
65
+ def build( attributes: {}, chaos: false )
51
66
  @instance = nil
52
67
  before_build if respond_to? :before_build
68
+
69
+ # TODO: make this cleverer to handle nested attributes
53
70
  assert_only_known_attributes_for_override( attributes )
54
71
 
55
72
  assert_chaos_options chaos if chaos
@@ -57,13 +74,19 @@ module FakerMaker
57
74
  optional_attributes
58
75
  required_attributes
59
76
 
60
- populate_instance instance, attributes, chaos
77
+ populate_instance(instance, attributes, chaos:)
61
78
  yield instance if block_given?
79
+
62
80
  after_build if respond_to? :after_build
63
81
  audit(@instance) if FakerMaker.configuration.audit?
64
82
  instance
65
83
  end
66
84
 
85
+ # Construct a Class object which will become the parent type of objects built
86
+ # by this factory.
87
+ #
88
+ # The Class object will be created and the attributes added to it. The returned value
89
+ # is a Ruby Class which can be instantiated.
67
90
  def assemble
68
91
  if @klass.nil?
69
92
  @klass = Class.new parent_class
@@ -91,7 +114,7 @@ module FakerMaker
91
114
  unless @json_key_map
92
115
  @json_key_map = {}.with_indifferent_access
93
116
  @json_key_map.merge!( FakerMaker[parent].json_key_map ) if parent?
94
- attributes.each_with_object( @json_key_map ) do |attr, map|
117
+ attributes(include_embeddings: false).each_with_object( @json_key_map ) do |attr, map|
95
118
  key = if attr.translation?
96
119
  attr.translation
97
120
  elsif @naming_strategy
@@ -106,29 +129,86 @@ module FakerMaker
106
129
  @json_key_map
107
130
  end
108
131
 
109
- def attribute_names( collection = [] )
110
- collection |= FakerMaker[parent].attribute_names( collection ) if parent?
111
- collection | @attributes.map( &:name )
132
+ # Returns a transformed list of attribute names from the `attributes` array.
133
+ # For each item in the array:
134
+ # - If the item is a Hash, recursively transforms its keys and values,
135
+ # replacing keys with their `name` and applying the same transformation to values.
136
+ # - Otherwise, replaces the item with its `name`.
137
+ #
138
+ # @return [Array] An array (possibly nested) of attribute names, with hashes' keys replaced by their `name`.
139
+ def attribute_names
140
+ transform = lambda do |arr|
141
+ arr.map do |item|
142
+ if item.is_a?(Hash)
143
+ item.transform_keys(&:name).transform_values { |v| transform.call(v) }
144
+ else
145
+ item.name
146
+ end
147
+ end
148
+ end
149
+ transform.call(attributes)
112
150
  end
113
151
 
114
- def attributes( collection = [] )
152
+ # Returns a collection of attributes for the factory, optionally including embedded factory attributes.
153
+ #
154
+ # @param collection [Array] an optional array of attributes to start with (default: empty array)
155
+ # @param include_embeddings [Boolean] whether to include attributes from embedded factories (default: true)
156
+ # @return [Array] the collection of attributes, possibly including embedded factory attributes as hashes
157
+ #
158
+ # If the factory has a parent, its attributes are merged in. Attributes without embedded factories are added directly.
159
+ # If `include_embeddings` is true, attributes with embedded factories are added as hashes mapping the attribute to the
160
+ # flattened attributes of its embedded factories. If false, only the attribute itself is added.
161
+ def attributes( collection = [], include_embeddings: true )
115
162
  collection |= FakerMaker[parent].attributes( collection ) if parent?
116
- collection | @attributes
163
+ collection |= @attributes.reject { |attr| attr.embedded_factories.any? }
164
+
165
+ # if there is an embedded factory(-ies) and we are including the embedded factory's
166
+ # fields, we are going to return a hash
167
+ if include_embeddings
168
+ @attributes.select { |attr| attr.embedded_factories.any? }.each do |attr|
169
+ collection << { attr => attr.embedded_factories.flat_map(&:attributes) }
170
+ end
171
+ # if there is an embedded factory(-ies) and we are not including the embedded factory's
172
+ # fields, just add the attribute into the set of returned fields
173
+ else
174
+ collection |= @attributes.select { |attr| attr.embedded_factories.any? }
175
+ end
176
+
177
+ collection
117
178
  end
118
179
 
180
+ # Finds and returns the first attribute matching the given name.
181
+ #
182
+ # This method searches through the attributes (excluding embeddings) and returns the first attribute
183
+ # whose name, translation, or the result of applying the naming strategy to its name matches the provided `name`.
184
+ #
185
+ # @param name [String] The name to search for among the attributes. Defaults to an empty string.
186
+ # @return [Object, nil] The first matching attribute object, or nil if no match is found.
119
187
  def find_attribute( name = '' )
120
- attributes.filter { |a| [a.name, a.translation, @naming_strategy&.name(a.name)].include? name }.first
188
+ attributes(include_embeddings: false).filter do |a|
189
+ [a.name, a.translation, @naming_strategy&.name(a.name)].include? name
190
+ end.first
121
191
  end
122
192
 
123
193
  protected
124
194
 
125
- def populate_instance( instance, attr_override_values, chaos )
126
- FakerMaker[parent].populate_instance instance, attr_override_values, chaos if parent?
195
+ # Populates the given instance with attribute values, optionally applying chaos/randomization.
196
+ #
197
+ # @param instance [Object] The object instance to populate with attribute values.
198
+ # @param attr_override_values [Hash] A hash of attribute names and their override values.
199
+ # @param chaos [Boolean, Integer, nil] If truthy, enables chaos mode which may randomize or select a subset of attributes.
200
+ # @return [void]
201
+ #
202
+ # If the factory has a parent, its attributes are populated first.
203
+ # Each attribute is assigned a value, either from the override values or generated.
204
+ # The factory instance is set on the populated object for reference.
205
+ def populate_instance( instance, attr_override_values, chaos: false )
206
+ FakerMaker[parent].populate_instance(instance, attr_override_values, chaos:) if parent?
127
207
 
128
208
  attributes = chaos ? chaos_select(chaos) : @attributes
129
209
 
130
210
  attributes.each do |attribute|
131
- value = value_for_attribute( instance, attribute, attr_override_values )
211
+ value = value_for_attribute( instance, attribute, attr_override_values, chaos: )
132
212
  instance.send "#{attribute.name}=", value
133
213
  end
134
214
  instance.instance_variable_set( :@fm_factory, self )
@@ -137,7 +217,10 @@ module FakerMaker
137
217
  private
138
218
 
139
219
  def assert_only_known_attributes_for_override( attr_override_values )
140
- unknown_attrs = attr_override_values.keys - attribute_names
220
+ unknown_attrs = attr_override_values.keys - attribute_names.flat_map do |item|
221
+ item.is_a?(Hash) ? item.keys : item
222
+ end
223
+
141
224
  issue = "Can't build an instance of '#{class_name}' " \
142
225
  "setting '#{unknown_attrs.join( ', ' )}', no such attribute(s)"
143
226
  raise FakerMaker::NoSuchAttributeError, issue unless unknown_attrs.empty?
@@ -156,34 +239,44 @@ module FakerMaker
156
239
  raise FakerMaker::ChaosConflictingAttributeError, issue unless conflicting_attributes.empty?
157
240
  end
158
241
 
159
- def attribute_hash_overridden_value?( attr, attr_override_values )
242
+ def overridden_value?( attr, attr_override_values )
160
243
  attr_override_values.keys.include?( attr.name )
161
244
  end
162
245
 
163
- def value_for_attribute( instance, attr, attr_override_values )
164
- if attribute_hash_overridden_value?( attr, attr_override_values )
246
+ def value_for_attribute( instance, attr, attr_override_values, chaos: false )
247
+ if !attr.embedded_factories? && overridden_value?( attr, attr_override_values )
165
248
  attr_override_values[attr.name]
166
249
  elsif attr.array?
167
250
  [].tap do |a|
168
251
  attr.cardinality.times do
169
- manufacture = manufacture_from_embedded_factory( attr )
170
- # if manufacture has been build and there is a block, instance_exec the block
252
+ manufacture = manufacture_from_embedded_factory( attr, attr_override_values[attr.name.to_sym], chaos: )
253
+ # if manufacture has been built and there is a block, instance_exec the block
171
254
  # otherwise just add the manufacture to the array
172
255
  a << (attr.block ? instance.instance_exec(manufacture, &attr.block) : manufacture)
173
256
  end
174
257
  end
175
258
  else
176
- manufacture = manufacture_from_embedded_factory( attr )
259
+ manufacture = manufacture_from_embedded_factory( attr, attr_override_values[attr.name.to_sym], chaos: )
177
260
  attr.block ? instance.instance_exec(manufacture, &attr.block) : manufacture
178
261
  end
179
262
  end
180
263
 
181
- def manufacture_from_embedded_factory( attr )
264
+ def manufacture_from_embedded_factory( attr, attributes = {}, chaos: false )
265
+ attributes ||= {}
182
266
  # The name of the embedded factory randomly selected from the list of embedded factories.
183
- embedded_factory_name = attr.embedded_factories.sample
267
+ embedded_factory = attr.embedded_factories.sample
268
+
269
+ # filter out attributes for non-chosen embedded factories to avoid triggering
270
+ # the NoSuchAttribute exception
271
+ attributes = attr
272
+ .embedded_factories
273
+ .reject { |e| e == embedded_factory }
274
+ .flat_map { |f| pp f.attributes.map(&:name) }
275
+ .then { |excl| attributes.delete_if { |k, _v| excl.include?(k) } }
276
+
184
277
  # The object that is being manufactured by the factory.
185
278
  # If an embedded factory name is provided, it builds the object using FakerMaker.
186
- embedded_factory_name ? FakerMaker[embedded_factory_name].build : nil
279
+ embedded_factory&.build(attributes:, chaos:)
187
280
  end
188
281
 
189
282
  def instantiate
@@ -264,13 +357,6 @@ module FakerMaker
264
357
  .concat(selected_attrs).uniq!
265
358
  @chaos_selected_attributes
266
359
  end
267
-
268
- def validate_deprecated_build(kwargs)
269
- usage = kwargs.each_with_object([]) { |kwarg, result| result << "#{kwarg.first}: #{kwarg.last}" }.join(', ')
270
-
271
- warn "[DEPRECATION] `FM[:#{name}].build(#{usage})` is deprecated. " \
272
- "Please use `FM[:#{name}].build(attributes: { #{usage} })` instead."
273
- end
274
360
  end
275
361
  end
276
362
  # rubocop:enable Metrics/ClassLength
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FakerMaker
4
- VERSION = '3.0.0'
4
+ VERSION = '4.0.0.beta1'
5
5
  end
@@ -13,10 +13,10 @@ result = FakerMaker[:basket].build
13
13
 
14
14
  will generate a new instance using the Basket factory. Because an actual class is defined (since v3.0.0 Classes generated by FakerMaker are in the `FakerMaker::Factory` namespace), you can instantiate an object directly through `Basket.new` but that will not populate any of the attributes.
15
15
 
16
- It's possible to override attributes at build-time, either by passing values as a hash:
16
+ It's possible to override attributes at build-time, either by passing values as a hash (preferred):
17
17
 
18
18
  ```ruby
19
- result = FakerMaker[:item].build( name: 'Electric Blanket' )
19
+ result = FakerMaker[:item].build( attributes: { name: 'Electric Blanket' } )
20
20
  ```
21
21
 
22
22
  or by passing in a block:
@@ -36,7 +36,7 @@ end
36
36
  if you're crazy enough to want to do both styles during creation, the values in the block will be preserved, e.g.
37
37
 
38
38
  ```ruby
39
- result = FakerMaker[:item].build( name: 'Electric Blanket' ) do |i|
39
+ result = FakerMaker[:item].build( attributes: { name: 'Electric Blanket' } ) do |i|
40
40
  i.name = 'Electric Sheep'
41
41
  end
42
42
  ```
@@ -58,3 +58,5 @@ As another convenience, `FakerMaker` is also assigned to the variable `FM` to it
58
58
  ```ruby
59
59
  result = FM[:basket].build
60
60
  ```
61
+
62
+ **For more complex instance building with embedded factories, see [Embedding Factories](docs/usage/embedding-factories/).**
@@ -31,11 +31,11 @@ FakerMaker.factory :item do
31
31
  end
32
32
 
33
33
  FakerMaker.factory :basket do
34
- items( has: 10, factory: [:item, :discount] )
34
+ items( has: 10, factory: [:item, :coupon] ) # either `item` or `coupon` will be randomly selected for each member
35
35
  end
36
36
  ```
37
37
 
38
- In this example, through 10 iterations, one of `item` and `discount` factories will be called to build their objects.
38
+ In this example, through 10 iterations, a random choice of `item` and `discount` factories will be called to build their objects.
39
39
 
40
40
  Blocks can still be provided and the referenced factory built object will be passed to the block:
41
41
 
@@ -49,11 +49,48 @@ FakerMaker.factory :basket do
49
49
  items( has: 10, factory: :item ) { |item| item.price = 10.99 ; item}
50
50
  end
51
51
  ```
52
+
53
+ ## Overriding values for nested factories in the enclosing factory
54
+
52
55
  **Important:** the value for the attribute will be the value returned from the block. If you want to modify the contents of the referenced factory's object, don't forget to return it at the end of the block (as above).
53
56
 
57
+ ## Overriding values for nested factories during build
58
+
59
+ If we look carefully at this factory
60
+
61
+ ```ruby
62
+ FakerMaker.factory :inventory do
63
+ item( factory: :item )
64
+ quantity { 10 }
65
+ end
66
+ ```
67
+
68
+ This will build a object of the form (in its `as_json` guise):
69
+
70
+ ```ruby
71
+ {item: {name: "toothpaste", price: 0.99}, quantity: 10}
72
+ ```
73
+
74
+ When it comes to overriding values at build time, a hash can be passed to set the nested values:
75
+
76
+ ```ruby
77
+ FM[:inventory].build( attributes: { item: { name: 'floor cleaner' } } )
78
+ ```
79
+
80
+ When you allow Faker Maker to make a choice of factory by giving it an array:
81
+
82
+ ```ruby
83
+ FakerMaker.factory :inventory do
84
+ item( factory: [:item, :coupon] )
85
+ quantity { 10 }
86
+ end
87
+ ```
88
+
89
+ ...either the `item` or `coupon` fields could be added to each build of the `inventory` factory. Faker Maker will ignore any fields for the non-chosen factory if they are paseed in the overrides hash. This means that a `NoSuchAttribute` error will not be raised.
90
+
54
91
  ## Alternative method
55
92
 
56
- There is an alternative style which might be of use:
93
+ There is an alternative style which might be of use, **but** you have less control using build-time overrides for values (you can't set nested values). *This is no longer a recommended pattern*.
57
94
 
58
95
  ```ruby
59
96
  FakerMaker.factory :item do
@@ -0,0 +1,20 @@
1
+ ---
2
+ title: "About"
3
+ layout: single
4
+ permalink: /pages/about/
5
+ author_profile: true
6
+ ---
7
+
8
+ Faker Maker was designed to be a trivial way to create data factories that could throw JSON payloads at an API endpoint. It has grown well beyond that original purpose but still remains a thing for building things that give you data.
9
+
10
+ It is much beloved by me. Although it's a personal project, it's used extensively by my employer and influenced by the needs of my colleagues. I hope it will be useful to you as well. I am very open to ideas, feedback and contributions.
11
+
12
+ Faker Maker is licenced under the [MIT licence](https://raw.githubusercontent.com/BillyRuffian/faker_maker/refs/heads/master/LICENSE.txt). Do with it what you will and have fun.
13
+
14
+ ### What's the Billy Ruffian thing?
15
+
16
+ HMS Bellerophon was a 74-gun third-rate ship of the line of the Royal Navy. Launched in 1786, she served during the French Revolutionary and Napoleonic Wars, mostly on blockades or convoy escort duties. She fought in three fleet actions: the Glorious First of June, the Battle of the Nile and the Battle of Trafalgar. She became famous as the ship upon which Napoleon surrendered and which transported him into exile in 1815.
17
+
18
+ Her sailors, not being educated in the Classics, struggled to pronounce her name and so she became known as the Billy Ruffian and her crew as the "Billy Ruffians". The name stuck and was used as a nickname for the ship for the rest of her career.
19
+
20
+ Since no one in coffee shops can spell my name, I adopted 'Billy' which turned into 'Billy Ruffian'. It's also a bloody good story.
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: faker_maker
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 4.0.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nigel Brookes-Thomas
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-25 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -264,6 +264,7 @@ files:
264
264
  - usefakermaker.com/docs/usage/lifecycle-hooks/index.md
265
265
  - usefakermaker.com/docs/usage/managing-dependencies/index.md
266
266
  - usefakermaker.com/docs/usage/omitting-fields/index.md
267
+ - usefakermaker.com/pages/about/index.md
267
268
  - usefakermaker.com/pages/index.markdown
268
269
  homepage: https://billyruffian.github.io/faker_maker/
269
270
  licenses:
@@ -286,7 +287,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
286
287
  - !ruby/object:Gem::Version
287
288
  version: '0'
288
289
  requirements: []
289
- rubygems_version: 3.6.2
290
+ rubygems_version: 3.7.2
290
291
  specification_version: 4
291
292
  summary: FakerMaker bakes fakes.
292
293
  test_files: []