SlideShare a Scribd company logo
Tips and Tricks for
Building API-Heavy Ruby
on Rails Applications

Tim Cull


@trcull
trcull@pollen.io
We've Done
Some Stuff
#1

Instagram +
CafePress
Tips and tricks for building api heavy ruby on rails applications
Tips and tricks for building api heavy ruby on rails applications
Tips and tricks for building api heavy ruby on rails applications
Tips and tricks for building api heavy ruby on rails applications
#2

Spreadsheet
+ Freshbooks
Tips and tricks for building api heavy ruby on rails applications
Tips and tricks for building api heavy ruby on rails applications
Tips and tricks for building api heavy ruby on rails applications
Tips and tricks for building api heavy ruby on rails applications
#3


Google Docs
+ Custom
Tips and tricks for building api heavy ruby on rails applications
Tips and tricks for building api heavy ruby on rails applications
Tips and tricks for building api heavy ruby on rails applications
$1.7 billion
 1,285%
We


We've Learned Some Things
Timeouts Happen
Authentication Still Hurts




                   image: http://wiki.oauth.net/
Do Everything in Background
Tasks
Y   T
O   H
U   R
    O
W   T
I   T
L   L
L   E
    D
B
E
Libraries
Pull In     Problems
Now For Some Meat
http = Net::HTTP.new "api.cafepress.com", 80

http.set_debug_output STDOUT
http = Net::HTTP.new "api.cafepress.com", 80

 http.set_debug_output STDOUT

<- "POST /oauth/AccessToken HTTP/1.1rnAccept: */*rnUser-Agent: OAuth gem v0.4.7rnContent-
Length: 0rnAuthorization: OAuth oauth_body_hash="2jmj7YBwk%3D", oauth_callback="http%3A%2F%
2Flocalhost%3A3000%2Fxero%2Foauth_callback", oauth_consumer_key="20Q0CD6", oauth_nonce="
BawDBGd0kRDdCM", oauth_signature="B7Bd%3D", oauth_signature_method="RSA-SHA1",
oauth_timestamp="1358966217", oauth_token="MyString", oauth_version="1.0"rnConnection:
closernHost: api-partner.network.xero.comrnrn"
-> "HTTP/1.1 401 Unauthorizedrn"
-> "Cache-Control: privatern"
-> "Content-Type: text/html; charset=utf-8rn"
-> "Server: Microsoft-IIS/7.0rn"
-> "X-AspNetMvc-Version: 2.0rn"
-> "WWW-Authenticate: OAuth Realm="108.254.144.237"rn"
-> "X-AspNet-Version: 4.0.30319rn"
-> "X-S: api1rn"
-> "Content-Length: 121rn"
-> "rn"
reading 121 bytes...
-> "oauth_problem=token_rejected&oauth_problem_advice=Token%20MyString%20does%20not%
20match%20an%20expected%20REQUEST%20token"
oauth = OAuth::Consumer.new( key, secret)

oauth.http.set_debug_output STDOUT
oauth = OAuth::Consumer.new( key, secret)

