Skip to content

Instantly share code, notes, and snippets.

@asenchi
Created March 17, 2012 03:01
Show Gist options
  • Select an option

  • Save asenchi/2054604 to your computer and use it in GitHub Desktop.

Select an option

Save asenchi/2054604 to your computer and use it in GitHub Desktop.
Sequel::Factory

Sequel::Factory

Sequel::Factory implements a tiny DSL on Sequel::Model that allows you to create factories for objects of a model class. A factory is simply a Ruby block that gets evaluated each time a new object is generated. Inside the block you can call methods that correspond to the names of attributes of the object you're creating. If a value is given to the method, it will set the value for that attribute. Regardless, the method will always return the current value for that attribute.

Factories have names (the default name is :default) and you can include a factory in another. When you do this, the included factory will run first.

A simple factory for a User class might look like this:

User.factory do
  name "Michael Jackson"
  handle name.downcase.gsub(/[^a-z]/, '')
  email "#{handle}@example.com"
end

User.factory(:with_avatar) do
  include User.factory
  avatar_url "http://example.org/#{handle}.jpg"
end

You can now call User.make and it will create an instance of User using the default factory. If you want to generate a User object with all the attributes of a "default" User object plus an avatar, use User.make(:with_avatar).

If you pass a block as the value of an attribute, it acts like a sequence. That is, each time an object is generated the block will be called with a unique, incrementing number. The following factory builds upon the previous example and uses a block to generate a unique value for the twitter_id attribute.

User.factory(:twitter_user) do
  include User.factory(:with_avatar)
  twitter_id {|n| n }
  twitter_handle "@#{handle}"
end

Now, when you do a User.make(:twitter_user), that object will have the attributes unique to the :twitter_user factory, plus those of the :with_avatar factory.

Of course, the whole point of factories is to be able to generate different instances each time you run them. Use the Randexp gem for great win here.

Save Method

By default factories use Sequel::Model.create to create new instances. If you don't want to use it, simply change the model's factory_method. For example, to avoid saving factory-created User objects to the database, you could do:

User.factory_method = :new

Another value that can sometimes be useful here is :find_or_create, e.g. when you're using a factory to create objects that have a unique constraint on some column.

require 'sequel'
module Sequel
class Factory
def initialize(values_proc)
@values_proc = values_proc
end
# The Proc this factory uses to generate a new set of values.
attr_reader :values_proc
# Merges all key/value pairs generated by this factory into the
# given +values+ hash.
def apply_values(values={})
@values = values
instance_eval(&values_proc)
@values
end
# Merges all key/value pairs that are generated by the given +factory+ into
# the values currently being generated by this factory. Should be called
# inside a values Proc to include values from some higher-level factory.
#
# User.factory do
# name Randgen.name
# end
#
# User.factory(:with_url) do
# include_factory User.factory
# url "http://www.example.com"
# end
#
# User.make # Has only a name property
# User.make(:with_url) # Has both name *and* url properties
def include_factory(factory)
factory.apply_values(@values)
end
alias_method :include, :include_factory
# Gets/sets the value of the given +key+. If any +args+ are provided, the
# value is set to the first one. Otherwise, if a block is given it will be
# called with a number unique to the given +key+ (like a sequence) and the
# return value of the block is used as the value.
def method_missing(key, *args)
if args.any?
@values[key] = args.first
elsif block_given?
@counts ||= {}
@counts[key] ||= 0
@counts[key] += 1
@values[key] = yield(@counts[key])
end
@values[key]
end
end
class Model
class << self
attr_writer :factory_method
end
# Returns the name of the Sequel::Model class method that this factory uses
# to make new instances. Defaults to +:create+. Other useful methods are
# +:new+ (to prevent saving to the database) and +:find_or_create+ (to avoid
# uniqueness constraints in the database).
def self.factory_method
return @factory_method unless @factory_method.nil?
return superclass.factory_method if superclass.respond_to?(:factory_method)
:create
end
# A Hash of factories for this model, keyed by factory name.
def self.factories
@factories ||= {}
end
# Gets/sets the factory with the given +name+. If a block is given, uses that
# block to create a new factory.
def self.factory(name=:default)
factories[name] = Factory.new(Proc.new) if block_given?
factories[name]
end
def self.has_factory?(name)
not factory(name).nil?
end
# Makes an instance of this model using the factories with the given
# +factory_names+. Any +values+ given to this method will override
# factory-produced values with the same name after all factory values have
# been generated. The default factory is used if no factory names are given.
#
# Note: If +values+ is not a Hash, it will be used as a factory name.
def self.make(values={}, *factory_names)
unless Hash === values
factory_names.unshift(values)
values = {}
end
factory_names << :default if factory_names.empty?
factory_values = factory_names.inject({}) do |memo, name|
fac = factory(name) or raise "Unknown #{self} factory: #{name}"
fac.apply_values(memo)
end
send factory_method, factory_values.merge(values)
end
# Forces the #factory_method to be the given +method+ temporarily while an
# instance is made, then reverts to the old factory method.
def self.make_with(method, *args)
tmp = @factory_method
@factory_method = method
obj = make(*args)
@factory_method = tmp
obj
end
# Sugar for +make_with(:new, *args)+. Useful when the #factory_method is
# something other than +:new+ but you still want to use it.
def self.build(*args)
make_with(:new, *args)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment