the86-client 0.0.8 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -15,3 +15,6 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+
19
+ # Exuberant ctags
20
+ /tags
@@ -3,6 +3,7 @@
3
3
  connection
4
4
  errors
5
5
  oauth_bearer_authorization
6
+ response
6
7
 
7
8
  resource
8
9
  resource_collection
@@ -24,6 +24,7 @@ module The86
24
24
  end
25
25
  end
26
26
 
27
+ # Insert a Faraday middleware at the top of the chain.
27
28
  def prepend(*parameters)
28
29
  @faraday.builder.insert(0, *parameters)
29
30
  end
@@ -43,6 +44,11 @@ module The86
43
44
  dispatch(:post, options)
44
45
  end
45
46
 
47
+ private
48
+
49
+ # Dispatch the HTTP request.
50
+ # Returns the The86::Client::Response which contains the
51
+ # HTTP status code, headers and decoded response body.
46
52
  def dispatch(method, options)
47
53
  path = options.fetch(:path)
48
54
  parameters = options[:parameters]
@@ -50,17 +56,21 @@ module The86
50
56
 
51
57
  if parameters
52
58
  path = Addressable::URI.parse(path).tap do |uri|
53
- uri.query_values = parameters
59
+ uri.query_values = (uri.query_values || {}).merge(parameters)
54
60
  end.to_s
55
61
  end
56
62
 
57
63
  headers = @faraday.headers.merge(options[:headers] || {})
58
64
  response = @faraday.run_request(method, path, data, headers)
65
+
59
66
  assert_http_status(response, options[:status])
60
- response.body
61
- end
62
67
 
63
- private
68
+ ::The86::Client::Response.new(
69
+ response.status,
70
+ response.headers,
71
+ response.body
72
+ )
73
+ end
64
74
 
65
75
  def url
66
76
  "%s://%s/api/v1" % [ Client.scheme, Client.domain ]
@@ -3,6 +3,7 @@ module The86::Client
3
3
 
4
4
  attribute :id, Integer
5
5
  attribute :content, String # For creating new Conversation.
6
+ attribute :bumped_at, DateTime
6
7
  attribute :created_at, DateTime
7
8
  attribute :updated_at, DateTime
8
9
 
@@ -16,5 +16,8 @@ module The86
16
16
  class ValidationFailed < Error
17
17
  end
18
18
 
19
+ class PaginationError < Error
20
+ end
21
+
19
22
  end
20
23
  end
@@ -78,7 +78,7 @@ module The86
78
78
  self.attributes = connection.get(
79
79
  path: resource_path,
80
80
  status: 200
81
- )
81
+ ).data
82
82
  self
83
83
  end
84
84
 
@@ -87,7 +87,7 @@ module The86
87
87
  path: resource_path,
88
88
  data: attributes,
89
89
  status: 200
90
- )
90
+ ).data
91
91
  end
92
92
 
93
93
  def sendable_attributes
@@ -112,7 +112,7 @@ module The86
112
112
  path: self.class.collection_path(@parent),
113
113
  data: sendable_attributes,
114
114
  status: 201
115
- )
115
+ ).data
116
116
  end
117
117
 
118
118
  def save_existing
@@ -120,7 +120,7 @@ module The86
120
120
  path: resource_path,
121
121
  data: sendable_attributes,
122
122
  status: 200
123
- )
123
+ ).data
124
124
  end
125
125
 
126
126
  def connection
@@ -1,3 +1,5 @@
1
+ require "addressable/uri"
2
+
1
3
  module The86::Client
2
4
  class ResourceCollection
3
5
 
@@ -6,9 +8,7 @@ module The86::Client
6
8
  # Connection is a The86::Client::Connection instance.
7
9
  # Path is the API-relative path, e.g. "users".
8
10
  # Klass is class of each record in the collection, e.g. User
9
- # Attributes is a Hash of attributes common to all items in collection,
10
- # and not fetched in HTTP response, e.g. parent items.
11
- # e.g. for conversations: { site: Site.new(slug: "...") }
11
+ # Parent is the parent resource of this collection and its items.
12
12
  # Records is an array of hashes, for pre-populating the collection.
13
13
  # e.g. when an API response contains collections of child resources.
14
14
  def initialize(connection, path, klass, parent, records = nil)
@@ -30,7 +30,12 @@ module The86::Client
30
30
  attr_writer :parameters
31
31
 
32
32
  def with_parameters(parameters)
33
- dup.tap do |collection|
33
+ self.class.new(
34
+ @connection,
35
+ @path,
36
+ @klass,
37
+ @parent
38
+ ).tap do |collection|
34
39
  collection.parameters = parameters
35
40
  end
36
41
  end
@@ -54,10 +59,31 @@ module The86::Client
54
59
  end
55
60
  end
56
61
 
