class PositionalGenerator::Builder
Attributes
Public Class Methods
# File lib/helpers/positional_generator.rb, line 37 def initialize(as_type) @components = [] @as_type = as_type end
Public Instance Methods
Generate the value.
@return [String] if as_type
is :string
# File lib/helpers/positional_generator.rb, line 161 def build graph = build_graph stack = build_stack(graph) values = generate_values(stack) convert(values) end
Fill the position with an arbitrary value.
@param name [Symbol] the name for this node in the group @param deps [Array<Symbol>] the name of other fields that this one depends on @param block [Method] the block that yields the arbitrary value. Its
arguments are the deps.
@return [void]
@example Today’s date
computed do Date.today end
@example A check digit
int(name: :a, length: 5) computed(deps: [:a]) do |a| a.to_s.bytes.sum % 10 end
# File lib/helpers/positional_generator.rb, line 117 def computed(name: nil, deps: [], &block) @components << Component.new(@components.count, name, deps, Computed.new(block)) end
A group of generators. Useful for {#oneof}.
@param name [Symbol] the name for this node in the group @param block [Method] a subgenerator block @return [void]
# File lib/helpers/positional_generator.rb, line 153 def group(name: nil, &block) @components << Component.new(@components.count, name, [], Group.new(@as_type, block)) end
Generate a value in the range of 0..9.
@param name [Symbol] the name for this node in the group @param length [Integer] how many digits to generate @param ranges [Array<Range, Array, Set>] an array of limitations on the
generation. Elements can be a Range to select from within that range, or an Array or Set to select an element from within the list.
@return [void]
@example a digit
int
@example five digits named :a
int(name: :a, length: 5)
@example digits of any length between 4 to 10
int(ranges: [1_000 .. 9_999_999_999)
# File lib/helpers/positional_generator.rb, line 60 def int(name: nil, length: 1, ranges: nil) @components << Component.new(@components.count, name, [], Int.new(length, ranges)) end
Generate a value in the range of ‘a’..‘Z’.
@param name [Symbol] the name for this node in the group @param length [Integer, Range] how many letters to generate @param ranges [Array<Range, Array, Set>] an array of limitations on the
generation. Elements can be a Range to select from within that range, or an Array or Set to select an element from within the list.
@return [void]
@example Generate a letter
letter
@example Generate five uppercase letters named :b
letter(name: :b, length: 5, ranges: ['A'..'Z'])
@example Generate three-letter strings from within specific values
letter(ranges: ['700'..'799', '7A0'..'7F9'])
# File lib/helpers/positional_generator.rb, line 82 def letter(name: nil, length: 1, ranges: ['a'..'z', 'A'..'Z']) @components << Component.new(@components.count, name, [], Letter.new(length, ranges)) end
Generate a literal String
@param value [String] @param name [Symbol] the name for this node in the group @return [void] @example
lit("-")
# File lib/helpers/positional_generator.rb, line 94 def lit(value, name: nil) @components << Component.new(@components.count, name, [], Literal.new(value)) end
Fill the position with one of the results from the given generators.
@param name [Symbol] the name for this node in the group @param block [Method] subgenerator block @return [void]
@example Either five digits, or two letters
oneof do |or_else| or_else.int(length: 5) or_else.letter(length: 2) end
@example Either one letter; or a slash, five digits, then a slash.
oneof do |or_else| or_else.letter or_else.group do |g_| g_.lit("/") g_.digit(length: 5) g_.lit("/") end end
# File lib/helpers/positional_generator.rb, line 143 def oneof(name: nil, &block) @components << Component.new(@components.count, name, [], Oneof.new(self, block)) end
Private Instance Methods
Turn the components into a graph following dependencies.
@return [Array<(Integer, Integer)>]
Components can have dependencies. Here’s one where a computation (b) depends on a value generated after it ©:
@components = [ Int.new(0, :a, 1, nil), Computed.new(1, :b, [:c]) { |c| c + 1 }, Int.new(2, :c, 1, nil), ]
We can think of a graph like so:
(a) (c) | | | (b) \ / end
Or in Mermaid:
“‘mermaid stateDiagram-v2
a --> [*] c --> b b --> [*]
“‘
This method builds that graph, using their positional locations as the ID. The end state is represented as nil
. So continuing the example above, it will give this output:
[ [0, nil], [2, 1], [1, nil], ]
Later we can look up the appropriate component by indexing into the +@components+ array.
# File lib/helpers/positional_generator.rb, line 213 def build_graph graph = [] # rubocop:disable Style/CombinableLoops @components.each do |component| component.deps.each do |dep| dep_component = @components.detect { |c| c.name == dep } raise if dep_component.nil? graph.push([dep_component.position, component.position]) end end @components.each do |component| graph.push([component.position, nil]) if graph.none? { |(from, _to)| from == component.position } end # rubocop:enable Style/CombinableLoops graph end
Produce a stack of components to evaluate in sequence.
@param graph [Array<(Integer, Integer)>] @return [Array<Array<Int>>]
Now that we have a graph, we know enough to determine how to traverse the generators such that all dependencies are met.
The initial stack is an array of all the free traversals to the goal (where the to
is nil
).
Loop over the top of the stack:
-
The next array is all the nodes that lead into the nodes atop the
stack.
-
If the next array has values, push that onto the top of the stack.
-
If the next array is empty, we are done.
For example, given the graph:
[ [0, nil], [2, 1], [1, nil], ]
The initial stack is:
[ [0, 1] ]
We loop over the top of the stack, +[0, 1]+, and find all the nodes of the graph that lead there. Nothing leads to 0
, and 2
leads to 1
.
Therefore, push [2]
to the top of the stack.
Repeat for [2]
. Nothing leads to 2
, so our new goal is []
. This is empty, so don’t push it onto the stack. We are done.
The final stack is:
[ [0, 1], [2] ]
# File lib/helpers/positional_generator.rb, line 280 def build_stack(graph) require 'set' terminals = graph.filter_map { |(from, to)| to.nil? && from } stack = [terminals] seen = Set.new(terminals) deps = [] loop do stack[-1].each do |e| deps = graph.select { |(from, to)| to == e && !seen.include?(from) }.map do |from, _to| seen << from from end stack << deps if deps.any? end break if deps.empty? end stack end
@param values [Array<Object>] @return [String] if +@as_type+ is :string
@raise [ArgumentError] if +@as_type+ is unsupported
# File lib/helpers/positional_generator.rb, line 344 def convert(values) case @as_type when :string values.inject('') do |acc, (_, _, v)| "#{acc}#{v}" end else raise ArgumentError, "unknown return type: #{@as_type}" end end
Turn a stack into a list of generated values.
@param stack [Array<Array<Int>>] @return [Array<Object>] values sorted by desired order
We start with a stack of components we need evaluated. We have been tracking these components by position, so first we need to look up the component in our list.
From there we can get a list of all the dependencies for the component. These have already been evaluated, since stack
is sorted, so we fetch them.
Since the stack was sorted by computation order, we must re-sort them into positional order at the end.
# File lib/helpers/positional_generator.rb, line 319 def generate_values(stack) result = [] while (top = stack.pop) top.each do |component_id| component = @components[component_id] raise if component.nil? values = result.filter_map do |(_id, name, value)| value if component.deps.include?(name) end result << [component.position, component.name, component.generator.generate(values)] end end result.sort_by do |component_position, _, _| component_position end end