Monkey patch class methods - ruby

I'm trying to do some monkey patching in ActiveShipping UPS class .
I need to add a class level method (starting with .self), so here it's what I'm trying to do:
module ActiveMerchant
module Shipping
class UPS < Carrier
def self.process_request(receiver, sender, packages, options = {})
# some code
end
def regular_method
"foobar"
end
end
end
end
Unfortunately when I'm trying to use it:
ActiveMerchant::Shipping::UPS.process_request(receiver etc)
I get an error:
NoMethodError: undefined method `process_request' for ActiveMerchant::Shipping::UPS:Class
from (irb):6
from C:/Ruby19/bin/irb.bat:19:in `<main>'
There is no class method named process_request in original class.
In original UPS class provided in gem there is one static method defined self.retry_safe = true
and I can use it without errors.
I can also use regular_method after creating instance of UPS class.
More details provided:
I'm working with Rails 2.3 ( :-( ) and Ruby 1.9.2. I have no influce on environment.
Monkey patched code is under plugins/my_plugin/lib/active_shipping/ext/carriers/ups.rb
In /active_shipping I have file named extensions.rb in which i have:
require 'active_shipping'
require_relative 'ext/carriers'
require_relative 'ext/carriers/ups'
It deals with loading everything properly (I suppose basing on regular_method beheaviour from first chunk of code in my question).
I try to invoke process_request in one of my Controllers. This part is little tricky, beacuse i'm using sth like this:
MyModel.courier_service.process_request(parameters)
where courier_service, in this case holds the ActiveMerchant::Shipping::UPS class.
I'm still a newbie in Ruby and don't know what sort of details i should provide.

Maybe you want to do it in another way
File patch_classes.rb:
module ActiveMerchantExpand
module Shipping
module ClassMethods
def self.process_request(receiver, sender, packages, options = {})
# some code
end
end
module InstanceMethods
def regular_method
"foobar"
end
end
def self.included(receiver)
receiver.extend ClassMethods
receiver.send :include, InstanceMethods
end
end
end
Then you have to load your class "ActiveMerchant::Shipping::UPS"
and after that you can attach your methods to your class via
Rails.configuration.to_prepare do
require_dependency [[file for ActiveMerchant::Shipping::UPS]]
require 'patch_classes' )
ActiveMerchant::Shipping::UPS.send(:include, ::ActiveMerchantExpand::Shipping)
end
This is from rails plugin writing, i hope this helps.
regards tingel2k

Do you explicitly require file with your monkey patch? If you just put it under your app or lib path without requiring, it wouldn't load because constant ActiveMerchant::Shipping::UPS is defined in gem and it doesn't trigger dependency resolution mechanism.

Related

Ruby encapsulate/package existing code into a namespace

