Standardizing api responses in a modular Sinatra application - ruby

I'm developing an api as a modular Sinatra web application and would like to standardize the responses that are returned without having to do so explicitly. I thought this could be achieved by using middleware but it fails in most scenarios. The below sample application is what I have so far.
config.ru
require 'sinatra/base'
require 'active_support'
require 'rack'
class Person
attr_reader :name, :surname
def initialize(name, surname)
#name, #surname = name, surname
end
end
class MyApp < Sinatra::Base
enable :dump_errors, :raise_errors
disable :show_exceptions
get('/string') do
"Hello World"
end
get('/hash') do
{"person" => { "name" => "john", "surname" => "smith" }}
end
get('/array') do
[1,2,3,4,5,6,7, "232323", '3245235']
end
get('/object') do
Person.new('simon', 'hernandez')
end
get('/error') do
raise 'Failure of some sort'
end
end
class ResponseMiddleware
def initialize(app)
#app = app
end
def call(env)
begin
status, headers, body = #app.call(env)
response = {'status' => 'success', 'data' => body}
format(status, headers, response)
rescue ::Exception => e
response = {'status' => 'error', 'message' => e.message}
format(500, {'Content-Type' => 'application/json'}, response)
end
end
def format(status, headers, response)
result = ActiveSupport::JSON.encode(response)
headers["Content-Length"] = result.length.to_s
[status, headers, result]
end
end
use ResponseMiddleware
run MyApp
Examples (in JSON):
/string
Expected: {"status":"success","data":"Hello World"}
Actual: {"status":"success","data":["Hello World"]}
/hash (works)
Expected: {"status":"success","data":{"person":{"name":"john","surname":"smith"}}}
Actual: {"status":"success","data":{"person":{"name":"john","surname":"smith"}}}
/array
Expected: {"status":"success","data": [1,2,3,4,5,6,7,"232323","3245235"]}
Actual: {"status":"error","message":"wrong number of arguments (7 for 1)"}
/object
Expected: {"status":"success","data":{"name":"simon","surname":"hernandez"}}
Actual: {"status":"success","data":[]}
/error (works)
Expected: {"status":"error","message":"Failure of some sort"}
Actual: {"status":"error","message":"Failure of some sort"}
If you execute the code, you will see that /hash and /error give back the required responses, but the rest do not. Ideally, I would not like to change anything in the MyApp class. It's currently being built on top of Sinatra 1.3.3, ActiveSupport 3.2.9 and Rack 1.4.1.

With some help from #sinatra on irc.freenode.org, I managed to get it down to what I want. I added the following to MyApp:
def route_eval
result = catch(:halt) { super }
throw :halt, {"result" => result}
end
I then changed the following line in ResponseMiddleware:
response = {'status' => 'success', 'data' => body}
to
response = {'status' => 'success', 'data' => body["result"]}
and all my test cases passed.

Related

How to perform an HTTP redirect (302) using Rack?

I am messing around with my first Rack application (this is just for experimentation).
When a call comes in I do something like this:
class Application
def call(env)
# Totally ignore favicons for the time being
if env['PATH_INFO'] == '/favicon.ico'
return [ 404, {'Content-Type' => 'text/html'}, [] ]
elsif env['PATH_INFO'] == '/'
return [ 302, {'http-equiv' => "refresh", 'content' => "2;url=http://google.com"}, [] ]
end
...
I know this is horrible... but, again, this isn't a serious project.
I'm trying to figure out how to do the redirect. What I have does not work. Basically, when you hit / on my site I want to redirect that require to google.com.
Here is working app with re-direct to Google
require 'rack'
require 'rack/server'
class HelloWorld
def response
[ 302, {'Location' =>"http://google.com"}, [] ]
end
end
class HelloWorldApp
def self.call(env)
HelloWorld.new.response
end
end
Rack::Server.start :app => HelloWorldApp

modular Sinatra App, setting error handling & configuration globally