oauth.http.set_debug_output STDOUT
oauth = OAuth::Consumer.new( key, secret, {
   :request_token_url => 'https://api.linkedin.
com/uas/oauth/requestToken'})

oauth.http.set_debug_output STDOUT

# NOTHING PRINTS!
oauth = OAuth::Consumer.new( key, secret, {
   :request_token_url => 'https://api.linkedin.
com/uas/oauth/requestToken'})

oauth.http.set_debug_output STDOUT
# NOTHING PRINTS!
OAuth::Consumer.rb (151)
def request(http_method, path, token = nil, *arguments)
  if path !~ /^//
    @http = create_http(path)
    _uri = URI.parse(path)
  end
  .....
end
!$@#%$
module OAuth
 class Consumer
  def create_http_with_featureviz(*args)
    @http ||= create_http_without_featureviz(*args).tap do |http|
     begin
      http.set_debug_output($stdout) unless options[:
suppress_debugging]
     rescue => e
      puts "error trying to set debugging #{e.message}"
     end
    end
  end
  alias_method_chain :create_http, :featureviz
 end
end
Class Structure

Make an API class
Responsibilities
● Network plumbing
● Translating xml/json/whatever to "smart"
  hashes *
● Converting Java-like or XML-like field
  names to Ruby-like field names
● Parsing string dates and integers and
  turning them into real Ruby data types
● Setting reasonable defaults
● Flattening awkward data structures
● That’s it!
172 Lines of Code
https://github.com/trcull/pollen-snippets/blob/master/lib/abstract_api.rb
class Instagram::Api
   def get_photos()
       resp = get("v1/users/#{@user_id}/media/recent")
       parse_response_and_stuff resp
   end

   def get(url)
      req = Net::HTTP::Get.new url
      do_request req
   end

   def do_request( req )
      net = Net::HTTP.new
      net.start do |http|
         http.request req
      end
   end
end
Goal

api = InstagramApi.new current_user

photos = api.get_photos

photos.each do |photo|
   puts photo.thumbnail_url
end
def get_photos
 rv = []
 response = get( "v1/users/#{@user_id}/media/recent",
                            {:access_token=>@access_token})
 data = JSON.parse(response.body)
 data['data'].each do |image|
  photo = ApiResult.new
  photo[:id] = image['id']
  photo[:thumbnail_url] = image['images']['thumbnail']['url']
  if image['caption'].present?
    photo[:caption] = image['caption']['text']
  else
    photo[:caption] = 'Instagram image'
  end
  rv << photo
 end
 rv
end
def get(url, params, with_retry=true)

 real_url = "#{@protocol}://#{@host}/#{url}?"
         .concat(params.collect{|k,v|
          "#{k}=#{CGI::escape(v.to_s)}"}.join("&"))

 request = Net::HTTP::Get.new(real_url)

 if with_retry
   response = do_request_with_retry request
 else
   response = do_request request
 end

 response

end
Goal

api = InstagramApi.new current_user

photos = api.get_photos

photos.each do |photo|
   puts photo.thumbnail_url
end
class ApiResult < Hash
  def []=(key, value)
    store(key.to_sym,value)
    methodify_key key.to_sym
  end

  def methodify_key(key)
   if !self.respond_to? key
     self.class.send(:define_method, key) do
       return self[key]
     end
     self.class.send(:define_method, "#{key}=") do |val|
       self[key] = val
     end
   end
  end
 end
See: https://github.com/trcull/pollen-snippets/blob/master/lib/api_result.rb
Effective Testing
describe Instagram::Api do
 subject do
  user = create(:user, :token=>token, :secret=>secret)
  Instagram::Api.new(user)
 end

 describe "making fake HTTP calls" do

 end

 describe "in a test bed" do

 end

 describe "making real HTTP calls" , :integration => true do

 end
end
in lib/tasks/functional.rake


 RSpec::Core::RakeTask.new("spec:integration") do |t|
  t.name = "spec:integration"
  t.pattern = "./spec/**/*_spec.rb"
  t.rspec_opts = "--tag integration --profile"
 end



in .rspec


--tag ~integration --profile


to run: "rake spec:integration"
describe Instagram::Api do
 subject do
      #sometimes have to be gotten manually, unfortunately.
      user = create(:user,
                           :token=>"stuff",
                           :secret=>"more stuff")
      Instagram::Api.new(user)
 end


 describe "in a test bed" do
  it "pulls the caption out of photos" do
     photos = subject.get_photos
     photos[0].caption.should eq 'My Test Photo'
  end
 end

end
describe Instagram::Api do
 subject do
      #sometimes have to be gotten manually, unfortunately.
      user = create(:user,
                           :token=>"stuff",
                           :secret=>"more stuff")
      Instagram::Api.new(user)
 end


 describe making real HTTP calls" , :integration => true do
  it "pulls the caption out of photos" do
     photos = subject.get_photos
     photos[0].caption.should eq 'My Test Photo'
  end
 end

end
describe Evernote::Api do
 describe "making real HTTP calls" , :integration => true do
    subject do
        req_token = Evernote::Api.oauth_request_token
         oauth_verifier = Evernote::Api.authorize_as('testacct','testpass', req_token)
         access_token = Evernote::Api.oauth_access_token(req_token,
oauth_verifier)
         Evernote::Api.new access_token.token, access_token.secret
    end

    it "can get note" do
      note_guid = "4fb9889d-813a-4fa5-b32a-1d3fe5f102b3"
      note = subject.get_note note_guid
      note.guid.should == note_guid
    end
 end
end
def authorize_as(user, password, request_token)
   #pretend like the user logged in
   get("Login.action") #force a session to start
   session_id = @cookies['JSESSIONID'].split('=')[1]
   login_response = post("Login.action;jsessionid=#{session_id}",
                            {:username=>user,
                             :password=>password,
                             :login=>'Sign In',
:targetUrl=>CGI.escape("/OAuth.action?oauth_token=#{request_token.token}")})

  response = post('OAuth.action', {:authorize=>"Authorize",
                                :oauth_token=>request_token.token})

  location = response['location'].scan(/oauth_verifier=(d*w*)/)

  oauth_verifier = location[0][0]
  oauth_verifier
 end
describe "making fake HTTP calls" do
   before do
       Net::HTTP.stub(:new).and_raise("unexpected network call")
   end

 it "pulls the thumbnail out of photos" do
     response = double("response")
     data_hash = {"data" =>
       [{
         "link"=>"apple.jpg",
         "id"=>"12",
         "images"=>
           {
             "thumbnail"=>{"url"=>"www12"},
             "standard_resolution"=>{"url"=>"www12"}
           }
       }]}
     response.stub(:body).and_return data_hash.to_json
      subject.should_receive(:do_request).and_return(response)
     photos = subject.get_photos
      photos[0].thumbnail_url.should eq 'www12'
  end
end
class Instagram::Api
   def get_photos()
       resp = get("v1/users/#{@user_id}/media/recent")
       parse_response_and_stuff resp
   end

   def get(url)
      req = Net::HTTP::Get.new url
      do_request req
   end

   def do_request( req )
      net = Net::HTTP.new
      net.start do |http|
         http.request req
      end
   end
end
describe "making fake HTTP calls" do
   before do
       Net::HTTP.stub(:new).and_raise("unexpected network call")
   end

 it "pulls the thumbnail out of photos" do
     response = double("response")
     data_hash = {"data" =>
       [{
         "link"=>"apple.jpg",
         "id"=>"12",
         "images"=>
           {
             "thumbnail"=>{"url"=>"www12"},
             "standard_resolution"=>{"url"=>"www12"}
           }
       }]}
     response.stub(:body).and_return data_hash.to_json
      subject.should_receive(:do_request).and_return(response)
     photos = subject.get_photos
      photos[0].thumbnail_url.should eq 'www12'
  end
end
describe EvernoteController do
 before do
   @api = double('fakeapi')
   Evernote::Api.stub(:new).and_return @api
 end

 describe "list_notes" do
   it "should list notes by title" do
        a_note = ApiResult.new({:title=>'test title'})
        @api.should_receive(:get_notes).and_return [a_note]
        get "/mynotes"
        response.body.should match /<td>test title</td>/
   end
 end
end
OAuth
Ask for a Request Token

Redirect User to Site (w/ Request Token)


            User Logs in and Authorizes


Site Redirects Back to You W/ OAuth Verifier

Trade OAuth Verifier and Request Token for
an Access Token

Store Access Token (Securely)

Make API Calls W/ Access Token
Standard?
Vanilla
@consumer = OAuth::Consumer.new("key","secret", :site => "https://agree2")

@callback_url = "http://127.0.0.1:3000/oauth/callback"
@request_token = @consumer
             .get_request_token(:oauth_callback => @callback_url)
session[:request_token] = @request_token

redirect_to @request_token.authorize_url(:oauth_callback => @callback_url)

#user is on other site, wait for callback

@access_token = session[:request_token]
           .get_access_token(:oauth_verifier=>params[:verifier])

@photos = @access_token.get('/photos.xml')




                                             Source: https://github.com/oauth/oauth-ruby
Instagram
#redirect user first, no request token necessary
@redirect_uri = CGI.escape('http:///myapp.com/oauth/callback')
redirect_to "https://api.instagram.com/oauth/authorize
     ?client_id=xyz123
     &redirect_uri=#{@redirect_uri}
     &response_type=code
     &scope=comments+basic"

#wait for callback

response = post( "oauth/access_token", {:client_id=>@client_id,
                           :client_secret=>@client_secret,
                           :redirect_uri=>@redirect_uri,
                           :grant_type=>'authorization_code',
                           :code=>params[:code]})
json = JSON.parse response.body

access_token = json['access_token']
instagram_name = json['user']['username']
instagram_id = json['user']['id']
Freshbooks
require 'oauth'
require 'oauth/signature/plaintext'


oauth = OAuth::Consumer.new( key, secret, {
            :scheme=> :query_string,
            :signature_method=>"PLAINTEXT",
            :oauth_callback=>callback,
            :authorize_path => "/oauth/oauth_authorize.php",
            :access_token_path=>"/oauth/oauth_access.php",
            :request_token_path=>"/oauth/oauth_request.php",
            :site=>"http://#{usersitename}.freshbooks.com"})
Xero
@consumer = OAuth::Consumer.new(@oauth_key, @oauth_secret,{
  :site => "https://api-partner.network.xero.com:443",
  :signature_method => 'RSA-SHA1',
  :private_key_str => ENV['XERO_CLIENT_PEM'] ,
  :ssl_client_cert=>ENV['XERO_ENTRUST_SSL_CERT'],
  :ssl_client_key=>ENV['XERO_ENTRUST_PRIVATE_PEM']})


module OAuth
 class Consumer
  def create_http_with_featureviz(*args)
    @http ||= create_http_without_featureviz(*args).tap do |http|
       http.cert = OpenSSL::X509::Certificate.new(options[:ssl_client_cert]) if
options[:ssl_client_cert]
       http.key = OpenSSL::PKey::RSA.new( options[:ssl_client_key]) if options[:
ssl_client_key]
    end
  end
  alias_method_chain :create_http, :featureviz
 end
end                                                             Thank you @tlconnor
@consumer = OAuth.new(@oauth_key, @oauth_secret,{
  :site => "https://api-partner.network.xero.com:443",
  :signature_method => 'RSA-SHA1',
  :private_key_str => ENV['XERO_CLIENT_PEM'] ,
  :ssl_client_cert=>ENV['XERO_ENTRUST_SSL_CERT'],
  :ssl_client_key=>ENV['XERO_ENTRUST_PRIVATE_PEM']})


module OAuth::Signature::RSA
 class SHA1 < OAuth::Signature::Base
  def digest
    private_key = OpenSSL::PKey::RSA.new(
      if options[:private_key_str]
        options[:private_key_str]
      elsif options[:private_key_file]
        IO.read(options[:private_key_file])
      else
        consumer_secret
      end
    )
    private_key.sign(OpenSSL::Digest::SHA1.new, signature_base_string)
  end
 end
end
Evernote
consumer = OAuth::Consumer.new(key, secret, {
   :site => "https://www.evernote.com",
   :authorize_path => "/OAuth.action"})

# redirect, yadda, yadda wait for callback

access_token = request_token.get_access_token(:oauth_verifier => oauth_verifier)

note_store_url = access_token.params['edam_noteStoreUrl']

#This is Thrift
transport = Thrift::HTTPClientTransport.new note_store_url
protocol = Thrift::BinaryProtocol.new transport
store = Evernote::EDAM::NoteStore::NoteStore::Client.new protocol

#Yay! Finally Notebooks!
notebooks = store.listNotebooks access_token.token
Background
Processing

   i.e. Do It
class SlurpImagesJob
      def self.enqueue(user_id)
            SlurpImagesJob.new.delay.perform(user_id)
      end

      #this makes unit testing with simulated errors easier
      def perform(user_id)
            begin
                  do_perform user_id
            rescue => e
                  Alerts.log_error "We encountered an error slurping your Instagram images please try again",
e
            end
      end

      def do_perform(user_id)
            user = User.find user_id
            cafepress = Cafepress::Api.new user
            instagram = Instagram::Api.new user

            photos = instagram.get_photos
            photos.each do |photo|
                 cafepress.upload_design photo.caption, photo.standard_resolution_url
            end
      end
end
class InstagramController < ApplicationController

   def oauth_callback
      request_access_token params
      SlurpImagesJob.enqueue current_user.id
   end
end


describe InstagramController do
   it "enqueues a job to slurp images" do
        SlurpImagesJob.should_receive :enqueue
        post '/oauth_callback'
   end
end
Webhooks
Register a Callback URL for a User

Site Verifies URL actually works (typically)


             User Does Something


Site Calls Back URL With an ID (typically)

Application Polls Site for More Details

Application Does Whatever
class FreshbooksApi < AbstractApi
    def register_callback(user_id)
         xml = "<request method="callback.create">
          <callback>
            <event>invoice.create</event>
            <uri>http://app.featureviz.com/webhook/freshbooks/#{user_id}</uri>
          </callback>
         </request>"
         post_with_body("api/2.1/xml-in", xml)
    end


      def verify_callback(our_user_id, verifier, callback_id)
           xml = "<request method="callback.verify">
            <callback>
              <callback_id>#{callback_id}</callback_id>
              <verifier>#{verifier}</verifier>
            </callback>
           </request>"
           post_with_body("api/2.1/xml-in", xml)
      end
end
class WebhooksController < ApplicationController
 # URL would be /webhook/freshbooks/:our_user_id
 def freshbooks_callback
   our_user_id = params[:our_user_id]
   event_name = params[:name]
   object_id = params[:object_id]

  api = Freshbooks::API.new User.find(our_user_id)

  if event_name == "callback.verify"
    verifier = params[:verifier]
    api.verify_callback our_user_id, verifier, object_id

   elsif event_name == "invoice.create"
    freshbooks_user_id = params[:user_id]
    InvoiceUpdatedJob.new.delay.perform our_user_id, object_id,
freshbooks_user_id
   end

  respond_to do |format|
   format.html { render :nothing => true}
   format.json { head :no_content}
  end
 end
class FreshbooksApi < AbstractApi
    def register_callback(user_id)
         xml = "<request method="callback.create">
          <callback>
            <event>invoice.create</event>
            <uri>http://app.featureviz.com/webhook/freshbooks/#{user_id}</uri>
          </callback>
         </request>"
         post_with_body("api/2.1/xml-in", xml)
    end


      def verify_callback(our_user_id, verifier, callback_id)
           xml = "<request method="callback.verify">
            <callback>
              <callback_id>#{callback_id}</callback_id>
              <verifier>#{verifier}</verifier>
            </callback>
           </request>"
           post_with_body("api/2.1/xml-in", xml)
      end
end
User Does Stuff
class WebhooksController < ApplicationController
 # URL would be /webhook/freshbooks/:our_user_id
 def freshbooks_callback
   our_user_id = params[:our_user_id]
   event_name = params[:name]
   object_id = params[:object_id]

  api = Freshbooks::API.new User.find(our_user_id)

  if event_name == "callback.verify"
    verifier = params[:verifier]
    api.verify_callback our_user_id, verifier, object_id

   elsif event_name == "invoice.create"
    freshbooks_user_id = params[:user_id]
    InvoiceUpdatedJob.new.delay.perform our_user_id, object_id,
freshbooks_user_id
   end

  respond_to do |format|
   format.html { render :nothing => true}
   format.json { head :no_content}
  end
 end
Questions?
Tips and Tricks for
Building API-Heavy Ruby
on Rails Applications

Tim Cull


@trcull
trcull@pollen.io
Libraries
net/http                         rfuzz               httparty
                open-uri
                                            mechanize
                           excon
simplehttp
                                      em-http-request
                      rest-client


    activeresource                  right_http_connection

                 rufus-verbs
wrest                                          faraday

                          curb            typhoeus
   patron

             httpclient          eventmachine
                                                        thanks @nahi
Tips and tricks for building api heavy ruby on rails applications
REST........ish
Tips and tricks for building api heavy ruby on rails applications
More Examples
<?xml version="1.0" encoding="utf-8"?>
    <response xmlns="http://www.freshbooks.com/api/" status="ok">
     <invoices page="1" per_page="10" pages="4" total="47">
       <invoice>
        <invoice_id>344</invoice_id>
         .......
       </invoice>
     </invoices>
    </response>




 def fb_invoices
  fb_collect('invoices','invoice'){|conn,page|conn.project.list(:
page=>page)}
 end
def fb_collect(root_element_name, result_element_name, &block)
   rv = []
   conn = fb_connection()
   page = 1
   pages = 1
   while page <= pages
    temp = yield conn, page
    page += 1
    if !temp[root_element_name].nil? && !temp[root_element_name]
[result_element_name].nil?
      if temp[root_element_name][result_element_name].kind_of?(Array)
        temp[root_element_name][result_element_name].each do |elem|
          rv.push(elem)
        end
      else
        #if there's only one result, the freshbooks api returns a bare hash instead of a
hash inside an array
        elem = temp[root_element_name][result_element_name]
        rv.push(elem)
      end
    end
   end
   return rv
  end

More Related Content

What's hot (19)

PPTX
jQuery for web development
iFour Institute - Sustainable Learning
 
PPTX
Plug in development
Lucky Ali
 
PDF
Developing iOS REST Applications
lmrei
 
PPTX
jQuery for Sharepoint Dev
Zeddy Iskandar
 
PPTX
CiklumJavaSat_15112011:Alex Kruk VMForce
Ciklum Ukraine
 
PDF
Flask patterns
it-people
 
PDF
Introduction to Ember.js and how we used it at FlowPro.io
Paul Knittel
 
PDF
Symfony tips and tricks
Javier Eguiluz
 
PDF
HTML5: where flash isn't needed anymore
Remy Sharp
 
KEY
LvivPy - Flask in details
Max Klymyshyn
 
PDF
iPhone Appleless Apps
Remy Sharp
 
ODP
Non Conventional Android Programming En
guest9bcef2f
 
PDF
Web Crawling with NodeJS
Sylvain Zimmer
 
PDF
Caldera Learn - LoopConf WP API + Angular FTW Workshop
CalderaLearn
 
PDF
Hey, I just met AngularJS, and this is crazy, so here’s my JavaScript, let’s ...
Alessandro Nadalin
 
PDF
Basic Crud In Django
mcantelon
 
PDF
Using Task Queues and D3.js to build an analytics product on App Engine
River of Talent
 
PDF
Introduction to plugin development
Caldera Labs
 
PDF
Python Flask Tutorial For Beginners | Flask Web Development Tutorial | Python...
Edureka!
 
jQuery for web development
iFour Institute - Sustainable Learning
 
Plug in development
Lucky Ali
 
Developing iOS REST Applications
lmrei
 
jQuery for Sharepoint Dev
Zeddy Iskandar
 
CiklumJavaSat_15112011:Alex Kruk VMForce
Ciklum Ukraine
 
Flask patterns
it-people
 
Introduction to Ember.js and how we used it at FlowPro.io
Paul Knittel
 
Symfony tips and tricks
Javier Eguiluz
 
HTML5: where flash isn't needed anymore
Remy Sharp
 
LvivPy - Flask in details
Max Klymyshyn
 
iPhone Appleless Apps
Remy Sharp
 
Non Conventional Android Programming En
guest9bcef2f
 
Web Crawling with NodeJS
Sylvain Zimmer
 
Caldera Learn - LoopConf WP API + Angular FTW Workshop
CalderaLearn
 
Hey, I just met AngularJS, and this is crazy, so here’s my JavaScript, let’s ...
Alessandro Nadalin
 
Basic Crud In Django
mcantelon
 
Using Task Queues and D3.js to build an analytics product on App Engine
River of Talent
 
Introduction to plugin development
Caldera Labs
 
Python Flask Tutorial For Beginners | Flask Web Development Tutorial | Python...
Edureka!
 

Viewers also liked (20)

PDF
Ruby An Introduction
Shrinivasan T
 
PPTX
Rails 5 All topic Notes
Manish Sakariya
 
PPTX
Moving From API Design to Deployment
LaunchAny
 
PPT
Polubi sebia
imatveeva
 
PDF
Ruby iterators
Tim Cull
 
PDF
Australia zoo
Stian Larsen
 
PPTX
Perempuan berpolitik
Isnainna Erlina
 
PDF
Feed me investors presentation final
Stian Larsen
 
PPTX
Jomyceci
ceciliayjomara
 
PPTX
Oat 276 latest in tech.
scalesdl
 
PDF
International curriculum vitale
Stian Larsen
 
PDF
Final (1)
Stian Larsen
 
PPTX
How to Design and Build a Great Web API
LaunchAny
 
PDF
Debugging Ruby
Aman Gupta
 
ODP
Desarrollo de Aplicaciones con Ruby on Rails y PostgreSQL
José Alfredo Ramírez
 
PPTX
اسم الزمان واسم المكان
Sara Ahmed
 
PPTX
5 Ways to Build Better Web APIs with Ruby and Rails
LaunchAny
 
PDF
Accounting project
Stian Larsen
 
PPT
Dim uin fad
Isnainna Erlina
 
PDF
Forty years with glyphosate
Elias Martinez Gonzales
 
Ruby An Introduction
Shrinivasan T
 
Rails 5 All topic Notes
Manish Sakariya
 
Moving From API Design to Deployment
LaunchAny
 
Polubi sebia
imatveeva
 
Ruby iterators
Tim Cull
 
Australia zoo
Stian Larsen
 
Perempuan berpolitik
Isnainna Erlina
 
Feed me investors presentation final
Stian Larsen
 
Jomyceci
ceciliayjomara
 
Oat 276 latest in tech.
scalesdl
 
International curriculum vitale
Stian Larsen
 
Final (1)
Stian Larsen
 
How to Design and Build a Great Web API
LaunchAny
 
Debugging Ruby
Aman Gupta
 
Desarrollo de Aplicaciones con Ruby on Rails y PostgreSQL
José Alfredo Ramírez
 
اسم الزمان واسم المكان
Sara Ahmed
 
5 Ways to Build Better Web APIs with Ruby and Rails
LaunchAny
 
Accounting project
Stian Larsen
 
Dim uin fad
Isnainna Erlina
 
Forty years with glyphosate
Elias Martinez Gonzales
 
Ad

Similar to Tips and tricks for building api heavy ruby on rails applications (20)

PPTX
Building RESTful APIs w/ Grape
Daniel Doubrovkine
 
PDF
Ruby HTTP clients comparison
Hiroshi Nakamura
 
PDF
Roll Your Own API Management Platform with nginx and Lua
Jon Moore
 
PDF
JSON and the APInauts
Wynn Netherland
 
PDF
Effectively Testing Services on Rails - Railsconf 2014
neal_kemp
 
KEY
I can haz HTTP - Consuming and producing HTTP APIs in the Ruby ecosystem
Sidu Ponnappa
 
PPTX
Demystifying REST
Kirsten Hunter
 
PDF
Building Mobile Friendly APIs in Rails
Jim Jeffers
 
PPTX
The Anatomy of Apps - How iPhone, Android & Facebook Apps Consume APIs
Apigee | Google Cloud
 
PDF
Rack Middleware
LittleBIGRuby
 
KEY
Building Web Service Clients with ActiveModel
pauldix
 
KEY
Building Web Service Clients with ActiveModel
pauldix
 
PDF
Web::Machine - Simpl{e,y} HTTP
Michael Francis
 
PDF
Testing http calls with Webmock and VCR
Kerry Buckley
 
PDF
Building Cloud Castles
Ben Scofield
 
PDF
Finding Restfulness - Madrid.rb April 2014
samlown
 
PDF
Magic of Ruby
Gabriele Lana
 
KEY
SOLID Ruby, SOLID Rails
Jens-Christian Fischer
 
PDF
Using an API
Adam Culp
 
KEY
RESTful Api practices Rails 3
Anton Narusberg
 
Building RESTful APIs w/ Grape
Daniel Doubrovkine
 
Ruby HTTP clients comparison
Hiroshi Nakamura
 
Roll Your Own API Management Platform with nginx and Lua
Jon Moore
 
JSON and the APInauts
Wynn Netherland
 
Effectively Testing Services on Rails - Railsconf 2014
neal_kemp
 
I can haz HTTP - Consuming and producing HTTP APIs in the Ruby ecosystem
Sidu Ponnappa
 
Demystifying REST
Kirsten Hunter
 
Building Mobile Friendly APIs in Rails
Jim Jeffers
 
The Anatomy of Apps - How iPhone, Android & Facebook Apps Consume APIs
Apigee | Google Cloud
 
Rack Middleware
LittleBIGRuby
 
Building Web Service Clients with ActiveModel
pauldix
 
Building Web Service Clients with ActiveModel
pauldix
 
Web::Machine - Simpl{e,y} HTTP
Michael Francis
 
Testing http calls with Webmock and VCR
Kerry Buckley
 
Building Cloud Castles
Ben Scofield
 
Finding Restfulness - Madrid.rb April 2014
samlown
 
Magic of Ruby
Gabriele Lana
 
SOLID Ruby, SOLID Rails
Jens-Christian Fischer
 
Using an API
Adam Culp
 
RESTful Api practices Rails 3
Anton Narusberg
 
Ad

Tips and tricks for building api heavy ruby on rails applications

  • 1. Tips and Tricks for Building API-Heavy Ruby on Rails Applications Tim Cull @trcull [email protected]
  • 20. Authentication Still Hurts image: http://wiki.oauth.net/
  • 21. Do Everything in Background Tasks
  • 22. Y T O H U R O W T I T L L L E D B E
  • 23. Libraries Pull In Problems
  • 24. Now For Some Meat
  • 25. http = Net::HTTP.new "api.cafepress.com", 80 http.set_debug_output STDOUT
  • 26. http = Net::HTTP.new "api.cafepress.com", 80 http.set_debug_output STDOUT <- "POST /oauth/AccessToken HTTP/1.1rnAccept: */*rnUser-Agent: OAuth gem v0.4.7rnContent- Length: 0rnAuthorization: OAuth oauth_body_hash="2jmj7YBwk%3D", oauth_callback="http%3A%2F% 2Flocalhost%3A3000%2Fxero%2Foauth_callback", oauth_consumer_key="20Q0CD6", oauth_nonce=" BawDBGd0kRDdCM", oauth_signature="B7Bd%3D", oauth_signature_method="RSA-SHA1", oauth_timestamp="1358966217", oauth_token="MyString", oauth_version="1.0"rnConnection: closernHost: api-partner.network.xero.comrnrn" -> "HTTP/1.1 401 Unauthorizedrn" -> "Cache-Control: privatern" -> "Content-Type: text/html; charset=utf-8rn" -> "Server: Microsoft-IIS/7.0rn" -> "X-AspNetMvc-Version: 2.0rn" -> "WWW-Authenticate: OAuth Realm="108.254.144.237"rn" -> "X-AspNet-Version: 4.0.30319rn" -> "X-S: api1rn" -> "Content-Length: 121rn" -> "rn" reading 121 bytes... -> "oauth_problem=token_rejected&oauth_problem_advice=Token%20MyString%20does%20not% 20match%20an%20expected%20REQUEST%20token"
  • 27. oauth = OAuth::Consumer.new( key, secret) oauth.http.set_debug_output STDOUT
  • 28. oauth = OAuth::Consumer.new( key, secret) oauth.http.set_debug_output STDOUT oauth = OAuth::Consumer.new( key, secret, { :request_token_url => 'https://api.linkedin. com/uas/oauth/requestToken'}) oauth.http.set_debug_output STDOUT # NOTHING PRINTS!
  • 29. oauth = OAuth::Consumer.new( key, secret, { :request_token_url => 'https://api.linkedin. com/uas/oauth/requestToken'}) oauth.http.set_debug_output STDOUT # NOTHING PRINTS! OAuth::Consumer.rb (151) def request(http_method, path, token = nil, *arguments) if path !~ /^// @http = create_http(path) _uri = URI.parse(path) end ..... end
  • 31. module OAuth class Consumer def create_http_with_featureviz(*args) @http ||= create_http_without_featureviz(*args).tap do |http| begin http.set_debug_output($stdout) unless options[: suppress_debugging] rescue => e puts "error trying to set debugging #{e.message}" end end end alias_method_chain :create_http, :featureviz end end
  • 33. Responsibilities ● Network plumbing ● Translating xml/json/whatever to "smart" hashes * ● Converting Java-like or XML-like field names to Ruby-like field names ● Parsing string dates and integers and turning them into real Ruby data types ● Setting reasonable defaults ● Flattening awkward data structures ● That’s it!
  • 34. 172 Lines of Code https://github.com/trcull/pollen-snippets/blob/master/lib/abstract_api.rb
  • 35. class Instagram::Api def get_photos() resp = get("v1/users/#{@user_id}/media/recent") parse_response_and_stuff resp end def get(url) req = Net::HTTP::Get.new url do_request req end def do_request( req ) net = Net::HTTP.new net.start do |http| http.request req end end end
  • 36. Goal api = InstagramApi.new current_user photos = api.get_photos photos.each do |photo| puts photo.thumbnail_url end
  • 37. def get_photos rv = [] response = get( "v1/users/#{@user_id}/media/recent", {:access_token=>@access_token}) data = JSON.parse(response.body) data['data'].each do |image| photo = ApiResult.new photo[:id] = image['id'] photo[:thumbnail_url] = image['images']['thumbnail']['url'] if image['caption'].present? photo[:caption] = image['caption']['text'] else photo[:caption] = 'Instagram image' end rv << photo end rv end
  • 38. def get(url, params, with_retry=true) real_url = "#{@protocol}://#{@host}/#{url}?" .concat(params.collect{|k,v| "#{k}=#{CGI::escape(v.to_s)}"}.join("&")) request = Net::HTTP::Get.new(real_url) if with_retry response = do_request_with_retry request else response = do_request request end response end
  • 39. Goal api = InstagramApi.new current_user photos = api.get_photos photos.each do |photo| puts photo.thumbnail_url end
  • 40. class ApiResult < Hash def []=(key, value) store(key.to_sym,value) methodify_key key.to_sym end def methodify_key(key) if !self.respond_to? key self.class.send(:define_method, key) do return self[key] end self.class.send(:define_method, "#{key}=") do |val| self[key] = val end end end end See: https://github.com/trcull/pollen-snippets/blob/master/lib/api_result.rb
  • 42. describe Instagram::Api do subject do user = create(:user, :token=>token, :secret=>secret) Instagram::Api.new(user) end describe "making fake HTTP calls" do end describe "in a test bed" do end describe "making real HTTP calls" , :integration => true do end end
  • 43. in lib/tasks/functional.rake RSpec::Core::RakeTask.new("spec:integration") do |t| t.name = "spec:integration" t.pattern = "./spec/**/*_spec.rb" t.rspec_opts = "--tag integration --profile" end in .rspec --tag ~integration --profile to run: "rake spec:integration"
  • 44. describe Instagram::Api do subject do #sometimes have to be gotten manually, unfortunately. user = create(:user, :token=>"stuff", :secret=>"more stuff") Instagram::Api.new(user) end describe "in a test bed" do it "pulls the caption out of photos" do photos = subject.get_photos photos[0].caption.should eq 'My Test Photo' end end end
  • 45. describe Instagram::Api do subject do #sometimes have to be gotten manually, unfortunately. user = create(:user, :token=>"stuff", :secret=>"more stuff") Instagram::Api.new(user) end describe making real HTTP calls" , :integration => true do it "pulls the caption out of photos" do photos = subject.get_photos photos[0].caption.should eq 'My Test Photo' end end end
  • 46. describe Evernote::Api do describe "making real HTTP calls" , :integration => true do subject do req_token = Evernote::Api.oauth_request_token oauth_verifier = Evernote::Api.authorize_as('testacct','testpass', req_token) access_token = Evernote::Api.oauth_access_token(req_token, oauth_verifier) Evernote::Api.new access_token.token, access_token.secret end it "can get note" do note_guid = "4fb9889d-813a-4fa5-b32a-1d3fe5f102b3" note = subject.get_note note_guid note.guid.should == note_guid end end end
  • 47. def authorize_as(user, password, request_token) #pretend like the user logged in get("Login.action") #force a session to start session_id = @cookies['JSESSIONID'].split('=')[1] login_response = post("Login.action;jsessionid=#{session_id}", {:username=>user, :password=>password, :login=>'Sign In', :targetUrl=>CGI.escape("/OAuth.action?oauth_token=#{request_token.token}")}) response = post('OAuth.action', {:authorize=>"Authorize", :oauth_token=>request_token.token}) location = response['location'].scan(/oauth_verifier=(d*w*)/) oauth_verifier = location[0][0] oauth_verifier end
  • 48. describe "making fake HTTP calls" do before do Net::HTTP.stub(:new).and_raise("unexpected network call") end it "pulls the thumbnail out of photos" do response = double("response") data_hash = {"data" => [{ "link"=>"apple.jpg", "id"=>"12", "images"=> { "thumbnail"=>{"url"=>"www12"}, "standard_resolution"=>{"url"=>"www12"} } }]} response.stub(:body).and_return data_hash.to_json subject.should_receive(:do_request).and_return(response) photos = subject.get_photos photos[0].thumbnail_url.should eq 'www12' end end
  • 49. class Instagram::Api def get_photos() resp = get("v1/users/#{@user_id}/media/recent") parse_response_and_stuff resp end def get(url) req = Net::HTTP::Get.new url do_request req end def do_request( req ) net = Net::HTTP.new net.start do |http| http.request req end end end
  • 50. describe "making fake HTTP calls" do before do Net::HTTP.stub(:new).and_raise("unexpected network call") end it "pulls the thumbnail out of photos" do response = double("response") data_hash = {"data" => [{ "link"=>"apple.jpg", "id"=>"12", "images"=> { "thumbnail"=>{"url"=>"www12"}, "standard_resolution"=>{"url"=>"www12"} } }]} response.stub(:body).and_return data_hash.to_json subject.should_receive(:do_request).and_return(response) photos = subject.get_photos photos[0].thumbnail_url.should eq 'www12' end end
  • 51. describe EvernoteController do before do @api = double('fakeapi') Evernote::Api.stub(:new).and_return @api end describe "list_notes" do it "should list notes by title" do a_note = ApiResult.new({:title=>'test title'}) @api.should_receive(:get_notes).and_return [a_note] get "/mynotes" response.body.should match /<td>test title</td>/ end end end
  • 52. OAuth
  • 53. Ask for a Request Token Redirect User to Site (w/ Request Token) User Logs in and Authorizes Site Redirects Back to You W/ OAuth Verifier Trade OAuth Verifier and Request Token for an Access Token Store Access Token (Securely) Make API Calls W/ Access Token
  • 55. Vanilla @consumer = OAuth::Consumer.new("key","secret", :site => "https://agree2") @callback_url = "http://127.0.0.1:3000/oauth/callback" @request_token = @consumer .get_request_token(:oauth_callback => @callback_url) session[:request_token] = @request_token redirect_to @request_token.authorize_url(:oauth_callback => @callback_url) #user is on other site, wait for callback @access_token = session[:request_token] .get_access_token(:oauth_verifier=>params[:verifier]) @photos = @access_token.get('/photos.xml') Source: https://github.com/oauth/oauth-ruby
  • 56. Instagram #redirect user first, no request token necessary @redirect_uri = CGI.escape('http:///myapp.com/oauth/callback') redirect_to "https://api.instagram.com/oauth/authorize ?client_id=xyz123 &redirect_uri=#{@redirect_uri} &response_type=code &scope=comments+basic" #wait for callback response = post( "oauth/access_token", {:client_id=>@client_id, :client_secret=>@client_secret, :redirect_uri=>@redirect_uri, :grant_type=>'authorization_code', :code=>params[:code]}) json = JSON.parse response.body access_token = json['access_token'] instagram_name = json['user']['username'] instagram_id = json['user']['id']
  • 57. Freshbooks require 'oauth' require 'oauth/signature/plaintext' oauth = OAuth::Consumer.new( key, secret, { :scheme=> :query_string, :signature_method=>"PLAINTEXT", :oauth_callback=>callback, :authorize_path => "/oauth/oauth_authorize.php", :access_token_path=>"/oauth/oauth_access.php", :request_token_path=>"/oauth/oauth_request.php", :site=>"http://#{usersitename}.freshbooks.com"})
  • 58. Xero @consumer = OAuth::Consumer.new(@oauth_key, @oauth_secret,{ :site => "https://api-partner.network.xero.com:443", :signature_method => 'RSA-SHA1', :private_key_str => ENV['XERO_CLIENT_PEM'] , :ssl_client_cert=>ENV['XERO_ENTRUST_SSL_CERT'], :ssl_client_key=>ENV['XERO_ENTRUST_PRIVATE_PEM']}) module OAuth class Consumer def create_http_with_featureviz(*args) @http ||= create_http_without_featureviz(*args).tap do |http| http.cert = OpenSSL::X509::Certificate.new(options[:ssl_client_cert]) if options[:ssl_client_cert] http.key = OpenSSL::PKey::RSA.new( options[:ssl_client_key]) if options[: ssl_client_key] end end alias_method_chain :create_http, :featureviz end end Thank you @tlconnor
  • 59. @consumer = OAuth.new(@oauth_key, @oauth_secret,{ :site => "https://api-partner.network.xero.com:443", :signature_method => 'RSA-SHA1', :private_key_str => ENV['XERO_CLIENT_PEM'] , :ssl_client_cert=>ENV['XERO_ENTRUST_SSL_CERT'], :ssl_client_key=>ENV['XERO_ENTRUST_PRIVATE_PEM']}) module OAuth::Signature::RSA class SHA1 < OAuth::Signature::Base def digest private_key = OpenSSL::PKey::RSA.new( if options[:private_key_str] options[:private_key_str] elsif options[:private_key_file] IO.read(options[:private_key_file]) else consumer_secret end ) private_key.sign(OpenSSL::Digest::SHA1.new, signature_base_string) end end end
  • 60. Evernote consumer = OAuth::Consumer.new(key, secret, { :site => "https://www.evernote.com", :authorize_path => "/OAuth.action"}) # redirect, yadda, yadda wait for callback access_token = request_token.get_access_token(:oauth_verifier => oauth_verifier) note_store_url = access_token.params['edam_noteStoreUrl'] #This is Thrift transport = Thrift::HTTPClientTransport.new note_store_url protocol = Thrift::BinaryProtocol.new transport store = Evernote::EDAM::NoteStore::NoteStore::Client.new protocol #Yay! Finally Notebooks! notebooks = store.listNotebooks access_token.token
  • 61. Background Processing i.e. Do It
  • 62. class SlurpImagesJob def self.enqueue(user_id) SlurpImagesJob.new.delay.perform(user_id) end #this makes unit testing with simulated errors easier def perform(user_id) begin do_perform user_id rescue => e Alerts.log_error "We encountered an error slurping your Instagram images please try again", e end end def do_perform(user_id) user = User.find user_id cafepress = Cafepress::Api.new user instagram = Instagram::Api.new user photos = instagram.get_photos photos.each do |photo| cafepress.upload_design photo.caption, photo.standard_resolution_url end end end
  • 63. class InstagramController < ApplicationController def oauth_callback request_access_token params SlurpImagesJob.enqueue current_user.id end end describe InstagramController do it "enqueues a job to slurp images" do SlurpImagesJob.should_receive :enqueue post '/oauth_callback' end end
  • 65. Register a Callback URL for a User Site Verifies URL actually works (typically) User Does Something Site Calls Back URL With an ID (typically) Application Polls Site for More Details Application Does Whatever
  • 66. class FreshbooksApi < AbstractApi def register_callback(user_id) xml = "<request method="callback.create"> <callback> <event>invoice.create</event> <uri>http://app.featureviz.com/webhook/freshbooks/#{user_id}</uri> </callback> </request>" post_with_body("api/2.1/xml-in", xml) end def verify_callback(our_user_id, verifier, callback_id) xml = "<request method="callback.verify"> <callback> <callback_id>#{callback_id}</callback_id> <verifier>#{verifier}</verifier> </callback> </request>" post_with_body("api/2.1/xml-in", xml) end end
  • 67. class WebhooksController < ApplicationController # URL would be /webhook/freshbooks/:our_user_id def freshbooks_callback our_user_id = params[:our_user_id] event_name = params[:name] object_id = params[:object_id] api = Freshbooks::API.new User.find(our_user_id) if event_name == "callback.verify" verifier = params[:verifier] api.verify_callback our_user_id, verifier, object_id elsif event_name == "invoice.create" freshbooks_user_id = params[:user_id] InvoiceUpdatedJob.new.delay.perform our_user_id, object_id, freshbooks_user_id end respond_to do |format| format.html { render :nothing => true} format.json { head :no_content} end end
  • 68. class FreshbooksApi < AbstractApi def register_callback(user_id) xml = "<request method="callback.create"> <callback> <event>invoice.create</event> <uri>http://app.featureviz.com/webhook/freshbooks/#{user_id}</uri> </callback> </request>" post_with_body("api/2.1/xml-in", xml) end def verify_callback(our_user_id, verifier, callback_id) xml = "<request method="callback.verify"> <callback> <callback_id>#{callback_id}</callback_id> <verifier>#{verifier}</verifier> </callback> </request>" post_with_body("api/2.1/xml-in", xml) end end
  • 70. class WebhooksController < ApplicationController # URL would be /webhook/freshbooks/:our_user_id def freshbooks_callback our_user_id = params[:our_user_id] event_name = params[:name] object_id = params[:object_id] api = Freshbooks::API.new User.find(our_user_id) if event_name == "callback.verify" verifier = params[:verifier] api.verify_callback our_user_id, verifier, object_id elsif event_name == "invoice.create" freshbooks_user_id = params[:user_id] InvoiceUpdatedJob.new.delay.perform our_user_id, object_id, freshbooks_user_id end respond_to do |format| format.html { render :nothing => true} format.json { head :no_content} end end
  • 72. Tips and Tricks for Building API-Heavy Ruby on Rails Applications Tim Cull @trcull [email protected]
  • 74. net/http rfuzz httparty open-uri mechanize excon simplehttp em-http-request rest-client activeresource right_http_connection rufus-verbs wrest faraday curb typhoeus patron httpclient eventmachine thanks @nahi
  • 79. <?xml version="1.0" encoding="utf-8"?> <response xmlns="http://www.freshbooks.com/api/" status="ok"> <invoices page="1" per_page="10" pages="4" total="47"> <invoice> <invoice_id>344</invoice_id> ....... </invoice> </invoices> </response> def fb_invoices fb_collect('invoices','invoice'){|conn,page|conn.project.list(: page=>page)} end
  • 80. def fb_collect(root_element_name, result_element_name, &block) rv = [] conn = fb_connection() page = 1 pages = 1 while page <= pages temp = yield conn, page page += 1 if !temp[root_element_name].nil? && !temp[root_element_name] [result_element_name].nil? if temp[root_element_name][result_element_name].kind_of?(Array) temp[root_element_name][result_element_name].each do |elem| rv.push(elem) end else #if there's only one result, the freshbooks api returns a bare hash instead of a hash inside an array elem = temp[root_element_name][result_element_name] rv.push(elem) end end end return rv end