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:
# 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.
# 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?
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:
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!
module Type
module Integer
def self.check(value)
!!Integer(value)
rescue ArgumentError
false
end
end
end
Type.register(:integer, Type::Integer)
module Type
module String
def self.check(value)
true
end
end
end
Type.register(:string, Type::String)
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}"
.