Skip to content

Commit

Permalink
Add a comment or two. Fix a couple broken tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
ozydingo committed May 22, 2021
1 parent 8edd39c commit b4fdb7a
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 10 deletions.
2 changes: 2 additions & 0 deletions lib/factory_burgers/builder.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Build resources from specified factories, traits, and attributes

module FactoryBurgers
class Builder
# TODO: clean up method signature
Expand Down
35 changes: 33 additions & 2 deletions lib/factory_burgers/cheating.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
# This module contrains utilities to manipulate sequences that might fail for
# usage in development environments that do not roll back db transactions for
# factory creations.

module FactoryBurgers
module Cheating
module_function

# Given a factory, discover what sequences are used in its attributes.
# Since factories' attributes are defined as blocks, there is no way to know
# which blocks use sequences without executing the block. We do this with
# SequenceCheater to take notes without actually using the seqence itself.
def discover_sequences(factory)
cheater = SequenceCheater.new
attributes = factory.definition.attributes
Expand All @@ -12,6 +20,13 @@ def discover_sequences(factory)
return cheater.sequence_names
end

# Find the highest value of a sequence in the database and advance the
# sequence past it to avoid uniqueness violations.
# This is by no means foolproof nor performant; use it with care.
# There isn't a good way to access the iterator state; instead, we measure
# how far we must advance the sequence and call `generate` that many times.
# This works well for sequential iterations, but more complex sequences
# might break this.
def advance_sequence(name, klass, column, sql: nil, regex: nil)
sequence = FactoryBot::Internal.sequences.find(name)
sql ||= sql_condition(sequence, column)
Expand All @@ -23,16 +38,32 @@ def advance_sequence(name, klass, column, sql: nil, regex: nil)
return FactoryBot.generate(name)
end

# ---
# TODO: Use this same principle, but without swallowing `method_missing`, to
# probe sequences at specific numeric values. This will allow us to determine if
# a sequence is doing unexpected things like descending, e.g.
# `sequence :negatives { |ii| -ii }`
# ---

# For a sequence defined with { |ii| "foo#{ii}" }, `sql_condition` returns
# the SQL fragemnt "<name> like 'foo%'"
# TODO: does this work in pg, sqlite?
# TODO: support mongo as well
def sql_condition(sequence, column)
# This proc is defined by the block used in the sequence definition
# This may be fragile, but may also be out only option
proc = sequence.instance_variable_get(:@proc)
injector = SequenceInjector.new("%")
sql_value = proc.call(injector)
return "#{column} like '#{sql_value}'"
end

def regex_pattern(sequence)
# For a sequence defined with { |ii| "foo#{ii}" }, `sql_condition` returns
# the regex /foo(\d+)/
def regex_pattern(sequence, numeric: true)
wildcard = numeric ? "\\d" : "."
proc = sequence.instance_variable_get(:@proc)
injector = SequenceInjector.new("(\\d+)")
injector = SequenceInjector.new("(#{wildcard}+)")
return Regexp.new(proc.call(injector))
end
end
Expand Down
3 changes: 3 additions & 0 deletions lib/factory_burgers/introspection.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Discover information about factories for a class, such as what associations
# are defined on that class that also have factories we can use

module FactoryBurgers
module Introspection
module_function
Expand Down
5 changes: 5 additions & 0 deletions lib/factory_burgers/presenter_builder.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# The PresenterBuilder is resposible for building anonymous subclasses of
# FactoryBurgers::Presenters::Base when FactoryBurgers::Presenters.present is
# called with a block. The block is evaluated in the context of a
# FactoryBurgers::PresenterBuilder instance, which understands the DSL.

module FactoryBurgers
class PresenterBuilder < BasicObject
def initialize(klass)
Expand Down
7 changes: 6 additions & 1 deletion lib/factory_burgers/presenters.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Module for adding, finding, and using application data presenters. Presenters
# are used to define what attributes to show in the front end, what to call the
# built objects, and managing links to the objects in the application if they
# exist.

