views:

48

answers:

3

I really had a hard time figuring out how to word this question, but in essence, I want to do this:

model = MyModel.new
model.title = "foo bar"
model.title.to_id #=> "foo_bar"

I have an ActiveRecord class for MyModel

class MyModel < ActiveRecord::Base
  def to_id(str)
    str.downcase.gsub(" ", "_")
  end
end

but, of course, it's looking for the to_id method on String, and I don't want to override string, because I don't require this behaviour on every string. Just strings associated with MyModel. I could keep it simple and do something like:

model.to_id(model.title)

But that's not very Ruby.

I know I've seen examples of this sort of method implemented before, I just can't track them down.

Halp anyone?

A: 

You should take a look at the functions within ActiveSupport::CoreExtensions::String::Inflections, specifically in your case I would use the underscore method that exist in the (expanded) String class.

Then to override the accessor you could do something like:

def title_id
  read_attribute(:title).underscore
end

I think that's what you want.

Daemin
+2  A: 

you can extend a specific object instance with a method, using modules.

module ToId
  def to_id
    self.downcase.gsub " ", "_"
  end
end

class MyClass
  def title=(value)
    value.extend ToId
    @title = value
  end

  def title
    @title
  end
end

m = MyClass.new
m.title = "foo bar"

puts m.title        #=> foo bar
puts m.title.to_id  #=> foo_bar

since the value passed into the .title= method is a string, when we extend the string string with the ToId module, "self" in the module's methods is a string. therefore, we have direct access to the string that was passed into the .title= method, and we can manipulate it directly.

this is all done without having to modify the String class directly. we are only extending the specific instance that represents the title.

Derick Bailey
This will probably not work on models - perfectly working on simple class but not on class inherited from ActiveRecord::Base (which is the case of @drmanitoba)
pawien
+2  A: 

I believe that true Ruby solution is based on meta-programming. I'd strongly recommend you this book http://pragprog.com/titles/ppmetr/metaprogramming-ruby ($20) if you are interested.

By the way - the solution proposed above probably will not work as overriding column accessors is not that simple.

So I would recommend to create a class method that you use in your model definition like this:

class MyModel < ActiveRecord::Base
  # adds to_id to the following attributes
  ideize :name, :title
end

Well, that was an easy part, now comes the tougher one - the module itself:

# 
# extends the ActiveRecord with the class method ideize that
# adds to_id method to selected attributes
#
module Ideizer
  module ClassMethods
    def ideize(*args)
      # generates accessors
      args.each do |name|
        define_method("#{name}") do
          # read the original value
          value = read_attribute(name)
          # if value does not contain to_id method then add it
          unless value.respond_to?(:to_id)
            # use eigen class for the value
            class << value
              def to_id
                self.downcase.gsub " ", "_"
              end
            end
          end
          # return the original value
          value
        end
      end
    end
  end

  def self.included(base)
    base.extend(ClassMethods)
  end
end

# extend the active record to include ideize method
ActiveRecord::Base.send(:include, Ideizer)

I have to admit that I did not write the solution above just from my memory so I've prepared some tests that I'm sharing here:

require 'spec_helper'

describe MyModel do
  before :each do
    @mod = MyModel.new(:name => "Foo Bar",
                       :title => "Bar Bar",
                       :untouched => "Dont touch me")
  end

  it "should have to_id on name" do
    @mod.name.respond_to?(:to_id).should be_true
    @mod.name.to_id.should eql "foo_bar"
  end

  it "should have to_id on title" do
    @mod.title.respond_to?(:to_id).should be_true
    @mod.title.to_id.should eql "bar_bar"
  end

  it "should NOT have to_id on untouched" do
    @mod.untouched.respond_to?(:to_id).should be_false
  end

  it "should work with real model" do
    @mod.save!
    @mod.name.to_id.should eql "foo_bar"

    # reload from the database
    @mod.reload
    @mod.name.to_id.should eql "foo_bar"
  end

end

Ruby rules!

pawien