62
+ # Load the next page of records, based on the pagination header, e.g.
63
+ # Link: <http://example.org/api/v1/sites/a/conversations?bumped_before=time>; rel="next"
64
+ def more
65
+ if more?
66
+ self.class.new(
67
+ @connection,
68
+ Addressable::URI.parse(http_response.links[:next]).request_uri,
69
+ @klass,
70
+ @parent
71
+ )
72
+ else
73
+ raise PaginationError, %{Collection has no 'Link: <url>; rel="next"' header}
74
+ end
75
+ end
76
+
77
+ # Whether there are more resources on a subsequent page.
78
+ # See documentation for #more method.
79
+ def more?
80
+ http_response.links.key? :next
81
+ end
82
+
57
83
  # Cache array representation.
58
84
  # Save building Resources for each record multiple times.
59
85
  def to_a
60
- @_to_a = super
86
+ @_to_a ||= super
61
87
  end
62
88
 
63
89
  def [](index)
@@ -66,13 +92,17 @@ module The86::Client
66
92
 
67
93
  private
68
94
 
69
- def records
70
- @records ||= @connection.get(
95
+ def http_response
96
+ @_http_response ||= @connection.get(
71
97
  path: @path,
72
98
  parameters: @parameters,
73
99
  status: 200
74
100
  )
75
101
  end
76
102
 
103
+ def records
104
+ @records || http_response.data
105
+ end
106
+
77
107
  end
78
108
  end
@@ -0,0 +1,31 @@
1
+ module The86::Client
2
+
3
+ # Representation of an HTTP response.
4
+ class Response
5
+
6
+ # status: The numeric HTTP status.
7
+ # headers: Hash of HTTP response headers.
8
+ # data: The decoded body of the response.
9
+ def initialize(status, headers, data)
10
+ @status = status
11
+ @headers = headers
12
+ @data = data
13
+ end
14
+
15
+ attr_reader :status
16
+ attr_reader :headers
17
+ attr_reader :data
18
+
19
+ # See: http://tools.ietf.org/html/rfc5988
20
+ def links
21
+ @_links ||= {}.tap do |links|
22
+ Array(headers["Link"] || headers["link"]).map do |link|
23
+ link.match %r{\A<([^>]+)>;\s*rel="([^"]+)"\z}
24
+ end.compact.each do |match|
25
+ links[match[2].downcase.to_sym] = match[1]
26
+ end
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  module The86
2
2
  module Client
3
- VERSION = "0.0.8"
3
+ VERSION = "1.0.0"
4
4
  end
5
5
  end
@@ -36,6 +36,28 @@ module The86::Client
36
36
  c.id.must_equal 10
37
37
  c.site.must_equal site
38
38
  end