module FactoryBurgers
module Presenters
@presenters = {}
Expand Down Expand Up @@ -33,7 +38,7 @@ def data_for(object)
presenter = presenter_for(object) or return nil
{
type: presenter.type,
attribuets: presenter.attributes,
attributes: presenter.attributes,
link: presenter.link_path,
}
end
Expand Down
5 changes: 5 additions & 0 deletions lib/factory_burgers/presenters/base.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Presenter classes are responsible for formatting application object data for
# the UI. This defatul presenter will display the id and name attributes, if
# they exist, and does not have an application link. Create subclasses of this
# class to present different information for different application models.

module FactoryBurgers
module Presenters
class Base
Expand Down
19 changes: 19 additions & 0 deletions lib/factory_burgers/sequence_cheater.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
# A SequenceCheater is able to take the block associated with a factory's
# attribute and discover if it uses a sequence (specifically, if it calls
# `generate`). It does this by evaluting the block in its own context, where
# the `generate` method has been defined to save the name it was called with.
#
# For example, in the following factory:
#
# FactoryBot.define do
# factory :foo do
# bar { generate :baz }
# end
# end
#
# We can discover the `bar` attribute of the factory, and throgh some slightly
# dangerous non-public access (it's Ruby, after all) we can get the block
# defined as `{ generate :baz }`. We then call that block on a SequenceCheater,
# which simply records that `generate` was called with the name `:baz`.


module FactoryBurgers
class SequenceCheater < BasicObject
attr_reader :sequence_names
Expand Down
16 changes: 16 additions & 0 deletions lib/factory_burgers/sequence_injector.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# A SequenceInjector is a probe that can be used to hijcak a FactoryBot sequence.
# It injects a specific replacement value in place of the sequence argument, or
# any usage of that argument.
#
# For an injector with a replacement value "foo":
#
# * A sequence defined with a block ``{ |ii| "thing-#{ii} "}`
# will evaluate to "thing-foo".
# * A sequence defined with a block ``{ |ii| "thing-#{ii.days.from_onw.month} "}`
# will also evaluate to "thing-foo".
#
# This allows us to generate wildcard, such as for SQL
# SequenceInjector.new("%")
# or Regex
# SequenceInjector.new(".")

module FactoryBurgers
class SequenceInjector < BasicObject
def initialize(replacement_value)
Expand Down
15 changes: 8 additions & 7 deletions spec/lib/factory_burgers/presenters_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
let(:user) { create :user, name: "Foo", login: "foobar" }
let(:data) { FactoryBurgers::Presenters.data_for(user) }

it "uses the class" do
it "uses the default class" do
expect(data[:type]).to eq("User")
expect(data[:link]).to include({url: nil})
expect(Set.new(data[:attributes].keys)).to eq(Set.new(["id", "name"]))
expect(data[:link]).to be_nil
end
end

Expand All @@ -30,7 +31,7 @@

it "uses the presenter class" do
expect(user_data[:type]).to eq("An example")
expect(user_data[:link]).to include(url: "link/to/foobar")
expect(user_data[:link]).to eq("link/to/foobar")
end
end

Expand All @@ -40,7 +41,7 @@

it "uses the presenter class" do
expect(admin_data[:type]).to eq("An example")
expect(admin_data[:link]).to include(url: "link/to/foobar!")
expect(admin_data[:link]).to eq("link/to/foobar!")
end
end

Expand All @@ -50,7 +51,7 @@

it "uses the default base presenter" do
expect(post_data[:type]).to eq("Post")
expect(post_data[:link]).to include(url: nil)
expect(post_data[:link]).to be_nil
end
end

Expand All @@ -67,7 +68,7 @@
end

expect(data[:type]).to eq("A user named Foo")
expect(data[:link]).to include(url: "path/to/user/#{user.id}/profile")
expect(data[:link]).to eq("path/to/user/#{user.id}/profile")
end

it "uses default values" do
Expand All @@ -76,7 +77,7 @@
end

expect(data[:type]).to eq("User")
expect(data[:link]).to include({url: nil})
expect(data[:link]).to be_nil
end
end
end

0 comments on commit b4fdb7a

Please sign in to comment.