api_client_builder 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.
- checksums.yaml +7 -0
- data/api_client_builder.gemspec +23 -0
- data/readme.md +389 -0
- data/spec/lib/api_client_builder/api_client_spec.rb +46 -0
- data/spec/lib/api_client_builder/get_collection_request_spec.rb +63 -0
- data/spec/lib/api_client_builder/get_item_request_spec.rb +47 -0
- data/spec/lib/api_client_builder/post_request_spec.rb +31 -0
- data/spec/lib/api_client_builder/put_request_spec.rb +31 -0
- data/spec/lib/api_client_builder/request_spec.rb +43 -0
- data/spec/lib/api_client_builder/response_spec.rb +27 -0
- data/spec/lib/api_client_builder/test_client/client.rb +22 -0
- data/spec/lib/api_client_builder/test_client/http_client_handler.rb +10 -0
- data/spec/lib/api_client_builder/test_client/response_handler.rb +47 -0
- data/spec/lib/api_client_builder/url_generator_spec.rb +33 -0
- data/spec/spec_helper.rb +4 -0
- metadata +115 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6a42589201e1aa43a64afa11738378eca1182f7a
|
4
|
+
data.tar.gz: 6c5fea2364dc9e85db7241ea02f9a8d66dced538
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e6362629e413db828fdfefc719be94de0c10a23a099edf33e136a4a9e2cb7394d7087eb999844a324d89e1b710ddeeb4570914a990a683490bc7659b51f16b17
|
7
|
+
data.tar.gz: f201de0c2a4c149e2958cc772260f9fa0c17c1bdd29c102d3f370043427b647e229342d30a24053eccb88794c58c1656d079f50ff2903190bf85c1a99bd8e66a
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/api_client_builder/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = "api_client_builder"
|
6
|
+
gem.summary = "API Client Builder provides an easy to use interface for creating HTTP api clients"
|
7
|
+
gem.description = "API Client Builder provides an easy to use interface for creating HTTP api clients"
|
8
|
+
gem.authors = ['Jayce Higgins']
|
9
|
+
gem.email = ['jhiggins@instructure.com', 'eng@instructure.com']
|
10
|
+
|
11
|
+
gem.files = %w[api_client_builder.gemspec readme.md]
|
12
|
+
|
13
|
+
gem.test_files = Dir.glob("spec/**/*")
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = APIClientBuilder::VERSION
|
16
|
+
gem.required_ruby_version = '>= 2.0'
|
17
|
+
|
18
|
+
gem.license = 'MIT'
|
19
|
+
|
20
|
+
gem.add_development_dependency 'pry'
|
21
|
+
gem.add_development_dependency 'rspec'
|
22
|
+
gem.add_development_dependency 'wwtd'
|
23
|
+
end
|
data/readme.md
ADDED
@@ -0,0 +1,389 @@
|
|
1
|
+
# API Client Builder
|
2
|
+
|
3
|
+
API Client Builder was created to reduce the overhead of creating API clients.
|
4
|
+
|
5
|
+
It provides a DSL for defining endpoints and only requires you to define handlers
|
6
|
+
for http requests and responses.
|
7
|
+
|
8
|
+
---
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
gem 'api_client_builder'
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install api_client_builder
|
23
|
+
|
24
|
+
---
|
25
|
+
|
26
|
+
## Defining a client
|
27
|
+
|
28
|
+
The basic client structure looks like this.
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
require 'api_client_builder/api_client'
|
32
|
+
require 'path/to/your/http_client_handler'
|
33
|
+
require 'path/to/your/response_handler'
|
34
|
+
|
35
|
+
class Client < APIClientBulder::APIClient
|
36
|
+
def initialize(**opts)
|
37
|
+
super(domain: opts[:domain],
|
38
|
+
http_client: HTTPClientHandler)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
The client requires a response handler to be defined in the following method.
|
44
|
+
Unlike the HTTPClientHandler that can be sent in as a reference to a class
|
45
|
+
and instantiated, the response handler has a few extra options that must
|
46
|
+
be defined concretely on a per-client basis.
|
47
|
+
|
48
|
+
Exponential back-off is optional for handling retries of requests. If unset,
|
49
|
+
the builder will ignore it and will resort to just calling error handlers
|
50
|
+
upon failure.
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
def response_handler_build(http_client, start_url, type)
|
54
|
+
ResponseHandler.new(http_client, start_url, type, exponential_backoff: true)
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
|
59
|
+
### Defining routes on the client
|
60
|
+
To define routes on the api client, use the DSL provided by the
|
61
|
+
builder's APIClient class. Four parts have been defined to help:
|
62
|
+
|
63
|
+
1) Action - `#get`, `#post`, or `#put`: will define the HTTP action and the first part
|
64
|
+
of the defined method.
|
65
|
+
|
66
|
+
2) Resource Type: will define the "type" of the route and finishes out the
|
67
|
+
defined method "get_resource_type".
|
68
|
+
- Note that this portion of the route has a special property
|
69
|
+
that allows you to add `_for_something_else` to the end while maintaining
|
70
|
+
everything before the "for" as the "type" that is sent back. This is helpful when
|
71
|
+
parsing the responses because you might want to get "students" for say "schools",
|
72
|
+
and "courses", and "sections", where the response object type is "students" for
|
73
|
+
all three routes.
|
74
|
+
|
75
|
+
3) Plurality - `:singular`, `:collection`: determines whether
|
76
|
+
or not the response will be a single object or multiple to know whether or
|
77
|
+
not pagination is required.
|
78
|
+
- Note that "put" and "post" don't need plurality defined
|
79
|
+
|
80
|
+
4) Route: defines the route to be appended to the provided domain.
|
81
|
+
- Note that any symbols in the route will be interpolated as required
|
82
|
+
params when calling the method on the client.
|
83
|
+
|
84
|
+
|
85
|
+
---
|
86
|
+
|
87
|
+
## Route Examples
|
88
|
+
|
89
|
+
#### Single Item Gets: Yields GetItemRequest
|
90
|
+
|
91
|
+
Define the route on the client
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
get :some_object, :singular, 'some_objects/:id'
|
95
|
+
```
|
96
|
+
|
97
|
+
Use the defined route
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
single_request = client.get_some_object(id: 123)
|
101
|
+
|
102
|
+
response_body = single_request.response
|
103
|
+
```
|
104
|
+
|
105
|
+
#### Collection Item Gets: Yields GetCollectionRequest
|
106
|
+
|
107
|
+
Define the route on the client
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
get :some_objects, :collection, 'some_objects'
|
111
|
+
```
|
112
|
+
|
113
|
+
Use the defined route
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
collection_request = client.get_some_objects
|
117
|
+
|
118
|
+
collection_request.each do |item|
|
119
|
+
#Item will be a Hash if you use the default response in the response handler
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
#### Put Item: Yields PutRequest
|
124
|
+
|
125
|
+
Define the route on the client
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
put :some_object, 'some_objects/:id'
|
129
|
+
```
|
130
|
+
|
131
|
+
Use the defined route (Takes a hash/JSON as the first arg)
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
request = client.put_some_object({}, id: 123)
|
135
|
+
|
136
|
+
response_body = request.response
|
137
|
+
```
|
138
|
+
|
139
|
+
#### Collection Item Gets: Yields PostRequest
|
140
|
+
|
141
|
+
Define the route on the client
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
post :some_objects, 'some_objects'
|
145
|
+
```
|
146
|
+
|
147
|
+
Use the defined route (Takes a hash/JSON as the first arg)
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
request = client.post_some_object({})
|
151
|
+
|
152
|
+
response_body = request.response
|
153
|
+
```
|
154
|
+
|
155
|
+
#### Multiple routes for same object
|
156
|
+
|
157
|
+
All of these routes will yield a collection with type "some_objects"
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
get :some_objects, :collection, 'some_objects'
|
161
|
+
get :some_objects_for_school, :collection, 'school/:school_id/some_objects'
|
162
|
+
get :some_objects_for_course, :collection, 'course/:course_id/some_objects'
|
163
|
+
```
|
164
|
+
|
165
|
+
---
|
166
|
+
|
167
|
+
## Defining an HTTP Client Handler
|
168
|
+
|
169
|
+
The HTTP Client Handler is designed to manage the http requests themselves. Since
|
170
|
+
actually making an HTTP request typically requires some amount of authentication,
|
171
|
+
it is suggested that authentication and headers are managed here as well.
|
172
|
+
|
173
|
+
The http client handler requires '#get', '#post', and '#put' to be defined here
|
174
|
+
with the shown method signature.
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
class HTTPClientHandler
|
178
|
+
#Do initialization here, generally authentication creds and a domain is sent in
|
179
|
+
|
180
|
+
def get(route, params = nil, headers = {})
|
181
|
+
client.get(route, params, headers)
|
182
|
+
end
|
183
|
+
|
184
|
+
def put(route, params = nil, headers = {})
|
185
|
+
client.put(route, params, headers)
|
186
|
+
end
|
187
|
+
|
188
|
+
def post(route, params = nil, headers = {})
|
189
|
+
client.post(route, params, headers)
|
190
|
+
end
|
191
|
+
|
192
|
+
#Define a client to use here. The HTTPClient gem is a good option
|
193
|
+
|
194
|
+
#Build up headers and authentication handling here as well
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
---
|
199
|
+
|
200
|
+
## Defining a Response Handler
|
201
|
+
|
202
|
+
The response handler is where everything comes together. As the name suggests,
|
203
|
+
defining how to get responses but also how to handle them is done here.
|
204
|
+
|
205
|
+
Define only the methods that match the requests that the client needs. For
|
206
|
+
simpler API's this considerably reduces the overhead of setting up the response
|
207
|
+
handler.
|
208
|
+
|
209
|
+
Through the methods defined here the builder will manage how requests are handled.
|
210
|
+
When defining the response handler, in general, a start url and an http_client_handler
|
211
|
+
is provided to the initializer. Since most API's send the "type" as the top level key,
|
212
|
+
the `#response_handler_build` that was defined in the client receives that type
|
213
|
+
as a parameter. It is used to extract the actual body from the response
|
214
|
+
as well. Furthermore, feel free to send any other options required to make
|
215
|
+
these actions simpler.
|
216
|
+
|
217
|
+
- Note: `#build_response` will be used in all examples and explained once
|
218
|
+
all required methods are defined
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
class ResponseHandler
|
222
|
+
def initialize(http_client_handler, start_url, type)
|
223
|
+
@http_client = http_client_handler
|
224
|
+
@start_url = start_url
|
225
|
+
@type = type
|
226
|
+
end
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
---
|
231
|
+
|
232
|
+
## Response Handler Examples
|
233
|
+
|
234
|
+
#### For single gets
|
235
|
+
|
236
|
+
The builder will only call `#get_first_page` when handling `:singular` for get routes.
|
237
|
+
If pagination is required this is a good place to figure out the number
|
238
|
+
of pages and also start the page counter.
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
def get_first_page
|
242
|
+
#Build the URL -- this could be to add pagination params to the route, or add
|
243
|
+
# whatever else is necessary to the route
|
244
|
+
http_response = @http_client.get("a URL")
|
245
|
+
|
246
|
+
#Generally the first page will contain information about how many pages a paginated
|
247
|
+
# response will have. Set that here. `@max_pages`
|
248
|
+
#Be sure to set the current page count as well -- @current_page
|
249
|
+
build_response(http_response)
|
250
|
+
end
|
251
|
+
```
|
252
|
+
|
253
|
+
#### For collection gets
|
254
|
+
|
255
|
+
The builder will call `#get_next_page` when handling `:collection` for get routes. It will
|
256
|
+
determine whether or not there are more pages by calling `#more_pages?` which must
|
257
|
+
return a boolean denoting the presence of more pages.
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
def get_next_page
|
261
|
+
#Build the URL -- this could be to add pagination params to the route, or add
|
262
|
+
# whatever else is necessary to the route
|
263
|
+
http_response = @http_client.get("a URL")
|
264
|
+
|
265
|
+
#If the http_response is valid then increment the page counter here
|
266
|
+
build_response(http_response)
|
267
|
+
end
|
268
|
+
|
269
|
+
def more_pages?
|
270
|
+
return false if @current_page > @max_pages
|
271
|
+
return true
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
275
|
+
#### For puts
|
276
|
+
|
277
|
+
The builder will call `#put_request` when handling put routes.
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
def put_request
|
281
|
+
#Build the URL -- this could be to add pagination params to the route, or add
|
282
|
+
# whatever else is necessary to the route
|
283
|
+
#Also send the body if thats how the client handler is configured
|
284
|
+
http_response = @http_client.put("a URL", {})
|
285
|
+
build_response(http_response)
|
286
|
+
end
|
287
|
+
```
|
288
|
+
|
289
|
+
#### For posts
|
290
|
+
|
291
|
+
The builder will call `#post_request` when handling post routes.
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
def post_request
|
295
|
+
#Build the URL -- this could be to add pagination params to the route, or add
|
296
|
+
# whatever else is necessary to the route
|
297
|
+
#Also send the body if thats how the client handler is configured
|
298
|
+
http_response = @http_client.post("a URL", {})
|
299
|
+
build_response(http_response)
|
300
|
+
end
|
301
|
+
```
|
302
|
+
|
303
|
+
#### Handling retry-able requests
|
304
|
+
|
305
|
+
If requests defined need to be retry-able, extend the response handler by providing
|
306
|
+
the following methods.
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
def retryable?(status_code)
|
310
|
+
if @opts[:exponential_backoff]
|
311
|
+
#Define the conditions of whether or not the provided status code is retry-able
|
312
|
+
else
|
313
|
+
return false
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
def reset_retries
|
318
|
+
#Track the number of retries so the request is not retried indefinitely.
|
319
|
+
# The builder will reset them when it no longer is retrying by calling this
|
320
|
+
# method
|
321
|
+
@retries = 0
|
322
|
+
end
|
323
|
+
|
324
|
+
def retry_request
|
325
|
+
#Increment the retries here so the request is not retried indefinitely.
|
326
|
+
@retries += 1
|
327
|
+
|
328
|
+
#Build the URL -- this could be to add pagination params to the route, or add
|
329
|
+
# whatever else is necessary to the route
|
330
|
+
response = @http_client.the_action_to_retry("a URL")
|
331
|
+
build_response(response)
|
332
|
+
end
|
333
|
+
```
|
334
|
+
|
335
|
+
#### Managing the http response
|
336
|
+
|
337
|
+
The builder defines a default `Response` object that will provide the minimally
|
338
|
+
required interface for managing an http response.
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
def build_response(http_response)
|
342
|
+
items = JSON.parse(http_response.body)
|
343
|
+
|
344
|
+
status = http_response.status
|
345
|
+
|
346
|
+
APIClientBuilder::Response.new(items, status, SUCCESS_RANGE)
|
347
|
+
end
|
348
|
+
```
|
349
|
+
|
350
|
+
The block above is the simplest use case for using the built-in `Response` object.
|
351
|
+
If a custom `Response` is required, define `#success?` and it will comply with
|
352
|
+
the builders contract with that object.
|
353
|
+
|
354
|
+
---
|
355
|
+
|
356
|
+
## Error handling
|
357
|
+
|
358
|
+
All requests made with the client will return a `Request` object of whatever type
|
359
|
+
of action that it was defined as. All `Request` objects will have a default error
|
360
|
+
handler defined, which will give you minimal insight into the issue and also
|
361
|
+
describe how to define a new error handler.
|
362
|
+
|
363
|
+
The actual request is not made until you call the `Request` response interface
|
364
|
+
either by `#each` or `#response`. Define an error handler before accessing the
|
365
|
+
response if custom error handling is required. Any number of error handlers
|
366
|
+
can be defined on a single request and will be called as soon as the response
|
367
|
+
is not a "success."
|
368
|
+
|
369
|
+
- Note that the error handlers will be ignored if you opted into retry-able
|
370
|
+
requests until the retry loop results in a success or completes its iterations.
|
371
|
+
|
372
|
+
```ruby
|
373
|
+
single_request = client.get_some_object(id: 123)
|
374
|
+
|
375
|
+
single_request.on_error do |page, handler|
|
376
|
+
#The page will have all of the status information
|
377
|
+
#The handler is the defined response_handler
|
378
|
+
#Use either to glean more information about why the request was an error and
|
379
|
+
# handle the error here
|
380
|
+
end
|
381
|
+
|
382
|
+
response_body = single_request.response
|
383
|
+
```
|
384
|
+
|
385
|
+
---
|
386
|
+
|
387
|
+
## License
|
388
|
+
|
389
|
+
API Client Builder is released under the MIT License.
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'lib/api_client_builder/test_client/client'
|
3
|
+
require 'api_client_builder/api_client'
|
4
|
+
|
5
|
+
module APIClientBuilder
|
6
|
+
describe APIClient do
|
7
|
+
let(:domain) {'https://www.domain.com/api/endpoints/'}
|
8
|
+
let(:client) {TestClient::Client.new(domain: domain)}
|
9
|
+
|
10
|
+
describe '.get' do
|
11
|
+
context 'plurality is :collection' do
|
12
|
+
it 'defines a get method that returns a GetCollectionRequest' do
|
13
|
+
expect(client).to respond_to(:get_some_objects)
|
14
|
+
expect(client.get_some_objects).to be_a(APIClientBuilder::GetCollectionRequest)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'plurality is :singular' do
|
19
|
+
it 'defines a get method that returns a GetItemRequest' do
|
20
|
+
expect(client).to respond_to(:get_some_object)
|
21
|
+
expect(client.get_some_object(some_id: '123')).to be_a(APIClientBuilder::GetItemRequest)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '.post' do
|
27
|
+
it 'defines a post method on the client' do
|
28
|
+
expect(client).to respond_to(:post_some_object)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'returns a PostRequest object' do
|
32
|
+
expect(client.post_some_object({})).to be_a(APIClientBuilder::PostRequest)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '.put' do
|
37
|
+
it 'defines a put method on the client' do
|
38
|
+
expect(client).to respond_to(:put_some_object)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'returns a PutRequest object' do
|
42
|
+
expect(client.put_some_object({})).to be_a(APIClientBuilder::PutRequest)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'api_client_builder/get_collection_request'
|
3
|
+
require 'lib/api_client_builder/test_client/client'
|
4
|
+
|
5
|
+
module APIClientBuilder
|
6
|
+
describe GetCollectionRequest do
|
7
|
+
describe '#each' do
|
8
|
+
context 'request was successful' do
|
9
|
+
it 'paginates the collection' do
|
10
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
11
|
+
|
12
|
+
some_objects = client.get_some_objects
|
13
|
+
expect(some_objects.count).to eq(9)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'request was unsuccessful' do
|
18
|
+
it 'calls the error handlers' do
|
19
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
20
|
+
|
21
|
+
bad_response = APIClientBuilder::Response.new('bad request', 500, [200])
|
22
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:get_first_page).and_return(bad_response)
|
23
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:retry_request).and_return(bad_response)
|
24
|
+
expect{ client.get_some_objects.each{} }.to raise_error(
|
25
|
+
APIClientBuilder::DefaultPageError,
|
26
|
+
"Default error for bad response. If you want to handle this error use #on_error on the response in your api consumer. Error Code: 500"
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'request was successful after retryable error' do
|
31
|
+
it 'yields the good response' do
|
32
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
33
|
+
|
34
|
+
bad_response = APIClientBuilder::Response.new('bad request', 500, [200])
|
35
|
+
good_response = APIClientBuilder::Response.new([1,2,3], 200, [200])
|
36
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:get_first_page).and_return(bad_response)
|
37
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:more_pages?).and_return(false)
|
38
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:retry_request).and_return(good_response)
|
39
|
+
|
40
|
+
some_objects = client.get_some_objects
|
41
|
+
expect(some_objects.count).to eq(3)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'request was unsuccessful after non-retryable error' do
|
46
|
+
it 'calls the error handlers' do
|
47
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
48
|
+
|
49
|
+
bad_response = APIClientBuilder::Response.new('bad request', 400, [200])
|
50
|
+
good_response = APIClientBuilder::Response.new([1,2,3], 200, [200])
|
51
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:get_first_page).and_return(bad_response)
|
52
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:more_pages?).and_return(false)
|
53
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:retry_request).and_return(good_response)
|
54
|
+
expect{ client.get_some_objects.each{} }.to raise_error(
|
55
|
+
APIClientBuilder::DefaultPageError,
|
56
|
+
"Default error for bad response. If you want to handle this error use #on_error on the response in your api consumer. Error Code: 400"
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'api_client_builder/get_item_request'
|
3
|
+
require 'lib/api_client_builder/test_client/client'
|
4
|
+
|
5
|
+
module APIClientBuilder
|
6
|
+
describe GetItemRequest do
|
7
|
+
describe '#read' do
|
8
|
+
context 'request was successful' do
|
9
|
+
it 'gives only the first page of the response' do
|
10
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
11
|
+
|
12
|
+
some_object = client.get_some_object(some_id: '123').response
|
13
|
+
expect(some_object.count).to eq(3)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'request was unsuccessful' do
|
18
|
+
it 'calls the error handlers' do
|
19
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
20
|
+
|
21
|
+
bad_response = APIClientBuilder::Response.new('bad request', 500, [200])
|
22
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:get_first_page).and_return(bad_response)
|
23
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:retry_request).and_return(bad_response)
|
24
|
+
expect{ client.get_some_object(some_id: '123').response }.to raise_error(
|
25
|
+
APIClientBuilder::DefaultPageError,
|
26
|
+
"Default error for bad response. If you want to handle this error use #on_error on the response in your api consumer. Error Code: 500"
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'retry is successful' do
|
31
|
+
it 'yields the good response' do
|
32
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
33
|
+
|
34
|
+
bad_response = APIClientBuilder::Response.new('bad request', 500, [200])
|
35
|
+
good_response = APIClientBuilder::Response.new([1,2,3], 200, [200])
|
36
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:get_first_page).and_return(bad_response)
|
37
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:more_pages?).and_return(false)
|
38
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:retry_request).and_return(good_response)
|
39
|
+
|
40
|
+
some_objects = client.get_some_objects
|
41
|
+
expect(some_objects.count).to eq(3)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'api_client_builder/post_request'
|
3
|
+
require 'lib/api_client_builder/test_client/client'
|
4
|
+
|
5
|
+
module APIClientBuilder
|
6
|
+
describe PostRequest do
|
7
|
+
describe '#response' do
|
8
|
+
context 'request was successful' do
|
9
|
+
it 'returns a response object' do
|
10
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
11
|
+
|
12
|
+
some_object = client.post_some_object({}).response
|
13
|
+
expect(some_object.body).to eq('good request')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'request was unsuccessful' do
|
18
|
+
it 'calls the error handlers' do
|
19
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
20
|
+
|
21
|
+
bad_response = APIClientBuilder::Response.new('bad request', 400, [200])
|
22
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:post_request).and_return(bad_response)
|
23
|
+
expect{ client.post_some_object({}).response }.to raise_error(
|
24
|
+
APIClientBuilder::DefaultPageError,
|
25
|
+
"Default error for bad response. If you want to handle this error use #on_error on the response in your api consumer. Error Code: 400"
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'api_client_builder/put_request'
|
3
|
+
require 'lib/api_client_builder/test_client/client'
|
4
|
+
|
5
|
+
module APIClientBuilder
|
6
|
+
describe PutRequest do
|
7
|
+
describe '#response' do
|
8
|
+
context 'request was successful' do
|
9
|
+
it 'returns a response object' do
|
10
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
11
|
+
|
12
|
+
some_object = client.put_some_object({}).response
|
13
|
+
expect(some_object.body).to eq('good request')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'request was unsuccessful' do
|
18
|
+
it 'calls the error handlers' do
|
19
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
20
|
+
|
21
|
+
bad_response = APIClientBuilder::Response.new('bad request', 400, [200])
|
22
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:put_request).and_return(bad_response)
|
23
|
+
expect{ client.put_some_object({}).response }.to raise_error(
|
24
|
+
APIClientBuilder::DefaultPageError,
|
25
|
+
"Default error for bad response. If you want to handle this error use #on_error on the response in your api consumer. Error Code: 400"
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'lib/api_client_builder/test_client/client'
|
3
|
+
require 'api_client_builder/request'
|
4
|
+
|
5
|
+
module APIClientBuilder
|
6
|
+
describe Request do
|
7
|
+
describe '#error_handlers' do
|
8
|
+
context 'no error handlers' do
|
9
|
+
it 'returns the default error handler' do
|
10
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
11
|
+
|
12
|
+
bad_response = APIClientBuilder::Response.new('bad request', 400, [200])
|
13
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:get_first_page).and_return(bad_response)
|
14
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:retry_request).and_return(bad_response)
|
15
|
+
expect{ client.get_some_object(some_id: '123').response }.to raise_error(
|
16
|
+
APIClientBuilder::DefaultPageError,
|
17
|
+
"Default error for bad response. If you want to handle this error use #on_error on the response in your api consumer. Error Code: 400"
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'defined error handler' do
|
23
|
+
it 'returns the custom handler' do
|
24
|
+
client = TestClient::Client.new(domain: 'https://www.domain.com/api/endpoints/')
|
25
|
+
|
26
|
+
bad_response = APIClientBuilder::Response.new('bad request', 400, [200])
|
27
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:get_first_page).and_return(bad_response)
|
28
|
+
allow_any_instance_of(TestClient::ResponseHandler).to receive(:retry_request).and_return(bad_response)
|
29
|
+
object_response = client.get_some_object(some_id: '123')
|
30
|
+
|
31
|
+
object_response.on_error do |page, response|
|
32
|
+
raise StandardError, "Something Bad Happened"
|
33
|
+
end
|
34
|
+
|
35
|
+
expect{object_response.response}.to raise_error(
|
36
|
+
StandardError,
|
37
|
+
"Something Bad Happened"
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'api_client_builder/response'
|
3
|
+
|
4
|
+
module APIClientBuilder
|
5
|
+
describe Response do
|
6
|
+
describe '#success?' do
|
7
|
+
it 'returns true when the given code is included in the given range' do
|
8
|
+
page = Response.new([], 200, [200])
|
9
|
+
|
10
|
+
expect(page.success?).to eq(true)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'returns false when the given code is included in the given range' do
|
14
|
+
page = Response.new([], 400, [200])
|
15
|
+
|
16
|
+
expect(page.success?).to eq(false)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'returns false when the response is otherwise marked as failed' do
|
20
|
+
page = Response.new([], 200, [200])
|
21
|
+
page.mark_failed "Something happened"
|
22
|
+
|
23
|
+
expect(page.success?).to eq(false)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'api_client_builder/api_client'
|
2
|
+
require 'lib/api_client_builder/test_client/response_handler'
|
3
|
+
require 'lib/api_client_builder/test_client/http_client_handler'
|
4
|
+
|
5
|
+
module TestClient
|
6
|
+
class Client < APIClientBuilder::APIClient
|
7
|
+
def initialize(**opts)
|
8
|
+
super(domain: opts[:domain],
|
9
|
+
http_client: TestClient::HTTPClientHandler)
|
10
|
+
end
|
11
|
+
|
12
|
+
def response_handler_build(http_client, start_url, type)
|
13
|
+
ResponseHandler.new(http_client, start_url, type, exponential_backoff: @exponential_backoff)
|
14
|
+
end
|
15
|
+
|
16
|
+
get :some_objects, :collection, 'some/url'
|
17
|
+
get :some_object, :singular, 'some/url/:some_id'
|
18
|
+
|
19
|
+
post :some_object, 'some/post/url'
|
20
|
+
put :some_object, 'som/put/url'
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'api_client_builder/response'
|
2
|
+
|
3
|
+
module TestClient
|
4
|
+
class ResponseHandler
|
5
|
+
SUCCESS_STATUS = 200
|
6
|
+
SUCCESS_RANGE = [200]
|
7
|
+
MAX_RETRIES = 4
|
8
|
+
|
9
|
+
def initialize(client, start_url, type, **opts)
|
10
|
+
@retries = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_first_page
|
14
|
+
@current_page = 0
|
15
|
+
APIClientBuilder::Response.new([1,2,3], SUCCESS_STATUS, SUCCESS_RANGE)
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_next_page
|
19
|
+
@current_page += 1
|
20
|
+
APIClientBuilder::Response.new([4,5,6], SUCCESS_STATUS, SUCCESS_RANGE)
|
21
|
+
end
|
22
|
+
|
23
|
+
def put_request(body)
|
24
|
+
APIClientBuilder::Response.new('good request', SUCCESS_STATUS, SUCCESS_RANGE)
|
25
|
+
end
|
26
|
+
|
27
|
+
def post_request(body)
|
28
|
+
APIClientBuilder::Response.new('good request', SUCCESS_STATUS, SUCCESS_RANGE)
|
29
|
+
end
|
30
|
+
|
31
|
+
def more_pages?
|
32
|
+
return false if @current_page >= 2
|
33
|
+
return true
|
34
|
+
end
|
35
|
+
|
36
|
+
def retryable?(status_code)
|
37
|
+
@retries += 1
|
38
|
+
status_code != 400 && @retries < MAX_RETRIES
|
39
|
+
end
|
40
|
+
|
41
|
+
def retry_request
|
42
|
+
end
|
43
|
+
|
44
|
+
def reset_retries
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'api_client_builder/url_generator'
|
3
|
+
|
4
|
+
module APIClientBuilder
|
5
|
+
describe URLGenerator do
|
6
|
+
describe '#build_route' do
|
7
|
+
context 'route with colon params and matching keys' do
|
8
|
+
it 'replaces to route keys with their respective values' do
|
9
|
+
url_generator = URLGenerator.new("https://www.domain.com/api/endpoints/")
|
10
|
+
|
11
|
+
route = url_generator.build_route(
|
12
|
+
'object_one/:object_one_id/:object_one_id/route_to_object/:other_object_id/object',
|
13
|
+
object_one_id: '4',
|
14
|
+
other_object_id: '10'
|
15
|
+
)
|
16
|
+
|
17
|
+
expect(route).to eq(URI.parse('https://www.domain.com/api/endpoints/object_one/4/4/route_to_object/10/object'))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'route with colon params and non matching keys' do
|
22
|
+
it 'raises an argument error' do
|
23
|
+
url_generator = URLGenerator.new("https://www.domain.com/api/endpoints/")
|
24
|
+
|
25
|
+
expect{url_generator.build_route(
|
26
|
+
'object_one/:object_one_id/:object_one_id/route_to_object/:other_object_id/object',
|
27
|
+
other_object_id: '10'
|
28
|
+
)}.to raise_error(ArgumentError, "Param :object_one_id is required")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: api_client_builder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jayce Higgins
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-06-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pry
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: wwtd
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: API Client Builder provides an easy to use interface for creating HTTP
|
56
|
+
api clients
|
57
|
+
email:
|
58
|
+
- jhiggins@instructure.com
|
59
|
+
- eng@instructure.com
|
60
|
+
executables: []
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- api_client_builder.gemspec
|
65
|
+
- readme.md
|
66
|
+
- spec/lib/api_client_builder/api_client_spec.rb
|
67
|
+
- spec/lib/api_client_builder/get_collection_request_spec.rb
|
68
|
+
- spec/lib/api_client_builder/get_item_request_spec.rb
|
69
|
+
- spec/lib/api_client_builder/post_request_spec.rb
|
70
|
+
- spec/lib/api_client_builder/put_request_spec.rb
|
71
|
+
- spec/lib/api_client_builder/request_spec.rb
|
72
|
+
- spec/lib/api_client_builder/response_spec.rb
|
73
|
+
- spec/lib/api_client_builder/test_client/client.rb
|
74
|
+
- spec/lib/api_client_builder/test_client/http_client_handler.rb
|
75
|
+
- spec/lib/api_client_builder/test_client/response_handler.rb
|
76
|
+
- spec/lib/api_client_builder/url_generator_spec.rb
|
77
|
+
- spec/spec_helper.rb
|
78
|
+
homepage:
|
79
|
+
licenses:
|
80
|
+
- MIT
|
81
|
+
metadata: {}
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '2.0'
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
requirements: []
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 2.0.14
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: API Client Builder provides an easy to use interface for creating HTTP api
|
102
|
+
clients
|
103
|
+
test_files:
|
104
|
+
- spec/lib/api_client_builder/api_client_spec.rb
|
105
|
+
- spec/lib/api_client_builder/get_collection_request_spec.rb
|
106
|
+
- spec/lib/api_client_builder/get_item_request_spec.rb
|
107
|
+
- spec/lib/api_client_builder/post_request_spec.rb
|
108
|
+
- spec/lib/api_client_builder/put_request_spec.rb
|
109
|
+
- spec/lib/api_client_builder/request_spec.rb
|
110
|
+
- spec/lib/api_client_builder/response_spec.rb
|
111
|
+
- spec/lib/api_client_builder/test_client/client.rb
|
112
|
+
- spec/lib/api_client_builder/test_client/http_client_handler.rb
|
113
|
+
- spec/lib/api_client_builder/test_client/response_handler.rb
|
114
|
+
- spec/lib/api_client_builder/url_generator_spec.rb
|
115
|
+
- spec/spec_helper.rb
|