I am using Sinatra to build a small Ruby API, and I would like to get some of the errors and configurations set to work at a global level so that i don't need to set them at the start of each of the classes.
My structure is this:
content_api.rb
require 'sinatra/base'
require 'sinatra/namespace'
require 'sinatra/json'
require 'service_dependencies'
require 'api_helpers'
require 'json'
module ApiApp
class ContentApi < Sinatra::Base
helpers Sinatra::JSON
helpers ApiApp::ApiHelpers
include ApiApp::ServiceDependencies
before do
content_type :json
end
get '/' do
content = content_service.get_all_content
content.to_json
end
get '/audio' do
package =content_service.get_type 'Audio'
package.to_json
end
get '/video' do
package =content_service.get_type 'Video'
package.to_json
end
get '/document' do
package =content_service.get_type 'Document'
package.to_json
end
end
end
config.ru:
$LOAD_PATH.unshift *Dir[File.join(File.dirname(__FILE__), '/src/**')]
$LOAD_PATH.unshift *Dir[File.join(File.dirname(__FILE__), '/src/api/**')]
require 'content_api'
require 'package_api'
require 'utility_api'
require 'sinatra/base'
configure do
set :show_exceptions => false
end
error { |err| Rack::Response.new([{'error' => err.message}.to_json], 500, {'Content-type' => 'application/json'}).finish }
Rack::Mount::RouteSet.new do |set|
set.add_route ApiApp::ContentApi, {:path_info => %r{^/catalogue*}}, {}, :catalogue
set.add_route ApiApp::PackageApi, {:path_info => %r{^/package*}}, {}, :package
set.add_route ApiApp::UtilityApi, {:path_info => %r{^/health_check*}}, {}, :health_check
end
When I run this, it will run okay, but when I force a 500 error (shut down MongoDb) I get a standard html type error that states:
<p id="explanation">You're seeing this error because you have
enabled the <code>show_exceptions</code> setting.</p>
If I add the
configure do
set :show_exceptions => false
end
error { |err| Rack::Response.new([{'error' => err.message}.to_json], 500, {'Content-type' => 'application/json'}).finish }
inside the content_api.rb file, then I get a JSON error returned as I would like to receive.
however, as I am building this modular, I don't want to be repeating myself at the top of each class.
Is there a simple way to make this work?
The simplest way to do this would be to reopen Sinatra::Base and add the code there:
class Sinatra::Base
set :show_exceptions => false
error { |err|
Rack::Response.new(
[{'error' => err.message}.to_json],
500,
{'Content-type' => 'application/json'}
).finish
}
end
This is probably not advisable though, and will cause problems if your app includes other non-json modules.
A better solution might be to create a Sinatra extension that you could use in your modules.
module JsonExceptions
def self.registered(app)
app.set :show_exceptions => false
app.error { |err|
Rack::Response.new(
[{'error' => err.message}.to_json],
500,
{'Content-type' => 'application/json'}
).finish
}
end
end
You would then use it by registering it in your modules:
# require the file where it is defined first
class ContentApi < Sinatra::Base
register JsonExceptions
# ... as before
end
As an alternative, inheritance:
class JsonErrorController < Sinatra::Base
configure do
set :show_exceptions => false
end
error { |err| Rack::Response.new([{'error' => err.message}.to_json], 500, {'Content-type' => 'application/json'}).finish }
end
class ContentApi < JsonErrorController
# rest of code follows…
end
From Sinatra Up and Running p73:
Not only settings, but every aspect of a Sinatra class will be
inherited by its subclasses. This includes defined routes, all the
error handlers, extensions, middleware, and so on.

Mocking methods in Puppet rspec tests

