# frozen_string_literal: true
module FactoryBot
module Blueprint
module RSpec
# Helper methods to integrate factory_bot-blueprint
with RSpec.
# This module is automatically extended to {RSpec::Core::ExampleGroup}.
#
# To use FactoryBot::Blueprint from RSpec with minimal effort, usually {#letbp} is the best choice.
module Driver
# Shorthand method for let(name) { ::FactoryBot::Blueprint.plan(...) }
.
# You can access the let binding context by #ext
in DSL code.
# @param name [Symbol] name of the object to be declared using RSpec's let
# @param inherit [Boolean] whether to extend the blueprint by super()
# @yield Write Blueprint DSL code here
# @example
# RSpec.describe "something" do
# let(:blog_id) { SecureRandom.uuid }
#
# let_blueprint(:blog_bp) do
# let.blog(id: ext.blog_id, title: "Daily log") do
# let.article(title: "Article 1")
# article(title: "Article 2")
# article(title: "Article 3")
# end
# end
# end
def let_blueprint(name, inherit: false, &)
let(name) { ::FactoryBot::Blueprint.plan(inherit ? super() : nil, ext: self, &) }
end
# Build objects by build
strategy in FactoryBot from a blueprint and declare them using RSpec's
# let
.
# @param map [Hash{Symbol => Object}]
# map data structure from source blueprints to instance definitions.
# Each instance will be built with FactoryBot::Blueprint.build(__send__(source))
# @example
# RSpec.describe "something" do
# let_blueprint(:blog_bp) do
# # ... Write some DSL ...
# end
#
# # Simplest example:
# # This is equivalent to `let_blueprint_build blog_bp: { representative: :blog }`
# let_blueprint_build blog_bp: :blog
#
# # Another shorthand example:
# # This is equivalent to `let_blueprint_build blog_bp: { items: %i[blog article] }`
# let_blueprint_build blog_bp: %i[blog article]
#
# # Most flexible example:
# # :representative specifies the name of the representative object to be declared. Defaults to nil
# # :items specifies the names of the objects to be declared. Defaults to []
# # :instance specifies the name of the instance object to be declared. Defaults to :"#{source}_instance"
# let_blueprint_build blog_bp: { representative: :blog, items: %i[article], instance: :blog_instance }
#
# # Above example will be expanded to:
# let(:blog_instance) { ::FactoryBot::Blueprint.build(blog_bp) } # the instance object
# let(:blog) { blog_instance[blog_bp.representative_node.name] } # the representative object
# let(:article) { blog_instance[:article] } # the item objects
# end
def let_blueprint_build(**map) = let_blueprint_instantiate(:build, **map)
# Build objects by create
strategy in FactoryBot from a blueprint and declare them using RSpec's
# let
.
# See {#let_blueprint_build} for more details.
# @param map [Hash{Symbol => Object}]
# map data structure from source blueprints to instance definitions.
# Each instance will be built with FactoryBot::Blueprint.build(__send__(source))
def let_blueprint_create(**map) = let_blueprint_instantiate(:create, **map)
# @!visibility private
def let_blueprint_instantiate(strategy, **map)
raise ArgumentError, "Unsupported strategy: #{strategy}" if strategy && !%i[create build].include?(strategy)
map.each do |source, definition|
raise TypeError, "source must be a Symbol" unless source.is_a?(Symbol)
definition =
case definition
when Symbol
{ representative: definition }
when Array
{ items: definition }
when Hash
definition
else
raise TypeError, "definition must be one of Symbol, Array, Hash"
end
representative_name = definition[:representative]
item_names = definition[:items] || []
instance = definition[:instance] || :"#{source}_instance"
if representative_name && !representative_name.is_a?(Symbol)
raise TypeError, "representative must be a Symbol"
end
if !item_names.is_a?(Array) || !item_names.all? { _1.is_a?(Symbol) }
raise TypeError, "items must be an Array of Symbols"
end
raise TypeError, "instance must be a Symbol" unless instance.is_a?(Symbol)
if strategy # If no strategy is specified, the instance is assumed to exist
let(instance) { ::FactoryBot::Blueprint.instantiate(strategy, __send__(source)) }
end
if representative_name
let(representative_name) { __send__(instance)[__send__(source).representative_node.name] }
end
item_names.each do |name|
let(name) { __send__(instance)[name] }
end
end
end
# Write the blueprint in DSL, create an instance of it, and declare each object of the instance using RSpec's
# let
.
#
# This is a shorthand for {#let_blueprint} with {#let_blueprint_build} or {#let_blueprint_create}.
# @param name [Symbol]
# name of the representative object to be declared using RSpec's let
.
# It is also used as a name prefix of the blueprint
# @param items [Array] names of the objects to be declared using RSpec's let
# @param inherit [Boolean] whether to extend the blueprint by super()
# @param strategy [:create, :build]
# FactoryBot strategy to use when building objects.
# This option is ignored if inherit: true
# @yield Write Blueprint DSL code here
# @example
# RSpec.describe "something" do
# letbp(:blog, %i[article]) do
# blog(title: "Daily log") do
# let.article(title: "Article 1")
# article(title: "Article 2")
# article(title: "Article 3")
# end
# end
#
# # Above example will be expanded to:
# let_blueprint(:blog_blueprint) do
# blog(title: "Daily log") do
# let.article(title: "Article 1")
# article(title: "Article 2")
# article(title: "Article 3")
# end
# end
# let_blueprint_create blog_blueprint: { representative: :blog, items: %i[article] }
# end
def letbp(name, items = [], inherit: false, strategy: :create, &)
raise TypeError, "name must be a Symbol" unless name.is_a?(Symbol)
source = :"#{name}_blueprint"
strategy = nil if inherit
let_blueprint(source, inherit:, &)
let_blueprint_instantiate strategy, source => { representative: name, items: }
end
end
end
end
end
# @!visibility private
module RSpec
module Core
class ExampleGroup
extend ::FactoryBot::Blueprint::RSpec::Driver
end
end
end