Guru on Rails

if you don’t sacrifice for your dream then your dream becomes your sacrifice.
Will Nguyen
Strategy Pattern in Factory Bot Rails (updating...)
Tue 18 Dec 2018

factory_bot.rb

require 'set'
require 'active_support/core_ext/module/delegation'
require 'active_support/deprecation'
require 'active_support/notifications'

require 'factory_bot/definition_hierarchy'
require 'factory_bot/configuration'
require 'factory_bot/errors'
require 'factory_bot/factory_runner'
require 'factory_bot/strategy_syntax_method_registrar'
require 'factory_bot/strategy_calculator'
require 'factory_bot/strategy/build'
require 'factory_bot/strategy/create'
require 'factory_bot/strategy/attributes_for'
require 'factory_bot/strategy/stub'
require 'factory_bot/strategy/null'
require 'factory_bot/registry'
require 'factory_bot/null_factory'
require 'factory_bot/null_object'
require 'factory_bot/evaluation'
require 'factory_bot/factory'
require 'factory_bot/attribute_assigner'
require 'factory_bot/evaluator'
require 'factory_bot/evaluator_class_definer'
require 'factory_bot/attribute'
require 'factory_bot/callback'
require 'factory_bot/callbacks_observer'
require 'factory_bot/declaration_list'
require 'factory_bot/declaration'
require 'factory_bot/sequence'
require 'factory_bot/attribute_list'
require 'factory_bot/trait'
require 'factory_bot/aliases'
require 'factory_bot/definition'
require 'factory_bot/definition_proxy'
require 'factory_bot/syntax'
require 'factory_bot/syntax_runner'
require 'factory_bot/find_definitions'
require 'factory_bot/reload'
require 'factory_bot/decorator'
require 'factory_bot/decorator/attribute_hash'
require 'factory_bot/decorator/class_key_hash'
require 'factory_bot/decorator/disallows_duplicates_registry'
require 'factory_bot/decorator/invocation_tracker'
require 'factory_bot/decorator/new_constructor'
require 'factory_bot/linter'
require 'factory_bot/version'

module FactoryBot
  def self.configuration
    @configuration ||= Configuration.new
  end

  def self.reset_configuration
    @configuration = nil
  end

  # Look for errors in factories and (optionally) their traits.
  # Parameters:
  # factories - which factories to lint; omit for all factories
  # options:
  #   traits: true - to lint traits as well as factories
  #   strategy: :create - to specify the strategy for linting
  def self.lint(*args)
    options = args.extract_options!
    factories_to_lint = args[0] || FactoryBot.factories
    linting_strategy = options[:traits] ? :factory_and_traits : :factory
    factory_strategy = options[:strategy] || :create
    Linter.new(factories_to_lint, linting_strategy, factory_strategy).lint!
  end

  class << self
    delegate :factories,
             :sequences,
             :traits,
             :callbacks,
             :strategies,
             :callback_names,
             :to_create,
             :skip_create,
             :initialize_with,
             :constructor,
             :duplicate_attribute_assignment_from_initialize_with,
             :duplicate_attribute_assignment_from_initialize_with=,
             :allow_class_lookup,
             :allow_class_lookup=,
             :use_parent_strategy,
             :use_parent_strategy=,
             to: :configuration
  end

  def self.register_factory(factory)
    factory.names.each do |name|
      factories.register(name, factory)
    end
    factory
  end

  def self.factory_by_name(name)
    factories.find(name)
  end

  def self.register_sequence(sequence)
    sequence.names.each do |name|
      sequences.register(name, sequence)
    end
    sequence
  end

  def self.sequence_by_name(name)
    sequences.find(name)
  end

  def self.rewind_sequences
    sequences.each(&:rewind)
  end

  def self.register_trait(trait)
    trait.names.each do |name|
      traits.register(name, trait)
    end
    trait
  end

  def self.trait_by_name(name)
    traits.find(name)
  end

  def self.register_strategy(strategy_name, strategy_class)
    strategies.register(strategy_name, strategy_class)
    StrategySyntaxMethodRegistrar.new(strategy_name).define_strategy_methods
  end

  def self.strategy_by_name(name)
    strategies.find(name)
  end

  def self.register_default_strategies
    register_strategy(:build,          FactoryBot::Strategy::Build)
    register_strategy(:create,         FactoryBot::Strategy::Create)
    register_strategy(:attributes_for, FactoryBot::Strategy::AttributesFor)
    register_strategy(:build_stubbed,  FactoryBot::Strategy::Stub)
    register_strategy(:null,           FactoryBot::Strategy::Null)
  end

  def self.register_default_callbacks
    register_callback(:after_build)
    register_callback(:after_create)
    register_callback(:after_stub)
    register_callback(:before_create)
  end

  def self.register_callback(name)
    name = name.to_sym
    callback_names << name
  end
