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?
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.
Related
I have a class with (as example) 3 attributes ,I want to convert the class's attribute to an array ,so I can store them into my csv file.
class Human
attr_accessor :name,:Lname,:id
....
end
when I create :
human1=Human.new("nice","human",1)
I need a function that return ["nice","human",1].
Is there a predefined one that I didn't find or I have to redefine to_a so it does the job.
note: the class has more than 3 attribute
is there a function to go through the object attribute or not.
I need a function that return ["nice","human",1]
Creating such method is trivial. If it is specifically for CSV, I would name it accordingly, e.g.:
class Human
attr_accessor :name, :lname, :id
# ...
def to_csv
[name, lname, id]
end
end
To generate a CSV:
require 'csv'
human1 = Human.new("nice", "human", 1)
csv_string = CSV.generate do |csv|
csv << ['name', 'lname', 'id']
csv << human1.to_csv
end
puts csv_string
# name,lname,id
# nice,human,1
Note that I've renamed Lname to lname in the above example. Uppercase is reserved for constants.
is there a function to go through the object attribute or not?
No, there is no built in way to actually do what you want and you might be falling for a common beginner trap.
attr_accessor does not "define attributes" since Ruby doesn't actually have properties/attributes/members like other langauges do. It defines a setter and getter method for an instance variable. Ruby doesn't keep track of which properties an object is presumed to have - only the actual instance variables which have been set.
But Ruby does provide the basic building blocks to make any kind of attributes system you want. This is very simplefied (and quite rubbish) example:
class Human
# this is a class instance variable
#attributes = []
# a class method that we use for "defining attributes"
def self.attribute(name)
attr_accessor name
#attributes << name
end
attribute(:name)
attribute(:l_name)
attribute(:id)
def initialize(**kwargs)
kwargs.each {|k,v| send("#{k}=", v) }
end
# the attributes that are defined for this class
def self.attributes
#attributes
end
# cast a human to an array
def to_a
self.class.attributes.map{ |attr| send(attr) }
end
# cast a human to an hash
def to_h
self.class.attributes.each_with_object({}) do |attr, hash|
hash[attr] = send(attr)
end
end
end
jd = Human.new(
name: 'John',
l_name: 'Doe',
id: 1
)
jd.to_a # ['John', Doe, 1]
jd.to_h # {:name=>"John", :l_name=>"Doe", :id=>1}
Here we are creating a class method attribute that adds the names of the "attributes" to a class instance variable as they are declared. Thus the class "knows" what attributes it has. It then uses attr_accessor to create the setter and getter as usual.
When we are "extracting" the attributes (to_a and to_h) we use the list we have defined in the class to call each corresponding setter.
Usually this kind functionality would go into a module or a base class and not the actual classes that represent your buisness logic. For example Rails provides this kind of functionality through ActiveModel::Attributes and ActiveRecord::Attributes.
i have a code as below
def address
p "address"
end
class Person
def method_missing(sym, *args)
"#{sym} not defined on #{self}"
end
def name
"My name is Person"
end
end
p = Person.new
p.name # => My name is Person
p.address # => expected output is 'address not defined on # <Person:0x007fb2bb022fe0>' but actual output is 'address'
I want to leverage method_missing. But lexical scoping comes to play here. So method_missing becomes obsolete. any workaround?
As #AlekseiMatiushkin says the code works fine in a file. If you really need it to work the way you want in irb, you could define address as a class method
def self.address
p "address"
end
then missing_method will not find it as an instance method of Object, but you can still call address as if it were a global function.
In my code base, I have a bunch of objects that all adhere to the same interface, which consists of something like this:
class MyTestClass
def self.perform(foo, bar)
new(foo, bar).perform
end
def initialize(foo, bar)
#foo = foo
#bar = bar
end
def perform
# DO SOMETHING AND CHANGE THE WORLD
end
end
The differentiating factor between the classes is the arity of the self.perform and initialize, plus the body of the #perform class.
So, I'd like to be able to create an ActiveSupport::Concern (or just a regular Module if that would work better) which allowed me to do something like this:
class MyTestClass
inputs :foo, :bar
end
which would then use some meta-programming to define self.perform and initialize of the above methods whose airty would depend on the airty specified by the self.inputs method.
Here is what I have so far:
module Commandable
extend ActiveSupport::Concern
class_methods do
def inputs(*args)
#inputs = args
class_eval %(
class << self
def perform(#{args.join(',')})
new(#{args.join(',')}).perform
end
end
def initialize(#{args.join(',')})
args.each do |arg|
instance_variable_set(##{arg.to_s}) = arg.to_s
end
end
)
#inputs
end
end
end
This seems to get the arity of the methods correct, but I'm having a tough time figuring out how to handle the body of the #initialize methods.
Can anybody help me figure out a way that I can successfully meta-program the body of #initialize so it behaves like the example I provided?
You could use this as body for #initialize:
#{args}.each { |arg| instance_variable_set("#\#{arg}", arg) }
However, I wouldn't string eval it. It usually leads to evil things. That said, here is an implementation which gives an incorrect Foo.method(:perform).arity, but still behaves as you would expect:
module Commandable
def inputs(*arguments)
define_method(:initialize) do |*parameters|
unless arguments.size == parameters.size
raise ArgumentError, "wrong number of arguments (given #{parameters.size}, expected #{arguments.size})"
end
arguments.zip(parameters).each do |argument, parameter|
instance_variable_set("##{argument}", parameter)
end
end
define_singleton_method(:perform) do |*parameters|
unless arguments.size == parameters.size
raise ArgumentError, "wrong number of arguments (given #{parameters.size}, expected #{arguments.size})"
end
new(*parameters).perform
end
end
end
class Foo
extend Commandable
inputs :foo, :bar
def perform
[#foo, #bar]
end
end
Foo.perform 1, 2 # => [1, 2]
You were so close! instance_variable_set takes two arguments, first is the instance variable and second is the value you want to set it to. You also need to get the value of the variable, which you can do using send.
instance_variable_set(##{arg.to_s}, send(arg.to_s))
Given:
class Foo
has_one :bar
def bar_name
bar.name
end
end
class Bar
belongs_to :foo
end
In the console or in a view, I can #foo.bar_name to get 'baz'.
I'm aware that I can #foo.as_json(methods: :bar_name) to get {"id"=>"abc123", "bar_name"=>"baz"}.
I could also denormalize the attribute and make it non-virtual, but I would rather not do that in this case.
Is it possible to automatically return the model with the virtual attribute included?
#<Foo id: "abc123", bar_name: "baz">
I want to do this because I am constructing a large object with nested collections of models, and the as_json call is abstracted away from me.
Not 100% sure I understand if your concern is related to as_json but if so this will work
class Foo
has_one :bar
def bar_name
bar.name
end
def as_json(options={})
super(options.merge!(methods: :bar_name))
end
end
Now a call to #foo.as_json will by default include the bar_name like your explicit example does.
Ugly would not recommend but you could change the inspection of foo e.g. #<Foo id: "abc123", bar_name: "baz"> as follows
class Foo
def inspect
base_string = "#<#{self.class.name}:#{self.object_id} "
fields = self.attributes.map {|k,v| "#{k}: #{v.inspect}"}
fields << "bar_name: #{self.bar_name.inspect}"
base_string << fields.join(", ") << ">"
end
end
Then the "inspection notation" would show that information although I am still unclear if this is your intention and if so why you would want this.
You could use attr_accessor - per the Rails docs:
Defines a named attribute for this module, where the name is symbol.id2name, creating an instance variable (#name) and a corresponding access method to read it. Also creates a method called name= to set the attribute.
I am facing a design decision I cannot solve. In the application a user will have the ability to create a campaign from a set of different campaign types available to them.
Originally, I implemented this by creating a Campaign and CampaignType model where a campaign has a campaign_type_id attribute to know which type of campaign it was.
I seeded the database with the possible CampaignType models. This allows me to fetch all CampaignType's and display them as options to users when creating a Campaign.
I was looking to refactor because in this solution I am stuck using switch or if/else blocks to check what type a campaign is before performing logic (no subclasses).
The alternative is to get rid of CampaignType table and use a simple type attribute on the Campaign model. This allows me to create Subclasses of Campaign and get rid of the switch and if/else blocks.
The problem with this approach is I still need to be able to list all available campaign types to my users. This means I need to iterate Campaign.subclasses to get the classes. This works except it also means I need to add a bunch of attributes to each subclass as methods for displaying in UI.
Original
CampaignType.create! :fa_icon => "fa-line-chart", :avatar=> "spend.png", :name => "Spend Based", :short_description => "Spend X Get Y"
In STI
class SpendBasedCampaign < Campaign
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
Neither way feels right to me. What is the best approach to this problem?
A not very performant solution using phantom methods. This technique only works with Ruby >= 2.0, because since 2.0, unbound methods from modules can be bound to any object, while in earlier versions, any unbound method can only be bound to the objects kind_of? the class defining that method.
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
enum :campaign_type => [:spend_based, ...]
def method_missing(name, *args, &block)
campaign_type_module.instance_method(name).bind(self).call
rescue NameError
super
end
def respond_to_missing?(name, include_private=false)
super || campaign_type_module.instance_methods(include_private).include?(name)
end
private
def campaign_type_module
Campaigns.const_get(campaign_type.camelize)
end
end
# app/models/campaigns/spend_based.rb
module Campaigns
module SpendBased
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
end
Update
Use class macros to improve performance, and keep your models as clean as possible by hiding nasty things to concerns and builder.
This is your model class:
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
include CampaignAttributes
enum :campaign_type => [:spend_based, ...]
campaign_attr :name, :fa_icon, :avatar, ...
end
And this is your campaign type definition:
# app/models/campaigns/spend_based.rb
Campaigns.build 'SpendBased' do
name 'Spend Based'
fa_icon 'fa-line-chart'
avatar 'spend.png'
end
A concern providing campaign_attr to your model class:
# app/models/concerns/campaign_attributes.rb
module CampaignAttributes
extend ActiveSupport::Concern
module ClassMethods
private
def campaign_attr(*names)
names.each do |name|
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{name}
Campaigns.const_get(campaign_type.camelize).instance_method(:#{name}).bind(self).call
end
EOS
end
end
end
end
And finally, the module builder:
# app/models/campaigns/builder.rb
module Campaigns
class Builder < BasicObject
def initialize
#mod = ::Module.new
end
def method_missing(name, *args)
value = args.shift
#mod.send(:define_method, name) { value }
end
def build(&block)
instance_eval &block
#mod
end
end
def self.build(module_name, &block)
const_set module_name, Builder.new.build(&block)
end
end