Access module Configuration constant from another class - ruby

I am trying to understand how the following code is able to do this:
attr_accessor *Configuration::VALID_CONFIG_KEYS
Without requiring the Configuration file. Here is part of the code:
require 'openamplify/analysis/context'
require 'openamplify/connection'
require 'openamplify/request'
module OpenAmplify
# Provides access to the OpenAmplify API http://portaltnx20.openamplify.com/AmplifyWeb_v20/
#
# Basic usage of the library is to call supported methods via the Client class.
#
# text = "After getting the MX1000 laser mouse and the Z-5500 speakers i fell in love with logitech"
# OpenAmplify::Client.new.amplify(text)
class Client
include OpenAmplify::Connection
include OpenAmplify::Request
attr_accessor *Configuration::VALID_CONFIG_KEYS
def initialize(options={})
merged_options = OpenAmplify.options.merge(options)
Configuration::VALID_CONFIG_KEYS.each do |key|
send("#{key}=", merged_options[key])
end
end
....
end
And this is the Configuration module:
require 'openamplify/version'
# TODO: output_format, analysis, scoring can be specied in the client and becomes the default unless overriden
module OpenAmplify
# Defines constants and methods for configuring a client
module Configuration
VALID_CONNECTION_KEYS = [:endpoint, :user_agent, :method, :adapter].freeze
VALID_OPTIONS_KEYS = [:api_key, :analysis, :output_format, :scoring].freeze
VALID_CONFIG_KEYS = VALID_CONNECTION_KEYS + VALID_OPTIONS_KEYS
DEFAULT_ENDPOINT = 'http://portaltnx20.openamplify.com/AmplifyWeb_v21/AmplifyThis'
DEFAULT_HTTP_METHOD = :get
DEFAULT_HTTP_ADAPTER = :net_http
DEFAULT_USER_AGENT = "OpenAmplify Ruby Gem #{OpenAmplify::VERSION}".freeze
DEFAULT_API_KEY = nil
DEFAULT_ANALYSIS = :all
DEFAULT_OUTPUT_FORMAT = :xml
DEFAULT_SCORING = :standard
DEFAULT_SOURCE_URL = nil
DEFAULT_INPUT_TEXT = nil
attr_accessor *VALID_CONFIG_KEYS
....
end
This is from this repository: OpenAmplify

First of all, in both configuration.rb and client.rb, they're using the same naming space, which is module OpenAmplify.
Even though configuration.rb is not required in client.rb, the convention of Ruby project usually requires all necessary files in one file (normally the same name as the name space, and placed in {ProjectName}/lib/, in this case the file is openamplify/lib/openamplify.rb).
So if you go to openamplify/lib/openamplify.rb, you'll notice it actually requires all those two files:
require 'openamplify/configuration'
require 'openamplify/client'
And since constants are already defined in configuration.rb:
module OpenAmplify
module Configuration
VALID_CONFIG_KEYS = ...
end
end
Then obviously constant VALID_CONFIG_KEYS is visible in the same module (re-opened by client.rb) by Configuration::VALID_CONFIG_KEYS (and the * in front just means exploding array, because VALID_CONFIG_KEYS is an array of symbols)
module OpenAmplify
class Client
attr_accessor *Configuration::VALID_CONFIG_KEYS
end
end

Related

Ruby Flexible Configuration across GEMS

How would you build a single GEM configuration that can be extended automatically if additional but optional child GEMS are included with a Ruby Application.
I am looking either for a code pattern or an existing GEM that can provide an extendable configuration system for an application I am building.
Scenario
Let's say you have a communication app that can send/receive messages and it uses some general configuration.
gem 'communication-app'
You can install any number of providers that can send/receive via different platforms.
gem 'communication-app-facebook'
gem 'communication-app-twitter'
gem 'communication-app-sms'
gem 'communication-app-email'
Each provider needs its own custom configuration. These should hang off the main configuration and can have a custom configuration graph but only be available if the plugin is installed.
Default Configuration Usage
CommsApp.configure do |config|
config.some_setting2 = 'customised setting'
end
puts CommsApp.configuration.some_setting1
puts CommsApp.configuration.some_setting2
# --> some default value 1
# --> customised setting
Example Code
module CommsApp
class << self
attr_writer :configuration
def configuration
#configuration ||= Configuration.new
end
def configure
yield(configuration)
end
end
class Configuration < BaseConfiguration
attr_accessor :some_setting1
attr_accessor :some_setting2
def initialize
super
#some_setting1 = 'some default value 1'
#some_setting2 = 'some default value 2'
end
end
end
If I try to access configuration for a child GEM that is not installed, I would expect to see some sort of error. eg.
puts CommsApp.configuration.twitter.access_token
# raises NoMethodError, "unknown method :twitter"
I have experimented with a class method on the base configuration that would allow me to attach child configuration properties for a new configuration class.
module CommsApp
class BaseConfiguration
class << self
# Attach a child configuration with it's own settings to a parent configuration
#
# #param [Class] klass_child what class would you like as the child
# #param [Class] klass_parent what class would you like to extend with a new child configuration
# #param [Symbol] accessor_name what is the name of the accessor that you are adding
def attach_to(klass_child, klass_parent, accessor_name)
# Create a memoized getter to an instance of the attaching class (:klass_child)
#
# def third_party
# #third_party ||= ThirdPartyGem::Configuration.new
# end
klass_parent.send(:define_method, accessor_name) do
return instance_variable_get("##{accessor_name}") if instance_variable_defined?("##{accessor_name}")
instance_variable_set("##{accessor_name}", klass_child.new)
end
end
end
end
end
Extended Configuration Usage
This is how the extend configuration could be used.
class ApplicationConfig < CommsApp::Configuration
attach_to(CommsApp::TwitterProvider::Configuration, self, :twitter)
attach_to(CommsApp::FacebookProvider::Configuration, self, :fb)
end
CommsApp.configuration = ApplicationConfig.new
CommsApp.configure do |config|
config.some_setting2 = 'customized setting'
config.twitter.access_token = '123'
config.fb.user_name = 'xmen'
config.fb.account_id = 'xmen123'
end
This technique works ok, but it does require a custom class to be created and configured and I want this to happen automagically if (and only if) the provider GEMs are present.
I know I can figure out how to do this, but I am wondering if there is already a pattern or GEM that is solving this particular problem.

