2

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?

Community
  • 1
  • 1
Anand
  • 3,690
  • 4
  • 33
  • 64
  • It seems like you'll need at least some code somewhere for every type, which prompts me to ask: What's the use of the database table? – Jordan Running Jan 12 '16 at 05:36
  • @Jordan Because I expect to add types later as plugins, and they 'just work' without having to modify the core code. – Anand Jan 12 '16 at 05:40
  • But *how* will they "just work"? You'll need to add code *somewhere*, so what do you gain by having each type tied to a database record? – Jordan Running Jan 12 '16 at 05:43
  • I could have a method in the Type class called verify_value(type_name, value) which could delegate to that type without bothering about what the per type behavior is. – Anand Jan 12 '16 at 05:45
  • Right. So your Type class will know where the code for each type lives. What do you gain by having each type tied to a database record? – Jordan Running Jan 12 '16 at 05:46
  • So I can find available types in my system – Anand Jan 12 '16 at 05:53
  • But the Type class already knows what types are available. Or anyway it should, and can easily, be implemented that way. – Jordan Running Jan 12 '16 at 05:54
  • And the Type model doesn't need to know where he class lives. Rails convention will load code in the library folder, for example – Anand Jan 12 '16 at 05:54
  • OK, forget about the database. How can I implement this in the rails App? – Anand Jan 12 '16 at 06:01

1 Answers1

2

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}".

Jordan Running
  • 102,619
  • 17
  • 182
  • 182
  • Thanks, Jordan - this is a good ruby answer. Would this work if I just plopped the 'types' folder with type.rb, string.rb, etc. from your example under lib? I can get by without the type defined in the database. My thought was to enable people registering types in the future through an interface and a plugin framework, but I can extend it when I get there. – Anand Jan 12 '16 at 06:44
  • It should work with minimal modification. You may have to fiddle with the paths in e.g. `load_types!` a bit. As always you should write some tests to cover the expected functionality. As for making a plugin framework, there's lots of prior art out there, so it's definitely worth looking at what has come before to avoid repeating their mistakes. – Jordan Running Jan 12 '16 at 06:53
  • P.S. I edited my answer to simplify it a bit. Now it uses a simple hash to keep registered type checkers, instead of having each `extend` the base module. This also makes it easier to get a list of the registered types (and add other features, e.g. each could respond to `description` to get a human-friendly name or help text). – Jordan Running Jan 12 '16 at 06:54
  • Thanks, I agree with the need to modify the paths in load_types! - will also look at plugin models later. Accepting answer now. This other thread http://stackoverflow.com/a/19650564/456735 has some information about how to reload lib files on a per request versus at server start time - could be handy to coerce this approach into rails. – Anand Jan 12 '16 at 07:24