api_client_builder 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,10 @@
1
+ module TestClient
2
+ class HTTPClientHandler
3
+ def initialize(domain)
4
+ @domain = domain
5
+ end
6
+
7
+ def get(route, params = nil, headers = {})
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,4 @@
1
+ require 'bundler/setup'
2
+ require 'api_client_builder'
3
+
4
+ SPEC_DIR = File.dirname(__FILE__)
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