I have a plain ruby class Espresso::MyExampleClass.
module Espresso
class MyExampleClass
def my_first_function(value)
puts "my_first_function"
end
def my_function_to_run_before
puts "Running before"
end
end
end
With some of the methods in the class, I want to perform a before or after callback similar to ActiveSupport callbacks before_action or before_filter. I'd like to put something like this in my class, which will run my_function_to_run_before before my_first_function:
before_method :my_function_to_run_before, only: :my_first_function
The result should be something like:
klass = Espresso::MyExampleClass.new
klass.my_first_function("yes")
> "Running before"
> "my_first_function"
How do I use call backs in a plain ruby class like in Rails to run a method before each specified method?
Edit2:
Thanks #tadman for recommending XY problem. The real issue we have is with an API client that has a token expiration. Before each call to the API, we need to check to see if the token is expired. If we have a ton of function for the API, it would be cumbersome to check if the token was expired each time.
Here is the example class:
require "rubygems"
require "bundler/setup"
require 'active_support/all'
require 'httparty'
require 'json'
module Espresso
class Client
include HTTParty
include ActiveSupport::Callbacks
def initialize
login("admin#example.com", "password")
end
def login(username, password)
puts "logging in"
uri = URI.parse("localhost:3000" + '/login')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Post.new(uri.request_uri)
request.set_form_data(username: username, password: password)
response = http.request(request)
body = JSON.parse(response.body)
#access_token = body['access_token']
#expires_in = body['expires_in']
#expires = #expires_in.seconds.from_now
#options = {
headers: {
Authorization: "Bearer #{#access_token}"
}
}
end
def is_token_expired?
#if Time.now > #expires.
if 1.hour.ago > #expires
puts "Going to expire"
else
puts "not going to expire"
end
1.hour.ago > #expires ? false : true
end
# Gets posts
def get_posts
#Check if the token is expired, if is login again and get a new token
if is_token_expired?
login("admin#example.com", "password")
end
self.class.get('/posts', #options)
end
# Gets comments
def get_comments
#Check if the token is expired, if is login again and get a new token
if is_token_expired?
login("admin#example.com", "password")
end
self.class.get('/comments', #options)
end
end
end
klass = Espresso::Client.new
klass.get_posts
klass.get_comments
A naive implementation would be;
module Callbacks
def self.extended(base)
base.send(:include, InstanceMethods)
end
def overridden_methods
#overridden_methods ||= []
end
def callbacks
#callbacks ||= Hash.new { |hash, key| hash[key] = [] }
end
def method_added(method_name)
return if should_override?(method_name)
overridden_methods << method_name
original_method_name = "original_#{method_name}"
alias_method(original_method_name, method_name)
define_method(method_name) do |*args|
run_callbacks_for(method_name)
send(original_method_name, *args)
end
end
def should_override?(method_name)
overridden_methods.include?(method_name) || method_name =~ /original_/
end
def before_run(method_name, callback)
callbacks[method_name] << callback
end
module InstanceMethods
def run_callbacks_for(method_name)
self.class.callbacks[method_name].to_a.each do |callback|
send(callback)
end
end
end
end
class Foo
extend Callbacks
before_run :bar, :zoo
def bar
puts 'bar'
end
def zoo
puts 'This runs everytime you call `bar`'
end
end
Foo.new.bar #=> This runs everytime you call `bar`
#=> bar
The tricky point in this implementation is, method_added. Whenever a method gets bind, method_added method gets called by ruby with the name of the method. Inside of this method, what I am doing is just name mangling and overriding the original method with the new one which first runs the callbacks then calls the original method.
Note that, this implementation neither supports block callbacks nor callbacks for super class methods. Both of them could be implemented easily though.
Related
I'm having issues generating a Signature via a Ruby class. When I go into my docker container, I'm able to see that all the instance variables in the initialize method are nil expect the #api_key variable.
I have the following class
require 'openssl'
require 'base64'
module SeamlessGov
class Form
include HTTParty
attr_accessor :form_id
base_uri "https://nycopp.seamlessdocs.com/api"
def initialize()
#api_key = ENV['SEAMLESS_GOV_API_KEY']
#signature = generate_signature
#form_id = ENV['SEAMLESS_GOV_FORM_ID']
#timestamp = Time.now.to_i.to_s
end
def relative_uri
"/form/#{#form_id}/elements"
end
def create_form
self.class.get(relative_uri, headers: generate_headers)
end
private
def generate_signature
OpenSSL::HMAC.hexdigest('sha256', ENV['SEAMLESS_GOV_SECRET'], "GET+#{relative_uri}+#{#timestamp}")
binding.pry
end
def generate_headers
{
"Authorization" => "HMAC-SHA256 api_key='#{#api_key}' signature='#{#timestamp}'",
Net::HTTP::ImmutableHeaderKey.new('AuthDate') => "#{#timestamp}"
}
end
end
end
As you see, from the binding.pry in the generate_signature method I'm able to see the instance variables:
The relative_uri method needed to generate the signature doesn't load the #form_id variable in the string.
Here is the controller:
class FormsController < ApplicationController
def display_form
#form = SeamlessGov::Form.new().create_form
end
end
Work around net/http headers case sensitivity:
lib/net_http
require 'net/http'
class Net::HTTP::ImmutableHeaderKey
attr_reader :key
def initialize(key)
#key = key
end
def downcase
self
end
def capitalize
self
end
def split(*)
[self]
end
def hash
key.hash
end
def eql?(other)
key.eql? other.key.eql?
end
def to_s
def self.to_s
key
end
self
end
end
If I call create_form this is the output:
{"error"=>true,
"error_log"=>
[{"error_code"=>"missing_date_headers",
"error_message"=>"Request is missing date headers",
"error_description"=>
"{\"Host\":\"nycopp.seamlessdocs.com\",\"Connection\":\"close\",\"X-Real-IP\":\"71.249.243.7\",\"X-Forwarded-For\":\"71.249.243.7\",\"X-Forwarded-Host\":\"nycopp.seamlessdocs.com\",\"X-Forwarded-Port\":\"443\",\"X-Forwarded-Proto\":\"https\",\"X-Original-URI\":\"\\/api\\/form\\/\\/elements\",\"X-Scheme\":\"https\",\"Authorization\":\"HMAC-SHA256 api_key='h123xxxxxxxxxx' signature=''\",\"AuthDate\":\"\"}"},
{"error_code"=>"external_auth_error", "error_message"=>"Date header is missing or timestamp out of bounds"}]}
What is the issue?
The mistake is in the order of operations/calculations.
def initialize()
#api_key = ENV['SEAMLESS_GOV_API_KEY']
#signature = generate_signature # <= at this point, neither form_id nor timestamp are set. but api key is.
#form_id = ENV['SEAMLESS_GOV_FORM_ID']
#timestamp = Time.now.to_i.to_s
end
I am very new to ruby and I just spent time studying patterns from the existing ruby projects in github. Now, I landed on the twitter's ruby project and noticed these lines in their configuration:
client = Twitter::REST::Client.new do |config|
config.consumer_key = "YOUR_CONSUMER_KEY"
config.consumer_secret = "YOUR_CONSUMER_SECRET"
config.access_token = "YOUR_ACCESS_TOKEN"
config.access_token_secret = "YOUR_ACCESS_SECRET"
end
In the declaration of this method call, I also noticed this:
module Twitter
class Client
include Twitter::Utils
attr_accessor :access_token, :access_token_secret, :consumer_key, :consumer_secret, :proxy
def initialize(options = {})
options.each do |key, value|
instance_variable_set("##{key}", value)
end
yield(self) if block_given?
end
...
Now, as I do my practice, I copied the same logic but observe the content of "initialize" method.
module Main
class Sample
attr_accessor :hello, :foo
def initialize(options={})
yield(self) if block_given?
end
def test
#hello
end
end
end
And call it (same on how twitter code above does)
sample = Main::Sample.new do |config|
config.hello = "world"
config.foo = "bar"
end
puts "#{sample.hello} #{sample.foo}" # outputs => world bar
puts sample.test # outputs => world
Now, my question is that even though I don't have these lines in my code (see the code block from twitter above) inside my "initialize" method,
options.each do |key, value|
instance_variable_set("##{key}", value)
end
the code
puts "#{sample.hello} #{sample.foo}" and puts sample.test still works fine. Why is this so? How was the instance variable really set here?
It's because you're manually calling them with thing like config.hello= and config.foo=.
What won't work without that chunk of code is this:
Main::Sample.new(hello: 'world')
You'll need that part to pick up the options and apply them.
That Twitter version is pretty slack. Normally you'd want to test that there's a property with that name instead of just randomly assigning instance variables. Typically this is done with a white-list of some sort:
ATTRIBUTES = %i[ hello world ]
attr_accessor *ATTRIBUTES
def initialize(options = nil)
options and options.each do |attr, value|
if (ATTRIBUTES.include?(attr))
send("#{attr}=", value)
else
raise "Unknown attribute #{attr.inspect}"
end
end
yield self if (block_given?)
end
That will raise exceptions if you call with invalid options.
So witness and observe the following code, my questions is why do i never make it to the on_connect after starting the cool.io loop in send_to_server, the l.run should fire off the request as per the documented example on the github, and how the code handles incoming connections in module Server #socket.attach(l)
l.run
which does work and accepts the incoming data and sends it to my parser, which does work and fires off all the way up until the aforementioned send_to_server. So what is going on here?
require 'cool.io'
require 'http/parser'
require 'uri'
class Hash
def downcase_key
keys.each do |k|
store(k.downcase, Array === (v = delete(k)) ? v.map(&:downcase_key) : v)
end
self
end
end
module ShadyProxy
extend self
module ClientParserCallbacks
extend self
def on_message_complete(conn)
lambda do
puts "on_message_complete"
PluginHooks.before_request_to_server(conn)
end
end
def on_headers_complete(conn)
lambda do |headers|
conn.headers = headers
end
end
def on_body(conn)
lambda do |chunk|
conn.body << chunk
end
end
end
module PluginHooks
extend self
def before_request_to_server(conn)
# modify request here
conn.parser.headers.delete "Proxy-Connection"
conn.parser.headers.downcase_key
send_to_server(conn)
end
def send_to_server(conn)
parser = conn.parser
uri = URI::parse(parser.request_url)
l = Coolio::Loop.default
puts uri.scheme + "://" + uri.host
c = ShadyHttpClient.connect(uri.scheme + "://" + uri.host,uri.port).attach(l)
c.connection_reference = conn
c.request(parser.http_method,uri.request_uri)
l.run
end
def before_reply_to_client(conn)
end
end
class ShadyHttpClient < Coolio::HttpClient
def connection_reference=(conn)
puts "haz conneciton ref"
#connection_reference = conn
end
def connection_reference
#connection_reference
end
def on_connect
super
#never gets here
#headers = nil
#body = ''
#buffer = ''
end
def on_connect_failed
super
# never gets here either
end
def on_response_header(header)
#headers = header
end
def on_body_data(data)
puts "on data?"
#body << data
STDOUT.write data
end
def on_request_complete
puts "Headers"
puts #headers
puts "Body"
puts #body
end
def on_error(reason)
STDERR.puts "Error: #{reason}"
end
end
class ShadyProxyConnection < Cool.io::TCPSocket
attr_accessor :headers, :body, :buffer, :parser
def on_connect
#headers = nil
#body = ''
#buffer = ''
#parser = Http::Parser.new
#parser.on_message_complete = ClientParserCallbacks.on_message_complete(self)
#parser.on_headers_complete = ClientParserCallbacks.on_headers_complete(self)
#parser.on_body = ClientParserCallbacks.on_body(self)
end
def on_close
puts "huh?"
end
def on_read(data)
#buffer << data
#parser << data
end
end
module Server
def run(opts)
begin
# Start our server to handle connections (will raise things on errors)
l = Coolio::Loop.new
#socket = Cool.io::TCPServer.new(opts[:host],opts[:port], ShadyProxy::ShadyProxyConnection)
#socket.attach(l)
l.run
# Handle every request in another thread
loop do
Thread.new s = #socket.accept
end
# CTRL-C
rescue Interrupt
puts 'Got Interrupt..'
# Ensure that we release the socket on errors
ensure
if #socket
#socket.close
puts 'Socked closed..'
end
puts 'Quitting.'
end
end
module_function :run
end
end
ShadyProxy::Server.run(:host => '0.0.0.0',:port => 1234)
I'm trying to set up a basic routing system in Rack, however.. I can't understand why the first route ('/') works, and the second ('/help') doesn't. What gets returned in the case of '/help' is a NoMethodError. Why is that, and how can I fix it? Thank you.
require 'rack'
class MyApp
def self.call(env)
new.call(env)
end
def self.get(path, &block)
##routes ||= {}
##routes[path] = block
end
def call(env)
dup.call!(env)
end
def call!(env)
#req = Rack::Request.new(env)
#res = Rack::Response.new
#res.write route(#req.path)
#res.finish
end
def route(path)
if ##routes[path]
##routes[path].call
else
'Not Found'
end
end
def to_do(arg)
arg
end
end
MyApp.get '/' do
'index'
end
MyApp.get '/help' do
to_do 'help'
end
run MyApp
I'm trying to understand yield in Ruby. In the self.save method of the Gateway class, it has 'yield gateway'. I understand that when yield is called the block from Person#save is called, but what does 'gateway' become in that block? Can you please explain a little with this code example
class Person
attr_accessor :first_name, :last_name, :ssn
def save
Gateway.save do |persist|
persist.subject = self
persist.attributes = [:first_name, :last_name, :ssn]
persist.to = 'http://www.example.com/person'
end
end
end
class Gateway
attr_acessor :subject, :attributes, :to
def self.save
gateway = self.new
yield gateway
gateway.execute
end
def execute
request = Net::HTTP::Post.new(url.path)
attribute_hash = attributes.inject({}) do | result, attribute |
result[attribute.to_s] = subject.send attribute
result
end
request.set_form(attribute_hash)
Net::HTTP.new(url.host, url.post).start { |http| http.request(request) }
end
def url
URI.parse(to)
end
end
The argument to yield will be parsed as an argument to the block. So in your example gateway's value is assigned to the persist parameter of the block.