Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't understand Ruby's magic

In railscasts project you can see this code:

before(:each) do
  login_as Factory(:user, :admin => true)
end

The corresponding definition for the function is:

Factory.define :user do |f|
  f.sequence(:github_username) { |n| "foo#{n}" }
end

I can't understand how the admin parameter is passing to function, while in the function there's no word about admin parameter. Thanks

like image 789
megas Avatar asked Feb 17 '11 04:02

megas


2 Answers

Factory.define is not a function definition, it is a method that takes a symbol or string (in this case user) and a block that defines the factory you are making. Factory(:user, :admin => true) makes a User object, with admin attributes. It is not calling the code in your second snippet, it is calling Factory() which initializes a factory, and selects one (in this case the one defined in second snippet). Then it passes options in hash form to Factory as well.

Factory selects the :user factory which is very generic. The option :admin=>true just tells Factory to set the admin instance variable on User to true.

This is actually what it is calling in factory.rb in factory girl

def initialize(name, options = {}) #:nodoc:
  assert_valid_options(options)
  @name = factory_name_for(name)
  @options = options
  @attributes = []
end

So Factory(name,options) is equivalent to Factory.new(name,options) in this code.

http://www.ruby-doc.org/core/classes/Kernel.html Notice Array and String etc have similar constructs. I am trying to figure out how they did that now.

This is all confusing even for decent Ruby programmers. I recommend strongly the book "Metaprogramming Ruby" It is probably the best book I have read in ruby and it tells you a lot about this magic stuff.

like image 91
Michael Papile Avatar answered Sep 30 '22 03:09

Michael Papile


Michael Papile's response is essentially correct. However, I'd like to elaborate upon it a bit as there are some technical nuances that you might wish to be aware of. I looked over the code for railscasts and factory_girl and I believe there are a few extra pieces to the puzzle that explain how the :admin => true arg ends up creating the admin attribute of the user factory. The attribute addition does not actually happen by way of Factory's initialize() method, although as Michael pointed out that method is indeed being called in service of building the new user factory object.

I'm going to include in this explanation all the steps I took in case you'd like to see how to go about investigating similar questions you might have.

Since your original post is dated Feb. 17th I looked at the version of railscasts that closely matches that date.

I looked in its Gemfile:

https://github.com/ryanb/railscasts/blob/d124319f4ca2a2367c1fa705f5c8229cce70921d/Gemfile

Line 18:

gem "factory_girl_rails"

I then checked out the commit of factory_girl_rails that most closely matched the Feb 17th date.

https://github.com/thoughtbot/factory_girl_rails/blob/544868740c3e26d8a5e8337940f9de4990b1cd0b/factory_girl_rails.gemspec

Line 16:

s.add_runtime_dependency('factory_girl', '~> 2.0.0.beta')

factory_girl version 2.0.0.beta was actually not so easy to find. There are no github tags with that name so I just checked out the closest in terms of commit date.

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/syntax/vintage.rb

Lines 122-128:

# Shortcut for Factory.default_strategy.
#
# Example:
#   Factory(:user, :name => 'Joe')
def Factory(name, attrs = {})
  Factory.default_strategy(name, attrs)
end

So the Factory invocation in railscasts is actually calling a convenience method which invokes the "default strategy", which is located in the same file:

Lines 39-52:

# Executes the default strategy for the given factory. This is usually create,
# but it can be overridden for each factory.
#
# Arguments:
# * name: +Symbol+ or +String+
#   The name of the factory that should be used.
# * overrides: +Hash+
#   Attributes to overwrite for this instance.
#
# Returns: +Object+
# The result of the default strategy.
def self.default_strategy(name, overrides = {})
  self.send(FactoryGirl.find(name).default_strategy, name, overrides)
end

Note that FactoryGirl.find is invoked to get the object on which to invoke default_strategy. The find method resolves to here:

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/registry.rb

Lines 12-14:

def find(name)
  @items[name.to_sym] or raise ArgumentError.new("Not registered: #{name.to_s}")
end

Here the name is :user. Thus we wish to invoke default_strategy on the user factory. As Michael Papile pointed out, this user factory was defined and registered by the railscasts code that you originally thought was the class definition for Factory.

https://ryanb/railscasts/blob/d124319f4ca2a2367c1fa705f5c8229cce70921d/spec/factories.rb

Lines 23-25:

Factory.define :user do |f|
  f.sequence(:github_username) { |n| "foo#{n}" }
end

So in investigating what the default strategy is for the user factory, I looked around in the railscasts project and found this:

https://ryanb/railscasts/blob/d124319f4ca2a2367c1fa705f5c8229cce70921d/spec/factories.rb

Lines 43-45:

def default_strategy #:nodoc:
  @options[:default_strategy] || :create
end

:create is the default strategy. We go back to factory_girl to find the def for create.

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/syntax/methods.rb

Lines 37-55:

# Generates, saves, and returns an instance from this factory. Attributes can
# be individually overridden by passing in a Hash of attribute => value
# pairs.
#
# Instances are saved using the +save!+ method, so ActiveRecord models will
# raise ActiveRecord::RecordInvalid exceptions for invalid attribute sets.
#
# Arguments:
# * name: +Symbol+ or +String+
#   The name of the factory that should be used.
# * overrides: +Hash+
#   Attributes to overwrite for this instance.
#
# Returns: +Object+
# A saved instance of the class this factory generates, with generated
# attributes assigned.
def create(name, overrides = {})
  FactoryGirl.find(name).run(Proxy::Create, overrides)
end 

The create strategy calls the run method defined here:

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/factory.rb

Lines 86-97:

def run(proxy_class, overrides) #:nodoc:
  proxy = proxy_class.new(build_class)
  overrides = symbolize_keys(overrides)
  overrides.each {|attr, val| proxy.set(attr, val) }
  passed_keys = overrides.keys.collect {|k| FactoryGirl.aliases_for(k) }.flatten
  @attributes.each do |attribute|
    unless passed_keys.include?(attribute.name)
      attribute.add_to(proxy)
    end
  end
  proxy.result(@to_create_block)
end

A translation/summarization of what this code is doing:

First, the proxy object is built by calling new on the proxy_class, which in this case is Proxy::Create, which is defined here:

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/proxy/create.rb

Basically all you need to know is that proxy is building a new user factory object and invoking callbacks before and after the factory object is created.

Going back to the run method, we see that all the extra args that were originally passed into the Factory convenience method (in this case, :admin => true) are now being labelled as overrides. The proxy object then invokes a set method, passing in each attribute-name/value pair as args.

The set() method is part of the Build class, the parent class of Proxy.

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/proxy/build.rb

Lines 12-14:

def set(attribute, value)
  @instance.send(:"#{attribute}=", value)
end

Here @instance refers to the proxied object, the user factory object.

This then, is how :admin => true is set as an attribute on the user factory that the railscasts spec code creates.

If you want, you can google "programming design patterns" and read about the following patterns: Factory, Proxy, Builder, Strategy.

Michael Papile wrote:

http://www.ruby-doc.org/core/classes/Kernel.html Notice Array and String etc have similar constructs. I am trying to figure out how they did that now.

If you are still curious, the Array and String you see in the Kernel doc are actually just factory methods used to create new objects of those types. That's why no new method invocation is needed. They are not actually constructor calls per se, but they do allocate and initialize Array and String objects, and hence under the hood they are doing the equivalent of calling initialize() on objects of those types. (In C though, of course, not Ruby)

like image 34
David Avatar answered Sep 30 '22 02:09

David