I've implemented a custom Puppet function that queries a Keystone server for information. The module that defines this function includes some helper methods that perform the actual work of querying keystone. Broadly, the structure looks like this:
def authenticate(auth_url, username, password)
...
end
def list_tenants(auth_url, token)
...
end
module Puppet::Parser::Functions
newfunction(:lookup_tenant, :type => :rvalue) do |args|
...
end
end
I would like to mock out the authenticate and list_tenants methods
during testing so that I can test the rest of the Puppet module in the
absence of an actual Keystone server.
I haven't previously worked with either Ruby or Rpsec before, and I'm
having a hard time finding examples of how to provide stubs for these
internal methods.
So far I have a stub rspec file that simply verified the existence of
the function:
require 'spec_helper'
describe 'lookup_tenant' do
it "should exist" do
Puppet::Parser::Functions.function("lookup_tenant").should == "function_lookup_tenant"
end
# This will fail because there is no keystone server.
it "should fail" do
should run.with_params(
'http://127.0.0.1:35357/v2.0',
'admin_user',
'admin_password',
'admin_tenant_name',
'target_tenant_name'
).and_raise_error(KeystoneError)
end
end
I would like to be able to provide custom returns from the
authenticate and list_tenants methods (or even raise exceptions
from inside these methods) so that I can test the behavior of the
lookup_tenant function in different failure scenarios.
WebMock could be used for simulating the http requests as stubs. Here is the link to the github repo: https://github.com/bblimke/webmock
For folks who haven't seen webmock before, I wanted to leave some information here about why it's particularly awesome.
So, I have in my module some code that makes an http request:
url = URI.parse("#{auth_url}/tokens")
req = Net::HTTP::Post.new url.path
req['content-type'] = 'application/json'
req.body = JSON.generate(post_args)
begin
res = Net::HTTP.start(url.host, url.port) {|http|
http.request(req)
}
if res.code != '200'
raise KeystoneError, "Failed to authenticate to Keystone server at #{auth_url} as user #{username}."
end
rescue Errno::ECONNREFUSED
raise KeystoneError, "Failed to connect to Keystone server at #{auth_url}."
end
By simply adding a require to the start of the spec file:
require `webmock`
Attempts to open a connection will result in:
WebMock::NetConnectNotAllowedError:
Real HTTP connections are disabled. Unregistered request: POST http://127.0.0.1:35357/v2.0/tokens with body '{"auth":{"passwordCredentials":{"username":"admin_user","password":"admin_password"},"tenantName":"admin_tenant"}}' with headers {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'}
You can stub this request with the following snippet:
stub_request(:post, "http://127.0.0.1:35357/v2.0/tokens").
with(:body => "{\"auth\":{\"passwordCredentials\":{\"username\":\"admin_user\",\"password\":\"admin_password\"},\"tenantName\":\"admin_tenant\"}}",
:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'}).
to_return(:status => 200, :body => "", :headers => {})
And that's just about all the information you need to stub out the
call. You can make the stubs as granular as necessary; I ended up
using something like:
good_auth_request = {
'auth' => {
'passwordCredentials' => {
'username' => 'admin_user',
'password' => 'admin_password',
},
'tenantName' => 'admin_tenant',
}
}
auth_response = {
'access' => {
'token' => {
'id' => 'TOKEN',
}
}
}
stub_request(:post, "http://127.0.0.1:35357/v2.0/tokens").
with(:body => good_auth_request.to_json).
to_return(:status => 200, :body => auth_response.to_json, :headers => {})
And now I can test my module when there is no Keystone server
available.

Force rebuild the SOAP-Header after authenticating

My SOAP-Server expects every request to have a valid token in the soap-header to authenticate the soap-client. This token is only valid for a certain period of time, so I have to expect it to be invalid in every call.
I am trying to find a way to force savon to rebuild the SOAP-Header (i.e. use the new auth-token) after I (re)authenticate with the SOAP-Server. I am not sure, if that is either a savon problem or a ruby one. Here is what I have so far.
class Soapservice
extend Savon::Model
# load stored auth-token
##header_data = YAML.load_file "settings.yaml"
client wsdl: 'locally-cached-wsdl.xml',
soap_header: {'verifyingToken' => ##header_data}
operations :get_authentification_token, :get_server_time
# request a new auth-token and store it
def get_authentification_token
response = super(:message => {
'oLogin' => {
'Username' => 'username',
'Userpass' => 'password'
}
})
settings = {
'UserID' => response[:user_id].to_i,
'Token' => response[:token],
}
File.open("settings.yaml", "w") do |file|
file.write settings.to_yaml
end
##header_data = settings
end
def get_server_time
return super()
rescue Savon::SOAPFault => error
fault_code = error.to_hash[:fault][:faultstring]
if fault_code == 'Unauthorized Request - Invalide Token'
get_authentification_token
retry
end
end
end
When I call
webservice = Soapservice.new
webservice.get_server_time
with an invalid Token, it reauthenticates and saves the new Token successfully, but the retry doesn't load the new header (the result is an infinite loop). Any ideas?
I added rubiii's answer from the GitHub-Issue here for future reference:
class Soapservice
# load stored auth-token
##header_data = YAML.load_file "settings.yaml"
def initialize
#client = Savon.client(wsdl: 'locally-cached-wsdl.xml')
end
def call(operation_name, locals = {})
#client.globals[:soap_header] = {'verifyingToken' => ##header_data}
#client.call(operation_name, locals)
end
# request a new auth-token and store it
def get_authentification_token
message = {
'Username' => 'username',
'Userpass' => 'password'
}
response = call(:get_authentification_token, :message => message)
settings = {
'UserID' => response[:user_id].to_i,
'Token' => response[:token],
}
File.open("settings.yaml", "w") do |file|
file.write settings.to_yaml
end
##header_data = settings
end
def get_server_time
call(:get_server_time)
rescue Savon::SOAPFault => error
fault_code = error.to_hash[:fault][:faultstring]
if fault_code == 'Unauthorized Request - Invalide Token'
get_authentification_token
retry
end
end
end
rubiii added:
notice that i removed Savon::Model, as you actually don't need it and i don't know if it supports this workaround.
if you look at the #call method, it accesses and changes the globals before every request.

Connect Rails 3 to Salesforce/Any other App via OAuth

Has anybody connected to Salesforce through Rails 3 App via oauth? Could you please post code for doing same. I am trying to same but I get some error below is my code
def oauth_client
consumer_key = '....'
consumer_secret = '....'
oauth_options = {
:site => 'https://login.salesforce.com',
:scheme => :body,
:request_token_path => '/_nc_external/system/security/oauth/RequestTokenHandler',
:authorize_path => '/setup/secur/RemoteAccessAuthorizationPage.apexp',
:access_token_path => '/_nc_external/system/security/oauth/AccessTokenHandler',
}
OAuth::Consumer.new consumer_key, consumer_secret, oauth_options
end
def oauth_redirect_uri
uri = URI.parse(request.url)
uri.path = '/sfdc/oauth_callback'
uri.query = nil
# uri = "http://localhost:3000/sfdc/oauth_callback"
uri.to_s
end
def oauth_connect
consumer_key = '...' # from SalesForce
consumer = oauth_client
request_t = consumer.get_request_token
redirect_to request_t.authorize_url(
:redirect_uri => oauth_redirect_uri,
:oauth_consumer_key => consumer_key
)
end
def oauth_callback
access = request_t.get_access_token :oauth_verifier => params[:oauth_verifier]
p access
render :text => access.token
end
Error undefined method get_access_token for #<ActionDispatch::Request:0x12b79f370>. the request variable is nil here. How do I get it back?
The rforce gem has quite a bit of an example that I pasted below. However you might just want to use rforce instead of rolling your own.
def init_server(url)
#url = URI.parse(url)
if (#oauth)
consumer = OAuth::Consumer.new \
#oauth[:consumer_key],
#oauth[:consumer_secret],
{
:site => url,
:proxy => #proxy
}
consumer.http.set_debug_output $stderr if show_debug
#server = OAuth::AccessToken.new \
consumer,
#oauth[:access_token],
#oauth[:access_secret]
class << #server
alias_method :post2, :post
end
else
#server = Net::HTTP.Proxy(#proxy).new(#url.host, #url.port)
#server.use_ssl = #url.scheme == 'https'
#server.verify_mode = OpenSSL::SSL::VERIFY_NONE
# run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps.
#server.set_debug_output $stderr if show_debug
end
end

Resources