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:
- receive the args params in the block of
define_method
, not as arguments to it. - 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
Ruby on Rails View Rendering Db Info on Page
Regex to Remove the Webpage Part of a Url in Ruby
How to Run Code After Ruby Kernel.Exec
Ruby, No Implicit Conversion of Symbol into Integer
Show All the Table Entries That Belong to a Specific Entry in Rails
How to Fix My Cucumber Expectation Error When Polling
How to Set Up These Crud Controller Actions for Has_Many_Polymorphs and an Error
Adding a String in Front of a Parameter for a Form
Ruby Roo Loaderror: Cannot Load Such File -- Spreadsheet/Note
Invalid Route Name, Already in Use: 'Admin_Root' (Argumenterror) - Failed Activeadmin Install
Pulling a Value from One CSV Based on a Value in Another
Access Image from Different View in a View with Paperclip Gem Ruby on Rails
Emulating Int64 Overflows in Ruby
Duplicates in 2 Dimensional Array
Knowing When Resque Worker Had Completed Job