faker 3.2.0 → 3.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,480 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # A high level way to generate a list of generated values that fit a specific
5
+ # format, such as an ID, postal code, or phone number.
6
+ #
7
+ # It provides generators for random digits and letters, hardcoded literal
8
+ # strings, computed values based on previously-generated values, union (one-of)
9
+ # selectors, and grouped generators.
10
+ #
11
+ # The generation allows for dependencies on previously generated values -- most
12
+ # useful for computations -- and this object knows how to build that dependency
13
+ # graph.
14
+ #
15
+ # See {PositionalGenerator::Builder} for more.
16
+ class PositionalGenerator
17
+ ##
18
+ # @param as_type [Symbol] +:string+ to generate a String
19
+ # @param block [Method] a function that interacts with the {Builder}
20
+ def initialize(as_type, &block)
21
+ @block = block
22
+ @generator_builder = Builder.new(as_type)
23
+ end
24
+
25
+ ##
26
+ # @return [String] if +as_type+ is +:string+
27
+ def generate
28
+ @block.call(@generator_builder)
29
+ @generator_builder.build
30
+ end
31
+
32
+ Component = Struct.new(:position, :name, :deps, :generator)
33
+
34
+ class Builder
35
+ attr_reader :as_type
36
+
37
+ def initialize(as_type)
38
+ @components = []
39
+ @as_type = as_type
40
+ end
41
+
42
+ ##
43
+ # Generate a value in the range of 0..9.
44
+ #
45
+ # @param name [Symbol] the name for this node in the group
46
+ # @param length [Integer] how many digits to generate
47
+ # @param ranges [Array<Range, Array, Set>] an array of limitations on the
48
+ # generation. Elements can be a Range to select from within that range,
49
+ # or an Array or Set to select an element from within the list.
50
+ # @return [void]
51
+ #
52
+ # @example a digit
53
+ # int
54
+ #
55
+ # @example five digits named :a
56
+ # int(name: :a, length: 5)
57
+ #
58
+ # @example digits of any length between 4 to 10
59
+ # int(ranges: [1_000 .. 9_999_999_999)
60
+ def int(name: nil, length: 1, ranges: nil)
61
+ @components << Component.new(@components.count, name, [], Int.new(length, ranges))
62
+ end
63
+
64
+ ##
65
+ # Generate a value in the range of 'a'..'Z'.
66
+ #
67
+ # @param name [Symbol] the name for this node in the group
68
+ # @param length [Integer, Range] how many letters to generate
69
+ # @param ranges [Array<Range, Array, Set>] an array of limitations on the
70
+ # generation. Elements can be a Range to select from within that range,
71
+ # or an Array or Set to select an element from within the list.
72
+ # @return [void]
73
+ #
74
+ # @example Generate a letter
75
+ # letter
76
+ #
77
+ # @example Generate five uppercase letters named :b
78
+ # letter(name: :b, length: 5, ranges: ['A'..'Z'])
79
+ #
80
+ # @example Generate three-letter strings from within specific values
81
+ # letter(ranges: ['700'..'799', '7A0'..'7F9'])
82
+ def letter(name: nil, length: 1, ranges: ['a'..'z', 'A'..'Z'])
83
+ @components << Component.new(@components.count, name, [], Letter.new(length, ranges))
84
+ end
85
+
86
+ ##
87
+ # Generate a literal String
88
+ #
89
+ # @param value [String]
90
+ # @param name [Symbol] the name for this node in the group
91
+ # @return [void]
92
+ # @example
93
+ # lit("-")
94
+ def lit(value, name: nil)
95
+ @components << Component.new(@components.count, name, [], Literal.new(value))
96
+ end
97
+
98
+ ##
99
+ # Fill the position with an arbitrary value.
100
+ #
101
+ # @param name [Symbol] the name for this node in the group
102
+ # @param deps [Array<Symbol>] the name of other fields that this one depends on
103
+ # @param block [Method] the block that yields the arbitrary value. Its
104
+ # arguments are the deps.
105
+ # @return [void]
106
+ #
107
+ # @example Today's date
108
+ # computed do
109
+ # Date.today
110
+ # end
111
+ #
112
+ # @example A check digit
113
+ # int(name: :a, length: 5)
114
+ # computed(deps: [:a]) do |a|
115
+ # a.to_s.bytes.sum % 10
116
+ # end
117
+ def computed(name: nil, deps: [], &block)
118
+ @components << Component.new(@components.count, name, deps, Computed.new(block))
119
+ end
120
+
121
+ ##
122
+ # Fill the position with one of the results from the given generators.
123
+ #
124
+ # @param name [Symbol] the name for this node in the group
125
+ # @param block [Method] subgenerator block
126
+ # @return [void]
127
+ #
128
+ # @example Either five digits, or two letters
129
+ # oneof do |or_else|
130
+ # or_else.int(length: 5)
131
+ # or_else.letter(length: 2)
132
+ # end
133
+ #
134
+ # @example Either one letter; or a slash, five digits, then a slash.
135
+ # oneof do |or_else|
136
+ # or_else.letter
137
+ # or_else.group do |g_|
138
+ # g_.lit("/")
139
+ # g_.digit(length: 5)
140
+ # g_.lit("/")
141
+ # end
142
+ # end
143
+ def oneof(name: nil, &block)
144
+ @components << Component.new(@components.count, name, [], Oneof.new(self, block))
145
+ end
146
+
147
+ ##
148
+ # A group of generators. Useful for {#oneof}.
149
+ #
150
+ # @param name [Symbol] the name for this node in the group
151
+ # @param block [Method] a subgenerator block
152
+ # @return [void]
153
+ def group(name: nil, &block)
154
+ @components << Component.new(@components.count, name, [], Group.new(@as_type, block))
155
+ end
156
+
157
+ ##
158
+ # Generate the value.
159
+ #
160
+ # @return [String] if +as_type+ is +:string+
161
+ def build
162
+ graph = build_graph
163
+ stack = build_stack(graph)
164
+ values = generate_values(stack)
165
+ convert(values)
166
+ end
167
+
168
+ private
169
+
170
+ ##
171
+ # Turn the components into a graph following dependencies.
172
+ #
173
+ # @return [Array<(Integer, Integer)>]
174
+ #
175
+ # Components can have dependencies. Here's one where a computation (b)
176
+ # depends on a value generated after it (c):
177
+ #
178
+ # @components = [
179
+ # Int.new(0, :a, 1, nil),
180
+ # Computed.new(1, :b, [:c]) { |c| c + 1 },
181
+ # Int.new(2, :c, 1, nil),
182
+ # ]
183
+ #
184
+ # We can think of a graph like so:
185
+ #
186
+ # (a) (c)
187
+ # | |
188
+ # | (b)
189
+ # \ /
190
+ # end
191
+ #
192
+ # Or in Mermaid:
193
+ #
194
+ # ```mermaid
195
+ # stateDiagram-v2
196
+ # a --> [*]
197
+ # c --> b
198
+ # b --> [*]
199
+ # ```
200
+ #
201
+ # This method builds that graph, using their positional locations as the
202
+ # ID. The end state is represented as +nil+. So continuing the example
203
+ # above, it will give this output:
204
+ #
205
+ # [
206
+ # [0, nil],
207
+ # [2, 1],
208
+ # [1, nil],
209
+ # ]
210
+ #
211
+ # Later we can look up the appropriate component by indexing into the
212
+ # +@components+ array.
213
+ def build_graph
214
+ graph = []
215
+
216
+ # rubocop:disable Style/CombinableLoops
217
+ @components.each do |component|
218
+ component.deps.each do |dep|
219
+ dep_component = @components.detect { |c| c.name == dep }
220
+ raise if dep_component.nil?
221
+
222
+ graph.push([dep_component.position, component.position])
223
+ end
224
+ end
225
+
226
+ @components.each do |component|
227
+ graph.push([component.position, nil]) if graph.none? { |(from, _to)| from == component.position }
228
+ end
229
+ # rubocop:enable Style/CombinableLoops
230
+
231
+ graph
232
+ end
233
+
234
+ ##
235
+ # Produce a stack of components to evaluate in sequence.
236
+ #
237
+ # @param graph [Array<(Integer, Integer)>]
238
+ # @return [Array<Array<Int>>]
239
+ #
240
+ # Now that we have a graph, we know enough to determine how to traverse the
241
+ # generators such that all dependencies are met.
242
+ #
243
+ # The initial stack is an array of all the free traversals to the goal
244
+ # (where the +to+ is +nil+).
245
+ #
246
+ # Loop over the top of the stack:
247
+ # - The next array is all the nodes that lead into the nodes atop the
248
+ # stack.
249
+ # - If the next array has values, push that onto the top of the stack.
250
+ # - If the next array is empty, we are done.
251
+ #
252
+ # For example, given the graph:
253
+ #
254
+ # [
255
+ # [0, nil],
256
+ # [2, 1],
257
+ # [1, nil],
258
+ # ]
259
+ #
260
+ # The initial stack is:
261
+ #
262
+ # [
263
+ # [0, 1]
264
+ # ]
265
+ #
266
+ # We loop over the top of the stack, +[0, 1]+, and find all the nodes of
267
+ # the graph that lead there. Nothing leads to +0+, and +2+ leads to +1+.
268
+ #
269
+ # Therefore, push +[2]+ to the top of the stack.
270
+ #
271
+ # Repeat for +[2]+. Nothing leads to +2+, so our new goal is +[]+. This is
272
+ # empty, so don't push it onto the stack. We are done.
273
+ #
274
+ # The final stack is:
275
+ #
276
+ # [
277
+ # [0, 1],
278
+ # [2]
279
+ # ]
280
+ def build_stack(graph)
281
+ require 'set'
282
+
283
+ terminals = graph.filter_map { |(from, to)| to.nil? && from }
284
+ stack = [terminals]
285
+ seen = Set.new(terminals)
286
+ deps = []
287
+
288
+ loop do
289
+ stack[-1].each do |e|
290
+ deps = graph.select { |(from, to)| to == e && !seen.include?(from) }.map do |from, _to|
291
+ seen << from
292
+ from
293
+ end
294
+ stack << deps if deps.any?
295
+ end
296
+
297
+ break if deps.empty?
298
+ end
299
+
300
+ stack
301
+ end
302
+
303
+ ##
304
+ # Turn a stack into a list of generated values.
305
+ #
306
+ # @param stack [Array<Array<Int>>]
307
+ # @return [Array<Object>] values sorted by desired order
308
+ #
309
+ # We start with a stack of components we need evaluated. We have been
310
+ # tracking these components by position, so first we need to look up the
311
+ # component in our list.
312
+ #
313
+ # From there we can get a list of all the dependencies for the component.
314
+ # These have already been evaluated, since +stack+ is sorted, so we fetch
315
+ # them.
316
+ #
317
+ # Since the stack was sorted by computation order, we must re-sort them
318
+ # into positional order at the end.
319
+ def generate_values(stack)
320
+ result = []
321
+
322
+ while (top = stack.pop)
323
+ top.each do |component_id|
324
+ component = @components[component_id]
325
+ raise if component.nil?
326
+
327
+ values = result.filter_map do |(_id, name, value)|
328
+ value if component.deps.include?(name)
329
+ end
330
+
331
+ result << [component.position, component.name, component.generator.generate(values)]
332
+ end
333
+ end
334
+
335
+ result.sort_by do |component_position, _, _|
336
+ component_position
337
+ end
338
+ end
339
+
340
+ ##
341
+ # @param values [Array<Object>]
342
+ # @return [String] if +@as_type+ is +:string+
343
+ # @raise [ArgumentError] if +@as_type+ is unsupported
344
+ def convert(values)
345
+ case @as_type
346
+ when :string
347
+ values.inject('') do |acc, (_, _, v)|
348
+ "#{acc}#{v}"
349
+ end
350
+ else
351
+ raise ArgumentError, "unknown return type: #{@as_type}"
352
+ end
353
+ end
354
+
355
+ class Group
356
+ def initialize(as_type, block)
357
+ @as_type = as_type
358
+ @block = block
359
+ end
360
+
361
+ def generate(_)
362
+ builder = Builder.new(@as_type)
363
+ @block.call(builder)
364
+ builder.build
365
+ end
366
+ end
367
+
368
+ class Oneof
369
+ def initialize(builder, block)
370
+ @block = block
371
+ @builder = builder
372
+ end
373
+
374
+ def generate(...)
375
+ subgens = OneofSelector.new(@builder)
376
+ @block.call(subgens)
377
+ subgens.sample
378
+ subgens.generate(...)
379
+ end
380
+
381
+ class OneofSelector
382
+ def initialize(builder)
383
+ @subgens = []
384
+ @builder = Builder.new(builder.as_type)
385
+ end
386
+
387
+ def method_missing(meth, *args, **kwargs, &block)
388
+ @subgens << [meth, args, kwargs, block]
389
+ end
390
+
391
+ def respond_to_missing?(method_name, include_private = false)
392
+ @builder.respond_to?(method_name, include_private)
393
+ end
394
+
395
+ def sample
396
+ (meth, args, kwargs, block) = Faker::Base.sample(@subgens)
397
+ @builder.send(meth, *args, **kwargs, &block)
398
+ end
399
+
400
+ def generate(...)
401
+ @builder.build
402
+ end
403
+ end
404
+ end
405
+
406
+ class Int
407
+ def initialize(length, ranges)
408
+ # Internally we store only an Enumerable of Range values. So if we are
409
+ # not given any Ranges but are given a length, we need to convert the
410
+ # length to a Range.
411
+ #
412
+ # If the length is `5`, that means we should compute the Range `10000..99999`.
413
+ # We can compute the lower end with a simple exponent: 10^4 = 10000.
414
+ # The upper end is one less than an exponent: 10^5 - 1 = 99999.
415
+ if ranges.nil?
416
+ lower = 10**(length - 1)
417
+ upper = (10**length) - 1
418
+ ranges = [lower..upper]
419
+ end
420
+
421
+ @ranges = ranges
422
+ end
423
+
424
+ def generate(_)
425
+ Faker::Base.rand(@ranges.sample(random: Faker::Config.random))
426
+ end
427
+ end
428
+
429
+ class Letter
430
+ def initialize(length, ranges)
431
+ @length = length
432
+ @ranges = ranges
433
+ end
434
+
435
+ def generate(_)
436
+ @length.times.inject('') do |acc, _index|
437
+ generated_character = char
438
+ "#{acc}#{generated_character}"
439
+ end
440
+ end
441
+
442
+ private
443
+
444
+ def char
445
+ if @ranges
446
+ case s = @ranges.sample(random: Faker::Config.random)
447
+ when Range
448
+ s.to_a.sample(random: Faker::Config.random)
449
+ when Array, Set
450
+ s.sample(random: Faker::Config.random)
451
+ else
452
+ raise ArgumentError, "unsupported range type: #{s.inspect}"
453
+ end
454
+ else
455
+ Faker::Base.sample(Faker::Base::Letters)
456
+ end
457
+ end
458
+ end
459
+
460
+ class Literal
461
+ def initialize(value)
462
+ @value = value
463
+ end
464
+
465
+ def generate(_)
466
+ @value
467
+ end
468
+ end
469
+
470
+ class Computed
471
+ def initialize(block)
472
+ @block = block
473
+ end
474
+
475
+ def generate(args)
476
+ @block.call(*args)
477
+ end
478
+ end
479
+ end
480
+ end
@@ -9,7 +9,7 @@ Here's how to set it:
9
9
  Faker::Config.locale = 'zh-CN'
