How to dynamically create methods with parameter in ruby? - ruby-on-rails

How can I dynamically create methods like this using ruby metaprogramming ?
class CommentBridge < Bridge
def id(comment)
comment.id
end
def message(comment)
comment.message
end
def votes_count(comment)
comment.votes_count
end
end
I tried this but it is not working.
['id', 'message', 'votes_count'].each do |method|
define_method "#{method}" do |parameter|
method(parameter.method)
end
end

You should use public_send to call methods based on their name:
['id', 'message', 'votes_count'].each do |method|
define_method "#{method}" do |parameter|
parameter.public_send(method)
end
end

I do not think that you need different comment every time (probably you do). So I'd recommend to simply get rid of this comment argument.
There are the options.
Using RubyOnRails (I see you question is tagged so) you can use delegate (as #SimpleLime has already commented)
class CommentBridge < Bridge
attr_reader :comment
def initialize(comment_)
#comment = comment_)
end
delegate :id, :message, :votes_count, to: :comment
end
In case of pure Ruby 2 use Forwardable:
class CommentBridge
extend Forwardable
attr_reader :comment
def initialize(comment_)
#comment = comment_)
end
def_delegators :comment, :id, :message, :votes_count
end
If you want to provide additional methods on top of you comment object and forward all the rest methods use SimpleDelegator (assuming that this Brigde in namgin means that your class is just a wrapper):
class CommentDecorator < SimpleDelegator
def hello
'hello'
end
end
comment = Commend.find(params[:id])
decorated_comment = CommentDecorator.new(comment)
You can also define method missing:
class CommentBridge < Bridge
attr_reader :comment
def initialize(comment_)
#comment = comment_)
end
def method_missing(m, *args)
if [:id, :message, :comment].include?(m)
comment.public_send(method, *args)
else
super
end
end
end
Finally, you can create your own delegation-DSL on top of define_method, but I think this is the extra in that case.
I don't think that method_missing or define_method inside loop is neat although it works.

Related

Active admin concern dynamic methods

I have and active admin resource. How i can dynamic extend resource. I try do it like this:
ActiveAdmin.register Order do
include UpdatePriceBlock
price_blocks_names names: [:last, :actual]
end
module UpdatePriceBlock
extend ActiveSupport::Concern
def price_blocks_names(options = {})
#price_blocks_names ||= options[:names]
end
def self.included(base)
#price_blocks_names.each do |name|
base.send :member_action, name, method: :get do
end
end
end
end
Now I has an error:
undefined method `price_blocks_names' for #<ActiveAdmin::ResourceDSL
This is a possible way, I don't know yet how you could keep the names inside the active admin register block. Add the price_blocks_names to your model:
class Order < ApplicationRecord
def self.price_bloks_names
%i(last actual)
end
end
And then place this in config/initializers/active_admin_update_price_block.rb
module ActiveAdminUpdatePriceBlock
def self.extended(base)
base.instance_eval do
self.controller.resource_class.price_bloks_names.each do |name|
member_action name, method: :get do
raise resource.inspect
end
end
end
end
end
Now you can extend, but the configuration needs to reside in the model as a class method this way. Haven't found a cleaner way so far.
ActiveAdmin.register Order do
extend UpdatePriceBlock
end
I think I found it:
ActiveAdmin.register Order do
controller do
include UpdatePriceBlock
end
end
What's going on:
Within the register Order do block, self is a special Active Admin thing:
ActiveAdmin.register Order do
puts "What's self here? #{self}"
end
=>
What's self here? #<ActiveAdmin::ResourceDSL:0x000000012b948230>
Within the controller do block, it's the controller class (so, pretty much the same as the body of a class definition):
ActiveAdmin.register Order do
controller do
puts "What's self here? #{self}"
include UpdatePriceBlock
end
end
=> What's self here? Admin::OrdersController
Within a member_action block, it's an instance of the controller, just like in a regular Rails controller action:
ActiveAdmin.register Order do
member_action :action do
puts "What's self here? #{self}"
end
end
=> What's self here? #<Admin::OrdersController:0x00000001259e7e80>

Run block defined on class within instance's scope

I would like to create something similar to ActiveRecord validation: before_validate do ... end. I am not sure how could I reference attributes of class instance from the block given. Any idea?
class Something
attr_accessor :x
def self.before_validate(&block)
#before_validate_block = block
end
before_validate do
self.x.downcase
end
def validate!
# how should this method look like?
# I would like that block would be able to access instance attributes
end
end
#3limin4t0r's answer covers mimicing the behavior in plain ruby very well. But if your are working in Rails you don't need to reinvent the wheel just because you're not using ActiveRecord.
You can use ActiveModel::Callbacks to define callbacks in any plain old ruby object:
class Something
extend ActiveModel::Callbacks
define_model_callbacks :validate, scope: :name
before_validate do
self.x.downcase
end
def validate!
run_callbacks :validate do
# do validations here
end
end
end
Featurewise it blows the socks off any of the answers you'll get here. It lets define callbacks before, after and around the event and handles multiple callbacks per event.
If validations are what you really are after though you can just include ActiveModel::Validations which gives you all the validations except of course validates_uniqueness_of which is defined by ActiveRecord.
ActiveModel::Model includes all the modules that make up the rails models API and is a good choice if your are declaring a virtual model.
This can be achieved by using instance_eval or instance_exec.
class Something
attr_accessor :x
# You need a way to retrieve the block when working with the
# instance of the class. So I've changed the method so it
# returns the +#before_validate_block+ when no block is given.
# You could also add a new method to do this.
def self.before_validate(&block)
if block
#before_validate_block = block
else
#before_validate_block
end
end
before_validate do
self.x.downcase
end
def validate!
block = self.class.before_validate # retrieve the block
instance_eval(&block) # execute it in instance context
end
end
How about this?
class Something
attr_accessor :x
class << self
attr_reader :before_validate_blocks
def before_validate(&block)
#before_validate_blocks ||= []
#before_validate_blocks << block
end
end
def validate!
blocks = self.class.before_validate_blocks
blocks.each {|b| instance_eval(&b)}
end
end
Something.before_validate do
puts x.downcase
end
Something.before_validate do
puts x.size
end
something = Something.new
something.x = 'FOO'
something.validate! # => "foo\n3\n"
This version allows us to define multiple validations.

NoMethodError (undefined method) from Private method in class

Why can't I use a private method from within my class? How do I fix my code to prevent the error?
module CarRegistration
class Basics < Base
fields_of_model(:car).each do |attr|
delegate attr.to_sym, "#{attr}=".to_sym, to: :car
end
private
car_structure = #array of hashes
def fields_of_model(model)
car_structure.select {|record| record[:model] == model}.map{|record| record[:name]}
end
end
error
NoMethodError (undefined method `fields_of_model' for
CarRegistration::Basics:Class):
I think you have a number of problems going on here.
First, you've defined fields_of_model as an instance method, here:
def fields_of_model(model)
car_structure.select {|record| record[:model] == model}.map{|record| record[:name]}
end
but you're trying to call it from the class, here:
fields_of_model(:car).each do |attr|
delegate attr.to_sym, "#{attr}=".to_sym, to: :car
end
So, you'll want to make fields_of_model a class method, and define it before you call it. Something like:
module CarRegistration
class Basics < Base
private
car_structure = #array of hashes
class << self
def fields_of_model(model)
car_structure.select {|record| record[:model] == model}.map{|record| record[:name]}
end
end
fields_of_model(:car).each do |attr|
delegate attr.to_sym, "#{attr}=".to_sym, to: :car
end
end
You'll also have problems with that car_structure variable, I think, because it'll be out of scope for the class method. So, I think you need to make a class-level instance variable. So, give this a try:
module CarRegistration
class Basics < Base
#car_structure = #array of hashes
class << self
def fields_of_model(model)
#car_structure.select {|record| record[:model] == model}.map{|record| record[:name]}
end
private :fields_of_model
end
fields_of_model(:car).each do |attr|
delegate attr.to_sym, "#{attr}=".to_sym, to: :car
end
end
Note that I made the class method, :fields_of_models private using private :fields_of_model.
To demonstrate the whole thing, I ginned up this RSpec test:
require 'rails_helper'
class Car
attr_accessor *%w(
color
make
year
).freeze
end
module CarRegistration
class Basic
#car_structure = [
{model: :car, name: :color},
{model: :car, name: :make},
{model: :car, name: :year}
]
class << self
def fields_of_model(model)
#car_structure.select {|record| record[:model] == model}.map{|record| record[:name]}
end
private :fields_of_model
end
fields_of_model(:car).each do |attr|
delegate attr.to_sym, "#{attr}=".to_sym, to: :car
end
def car
#car ||= Car.new
end
end
end
RSpec.describe CarRegistration::Basic do
it "has :fields_of_model as a private class method" do
expect(CarRegistration::Basic.public_methods).not_to include(:fields_of_model)
expect(CarRegistration::Basic.private_methods).to include(:fields_of_model)
end
it "responds to :color and :color=" do
expect(car_registration).to respond_to(:color)
expect(car_registration).to respond_to(:color=)
end
it "sets and gets attributes on car" do
expect(car_registration.color).to be_nil
expect(car_registration.car.color).to be_nil
car_registration.color = :red
expect(car_registration.car.color).to eq(:red)
expect(car_registration.color).to eq(:red)
expect(car_registration.instance_variable_get(:#color)).to be_nil
end
end
def car_registration
#car_registration ||= described_class.new
end
Which, when run, yields:
CarRegistration::Basic
has :fields_of_model as a private class method
responds to :color and :color=
sets and gets attributes on car
Finished in 0.733 seconds (files took 27.84 seconds to load)
3 examples, 0 failures
BTW, having this code in your class outside of a def-end is just fine and not the root of your problem. In fact, it's quite normal.
Also, I will note that Jörg W Mittag wishes to say:
I am one of those Ruby Purists who likes to point out that there is no such thing as a class method in Ruby. I am perfectly fine, though, with using the term class method colloquially, as long as it is fully understood by all parties that it is a colloquial usage. In other words, if you know that there is no such thing as a class method and that the term "class method" is just short for "instance method of the singleton class of an object that is an instance of Class", then there is no problem. But otherwise, I have only seen it obstruct understanding.
Let it be fully understood by all parties that the term class method is used above in its colloquial sense.
Because you wrote the method not in def-end clause; you should write it like
def my_method
fields_of_model(:car).each do |attr|
delegate attr.to_sym, "#{attr}=".to_sym, to: :car
end
end
That is why the error message says CarRegistration::Basics:Class as opposed to CarRegistration::Basics
Here is a sample code that works.
Usually there is no need to put a class inside Module, but if you must for some reason, this is a way.
module CarRegistration
class Basics < Object
def run(model)
fields_of_model(model)
end
private
def fields_of_model(model)
puts model
end
end
end
a = CarRegistration::Basics.new
a.run('xyz') # => 'xyz' is printed.

How to access class method from instance method in ruby on rails non activerecord model

I have a non activerecord rails model:
class Document
attr_accessor :a, :b
include ActiveModel::Model
def find(id)
initialize_parameters(id)
end
def save
...
end
def update
...
end
private
def initialize_parameters(id)
#a = 1
#b = 2
end
end
In order to find the Document, I can use:
Document.new.find(3)
So, to get it directly I changed the find method to
def self.find(id)
initialize_parameters(id)
end
And I get the following error when I run
Document.find(3)
undefined method `initialize_parameters' for Document:Class
How can I make this work?
You can't access an instance method from a class method that way, to do it you should instantiate the class you're working in (self) and access that method, like:
def self.find(id)
self.new.initialize_parameters(id)
end
But as you're defining initialize_parameters as a private method, then the way to access to it is by using send, to reach that method and pass the id argument:
def self.find(id)
self.new.send(:initialize_parameters, id)
end
private
def initialize_parameters(id)
#a = 1
#b = 2
end
Or just by updating initialize_parameters as a class method, and removing the private keyword, that wouldn't be needed anymore.
This:
class Document
attr_accessor :a, :b
def self.find(id)
initialize_parameters(id)
end
end
Is not trying to "access class method from instance method" as your title states. It is trying to access a (non-existent) class method from a class method.
Everything Sebastian said is spot on.
However, I guess I would ask: 'What are you really trying to do?' Why do you have initialize_parameters when ruby already gives you initialize that you can override to your heart's content? IMO, it should look something more like:
class Document
attr_accessor :a, :b, :id
class << self
def find(id)
new(id).find
end
end
def initialize(id)
#a = 1
#b = 2
#id = id
end
def find
# if you want you can:
call_a_private_method
end
private
def call_a_private_method
puts id
end
end

Mark ActiveRecord attribute as html_safe

We have an ActiveRecord model with an html attribute (say Post#body). Is there a nice way that calling body on a post returns an html_safe? string? E.g.:
class Post < ActiveRecord::Base
# is_html_escaped :body or somesuch magic
end
Post.first.body.html_safe? # => true
The problem otherwise is that we have to call raw everything we show that field.
Here's a way I found:
class Post < ActiveRecord::Base
def message
super.html_safe
end
def message=(new_mess)
new_mess = ERB::Util.html_escape(new_mess.sanitize) unless new_mess.html_safe?
super(new_mess)
end
end
FYI. I made a module for this
module SanitizeOnly
def self.included(mod)
mod.extend(ClassMethods)
end
module ClassMethods
def sanitize_on_input_only(*attribute_names)
attribute_names.map(&:to_s).each do | attribute_name |
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{attribute_name}
super.html_safe
end
def #{attribute_name}=(new_val)
new_val = ERB::Util.html_escape(new_val.sanitize) unless new_val.html_safe?
super(new_val)
end
RUBY
end
end
end
end
to use it just include it in your model and add the attributes you want to avoid using raw for to a sanitize_on_input_only line like the following:
sanitize_on_input_only :message, :another_attribute, ...

Resources