Question

I'm working on a module that, among other things, will add some generic 'finder' type functionality to the class you mix it into. The problem: for reasons of convenience and aesthetics, I want to include some functionality outside the class, in the same scope as the class itself.

For example:

class User
  include MyMagicMixin
end

# Should automagically enable:

User.name('Bob')   # Returns first user named Bob
Users.name('Bob')  # Returns ALL users named Bob 
User(5)            # Returns the user with an ID of 5
Users              # Returns all users

I can do the functionality within these methods, no problem. And case 1 (User.name('Bob')) is easy. Cases 2–4, however, require being able to create new classes and methods outside User. The Module.included method gives me access to the class, but not to its containing scope. There is no simple "parent" type method that I can see on Class nor Module. (For namespace, I mean, not superclass nor nested modules.)

The best way I can think to do this is with some string parsing on the class's #name to break out its namespace, and then turn the string back into a constant. But that seems clumsy, and given that this is Ruby, I feel like there should be a more elegant way.

Does anyone have ideas? Or am I just being too clever for my own good?

Was it helpful?

Solution

In your example, User is just a constant that points to a Class object. You can easily create another constant pointer when MyMagicMixin is included:

module MyMagicMixin
  class <<self
    def self.included(klass)
      base.extend MyMagicMixin::ClassMethods
      create_pluralized_alias(klass)
    end

    private

    def create_pluralized_alias(klass)
      fq_name = klass.to_s
      class_name = fq_name.demodulize
      including_module = fq_name.sub(Regexp.new("::#{class_name}$", ''))
      including_module = including_module.blank? ? Object : including_module.constantize
      including_module.const_set class_name.pluralize, klass
    end
  end

  module ClassMethods
    # cass methods here
  end
end

Of course this doesn't answer whether you should do such a thing.

OTHER TIPS

I'd lean toward being too clever.

Even if there was an elegant solution, it seems rather odd to be including a module inside a class which creates classes outside the class.

This is a problem that comes up sometimes on the mailinglists. It's also a problem that comes up in Rails. The solution is, as you already suspected, basically Regexp munging.

However, there is a more fundamental problem: in Ruby, classes do not have a name! A class is just an object like any other. You can assign it to an instance variable, to a local variable, to a global variable, to a constant or even not assign it to anything at all. The Module#name method is basically just a convenience method that works like this: it looks through the list of defined constants until it finds one that points to the receiver. If it finds one, it returns the first one it can find, otherwise it returns nil.

So, there's two failure modes here:

a = Class.new
a.name # => nil
B = a
B.name # => "B"
A = B
A.name # => "B"
  • a class might not have a name at all
  • a class might have more than one name, but Module#name will only return the first one it finds

Now, if someone tries to call As to get a list of As, they will be pretty surprised to find that that method doesn't exist, but that they can call Bs instead to get the same result.

This does actually happen in reality. In MacRuby, for example String.name returns NSMutableString, Hash.name returns NSMutableDictionary and Object.name returns NSObject. The reason for this is that MacRuby integrates the Ruby runtime and the Objective-C runtime into one, and since the semantics of an Objective-C mutable string are identical to a Ruby string, the entire implementation of Ruby's string class is essentially a single line: String = NSMutableString. And since MacRuby sits on top of Objective-C, that means that Objective-C starts first, which means that NSMutableString gets inserted into the symbol table first, which means it gets found first by Module#name.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top