10
10
  ```
11
11
 
12
- It works so that once the Faker locale is set to a different location, the translate method will check that .yml file for an equivalent and use that data. If it doesn't exist, it defaults back to English. It uses the "i18n" gem to do this.
12
+ It works so that once the Faker locale is set to a different location, the translate method will check that `.yml` file for an equivalent and use that data. If it doesn't exist, it defaults back to English. It uses the [I18n](https://github.com/ruby-i18n/i18n) gem to do this.
13
13
 
14
14
  Using Chinese as an example, when the locale is set to Chinese and you attempt to call for hipster ipsem (which doesn't exist at the time of this writing), you will get English back. It checks the "zh-CH.yml" file, does not find "hipster" and then checks the "en.yml" file and returns a word from that array.
15
15
 
@@ -18,7 +18,9 @@ Faker::Config.locale = 'zh-CN'
18
18
  Faker::Hipster.word #=> "kogi"
19
19
  ```
20
20
 
21
- In order to update a locale with more translation features, simply add a new field to the .yml file that corresponds to an existing piece of functionality in the "en.yml" file. In this example, that would mean providing Chinese hipster words.
21
+ ## How to update a locale with more translations
22
+
23
+ T update a locale with more translation features, simply add a new field to the .yml file that corresponds to an existing piece of functionality in the "en.yml" file. In this example, that would mean providing Chinese hipster words.
22
24
 
23
25
  ```yaml
24
26
  # /lib/locales/zh-CN.yml
@@ -38,3 +40,17 @@ In our hypothetical example here, one would add something like this to the "test
38
40
  ```ruby
39
41
  assert Faker::Hipster.word.is_a? String
40
42
  ```
43
+
44
+ ## How to set the default locale for in threaded server environments
45
+
46
+ If you want to modify the default locale that will be used in new threads, set it in your configuration:
47
+
48
+ ```ruby
49
+ Faker::Config.default_locale = :pt
50
+ ```
51
+
52
+ In threaded server environments, e.g., Puma, locale per thread can be set as the following:
53
+
54
+ ```ruby
55
+ Faker::Config.locale = :es
56
+ ```