Does Ruby Provide a Constant_Added Hook Method

Does Ruby provide a constant_added hook method?

I once did it in Ruby 1.9 using Kernel.set_trace_func. You can keep checking for "line" events. Whenever such event occurs, take the difference of the constants from the previous time using the constants method. If you detect a difference, then that is the moment a constant was added/removed.

Kernel.set_trace_func ->event, _, _, _, _, _{
case event
when "line"
some_routine
end
}

Ruby 2.0 may have more powerful API, which allows for a more straightforward way to do it, but I am not sure.

How to access class method from the included hook of a Ruby module

There'a a method_added callback you could use:

module MyModule
def self.included(includer)
def includer.method_added(name)
puts "Method added #{name.inspect}"
end
end
end

class MyClass
include MyModule

def foo ; end
end

Output:

Method added :foo

If you want to track both, existing and future methods, you might need something like this:

module MyModule
def self.on_method(name)
puts "Method #{name.inspect}"
end

def self.included(includer)
includer.instance_methods(false).each do |name|
on_method(name)
end

def includer.method_added(name)
MyModule.on_method(name)
end
end
end

Example:

class MyClass
def foo ; end

include MyModule

def bar; end
end

# Method :foo
# Method :bar

Custom Hook/Callback/Macro Methods

Here's a solution that uses prepend. When you call before_operations for the first time it creates a new (empty) module and prepends it to your class. This means that when you call method foo on your class, it will look first for that method in the module.

The before_operations method then defines simple methods in this module that first invoke your 'before' method, and then use super to invoke the real implementation in your class.

class ActiveClass
def self.before_operations(before_method,*methods)
prepend( @active_wrapper=Module.new ) unless @active_wrapper
methods.each do |method_name|
@active_wrapper.send(:define_method,method_name) do |*args,&block|
send before_method
super(*args,&block)
end
end
end
end

class SubClass < ActiveClass
before_operations :first_validate_something, :do_this_method, :do_that_method

def do_this_method(*args,&block)
p doing:'this', with:args, and:block
end
def do_that_method; end

private

def first_validate_something
p :validating
end
end

SubClass.new.do_this_method(3,4){ |x| p x }
#=> :validating
#=> {:doing=>"this", :with=>[3, 4], :and=>#<Proc:0x007fdb1301fa18@/tmp.rb:31>}

If you want to make the idea by @SteveTurczyn work you must:

  1. receive the args params in the block of define_method, not as arguments to it.
  2. call before_operations AFTER your methods have been defined if you want to be able to alias them.

 

class ActiveClass
def self.before_operations(before_method, *methods)
methods.each do |meth|
raise "No method `#{meth}` defined in #{self}" unless method_defined?(meth)
orig_method = "_original_#{meth}"
alias_method orig_method, meth
define_method(meth) do |*args,&block|
send before_method
send orig_method, *args, &block
end
end
end
end

class SubClass < ActiveClass
def do_this_method(*args,&block)
p doing:'this', with:args, and:block
end
def do_that_method; end

before_operations :first_validate_something, :do_this_method, :do_that_method

private
def first_validate_something
p :validating
end
end

SubClass.new.do_this_method(3,4){ |x| p x }
#=> :validating
#=> {:doing=>"this", :with=>[3, 4], :and=>#<Proc:0x007fdb1301fa18@/tmp.rb:31>}

add methods directly called on model

So I tested this myself && further to @max's answer (which I kind of thought anyway, but he definitely pointed it out properly)...


There are a number of resources you can use:

  • Paperclip's has_attached_file
  • FriendlyID's friendly_id
  • Ruby Metaprogramming: Declaratively Adding Methods to a Class
  • Intend to extend

After some brief researching, I found this question:
Ruby on Rails - passing a returned value of a method to has_attached_file. Do I get Ruby syntax wrong?

The problem is that you're trying to use an instance method picture_sizes_as_strings in a declaration (has_attached_image)

What you're looking for is something to do with declaring a class. I have as much experience as you with this, so I'm writing this for my own benefit:

For those coming from static object oriented languages, such as C++ and Java, the concept of open classes is quite foreign. What does it mean that Ruby has open classes? It means that at run time the definition of a class can be changed. All classes in Ruby are open to be changed by the user at all times.

class Talker
def self.say(*args)
puts "Inside self.say"
puts "self = #{self}"
args.each do |arg|
method_name = ("say_" + arg.to_s).to_sym
send :define_method, method_name do
puts arg
end
end
end
end

class MyTalker < Talker
say :hello
end

m = MyTalker.new

m.say_hello

It seems that if you delcare the class, it will run the declarative (?) methods at init. These methods can be used to populate other parts of the object... in the case of has_many :associations, it would create an instance method of @parent.associations.

Since ActiveRecord::Concerns are modules, you need to treat them as such (according to the epic tutorial I found):

#app/models/concerns.rb
require 'active_support/concern'

module Helper
extend ActiveSupport::Concern

module ClassMethods
def help(*args) #-> each argument represents a method
args.each do |arg|
method_name = ("say_" + arg.to_s).to_sym
send :define_method, method_name do
puts arg
end
end
end
end

end

#app/models/x.rb
class X < ActiveRecord::Base
include Helper
help :help
end

@x = X.new
@x.say_help #-> puts "help"

[[still working out the rest]] -- how to add to instance methods; seems super doesn't work so well

How does self.class.method work when including a module?

The get method is defined at line 484 of httparty/httparty.rb

def get(path, options = {}, &block)
perform_request Net::HTTP::Get, path, options, &block
end

This is defined on a module called ClassMethods. If you look further up the file httparty/httparty.rb. At line 20 you will see:

def self.included(base)
base.extend ClassMethods

The method included is called when a Module is included into another Module or Class.

