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.
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 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.
I am building a DSL and have this module
module EDAApiBuilder
module Client
attr_accessor :api_client, :endpoint, :url
def api_client(api_name)
#apis ||= {}
raise ArgumentError.new('API name already exists.') if #apis.has_key?(api_name)
#api_client = api_name
#apis[#api_client] = {}
yield(self) if block_given?
end
def fetch_client(api_name)
#apis[api_name]
end
def endpoint(endpoint_name)
raise ArgumentError.new("Endpoint #{endpoint_name} already exists for #{#api_client} API client.") if fetch_client(#api_client).has_key?(endpoint_name)
#endpoint = endpoint_name
#apis[#api_client][#endpoint] = {}
yield(self) if block_given?
end
def url=(endpoint_url)
fetch_client(#api_client)[#endpoint]['url'] = endpoint_url
end
end
end
so that I have tests like
context 'errors' do
it 'raises an ArgumentError when trying to create an already existent API client' do
expect {
obj = MixinTester.new
obj.api_client('google')
obj.api_client('google')
}.to raise_error(ArgumentError,'API name already exists.')
end
it 'raises an ArgumentError when trying to create a repeated endpoint for the same API client' do
expect {
obj = MixinTester.new
obj.api_client('google') do |apic|
apic.endpoint('test1')
apic.endpoint('test1')
end
}.to raise_error(ArgumentError,"Endpoint test1 already exists for google API client.")
end
end
I would rather have #api_clientwritten as an assignment block
def api_client=(api_name)
so that I could write
obj = MixinTester.new
obj.api_client = 'google' do |apic| # <=== Notice the difference here
apic.endpoint('test1')
apic.endpoint('test1')
end
because I think this notation (with assignment) is more meaningful. But then, when I run my tests this way I just get an error saying that the keyworkd_do is unexpected in this case.
It seems to me that the definition of an assignment block is syntactic sugar which won't contemplate blocks.
Is this correct? Does anyone have some information about this?
By the way: MixinTester is just a class for testing, defined in my spec/spec_helper.rb as
class MixinTester
include EDAApiBuilder::Client
end
SyntaxError
It seems to me that the definition of an assignment [method] is syntactic
sugar which won't contemplate blocks.
It seems you're right. It looks like no method with = can accept a block, even with the normal method call and no syntactic sugar :
class MixinTester
def name=(name,&block)
end
def set_name(name, &block)
end
end
obj = MixinTester.new
obj.set_name('test') do |x|
puts x
end
obj.name=('test') do |x| # <- syntax error, unexpected keyword_do, expecting end-of-input
puts x
end
Alternative
Hash parameter
An alternative could be written with a Hash :
class MixinTester
def api(params, &block)
block.call(params)
end
end
obj = MixinTester.new
obj.api client: 'google' do |apic|
puts apic
end
#=> {:client=>"google"}
You could adjust the method name and hash parameters to taste.
Parameter with block
If the block belongs to the method parameter, and not the setter method, the syntax is accepted :
def google(&block)
puts "Instantiate Google API"
block.call("custom apic object")
end
class MixinTester
attr_writer :api_client
end
obj = MixinTester.new
obj.api_client = google do |apic|
puts apic
end
# =>
# Instantiate Google API
# custom apic object
It looks weird, but it's pretty close to what you wanted to achieve.
I'd like to use a block within method initialize. With this code:
class Settlement
attr_reader :url, :parameters
def initialize(&block)
instance_eval block.call
end
def parameters(x) ; #parameters = x; end
def url(x) ; #url = x; end
end
settlement = Settlement.new do
url "https://xxxxx"
parameters("ffff")
end
I got the error message below:
NoMethodError - undefined method parameters
Any ideas?
When you call instance_eval block.call block.call is evaluated before instance_eval is called. This means the block is called without the instance binding.
This should work:
def initialize(&block)
instance_eval &block
end
I'm learning metaprogramming and am trying to make a little DSL to generate HTML. The #result instance variable is not generating the correct answer because when the h1 method is called, the #result instance variable is reset. Is there an elegant way to deal with these 'nested' method calls (I know Ruby doesn't technically have nested methods). Here's my code:
class HtmlDsl
attr_reader :result
def initialize(&block)
instance_eval(&block)
end
private
def method_missing(name, *args, &block)
tag = name.to_s
content = args.first
#result = "<#{tag}>#{block_given? ? instance_eval(&block) : content}</#{tag}>"
end
end
html = HtmlDsl.new do
html do
head do
title 'yoyo'
end
body do
h1 'hey'
end
end
end
p html.result # => "<html><body><h1>hey</h1></body></html>"
# desired result # => "<html><head><title>yoyo</title></head><body><h1>hey</h1></body></html>"
Your problem is not that #result is reset, only that you add into the #result the return value of instance_eval(&block), which is the last line in the block, and not the aggregated block. This should work better (although not perfectly):
class HtmlDsl
attr_reader :result
def initialize(&block)
instance_eval(&block)
end
private
def method_missing(name, *args, &block)
tag = name.to_s
content = args.first
(#result ||= '') << "<#{tag}>"
if block_given?
instance_eval(&block)
else
#result << content
end
#result << "</#{tag}>"
end
end
So now:
html = HtmlDsl.new do
html do
head do
title 'yoyo'
end
body do
h1 'hey'
end
end
end
p html.result
#=> "<html><head><title>yoyo</title></head><body><h1>hey</h1></body></html>"
What I've done is that each call actually renders a fragment to the #result, so inner calls render inner fragments, each wrapping its own inner fragments with tags.