Search code examples
ruby-on-railsmetaprogrammingdelegation

How to create an interface on a per object type level?


In my rails (4.2.1) app, I have a Type (model) that contains records with :name of "string", "integer", etc.

I want the user to be able to pass in values and check if it is a valid object of a given type. So, the pseudocode is:

check_value(:integer, "1") #=> true
check_value(:integer, "foo") #=>false

I would like to add new types over time which have their own logic to check_value for that type.

Here are a few alternatives I have looked at:

1 Add one method per type directly to Type model -

# app/models/type.rb
# inside class Type...
def check_string_value(val)
 true
end

def integer_value(val)
 begin
  Integer(val)
 rescue
  return false
 end
 return true    
end

This would work, but would require me to modify the type.rb file each time a new field type is added, which I would like to avoid.

2 per object methods in a file per type:

# lib/types/integer_type/integer_type.rb
int = Type.where(name: "integer").first
class << int
  def check_value(val)
    begin
     Integer(val)
    rescue
     return false
    end
    return true   
  end
end

The problem with this is that I cannot call that particular instance of the integer type to pass in the verification call, since I do not construct it in my calling code.

So, neither of these seems ideal - I would like a technique that delegates the verify call from type.rb to the individual type to handle. Is this possible? How could I do it?


Solution

  • There are a number of ways you could do this in Ruby. Here's one extremely basic way. Have each type module define a check method and then "register" itself with the Type module with e.g. Type.register(:integer, IntegerType). Then Type.check(type, value) need only check the registered types and, if one matches, delegate to its check method:

    type.rb

    module Type
      @@checkers = {}
    
      def self.check(type, value)
        if @@checkers.key?(type)
          @@checkers[type].check(value)
        else
          raise "No registered type checker for type `#{type}'"
        end
      end
    
      def self.register(type, mod)
        @@checkers[type] = mod
      end
    
      def self.registered_types
        @@checkers.keys
      end
    
      def self.load_types!
        Dir['./types/*.rb'].each do |file|
          require file
        end
      end
    end
    
    Type.load_types!
    

    types/integer.rb

    module Type
      module Integer
        def self.check(value)
          !!Integer(value)
        rescue ArgumentError
          false
        end
      end
    end
    
    Type.register(:integer, Type::Integer)
    

    types/string.rb

    module Type
      module String
        def self.check(value)
          true
        end
      end
    end
    
    Type.register(:string, Type::String)
    

    And then...

    p Type.registered_types # => [ :integer, :string ]
    p Type.check(:integer, "1") # => true
    p Type.check(:integer, "a") # => false
    p Type.check(:string, "a") # => true
    

    Of course, you could go much fancier than this with metaprogramming (see the previous revision of this answer for a solution that used Module#extend instead of keeping registered modules in a simple hash) or, say, lazy loading, but you get the idea. The "core" type module doesn't have to know the names of the other modules (and you could define load_types! however you want, or do that somewhere else entirely). The only requirement is that the modules respond to "check_#{type}".