39
+
40
+ it "handles pagination headers" do
41
+ url = "#{conversations_url}?limit=2"
42
+ next_url = "#{url}&bumped_before=timestamp"
43
+ expect_get_conversations(
44
+ url: basic_auth_url(url),
45
+ response_body: [{id: 1}, {id: 2}],
46
+ response_headers: {"Link" => %{<#{next_url}>; rel="next"}}
47
+ )
48
+ expect_get_conversations(
49
+ url: basic_auth_url(next_url),
50
+ response_body: [{id: 3}, {id: 4}],
51
+ )
52
+ page1 = site.conversations.with_parameters(limit: 2)
53
+ page1.more?.must_equal true
54
+
55
+ page2 = page1.more
56
+ page2.more?.must_equal false
57
+
58
+ page1.map(&:id).must_equal([1,2])
59
+ page2.map(&:id).must_equal([3,4])
60
+ end
39
61
  end
40
62
 
41
63
  describe "creating conversations" do
@@ -64,7 +86,7 @@ module The86::Client
64
86
  describe "finding a conversation" do
65
87
  it "gets the conversation, loads data into the resource" do
66
88
  expect_request(
67
- url: "https://user:pass@example.org/api/v1/sites/test/conversations/4",
89
+ url: basic_auth_url("https://example.org/api/v1/sites/test/conversations/4"),
68
90
  method: :get,
69
91
  status: 200,
70
92
  response_body: {id: 4, posts: [{id: 8, content: "A post."}]},
@@ -77,8 +99,9 @@ module The86::Client
77
99
 
78
100
  describe "hiding and unhiding a conversation" do
79
101
  let(:conversation) { site.conversations.build(id: 2) }
80
- let(:oauth_url) { "#{conversations_url}/2" }
81
- let(:basic_auth_url) { oauth_url.sub("//", "//user:pass@") }
102
+ let(:user_auth_url) { "#{conversations_url}/2" }
103
+ let(:site_auth_url) { user_auth_url.sub("//", "//user:pass@") }
104
+ let(:site_auth_url) { basic_auth_url(user_auth_url) }
82
105
  let(:headers) { Hash.new }
83
106
  def expectation(url, hidden_param)
84
107
  {
@@ -92,20 +115,20 @@ module The86::Client
92
115
  end
93
116
  describe "without oauth" do
94
117
  it "patches the conversation as hidden_by_site when no oauth_token" do
95
- expect_request(expectation(basic_auth_url, hidden_by_site: true))
118
+ expect_request(expectation(site_auth_url, hidden_by_site: true))
96
119
  conversation.hide
97
120
 
98
- expect_request(expectation(basic_auth_url, hidden_by_site: false))
121
+ expect_request(expectation(site_auth_url, hidden_by_site: false))
99
122
  conversation.unhide
100
123
  end
101
124
  end
102
125
  describe "with oauth" do
103
126
  let(:headers) { {"Authorization" => "Bearer secret"} }
104
127
  it "patches the conversation as hidden_by_user when oauth_token present" do
105
- expect_request(expectation(oauth_url, hidden_by_user: true))
128
+ expect_request(expectation(user_auth_url, hidden_by_user: true))
106
129
  conversation.hide(oauth_token: "secret")
107
130
 
108
- expect_request(expectation(oauth_url, hidden_by_user: false))
131
+ expect_request(expectation(user_auth_url, hidden_by_user: false))
109
132
  conversation.unhide(oauth_token: "secret")
110
133
  end
111
134
  end
@@ -113,12 +136,15 @@ module The86::Client
113
136
 
114
137
  def expect_get_conversations(options)
115
138
  expect_request({
116
- url: conversations_url.sub("//", "//user:pass@"),
139
+ url: basic_auth_url(conversations_url),
117
140
  method: :get,
118
141
  status: 200,
119
142
  }.merge(options))
120
143
  end
121
144
 
145
+ def basic_auth_url(url)
146
+ url.sub("//", "//user:pass@")
147
+ end
122
148
  end
123
149
 
124
150
  end
@@ -0,0 +1,28 @@
1
+ require_relative "spec_helper"
2
+
3
+ module The86::Client
4
+ describe Response do
5
+
6
+ it "stores status, headers, data" do
7
+ r = Response.new(201, {"X-Test" => "test"}, {id: 4})
8
+ r.status.must_equal 201
9
+ r.headers.must_equal("X-Test" => "test")
10
+ r.data.must_equal(id: 4)
11
+ end
12
+
13
+ describe "#links" do
14
+ let(:response) { Response.new(200, {"Link" => links}, nil) }
15
+ let(:links) { [
16
+ %{<https://example.org/page3>; rel="next"},
17
+ %{<http://example.org/page1?a=b>; rel="prev"},
18
+ ] }
19
+
20
+ it "exposes RFC 5988 link headers" do
21
+ response.links[:next].must_equal "https://example.org/page3"
22
+ response.links[:prev].must_equal "http://example.org/page1?a=b"
23
+ response.links[:test].must_equal nil
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -26,8 +26,9 @@ module RequestExpectations
26
26
  parameters = options[:parameters]
27
27
  method = options.fetch(:method)
28
28
  request_body = options[:request_body]
29
- response_body = options[:response_body]
30
29
  request_headers = options[:request_headers]
30
+ response_body = options[:response_body]
31
+ response_headers = options[:response_headers]
31
32
 
32
33
  if parameters
33
34
  url = Addressable::URI.parse(url).tap do |url|
@@ -41,6 +42,7 @@ module RequestExpectations
41
42
 
42
43
  response = {status: options[:status] || 200}
43
44
  response[:body] = JSON.generate(response_body) if response_body
45
+ response[:headers] = response_headers if response_headers
44
46
 
45
47
  stub_and_assert_request(method, url).
46
48
  with(request).
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: the86-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 1.0.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-08-09 00:00:00.000000000 Z
12
+ date: 2012-08-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faraday
@@ -227,6 +227,7 @@ files:
227
227
  - lib/the86-client/post.rb
228
228
  - lib/the86-client/resource.rb
229
229
  - lib/the86-client/resource_collection.rb
230
+ - lib/the86-client/response.rb
230
231
  - lib/the86-client/site.rb
231
232
  - lib/the86-client/user.rb
232
233
  - lib/the86-client/version.rb
@@ -235,6 +236,7 @@ files:
235
236
  - spec/post_spec.rb
236
237
  - spec/posts_spec.rb
237
238
  - spec/resource_spec.rb
239
+ - spec/response_spec.rb
238
240
  - spec/spec_helper.rb
239
241
  - spec/support/webmock.rb
240
242
  - spec/user_spec.rb
@@ -270,6 +272,7 @@ test_files:
270
272
  - spec/post_spec.rb
271
273
  - spec/posts_spec.rb
272
274
  - spec/resource_spec.rb
275
+ - spec/response_spec.rb
273
276
  - spec/spec_helper.rb
274
277
  - spec/support/webmock.rb
275
278
  - spec/user_spec.rb