end

FactoryBot.register_default_strategies
FactoryBot.register_default_callbacks

Where `attributes_for` comes from?

def self.register_default_strategies
    register_strategy(:build,          FactoryBot::Strategy::Build)
    register_strategy(:create,         FactoryBot::Strategy::Create)
    register_strategy(:attributes_for, FactoryBot::Strategy::AttributesFor)
    register_strategy(:build_stubbed,  FactoryBot::Strategy::Stub)
    register_strategy(:null,           FactoryBot::Strategy::Null)
end

Register a strategy by:

def self.register_strategy(strategy_name, strategy_class)
    strategies.register(strategy_name, strategy_class)
    StrategySyntaxMethodRegistrar.new(strategy_name).define_strategy_methods
end

module FactoryBot
  class Registry
    include Enumerable

    attr_reader :name

    def initialize(name)
      @name  = name
      @items = Decorator::ClassKeyHash.new({})
    end

    def clear
      @items.clear
    end

    def each(&block)
      @items.values.uniq.each(&block)
    end

    def find(name)
      if registered?(name)
        @items[name]
      else
        raise ArgumentError, "#{@name} not registered: #{name}"
      end
    end

    alias :[] :find

    def register(name, item)
      @items[name] = item
    end

    def registered?(name)
      @items.key?(name)
    end
  end
end

(attributes_for.rb) There are many strategies in Strategy module (many classes)

module FactoryBot
  module Strategy
    class AttributesFor
      def association(runner)
        runner.run(:null)
      end

      def result(evaluation)
        evaluation.hash
      end
    end
  end
end

(strategy_syntax_method_registrar.rb) Using meta programming to define new method.

module FactoryBot
  # @api private
  class StrategySyntaxMethodRegistrar
    def initialize(strategy_name)
      @strategy_name = strategy_name
    end

    def define_strategy_methods
      define_singular_strategy_method
      define_list_strategy_method
      define_pair_strategy_method
    end

    private

    def define_singular_strategy_method
      strategy_name = @strategy_name

      define_syntax_method(strategy_name) do |name, *traits_and_overrides, &block|
        FactoryRunner.new(name, strategy_name, traits_and_overrides).run(&block)
      end
    end

    def define_list_strategy_method
      strategy_name = @strategy_name

      define_syntax_method("#{strategy_name}_list") do |name, amount, *traits_and_overrides, &block|
        unless amount.respond_to?(:times)
          raise ArgumentError, "count missing for #{strategy_name}_list"
        end

        amount.times.map { send(strategy_name, name, *traits_and_overrides, &block) }
      end
    end

    def define_pair_strategy_method
      strategy_name = @strategy_name

      define_syntax_method("#{strategy_name}_pair") do |name, *traits_and_overrides, &block|
        2.times.map { send(strategy_name, name, *traits_and_overrides, &block) }
      end
    end

    def define_syntax_method(name, &block)
      FactoryBot::Syntax::Methods.module_exec do
        if method_defined?(name) || private_method_defined?(name)
          undef_method(name)
        end

        define_method(name, &block)
      end
    end
  end
end