Capybara.page not in scope after extending capybara-screenshot's after_failed_example method

I'm trying to override the after_failed_example method so I can inflict some custom file naming on our screenshots. I'm loading the module as an initializer.
So far, so good, but the Capybara.page.current_url is blank, making me think I need to require something additional?
require "capybara-screenshot/rspec"
module Capybara
module Screenshot
module RSpec
class << self
attr_accessor :use_description_as_filename
attr_accessor :save_html_file
end
self.use_description_as_filename = true
self.save_html_file = true
def self.after_failed_example(example)
if example.example_group.include?(Capybara::DSL) # Capybara DSL method has been included for a feature we can snapshot
Capybara.using_session(Capybara::Screenshot.final_session_name) do
puts ">>>> Capybara.page.current_url: " + Capybara.page.current_url.to_s
if Capybara::Screenshot.autosave_on_failure && failed?(example) && Capybara.page.current_url != ''
saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, Capybara::Screenshot.save_html_file?, set_saver_filename_prefix(example))
saver.save
example.metadata[:screenshot] = {}
example.metadata[:screenshot][:html] = saver.html_path if saver.html_saved?
example.metadata[:screenshot][:image] = saver.screenshot_path if saver.screenshot_saved?
end
end
end
private
def self.set_saver_filename_prefix(example)
return example.description.to_s.gsub(" ", "-") if Capybara::Screenshot.use_description_as_filename?
return Capybara::Screenshot.filename_prefix_for(:rspec, example)
end
end
end
end
end
This is successfully overriding the capybara-screenshot/rspec method, and any of the Capybara::Screenshot static information is accessible, but not Capybara session related information (afa I can tell).
For example, Capybara.page.current_url.to_s is null when overridden, but present when not.
I was missing a require (kind of silly mistake):
require 'capybara/rspec'

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.

Monkey patch class methods

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.

Should the Applicant class "require 'mad_skills'" or "include 'mad_skills'"?

Also, what does "self.send attr" do? Is attr assumed to be a private instance variable of the ActiveEngineer class? Are there any other issues with this code in terms of Ruby logic?
class Applicant < ActiveEngineer
require 'ruby'
require 'mad_skills'
require 'oo_design'
require 'mysql'
validates :bachelors_degree
def qualified?
[:smart, :highly_productive, :curious, :driven, :team_player ].all? do
|attr|
self.send attr
end
end
end
class Employer
include TopTalent
has_millions :subscribers, :include=>:mostly_women
has_many :profits, :revenue
has_many :recent_press, :through=>[:today_show, :good_morning_america,
:new_york_times, :oprah_magazine]
belongs_to :south_park_sf
has_many :employees, :limit=>10
def apply(you)
unless you.build_successful_startups
raise "Not wanted"
end
unless you.enjoy_working_at_scale
raise "Don't bother"
end
end
def work
with small_team do
our_offerings.extend you
subscribers.send :thrill
[:scaling, :recommendation_engines, : ].each do |challenge|
assert intellectual_challenges.include? challenge
end
%w(analytics ui collaborative_filtering scraping).each{|task|
task.build }
end
end
end
def to_apply
include CoverLetter
include Resume
end
require 'mad_skills' loads the code in mad_skills.rb (or it loads mad_skills.so/.dll depending on which one exists). You need to require a file before being able to use classes, methods etc. defined in that file (though in rails files are automatically loaded when trying to access classes that have the same name as the file). Putting require inside a class definition, does not change its behaviour at all (i.e. putting it at the top of the file would not make a difference).
include MadSkills takes the module MadSkills and includes it into Applicant's inheritance chain, i.e. it makes all the methods in MadSkills available to instances of Applicant.
self.send attr executes the method with the name specified in attr on self and returns its return value. E.g. attr = "hello"; self.send(attr) will be the same as self.hello. In this case it executes the methods smart, highly_productive, curious, driven, and team_player and checks that all of them return true.

Resources