This code ensures that when the HTTParty module is included into another module or class, the methods defined in HTTParty::ClassMethods are extended (added as class methods) onto the host object. They become class methods.

Using a Child's constant within a Parent's Validation

Nice chatting with you yesterday. To recap, your motivation for putting the validates call in PaymentType is DRYing up your code (because it's identical in all children of PaymentType).

The problem is that Ruby loads PaymentType before it loads WireTransfer (due to inheritance, I believe) so validates can't find ADDRESS_FIELDS (because it's defined on WireTransfer, which hasn't been loaded yet). That's the first test in the RSpec test, below.

rspec 'spec/stack_overflow/child_constant_parent_validation_spec.rb' -fd

Using a Child's Constant within a Parent's Validation
when 'validates' in parent
raises error

Now, you could just put validates in each child. But, that kinda sucks because you have to define it in every child - yet it's the same across all children. So, you're not as DRY as you'd like to be. That's the second test, below.

rspec 'spec/stack_overflow/child_constant_parent_validation_spec.rb' -fd

Using a Child's Constant within a Parent's Validation
when 'validates' in parent
raises error
when 'validates' in child
doesn't raise an error
has the correct class methods
has the correct instance methods
kinda sucks because 'validates' has to be defined in every child.

So, are you doomed to sogginess? Not necessarily. You could put your validates in a module so that you can define it once and use it everywhere. You would then include the module in your children classes. The trick is (1) using the included hook and accessing base::ADDRESS_FIELDS, and (2) making sure that you include the module AFTER you have set ADDRESS_FIELDS in the child. That's the third test, below.

rspec 'spec/stack_overflow/child_constant_parent_validation_spec.rb' -fd

Using a Child's Constant within a Parent's Validation
when 'validates' in parent
raises error
when 'validates' in child
doesn't raise an error
has the correct class methods
has the correct instance methods
kinda sucks because 'validates' has to be defined in every child.
when 'validates' in module
doesn't raise an error
has the correct class methods
has the correct instance methods
is a little better because you can define 'validates' once and use in all children

Finished in 0.00811 seconds (files took 0.1319 seconds to load)
9 examples, 0 failures

Of course, you still have to remember to include the module in every child, but that shouldn't be too bad. And better than defining validates everywhere.

After everything, your classes might look something like:

class PaymentType
class << self
def a_useful_class_method_from_payment_base; end
end
def a_useful_instance_method_from_payment_base; end
end

module PaymentTypeValidations
def self.included(base)
validates :address, hash_key: { presence: base::ADDRESS_FIELDS }
end
end

class WireTransfer < PaymentType
ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
include PaymentTypeValidations
end

class Bitcoin < PaymentType
ADDRESS_FIELDS = %i(wallet_address)
include PaymentTypeValidations
end

I've put the entire RSpec test below in case you want to run it yourself.

RSpec.describe "Using a Child's Constant within a Parent's Validation " do

before(:all) do

module Validations
def validates(field, options={})
define_method("valid?") do
end
define_method("valid_#{field}?") do
end
end
end

module PaymentType
class Base
extend Validations
class << self
def a_useful_class_method_from_payment_base; end
end
def a_useful_instance_method_from_payment_base; end
end
end

module WireTransfer
end

end

context "when 'validates' in parent" do
it "raises error" do

expect{

class PaymentType::WithValidates < PaymentType::Base
validates :address, hash_key: { presence: self::ADDRESS_FIELDS }
end

class WireTransfer::Base < PaymentType::WithValidation
ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
end

}.to raise_error(NameError)

end
end

context "when 'validates' in child" do
it "doesn't raise an error" do

expect{

class PaymentType::WithoutValidates < PaymentType::Base
end

class WireTransfer::WithValidates < PaymentType::WithoutValidates
ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
validates :address, hash_key: { presence: self::ADDRESS_FIELDS }
end

}.to_not raise_error
end
it "has the correct class methods" do
expect(WireTransfer::WithValidates).to respond_to("a_useful_class_method_from_payment_base")
end
it "has the correct instance methods" do
wire_transfer = WireTransfer::WithValidates.new
["valid?","valid_address?","a_useful_instance_method_from_payment_base"].each do |method|
expect(wire_transfer).to respond_to(method)
end
end
it "kinda sucks because 'validates' has to be defined in every child." do
module Bitcoin
class Base < PaymentType::WithoutValidates
end
end
bitcoin = Bitcoin::Base.new
["valid?","valid_address?"].each do |method|
expect(bitcoin).to_not respond_to(method)
end
end
end

context "when 'validates' in module" do
it "doesn't raise an error" do
expect{

module PaymentTypeValidations
extend Validations
def self.included(base)
validates :address, hash_key: { presence: base::ADDRESS_FIELDS }
end
end

class WireTransfer::IncludingValidationsModule < PaymentType::WithoutValidates
ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
include PaymentTypeValidations
end

}.to_not raise_error

end

it "has the correct class methods" do
expect(WireTransfer::IncludingValidationsModule).to respond_to("a_useful_class_method_from_payment_base")
end

it "has the correct instance methods" do
wire_transfer = WireTransfer::IncludingValidationsModule.new
["valid?","valid_address?","a_useful_instance_method_from_payment_base"].each do |method|
expect(wire_transfer).to respond_to(method)
end
end

it "is a little better because you can define 'validates' once and use in all children" do
class Bitcoin::IncludingValidationsModule < PaymentType::WithoutValidates
ADDRESS_FIELDS = %i(wallet_address)
include PaymentTypeValidations
end

bitcoin = Bitcoin::IncludingValidationsModule.new
["valid?","valid_address?","a_useful_instance_method_from_payment_base"].each do |method|
expect(bitcoin).to respond_to(method)
end

end

end

end


Related Topics



Leave a reply



Submit