the86-client 0.0.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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