songkick-transport 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -10,7 +10,8 @@ and serialization details. It transparently deals with parameter serialization,
10
10
  including the following:
11
11
 
12
12
  * Correctly CGI-escaping any data you pass in
13
- * Nested parameters, e.g. <tt>'foo' => {'bar' => 'qux'}</tt>
13
+ * Nested parameters, e.g. <tt>'foo' => {'a' => 'b', 'c' => 'd'}</tt> becomes
14
+ <tt>foo[a]=b&foo[c]=d</tt>
14
15
  * File uploads and multipart requests
15
16
  * Entity body for POST/PUT, query string for everything else
16
17
 
@@ -20,7 +21,9 @@ We currently support three backends:
20
21
  * Talking HTTP with {HTTParty}[http://httparty.rubyforge.org/]
21
22
  * Talking directly to a {Rack}[http://rack.rubyforge.org/] app with Rack::Test
22
23
 
23
- It is assumed all service applications speak JSON as their serialization format.
24
+ If the service you're talking to returns <tt>Content-Type: application/json</tt>,
25
+ we automatically parse the response for you. You can register parsers for other
26
+ content types as described below.
24
27
 
25
28
 
26
29
  == Using the transports
@@ -31,6 +34,7 @@ exposes some JSON:
31
34
  require 'sinatra'
32
35
 
33
36
  get '/ohai' do
37
+ headers 'Content-Type' => 'application/json'
34
38
  '{"hello":"world"}'
35
39
  end
36
40
 
@@ -63,8 +67,9 @@ takes a reference to a Rack application, for example:
63
67
 
64
68
  All transports expose exactly the same instance methods.
65
69
 
66
- The client supports the +get+, +post+, +put+, +delete+ and +head+ methods,
67
- which all take a path and an optional +Hash+ of parameters, for example:
70
+ The client supports the +delete+, +get+, +head+, +patch+, +post+, +put+ and
71
+ +options+ methods, which all take a path and an optional +Hash+ of parameters,
72
+ for example:
68
73
 
69
74
  client.post('/users', :username => 'bob', :password => 'foo')
70
75
 
@@ -94,10 +99,10 @@ This library was primarily developed to talk to Songkick's backend services, and
94
99
  as such adopts some conventions that put it at a higher level of abstraction
95
100
  than a vanilla HTTP client.
96
101
 
97
- It assumes successful responses will all contain JSON data. A response object
98
- has the following properties:
102
+ A response object has the following properties:
99
103
 
100
- * +data+ -- the result of parsing the body as JSON
104
+ * +body+ -- the raw response body
105
+ * +data+ -- the result of parsing the body according to its content-type
101
106
  * +headers+ -- a read-only hash-like object containing response headers
102
107
  * +status+ -- the response's status code
103
108
 
@@ -127,6 +132,17 @@ If the request raises an exception, it will be of one of the following types:
127
132
  non-successful status code, e.g. 404 or 500
128
133
 
129
134
 
135
+ === Registering response parsers
136
+
137
+ <tt>Songkick::Transport</tt> automatically sets <tt>response.data</tt> if the
138
+ content-type of the response is <tt>application/json</tt>. You can register
139
+ parsers for other content-types like so:
140
+
141
+ Songkick::Transport.register_parser('application/yaml', YAML)
142
+
143
+ The parser object you register must respond to <tt>parse(string)</tt>.
144
+
145
+
130
146
  === Nested parameters
131
147
 
132
148
  All transports support serialization of nested parameters, for example you can
@@ -297,7 +313,7 @@ the total time spent calling backend services during the block.
297
313
 
298
314
  The MIT License
299
315
 
300
- Copyright (c) 2012 Songkick
316
+ Copyright (c) 2012-2013 Songkick
301
317
 
302
318
  Permission is hereby granted, free of charge, to any person obtaining a copy of
303
319
  this software and associated documentation files (the "Software"), to deal in
@@ -9,7 +9,7 @@ module Songkick
9
9
  DEFAULT_TIMEOUT = 5
10
10
  DEFAULT_FORMAT = :json
11
11
 
12
- HTTP_VERBS = %w[get post put delete head]
12
+ HTTP_VERBS = %w[options head get patch post put delete]
13
13
  USE_BODY = %w[post put]
14
14
  FORM_ENCODING = 'application/x-www-form-urlencoded'
15
15
 
@@ -33,6 +33,21 @@ module Songkick
33
33
  autoload :ConnectionFailedError,ROOT + '/transport/upstream_error'
34
34
  autoload :InvalidJSONError, ROOT + '/transport/upstream_error'
35
35
  autoload :HttpError, ROOT + '/transport/http_error'
36
+
37
+ def self.regsiter_parser(content_type, parser)
38
+ @parsers ||= {}
39
+ @parsers[content_type] = parser
40
+ end
41
+
42
+ def self.parser_for(content_type)
43
+ parser = @parsers && @parsers[content_type]
44
+ unless parser
45
+ raise TypeError, "Could not find a parser for content-type: #{content_type}"
46
+ end
47
+ parser
48
+ end
49
+
50
+ regsiter_parser 'application/json', Yajl::Parser
36
51
 
37
52
  IO = UploadIO
38
53
 
@@ -41,7 +41,7 @@ module Songkick
41
41
  def headers
42
42
  Headers.new(
43
43
  'Connection' => 'close',
44
- 'User-Agent' => user_agent || ''
44
+ 'User-Agent' => user_agent
45
45
  )
46
46
  end
47
47
 
@@ -53,7 +53,7 @@ module Songkick
53
53
  if req.use_body?
54
54
  connection.__send__("http_#{req.verb}", req.body)
55
55
  else
56
- connection.__send__("http_#{req.verb}")
56
+ connection.http(req.verb.upcase)
57
57
  end
58
58
 
59
59
  process(req, connection.response_code, response_headers, connection.body_str)
@@ -19,6 +19,7 @@ module Songkick
19
19
  def initialize(hash = {})
20
20
  @hash = {}
21
21
  hash.each do |key, value|
22
+ next if value.nil?
22
23
  @hash[self.class.normalize(key)] = value
23
24
  end
24
25
  end
@@ -16,16 +16,20 @@ module Songkick
16
16
  Transport.logger.warn "Request returned invalid JSON: #{request}"
17
17
  raise Transport::InvalidJSONError, request
18
18
  end
19
+
20
+ def self.parse(body, content_type)
21
+ return body unless body.is_a?(String)
22
+ return nil if body.strip == ''
23
+
24
+ content_type = (content_type || '').split(/\s*;\s*/).first
25
+ Transport.parser_for(content_type).parse(body)
26
+ end
19
27
 
20
- attr_reader :data, :headers, :status
28
+ attr_reader :body, :data, :headers, :status
21
29
 
22
30
  def initialize(status, headers, body)
23
- @data = if body.is_a?(String)
24
- body.strip == '' ? nil : Yajl::Parser.parse(body)
25
- else
26
- body
27
- end
28
-
31
+ @body = body
32
+ @data = Response.parse(body, headers['Content-Type'])
29
33
  @headers = Headers.new(headers)
30
34
  @status = status.to_i
31
35
  end
@@ -17,7 +17,7 @@ module Songkick
17
17
  @headers = {}
18
18
  end
19
19
 
20
- def http_get
20
+ def http(verb)
21
21
  raise(@error, "bang") if @error
22
22
  end
23
23
 
@@ -57,4 +57,4 @@ module Songkick
57
57
  end
58
58
 
59
59
  end
60
- end
60
+ end
@@ -6,7 +6,7 @@ describe Songkick::Transport::Response do
6
6
  end
7
7
 
8
8
  describe "200 with a body" do
9
- let(:response) { process("", 200, {}, '{"hello":"world"}') }
9
+ let(:response) { process("", 200, {"Content-Type" => "application/json; charset=utf-8"}, '{"hello":"world"}') }
10
10
 
11
11
  it "is an OK" do
12
12
  response.should be_a(Songkick::Transport::Response::OK)
@@ -18,7 +18,7 @@ describe Songkick::Transport::Response do
18
18
  end
19
19
 
20
20
  describe "200 with a body with an empty line in it" do
21
- let(:response) { process("", 200, {}, "{\"hello\":\"world\"\n\n}") }
21
+ let(:response) { process("", 200, {"Content-Type" => "application/json"}, "{\"hello\":\"world\"\n\n}") }
22
22
 
23
23
  it "is an OK" do
24
24
  response.should be_a(Songkick::Transport::Response::OK)
@@ -30,7 +30,7 @@ describe Songkick::Transport::Response do
30
30
  end
31
31
 
32
32
  describe "200 with a parsed body" do
33
- let(:response) { process("", 200, {}, {"hello" => "world"}) }
33
+ let(:response) { process("", 200, {"Content-Type" => "application/json"}, {"hello" => "world"}) }
34
34
 
35
35
  it "exposes its data" do
36
36
  response.data.should == {"hello" => "world"}
@@ -62,7 +62,7 @@ describe Songkick::Transport::Response do
62
62
  end
63
63
 
64
64
  describe "409 with a body" do
65
- let(:response) { process("", 409, {}, '{"errors":[]}') }
65
+ let(:response) { process("", 409, {"Content-Type" => "application/json"}, '{"errors":[]}') }
66
66
 
67
67
  it "is a UserError" do
68
68
  response.should be_a(Songkick::Transport::Response::UserError)
@@ -66,6 +66,13 @@ describe Songkick::Transport do
66
66
  end
67
67
  end
68
68
 
69
+ describe :options do
70
+ it "sends an OPTIONS request" do
71
+ response = transport.options("/.well-known/host-meta")
72
+ response.headers["Access-Control-Allow-Methods"].should == "GET, PUT, DELETE"
73
+ end
74
+ end
75
+
69
76
  describe :post do
70
77
  it "sends data using POST" do
71
78
  data = transport.post("/artists", :name => "Amon Tobin").data
data/spec/spec_helper.rb CHANGED
@@ -29,6 +29,11 @@ class TestApp < Sinatra::Base
29
29
  get '/artists/:id' do
30
30
  Yajl::Encoder.encode('id' => params[:id].to_i)
31
31
  end
32
+
33
+ options '/.well-known/host-meta' do
34
+ headers 'Access-Control-Allow-Methods' => 'GET, PUT, DELETE'
35
+ ''
36
+ end
32
37
 
33
38
  post '/artists' do
34
39
  Yajl::Encoder.encode('id' => 'new', 'name' => params[:name].upcase)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: songkick-transport
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-03 00:00:00.000000000 Z
12
+ date: 2013-03-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: multipart-post
@@ -91,22 +91,6 @@ dependencies:
91
91
  - - ! '>='
92
92
  - !ruby/object:Gem::Version
93
93
  version: 0.4.0
94
- - !ruby/object:Gem::Dependency
95
- name: guard-rspec
96
- requirement: !ruby/object:Gem::Requirement
97
- none: false
98
- requirements:
99
- - - ! '>='
100
- - !ruby/object:Gem::Version
101
- version: '0'
102
- type: :development
103
- prerelease: false
104
- version_requirements: !ruby/object:Gem::Requirement
105
- none: false
106
- requirements:
107
- - - ! '>='
108
- - !ruby/object:Gem::Version
109
- version: '0'
110
94
  - !ruby/object:Gem::Dependency
111
95
  name: rspec
112
96
  requirement: !ruby/object:Gem::Requirement