Skip to content

Commit

Permalink
Merge pull request #93 from tarcieri/feature/mime-type-parse
Browse files Browse the repository at this point in the history
Brings back MIME-type parser/emiter
  • Loading branch information
sferik committed Mar 13, 2014
2 parents 0785bcb + 36407a8 commit 834217f
Show file tree
Hide file tree
Showing 13 changed files with 217 additions and 6 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ HTTP.get "http://example.com/resource", :params => {:foo => "bar"}
Want to POST with a specific body, JSON for instance?

```ruby
HTTP.post "http://example.com/resource", :body => JSON.dump(:foo => '42')
HTTP.post "http://example.com/resource", :json => { :foo => '42' })
```

It's easy!
Expand Down Expand Up @@ -183,8 +183,7 @@ right? But usually it's not, and so we end up adding ".json" onto the ends of
our URLs because the existing mechanisms make it too hard. It should be easy:

```ruby
HTTP.accept('application/json').
get("https://github.com/tarcieri/http/commit/HEAD")
HTTP.accept(:json).get("https://github.com/tarcieri/http/commit/HEAD")
```

This adds the appropriate Accept header for retrieving a JSON response for the
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ end

require 'yardstick/rake/verify'
Yardstick::Rake::Verify.new do |verify|
verify.threshold = 56.8
verify.threshold = 57.1
end

task :default => [:spec, :rubocop, :verify_measurements]
2 changes: 1 addition & 1 deletion lib/http/chainable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def with_headers(headers)

# Accept the given MIME type(s)
def accept(type)
with :accept => type
with :accept => MimeType.normalize(type)
end

# Make a request with the given Authorization header
Expand Down
3 changes: 3 additions & 0 deletions lib/http/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ def make_request_body(opts, headers)
elsif opts.form
headers['Content-Type'] ||= 'application/x-www-form-urlencoded'
URI.encode_www_form(opts.form)
elsif opts.json
headers['Content-Type'] ||= 'application/json'
MimeType[:json].encode opts.json
end
end

Expand Down
76 changes: 76 additions & 0 deletions lib/http/mime_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
module HTTP
# MIME type encode/decode adapters
module MimeType
class << self
# Associate MIME type with adapter
#
# @example
#
# module JsonAdapter
# class << self
# def encode(obj)
# # encode logic here
# end
#
# def decode(str)
# # decode logic here
# end
# end
# end
#
# HTTP::MimeType.register_adapter 'application/json', MyJsonAdapter
#
# @param [#to_s] type
# @param [#encode, #decode] adapter
# @return [void]
def register_adapter(type, adapter)
adapters[type.to_s] = adapter
end

# Returns adapter associated with MIME type
#
# @param [#to_s] type
# @raise [Error] if no adapter found
# @return [void]
def [](type)
adapters[normalize type] || fail(Error, "Unknown MIME type: #{type}")
end

# Register a shortcut for MIME type
#
# @example
#
# HTTP::MimeType.register_alias 'application/json', :json
#
# @param [#to_s] type
# @param [#to_sym] shortcut
# @return [void]
def register_alias(type, shortcut)
aliases[shortcut.to_sym] = type.to_s
end

# Resolves type by shortcut if possible
#
# @param [#to_s] type
# @return [String]
def normalize(type)
aliases.fetch type, type.to_s
end

private

# :nodoc:
def adapters
@adapters ||= {}
end

# :nodoc:
def aliases
@aliases ||= {}
end
end
end
end

# built-in mime types
require 'http/mime_type/json'
24 changes: 24 additions & 0 deletions lib/http/mime_type/adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require 'forwardable'
require 'singleton'

module HTTP
module MimeType
# Base encode/decode MIME type adapter
class Adapter
include Singleton

class << self
extend Forwardable
def_delegators :instance, :encode, :decode
end

%w{ encode decode }.each do |operation|
class_eval <<-RUBY, __FILE__, __LINE__
def #{operation}(*)
fail Error, "\#{self.class} does not supports ##{operation}"
end
RUBY
end
end
end
end
23 changes: 23 additions & 0 deletions lib/http/mime_type/json.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require 'json'
require 'http/mime_type/adapter'

module HTTP
module MimeType
# JSON encode/decode MIME type adapter
class JSON < Adapter
# Encodes object to JSON
def encode(obj)
return obj.to_json if obj.respond_to?(:to_json)
::JSON.dump obj
end

# Decodes JSON
def decode(str)
::JSON.load str
end
end

register_adapter 'application/json', JSON
register_alias 'application/json', :json
end
end
13 changes: 12 additions & 1 deletion lib/http/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class Options
# Form data to embed in the request
attr_accessor :form

# JSON data to embed in the request
attr_accessor :json

# Explicit request body of the request
attr_accessor :body

Expand All @@ -31,7 +34,7 @@ class Options
# Follow redirects
attr_accessor :follow

protected :response=, :headers=, :proxy=, :params=, :form=, :follow=
protected :response=, :headers=, :proxy=, :params=, :form=, :json=, :follow=

@default_socket_class = TCPSocket
@default_ssl_socket_class = OpenSSL::SSL::SSLSocket
Expand All @@ -52,6 +55,7 @@ def initialize(options = {})
@body = options[:body]
@params = options[:params]
@form = options[:form]
@json = options[:json]
@follow = options[:follow]

@socket_class = options[:socket_class] || self.class.default_socket_class
Expand Down Expand Up @@ -88,6 +92,12 @@ def with_form(form)
end
end

def with_json(data)
dup do |opts|
opts.json = data
end
end

def with_body(body)
dup do |opts|
opts.body = body
Expand Down Expand Up @@ -128,6 +138,7 @@ def to_hash
:proxy => proxy,
:params => params,
:form => form,
:json => json,
:body => body,
:follow => follow,
:socket_class => socket_class,
Expand Down
11 changes: 11 additions & 0 deletions lib/http/response.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'delegate'
require 'http/headers'
require 'http/content_type'
require 'http/mime_type'

module HTTP
class Response
Expand Down Expand Up @@ -112,6 +113,16 @@ def charset
@charset ||= content_type.charset
end

# Parse response body with corresponding MIME type adapter.
#
# @param [#to_s] as Parse as given MIME type
# instead of the one determined from headers
# @raise [Error] if adapter not found
# @return [Object]
def parse(as = nil)
MimeType[as || mime_type].decode to_s
end

# Inspect a response
def inspect
"#<#{self.class}/#{@version} #{status} #{reason} headers=#{headers.inspect}>"
Expand Down
13 changes: 13 additions & 0 deletions spec/http/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,17 @@ def simple_response(body, status = 200)
client.get('http://example.com/?foo=bar', :params => {:baz => 'quux'})
end
end

describe 'passing json' do
it 'encodes given object' do
client = HTTP::Client.new
allow(client).to receive(:perform)

expect(HTTP::Request).to receive(:new) do |*args|
expect(args.last).to eq('{"foo":"bar"}')
end

client.get('http://example.com/', :json => {:foo => :bar})
end
end
end
17 changes: 17 additions & 0 deletions spec/http/options/json_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require 'spec_helper'

describe HTTP::Options, 'json' do

let(:opts) { HTTP::Options.new }

it 'defaults to nil' do
expect(opts.json).to be nil
end

it 'may be specified with with_json data' do
opts2 = opts.with_json(:foo => 42)
expect(opts.json).to be nil
expect(opts2.json).to eq(:foo => 42)
end

end
3 changes: 3 additions & 0 deletions spec/http/options/merge_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
:params => {:baz => 'bar'},
:form => {:foo => 'foo'},
:body => 'body-foo',
:json => {:foo => 'foo'},
:headers => {:accept => 'json', :foo => 'foo'},
:proxy => {})

Expand All @@ -32,6 +33,7 @@
:params => {:plop => 'plip'},
:form => {:bar => 'bar'},
:body => 'body-bar',
:json => {:bar => 'bar'},
:headers => {:accept => 'xml', :bar => 'bar'},
:proxy => {:proxy_address => '127.0.0.1', :proxy_port => 8080})

Expand All @@ -40,6 +42,7 @@
:params => {:plop => 'plip'},
:form => {:bar => 'bar'},
:body => 'body-bar',
:json => {:bar => 'bar'},
:headers => {:accept => 'xml', :foo => 'foo', :bar => 'bar', 'User-Agent' => user_agent},
:proxy => {:proxy_address => '127.0.0.1', :proxy_port => 8080},
:follow => nil,
Expand Down
31 changes: 31 additions & 0 deletions spec/http/response_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,35 @@
it { should eq 'utf-8' }
end
end

describe '#parse' do
let(:headers) { {'Content-Type' => content_type} }
let(:body) { '{"foo":"bar"}' }
let(:response) { HTTP::Response.new 200, '1.1', headers, body }

context 'with known content type' do
let(:content_type) { 'application/json' }
it 'returns parsed body' do
expect(response.parse).to eq 'foo' => 'bar'
end
end

context 'with unknown content type' do
let(:content_type) { 'application/deadbeef' }
it 'raises HTTP::Error' do
expect { response.parse }.to raise_error HTTP::Error
end
end

context 'with explicitly given mime type' do
let(:content_type) { 'application/deadbeef' }
it 'ignores mime_type of response' do
expect(response.parse 'application/json').to eq 'foo' => 'bar'
end

it 'supports MIME type aliases' do
expect(response.parse :json).to eq 'foo' => 'bar'
end
end
end
end

0 comments on commit 834217f

Please sign in to comment.