I've been looking all around and didn't found any kind of answer to the problem i'm facing in Ruby. I'm writing an app that use core modules sets that are available in different versions. If I'm sourcing a core set version after an another one both code version will be sourced at the same and will clash with each other. That is quite normal and I'm ok with this.
One approach could be to unload the previous version to load the new one but I'd like to keep all the ones loaded into specific namespaces (to avoid time consuming to unload/reload code all the time). 2 possible solutions to me (or maybe other)
Either source the code then move it into a version namespace (some clues to do it see below but doesn't work yet)
Or source the code directly into a version namespace (don't know how to do it exactly, maybe with module_eval but need to recode the require process with dependencies). Does any solution seems possible ?
Here is a very simple poc of what I'm trying to achieve
file : coreset_1.0.0.rb
module CoreA
def self.who_am_i?; self.to_s; end
def self.get_coreb; CoreB end
end
module CoreB
def self.who_am_i?; self.to_s; end
end
file : coreset_2.0.0.rb (got some changes)
module CoreA
def self.my_name; self.to_s; end
def self.get_coreb; CoreB end
end
module CoreB
def self.my_name; self.to_s; end
end
file : coreManager.rb
module CoreManager
def self.load_version(arg_version)
#Create a module set for the selected version
core_set_name = CoreSet + '_' + arg_version.gsub('.', '_')
core_set = eval("Module #{core_set_name}; end; #{core_set_name}"
#Load the requested code
require "coreset_#{arg_version}.rb"
#Move loaded code into it core set module
core_set.const_set(:CoreA, Object.send(:remove_const, :CoreA))
core_set.const_set(:CoreB, Object.send(:remove_const,:CoreB))
#Return the created core set
core_set
end
end
If running the code :
require 'coreManager.rb'
core_set = CoreManager.load_version("1.0.0")
puts core_set::CoreA.who_am_i?
puts core_set::CoreA.get_coreB
it returns :
CoreA #not CoreSet_1_0_0::CoreA
uninitialized constant CoreA::CoreB (NameError)
If running something statically defined, it works
module CoreSet
module CoreA
def self.who_am_i?; self.to_s; end
def self.get_coreb; CoreB end
end
module CoreB
def self.who_am_i?; self.to_s; end
end
end
CoreSet::CoreA.get_coreb
It returns as expected :
CoreSet::CoreB
Depite of what is usually said :"a module is a constant", it seems to be more than that. What are the differencies and how to make the dynamic version working ?
Any other ideas ?
Thanks for your help folks :)
There are several things broken in your code (which is ok for POC, I guess), but the main one is that require loads constants and globals into the global namespace.
So, your Core<X> modules are not namespaced this way, as you might expect. There is Kernel#load method that allows "wrapped" execution of the loaded file, but it's wrapped into an anonymous module, so you can prevent the global namespace from being polluted, but you cannot "target" the constants to be defined into a particular namespace this way.
What you could try is to load the code as text and then eval it within the dynamically created module, matching your version. For example, look at this quick and very dirty sketch (coreset_...rb files are expected to sit into coresets directory):
module CoreSet; end
class CoreManager
class << self
def load_version(ver)
raise "Vesion #{ver} unknown" unless exists?(ver)
file = filename(ver)
code = File.read(file)
versioned_wrapper = Module.new do
class_eval code
end
CoreSet.const_set("V_#{ver.gsub('.', '_')}", versioned_wrapper)
end
private
def exists?(ver)
File.exists? filename(ver)
end
def filename(ver)
"coresets/coreset_#{ver.gsub('.', '_')}.rb"
end
end
end
CoreManager.load_version("1.0.0")
CoreManager.load_version("2.0.0")
p CoreSet::V_1_0_0::CoreA.who_am_i? # => "CoreSet::V_1_0_0::CoreA"
p CoreSet::V_1_0_0::CoreA.get_coreb # => CoreSet::V_1_0_0::CoreB
p CoreSet::V_2_0_0::CoreA.my_name # => "CoreSet::V_2_0_0::CoreA"
p CoreSet::V_2_0_0::CoreA.get_coreb # => CoreSet::V_2_0_0::CoreB
But, DON'T do this at home, please :) At least, I would think twice.
If all you need is to have all the versions loaded at once (you need namespaces exactly for this, right?) what stops you from defining them statically, like CoreSet::V1::Core<X> etc and use the idiomatic and safe ways to (auto)load them? :) Playing with dynamic nested constants definition (and, especially, their removing) is one of the easiest ways to shoot your own foot...
Ok I finally came to a solution that may help others or that can be discussed.
Getting the error uninitialized constant CoreA::CoreB (NameError) leads me to take the problem under a new angle. If I'm not able to access to CoreB module from CoreA (because the module nesting has been broken when redefining module constants into the CoreSet module) then why not referencing in each core module the other ones in the set ? And finaly it works without any dirty hack, I'm just creating pointers and the Ruby Core find it natively ;)
module CoreManager
def self.load_version(arg_version)
#Create a module set for the selected version
core_set_name = CoreSet + '_' + arg_version.gsub('.', '_')
core_set = eval("Module #{core_set_name}; end; #{core_set_name}"
#Load the requested code
toplevel_consts = Object.constants
require "coreset_#{arg_version}.rb"
core_modules = Object.constants - toplevel_consts
#Move the core modules to the set namespace
core_modules.collect! do |core_module|
core_module_sym = core_module.to_s.to_sym
core_set.const_set(core_module_sym, Object.send(:remove_const, core_module_sym))
eval("#{core_set}::#{core_module}")
end
#Create connexion between set cores to skirt broken module nesting
core_modules.each do |current_core|
core_modules.each do |other_core|
current_core.const_set(other_core.to_s.to_sym, other_core) unless current_core == other_core
end
end
#Return the created core set
core_set
end
end

Ruby Module Constant Set and Read

I'm developing a module that contains standard methods as well as CLI (Thor) and API (Sinatra). I've created a Submodule that includes a class that inherits Sinatra::Base:
module Monteverde
module Activity
def self.version
"1.0.1"
end
class API < Sinatra::Base
set :port, 22340
get "/version" do
Monteverde::Activity.version
end
run! if defined? Monteverde::OPERATION
end
end
end
This "Activity" is invokes from the CLI (Thor):
module Monteverde
class CLI < Thor
desc "api", "Start Activity API"
def api
Monteverde.const_set("OPERATION", "started")
Monteverde::Activity::API
end
desc "aversion", "Get Activity Version"
def aversion
puts Monteverde::Activity.version
end
end
end
If I don't add an "if" to Sinatra's run! it will run automatically and take over the rest of the methods in the module:
...
class API < Sinatra::Base
register Sinatra::DefaultRoutes
set :port, 22340
get "/version" do
Monteverde::Activity.version
end
run!
end
$ ruby monteverde.rb aversion
$ == Sinatra (v2.0.0) has taken the stage on 22340 for development with backup from Puma
$ ...
My issue is that the OPERATION constant is not recognized even though it's set right before the module is called.
Why isn't OPERATION being recognized and how can I get it to be?
Is there another more intuitive/useful way to get Sinatra not to fire when I call the module?
In your API class definition, the run! line happens as soon as you require that file. It doesn't run again if you reference the class, as you're trying to do with the last line of your def api method.
To get around this, you can move the run! command into a method, and call that from Thor:
class API < Sinatra::Base
# ... other stuff
def self.start
run! if defined? Monteverde::OPERATION
end
end
Then in the Thor file:
def api
Monteverde.const_set("OPERATION", "started")
Monteverde::Activity::API.start
end
You can deduce that run! is a class method since it's callable in the scope of the class definition (not in an instance method), and so I define a class method to call it.
It would be simpler, though, to not define def self.start, and instead just call the run! method from the Thor file directly:
def api
Monteverde::Activity::API.run!
end

Why do I get the error uninitialized constant Stuff::HTTParty?

I have the HTTParty gem on my system and I can use it from within rails.
Now I want to use it standalone.
I am trying:
class Stuff
include HTTParty
def self.y
HTTParty.get('http://www.google.com')
end
end
Stuff.y
but I get
$ ruby test_httparty.rb
test_httparty.rb:2:in `<class:Stuff>': uninitialized constant Stuff::HTTParty (NameError)
from test_httparty.rb:1:in `<main>'
07:46:52 durrantm Castle2012 /home/durrantm/Dropnot/_/rails_apps/linker 73845718_get_method
$
You have to require 'httparty':
require 'httparty'
class Stuff
include HTTParty
# ...
end
Its all because of the include which exists with in the class
If you include a class with a module, that means you're "bringing in" the module's methods as instance methods.
If you need more clarity on include and require
I request you to refer to this wonderful SO Posting
What is the difference between include and require in Ruby?
Here is an example which I have taken from the same posting
module A
def say
puts "this is module A"
end
end
class B
include A
end
class C
extend A
end
B.say => undefined method 'say' for B:Class
B.new.say => this is module A
C.say => this is module A
C.new.say => undefined method 'say' for C:Class

Ruby require loop

I have the following code (simplified):
decorator.rb
require 'decoratable'
class Decorator < SimpleDelegator
include Decoratable
end
decoratable.rb
require 'decorator_builder'
module Decoratable
def decorate(*decorators)
decorators.inject(DecoratorBuilder.new(self)) do |builder, decorator|
builder.public_send(decorator)
end.build
end
end
decorator_builder.rb
require 'rare_decorator'
class DecoratorBuilder
def initialize(card)
#card = card
#decorators = []
end
def rare
#decorators << ->(card) { RareDecorator.new(card) }
self
end
def build
#decorators.inject(#card) do |card, decorator|
decorator.call(card)
end
end
end
rare_decorator.rb
require 'decorator'
class RareDecorator < Decorator
# Stuff here
end
When I require decorator.rb, it causes RareDecorator to be declared before Decorator is declared, which is a problem since RareDecorator inherits from Decorator.
A possible solution is to split up decorator.rb like so:
class Decorator < SimpleDelegator; end
require 'decoratable'
class Decorator
include Decoratable
end
However, declaring dependencies in the middle of a file doesn't seem doesn't seem like a very clean solution to me.
Is there a better solution to this problem?
Instead of specifying requirements within every file, create one file which will require all the the application's requirements. Call it for example environment.rb:
require 'decoratable'
require 'decorator'
require 'decorator_builder'
require 'rare_decorator'
You don't need to worry about Decoratable not knowing what DecoratorBuilder is, as it is used within the method and the check for the constant will be executed when this method is called. Since you require decorator moment later, all will work.

How to call a page-object from a class.rb at Support folder

I am using the page-object gem. Suppose i have a page-object on features/bussines/pages/booking_page.rb for a page like:
class Booking
include PageObject
span(:txtFirstName, :id => 'details_first_name')
end
...and i use a "tools" class located at features/support/tools.rb with something like:
class MyTools
def call_to_page_object
on Booking do |page|
puts page.txtFirstName
end
end
end
...but this approach fails because calling to the object from the class is not allowed:
undefined method `on' for #<Booking:0x108f5b0c8> (NoMethodError)
Pretty sure i'm missing some concept on the way to use the page-object from a class but don't realize whats the problem. Can you please give me an idea about what could be wrong here, please?
Thank you very much!
============================
Justin found the reason why the call to the class crash. The final class code results:
class MyTools
#Include this module so that the class has the 'on' method
include PageObject::PageFactory
def initialize(browser)
#Assign a browser object to #browser, which the 'on' method assumes to exist
#browser = browser
end
def getCurrentRewards
on Booking do |page|
rewards_text = page.rewards_amount
rewards_amount = rewards_text.match(/(\d+.*\d*)/)[1].to_f
puts "The current rewards amount are: #{rewards_amount}."
return rewards_amount
end
end
end
And the call to the function:
user_rewards = UserData.new(#browser).getCurrentRewards
Why it did not work me? Two main reasons:
I didn't pass the browser object to the class <== REQUIRED
I didn't include the PageObject::PageFactory in the class <== REQUIRED for the "on" method.
Thanks all!
To use the on (or on_page) method requires two things:
The method to be available, which is done by including the PageObject::PageFactory module.
Having a #browser variable (within the scope of the class) that is the browser.
So you could make your MyTools class work by doing:
class MyTools
#Include this module so that the class has the 'on' method
include PageObject::PageFactory
def initialize(browser)
#Assign a browser object to #browser, which the 'on' method assumes to exist
#browser = browser
end
def call_to_page_object
on Booking do |page|
puts page.txtFirstName
end
end
end
You would then be calling your MyTools class like:
#Assuming your Cucumber steps have the the browser stored in #browser:
MyTools.new(#browser).call_to_page_object
What are you trying to do?
Did you read Cucumber & Cheese book?
Pages should be in the features/support/pages folder. You can put other files that pages need there too.
If you want to use on method in a class, you have to add this to the class:
include PageObject
The code from MyTools class looks to me like it should be in Cucumber step file, not in a class.
Your class should use the extend keyword to access special class methods like span:
class Booking
extend PageObject
span(:txtFirstName, :id => 'details_first_name')
end
I hope this works.

Resources