Skip to content

Instantly share code, notes, and snippets.

@latentflip
Created July 4, 2012 20:47
Show Gist options
  • Save latentflip/3049461 to your computer and use it in GitHub Desktop.
Save latentflip/3049461 to your computer and use it in GitHub Desktop.
This is ridiculous, never ever do this.

So @MattWynne totally nerd-sniped me this evening, by asking this:

Is is possible to dynamically add ActiveModel::Naming compliance to an object at runtime? My meta-fu is letting me down.

As far as I can figure out, it's not doable, but I can get 'pretty' close, here's what I learned:

ActiveModel::Naming is normally invoked like this:

class Foo
  extend ActiveModel::Naming
end

Which lets you do things like:

Foo.model_name #=> 'Foo'

x = Foo.new
x.class.model_name #=> 'Foo'

So how can we add this functionality at runtime? The obvious way is:

x = Foo.new
x.class.extend(ActiveModel::Naming)

But if we do that, then we may as well have done it initially, as we are modifying the class not just the instance. This will affect all 'Foo's in our system, which isn't really what we want.

We could try to modify the eigenclass of an instance, like so:

require 'active_support/all'
require 'active_model'

class Foo
  def eigen
    class << self
      self
    end
  end
end

x = Foo.new
x.eigen.extend(ActiveModel::Naming)

But when doing a classwise lookup, ruby uses the actual class, not the eigen class:

x.class.model_name #=> wrap2.rb:14:in `<main>': undefined 
                   #   method `model_name' for Foo:Class 
                   # (NoMethodError)

So, we could override class, to return the eigenclass (now we are getting a bit crazy):

require 'active_support/all'
require 'active_model'

class Foo
  def eigen
    class << self
      self
    end
  end
  def class
    eigen
  end
end

x = Foo.new
x.eigen.extend(ActiveModel::Naming)

But this doesn't work either, as now ActiveModel::Naming get's confused as it's not in a named class:

x.class.model_name #=> Class name cannot be blank. You 
                   #   need to supply a name argument 
                   #   when anonymous class given 
                   #   (ArgumentError)

So the best I can come up with is what is in the file below. This instantiates a wrapper class with the same name (dynamically) as the original class, extends it with ActiveRecord::Naming, and forwards all calls to the original. The one caveat is that our class is now within a module, so the names will have the module name in... I don't know how this affects form_for, etc

require 'active_support/all'
require 'active_model'
class Foo
A_CONSTANT = 3.14
def greet
puts "Hi! + #{A_CONSTANT}"
end
def self.class_method
puts "This is a class method"
end
end
#We need a wrapper module, to namespace our "new" Foo
module Wrapper; end
def wrap(model)
original_class_name = model.class.to_s
#Let's create a class
wrapper_class = Class.new(Object) do
#Let's set a constant in the class, to store
#our original instance so we can forward things to it
self.const_set('MODEL', model)
#If we get any class methods on this instance,
#forward them to our original object
class << self
def method_missing(name, *args, &block)
self::MODEL.class.send(name, *args, &block)
end
end
#If we get any instance methods on this instance,
#forward them
def method_missing(name, *args, &block)
self.class::MODEL.send(name, *args, &block)
end
end
#rename our wrapper class within the Wrapper module
Wrapper.const_set(original_class_name, wrapper_class)
#Extend our wrapper class with activemodel::naming
Wrapper.const_get(original_class_name).
extend(ActiveModel::Naming)
#instantiate our new object
Wrapper.const_get(original_class_name).new
end
#Let's try it out
original = Foo.new
wrapped = wrap(original)
#instance methods work
original.greet #=> 'Hi!'
wrapped.greet #=> 'Hi!'
#class methods work
original.class.class_method #=> "This is a class method"
wrapped.class.class_method #=> "This is a class method"
#Name works (with the wrapper though)
p wrapped.class.model_name #=> 'Wrapper::Foo'
#But we haven't polluted our original class
p original.class.model_name #=> "wrap.rb:32:in
#`<main>': undefined method
#`model_name' for Foo:Class
#(NoMethodError)"
@mattwynne
Copy link

ha ha ha ha ha.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment