I really had a hard time figu开发者_StackOverflowring 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?
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.
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!
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.
精彩评论