JSON and the APInauts

Preview:

DESCRIPTION

Guide to writing great API wrappers in Ruby.

Citation preview

JSON AND THE ARGONAUTSAPI

WYNNNETHERLAND

whoami

I write API wrappersA lot of API wrappers!

& more!

Why create API wrappers?

After all, we have

curl

curl http://api.twitter.com/1/users/show.json?screen_name=pengwynn

Net::HTTP

url = "http://api.twitter.com/1/users/show.json?screen_name=pengwynn"Net::HTTP.get(URI.parse(url))

Because we're Rubyists, and we want

Idiomatic access

{"chart":{ "issueDate":2006-03-04, "description":"Chart", "chartItems":{ "firstPosition":1, "totalReturned":15, "totalRecords":25663, "chartItem":[{ "songName":"Lonely Runs Both Ways", "artistName":"Alison Krauss + Union Station", "peek":1, "catalogNo":"610525", "rank":1, "exrank":1, "weeksOn":65, "albumId":655684, ... }}

{"chart":{ "issueDate":2006-03-04, "description":"Chart", "chartItems":{ "firstPosition":1, "totalReturned":15, "totalRecords":25663, "chartItem":[{ "songName":"Lonely Runs Both Ways", "artistName":"Alison Krauss + Union Station", "peek":1, "catalogNo":"610525", "rank":1, "exrank":1, "weeksOn":65, "albumId":655684, ... }}

Rubyified keys

{"chart":{ "issueDate":2006-03-04, "description":"Chart", "chartItems":{ "firstPosition":1, "totalReturned":15, "totalRecords":25663, "chartItem":[{ "songName":"Lonely Runs Both Ways", "artistName":"Alison Krauss + Union Station", "peek":1, "catalogNo":"610525", "rank":1, "exrank":1, "weeksOn":65, "albumId":655684, ... }}

{"chart":{ "issue_date":2006-03-04, "description":"Chart", "chart_items":{ "first_position":1, "total_returned":15, "total_records":25663, "chart_item":[{ "song_name":"Lonely Runs Both Ways", "artist_name":"Alison Krauss + Union Station", "peek":1, "catalog_no":"610525", "rank":1, "exrank":1, "weeks_on":65, "album_id":655684, ... }}

... and method names

# Retrieve the details about a user by email# # +email+ (Required)# The email of the user to look within. To run getInfoByEmail on multiple addresses, simply pass a comma-separated list of valid email addresses.# def self.info_by_email(email) email = email.join(',') if email.is_a?(Array) Mash.new(self.get('/', ! ! :query => {! ! ! :method => 'user.getInfoByEmail', ! ! ! :email => email }.merge(Upcoming.default_options))).rsp.userend

# Retrieve the details about a user by email# # +email+ (Required)# The email of the user to look within. To run getInfoByEmail on multiple addresses, simply pass a comma-separated list of valid email addresses.# def self.info_by_email(email) email = email.join(',') if email.is_a?(Array) Mash.new(self.get('/', ! ! :query => {! ! ! :method => 'user.getInfoByEmail', ! ! ! :email => email }.merge(Upcoming.default_options))).rsp.userend

More Ruby like than

SYNTACTIC SUGAR

Twitter::Search.new.from('jnunemaker').to('pengwynn').hashed('ruby').fetch()

Twitter::Search.new.from('jnunemaker').to('pengwynn').hashed('ruby').fetch()

Method chaining

stores = client.stores({:area => ['76227', 50]}).products({:salePrice => {'$gt' => 3000}}).fetch.stores

stores = client.stores({:area => ['76227', 50]}).products({:salePrice => {'$gt' => 3000}}).fetch.stores

Method chaining

client.statuses.update.json! :status => 'this status is from grackle'

client.statuses.update.json! :status => 'this status is from grackle'

Method chaining Bang for POST

APPROACHES

Simple Wrapping

SOME TWITTER EXAMPLESTwitter Auth from @mbleighuser.twitter.post('/statuses/update.json', 'status' => 'Tweet, tweet!'

)

Grackle from @hayesdavisclient.statuses.update.json! :status => 'Tweet, tweet!'

Twitter from @jnunemaker client.update('Tweet, tweet!')

Wrapping

Wrapping... with style

Abstraction

Why simply wrap?

Insulate against change

Leverage API documentation

Why abstract?

Simplify a complex API

Provide a business domain

TOOLS

Transports

Net::HTTP

Parsers

JSON

multi_jsonhttp://github.com/intridea/multi_json

Higher-level libraries

HTTParty- Ruby module - GET, POST, PUT, DELETE - basic_auth, base_uri, default_params, etc.

- Net::HTTP for transport- Crack parses JSON and XML

HTTPartyclass Delicious  include HTTParty  base_uri 'https://api.del.icio.us/v1'    def initialize(u, p)    @auth = {:username => u, :password => p}  end

...

def recent(options={}) options.merge!({:basic_auth => @auth})    self.class.get('/posts/recent', options) end

...

RestClient- Simple DSL- ActiveResource support- Built-in shell

RestClient.post(! 'http://example.com/resource', :param1 => 'one', :nested => { :param2 => 'two' })

Weary- Simple DSL- Specify required, optional params- Async support

Wearydeclare "foo" do |r| r.url = "path/to/foo" r.via = :post r.requires = [:id, :bar] r.with = [:blah] r.authenticates = false r.follows = false r.headers = {'Accept' => 'text/html'}end

client.foo :id => 123, :bar => 'baz'

becomes

RackClient- Rack API- Middleware!

client = Rack::Client.new('http://whoismyrepresentative.com')

Faraday- Rack-like- Middleware!- Adapters

Faradayurl = 'http://api.twitter.com/1'conn = Faraday::Connection.new(:url => url ) do |builder| builder.adapter Faraday.default_adapter builder.use Faraday::Response::MultiJson builder.use Faraday::Response::Mashifyend

resp = conn.get do |req| req.url '/users/show.json', :screen_name => 'pengwynn'end

u = resp.bodyu.name# => "Wynn Netherland"

Faraday Middleware- Hashie- Multi JSON- OAuth, OAuth2 as needed

My current stack- Faraday- Faraday Middleware - Hashie - Multi JSON- OAuth, OAuth2 as needed

Hashie- Mash- Dash- Trash- Clash

HTTPScoop

Charles Proxy

If you have an iOS app, you have an API ;-)

Testing

VCRhttp://github.com/myronmarston/vcr

Artifice

Artifice.activate_with(rack_endpoint) do # make some requests using Net::HTTPend

a @wycats joint

http://github.com/wycats/artifice

ShamRackShamRack.at("sinatra.xyz").sinatra do get "/hello/:subject" do "Hello, #{params[:subject]}" endend

open("http://sinatra.xyz/hello/stranger").read

#=> "Hello, stranger"

ShamRackShamRack.at("rackup.xyz").rackup do use Some::Middleware use Some::Other::Middleware run MyApp.newend

Rack 'em up!

Authentication

Basic

OAuthhttp://oauth.rubyforge.org/

Recommended