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 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