api_client_builder 1.0.0 → 1.0.1
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 +4 -4
- data/.dockerignore +1 -0
- data/.gitignore +1 -0
- data/.travis.yml +18 -0
- data/Dockerfile +16 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/{readme.md → README.md} +0 -0
- data/api_client_builder-1.0.0.gem +0 -0
- data/api_client_builder.gemspec +5 -4
- data/build.sh +4 -0
- data/docker-compose.yml +4 -0
- data/lib/api_client_builder.rb +9 -0
- data/lib/api_client_builder/api_client.rb +85 -0
- data/lib/api_client_builder/get_collection_request.rb +43 -0
- data/lib/api_client_builder/get_item_request.rb +29 -0
- data/lib/api_client_builder/post_request.rb +21 -0
- data/lib/api_client_builder/put_request.rb +21 -0
- data/lib/api_client_builder/request.rb +66 -0
- data/lib/api_client_builder/response.rb +32 -0
- data/lib/api_client_builder/url_generator.rb +39 -0
- data/lib/api_client_builder/version.rb +3 -0
- metadata +21 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 325e44729f05185e293461182fd2f405824ef233
|
4
|
+
data.tar.gz: 44970894a7563a62609223a3efd9b7d38a0ce557
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: edcc960fa01482a12446b8be86a847df5d93a500559901758c410e5cf44d6773e251306b4783821b6b9e9741e10429333a9daaae49da285f937a759b9237e18e
|
7
|
+
data.tar.gz: a9e4eec3e7d812ca887b39324c9fd4ee0d21737d31d8c177df431c40393aea6861aea2936b27a3d0e90a89c0f08849f7e2045b9be56beb1b6a52703294fb310c
|
data/.dockerignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/.travis.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
language: ruby
|
2
|
+
sudo: false
|
3
|
+
cache: bundler
|
4
|
+
rvm:
|
5
|
+
# - ruby-head
|
6
|
+
- 2.1
|
7
|
+
- 2.2
|
8
|
+
- 2.3
|
9
|
+
matrix:
|
10
|
+
fast_finish: true
|
11
|
+
# WWTD doesn't support allow_failures... yet
|
12
|
+
# allow_failures:
|
13
|
+
# - rvm: ruby-head
|
14
|
+
script: bundle exec rspec
|
15
|
+
notifications:
|
16
|
+
email:
|
17
|
+
on_success: never
|
18
|
+
on_failure: never
|
data/Dockerfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
FROM instructure/rvm
|
2
|
+
MAINTAINER Instructure
|
3
|
+
|
4
|
+
COPY Gemfile* *.gemspec /usr/src/app/
|
5
|
+
COPY lib/api_client_builder/version.rb /usr/src/app/lib/api_client_builder/
|
6
|
+
|
7
|
+
USER root
|
8
|
+
RUN chown -R docker:docker /usr/src/app
|
9
|
+
USER docker
|
10
|
+
RUN /bin/bash -l -c "cd /usr/src/app && bundle install"
|
11
|
+
|
12
|
+
COPY . /usr/src/app
|
13
|
+
USER root
|
14
|
+
RUN chown -R docker:docker /usr/src/app/*
|
15
|
+
USER docker
|
16
|
+
CMD /bin/bash -l -c "cd /usr/src/app && pwd && bundle exec wwtd --parallel"
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2016 Instructure Inc.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/{readme.md → README.md}
RENAMED
File without changes
|
Binary file
|
data/api_client_builder.gemspec
CHANGED
@@ -8,10 +8,6 @@ Gem::Specification.new do |gem|
|
|
8
8
|
gem.authors = ['Jayce Higgins']
|
9
9
|
gem.email = ['jhiggins@instructure.com', 'eng@instructure.com']
|
10
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
11
|
gem.version = APIClientBuilder::VERSION
|
16
12
|
gem.required_ruby_version = '>= 2.0'
|
17
13
|
|
@@ -20,4 +16,9 @@ Gem::Specification.new do |gem|
|
|
20
16
|
gem.add_development_dependency 'pry'
|
21
17
|
gem.add_development_dependency 'rspec'
|
22
18
|
gem.add_development_dependency 'wwtd'
|
19
|
+
|
20
|
+
gem.files = `git ls-files`.split("\n")
|
21
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
23
|
+
gem.require_paths = ["lib"]
|
23
24
|
end
|
data/build.sh
ADDED
data/docker-compose.yml
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'api_client_builder/version'
|
2
|
+
require 'api_client_builder/api_client'
|
3
|
+
require 'api_client_builder/get_collection_request'
|
4
|
+
require 'api_client_builder/get_item_request'
|
5
|
+
require 'api_client_builder/post_request'
|
6
|
+
require 'api_client_builder/put_request'
|
7
|
+
require 'api_client_builder/request'
|
8
|
+
require 'api_client_builder/response'
|
9
|
+
require 'api_client_builder/url_generator'
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'api_client_builder/get_collection_request'
|
2
|
+
require 'api_client_builder/get_item_request'
|
3
|
+
require 'api_client_builder/post_request'
|
4
|
+
require 'api_client_builder/put_request'
|
5
|
+
require 'api_client_builder/url_generator'
|
6
|
+
|
7
|
+
module APIClientBuilder
|
8
|
+
# The base APIClient that defines the interface for defining an API Client.
|
9
|
+
# Should be sub-classed and then provided an HTTPClient handler and a
|
10
|
+
# response handler.
|
11
|
+
class APIClient
|
12
|
+
attr_reader :url_generator, :http_client
|
13
|
+
|
14
|
+
# @param opts [Hash] options hash
|
15
|
+
# @option opts [Symbol] :http_client The http client handler
|
16
|
+
# @option opts [Symbol] :paginator The response handler
|
17
|
+
def initialize(**opts)
|
18
|
+
@url_generator = APIClientBuilder::URLGenerator.new(opts[:domain])
|
19
|
+
@http_client = opts[:http_client]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Used to define a GET api route on the base class. Will
|
23
|
+
# yield a method that takes the shape of 'get_type' that will
|
24
|
+
# return a CollectionResponse or ItemResponse based on plurality.
|
25
|
+
#
|
26
|
+
# @param type [Symbol] defines the route model
|
27
|
+
# @param plurality [Symbol] defines the routes plurality
|
28
|
+
# @param route [String] defines the routes endpoint
|
29
|
+
#
|
30
|
+
# @return [Request] either a GetCollection or GetItem request
|
31
|
+
def self.get(type, plurality, route, **opts)
|
32
|
+
if plurality == :collection
|
33
|
+
define_method("get_#{type}") do |**params|
|
34
|
+
GetCollectionRequest.new(
|
35
|
+
type,
|
36
|
+
response_handler_build(http_client, @url_generator.build_route(route, **params), type)
|
37
|
+
)
|
38
|
+
end
|
39
|
+
elsif plurality == :singular
|
40
|
+
define_method("get_#{type}") do |**params|
|
41
|
+
GetItemRequest.new(
|
42
|
+
type,
|
43
|
+
response_handler_build(http_client, @url_generator.build_route(route, **params), type)
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Used to define a POST api route on the base class. Will
|
50
|
+
# yield a method that takes the shape of 'post_type' that will
|
51
|
+
# return a PostRequest.
|
52
|
+
#
|
53
|
+
# @param type [Symbol] defines the route model
|
54
|
+
# @param route [String] defines the routes endpoint
|
55
|
+
#
|
56
|
+
# @return [PostRequest] the request object that handles posts
|
57
|
+
def self.post(type, route)
|
58
|
+
define_method("post_#{type}") do |body, **params|
|
59
|
+
PostRequest.new(
|
60
|
+
type,
|
61
|
+
response_handler_build(http_client, @url_generator.build_route(route, **params), type),
|
62
|
+
body
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Used to define a PUT api route on the base class. Will
|
68
|
+
# yield a method that takes the shape of 'put_type' that will
|
69
|
+
# return a PutRequest.
|
70
|
+
#
|
71
|
+
# @param type [Symbol] defines the route model
|
72
|
+
# @param route [String] defines the routes endpoint
|
73
|
+
#
|
74
|
+
# @return [PutRequest] the request object that handles puts
|
75
|
+
def self.put(type, route)
|
76
|
+
define_method("put_#{type}") do |body, **params|
|
77
|
+
PutRequest.new(
|
78
|
+
type,
|
79
|
+
response_handler_build(http_client, @url_generator.build_route(route, **params), type),
|
80
|
+
body
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'api_client_builder/request'
|
2
|
+
|
3
|
+
module APIClientBuilder
|
4
|
+
# The multi item response object to be used as the container for
|
5
|
+
# collection responses from the defined API
|
6
|
+
class GetCollectionRequest < Request
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# Iterates over the pages and yields their items if they're successful
|
10
|
+
# responses. Else handles the error. Will retry the response if a retry
|
11
|
+
# strategy is defined concretely on the response handler.
|
12
|
+
#
|
13
|
+
# @return [JSON] the http response body
|
14
|
+
def each
|
15
|
+
if block_given?
|
16
|
+
each_page do |page|
|
17
|
+
if page.success?
|
18
|
+
page.body.each do |item|
|
19
|
+
yield(item)
|
20
|
+
end
|
21
|
+
elsif response_handler.respond_to?(:retryable?) && response_handler.retryable?(page.status_code)
|
22
|
+
retried_page = attempt_retry
|
23
|
+
|
24
|
+
retried_page.body.each do |item|
|
25
|
+
yield(item)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
notify_error_handlers(page)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
else
|
32
|
+
Enumerator.new(self, :each)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def each_page
|
39
|
+
yield(response_handler.get_first_page)
|
40
|
+
yield(response_handler.get_next_page) while response_handler.more_pages?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'api_client_builder/request'
|
2
|
+
|
3
|
+
module APIClientBuilder
|
4
|
+
# The single item response object to be used as the container for
|
5
|
+
# singular responses from the defined API
|
6
|
+
class GetItemRequest < Request
|
7
|
+
# Reads the first page from the pagination solution and yields the
|
8
|
+
# items if the response was successful. Else handles the error. Will
|
9
|
+
# retry the response if a retry strategy is defined concretely on the
|
10
|
+
# response handler.
|
11
|
+
#
|
12
|
+
# @return [JSON] the http response body
|
13
|
+
def response
|
14
|
+
page = response_handler.get_first_page
|
15
|
+
|
16
|
+
if page.success?
|
17
|
+
page.body
|
18
|
+
elsif response_handler.respond_to?(:retryable?) && response_handler.retryable?(page.status_code)
|
19
|
+
retried_page = attempt_retry
|
20
|
+
|
21
|
+
retried_page.body
|
22
|
+
else
|
23
|
+
error_handlers.each do |handler|
|
24
|
+
handler.call(page, self)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'api_client_builder/request'
|
2
|
+
|
3
|
+
module APIClientBuilder
|
4
|
+
class PostRequest < Request
|
5
|
+
# Yields the response body if the response was successful. Will call
|
6
|
+
# the response handlers if there was not a successful response.
|
7
|
+
#
|
8
|
+
# @return [JSON] the http response body
|
9
|
+
def response
|
10
|
+
response = response_handler.post_request(@body)
|
11
|
+
|
12
|
+
if response.success?
|
13
|
+
response
|
14
|
+
else
|
15
|
+
error_handlers.each do |handler|
|
16
|
+
handler.call(response, self)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'api_client_builder/request'
|
2
|
+
|
3
|
+
module APIClientBuilder
|
4
|
+
class PutRequest < Request
|
5
|
+
# Yields the response body if the response was successful. Will call
|
6
|
+
# the response handlers if there was not a successful response.
|
7
|
+
#
|
8
|
+
# @return [JSON] the http response body
|
9
|
+
def response
|
10
|
+
response = response_handler.put_request(@body)
|
11
|
+
|
12
|
+
if response.success?
|
13
|
+
response
|
14
|
+
else
|
15
|
+
error_handlers.each do |handler|
|
16
|
+
handler.call(response, self)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module APIClientBuilder
|
2
|
+
class DefaultPageError < StandardError;end
|
3
|
+
|
4
|
+
class Request
|
5
|
+
attr_reader :type, :response_handler, :body, :error_handlers_collection
|
6
|
+
|
7
|
+
# @param type [Symbol] defines the object type to be processed
|
8
|
+
# @option response_handler [ResponseHandler] the response handler. Usually
|
9
|
+
# a pagination strategy
|
10
|
+
# @option body [Hash] the body of the response from the source
|
11
|
+
def initialize(type, response_handler, body = {})
|
12
|
+
@type = type
|
13
|
+
@response_handler = response_handler
|
14
|
+
@body = body
|
15
|
+
@error_handlers_collection = []
|
16
|
+
end
|
17
|
+
|
18
|
+
# Yields the collection of error handlers that have been populated
|
19
|
+
# via the on_error interface. If none are defined, this will provide a
|
20
|
+
# default error handler that will provide context about the error and
|
21
|
+
# also how to define a new error handler.
|
22
|
+
#
|
23
|
+
# @return [Array<Block>] the error handlers collection
|
24
|
+
def error_handlers
|
25
|
+
if error_handlers_collection.empty?
|
26
|
+
self.on_error do |page, handler|
|
27
|
+
raise DefaultPageError,
|
28
|
+
"Default error for bad response. If you want to handle this" \
|
29
|
+
" error use #on_error on the response" \
|
30
|
+
" in your api consumer. Error Code: #{page.status_code}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
error_handlers_collection
|
34
|
+
end
|
35
|
+
|
36
|
+
# Used to define custom error handling on this response.
|
37
|
+
# The error handlers will be called if there is not a success
|
38
|
+
#
|
39
|
+
# @param block [Lambda] the error handling block to be stored in
|
40
|
+
# the error_handlers list
|
41
|
+
def on_error(&block)
|
42
|
+
@error_handlers_collection << block
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def attempt_retry
|
48
|
+
page = response_handler.retry_request
|
49
|
+
|
50
|
+
if page.success?
|
51
|
+
response_handler.reset_retries
|
52
|
+
return page
|
53
|
+
elsif response_handler.retryable?(page.status_code)
|
54
|
+
attempt_retry
|
55
|
+
else
|
56
|
+
notify_error_handlers(page)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def notify_error_handlers(page)
|
61
|
+
error_handlers.each do |handler|
|
62
|
+
handler.call(page, self)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module APIClientBuilder
|
2
|
+
# The default page object to be used to hold the response from the API
|
3
|
+
# in your response handler object. Any response object that will replace this
|
4
|
+
# must response to #success?, and #items
|
5
|
+
class Response
|
6
|
+
attr_accessor :body, :status_code
|
7
|
+
|
8
|
+
# @param body [String/Array/Hash] the response body
|
9
|
+
# @param status_code [Integer] the response status code
|
10
|
+
# @param success_range [Array<Integer>] the success range of this response
|
11
|
+
def initialize(body, status_code, success_range)
|
12
|
+
@body = body
|
13
|
+
@status_code = status_code
|
14
|
+
@success_range = success_range
|
15
|
+
@failed_reason = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
# Used to mark why the response failed
|
19
|
+
def mark_failed(reason)
|
20
|
+
@failed_reason = reason
|
21
|
+
end
|
22
|
+
|
23
|
+
# Defines the success conditional for a response by determining whether
|
24
|
+
# or not the status code of the response is within the defined success
|
25
|
+
# range
|
26
|
+
#
|
27
|
+
# @return [Boolean] whether or not the response is a success
|
28
|
+
def success?
|
29
|
+
@failed_reason.nil? && @success_range.include?(@status_code)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module APIClientBuilder
|
4
|
+
class NoURLError < StandardError;end
|
5
|
+
class URLGenerator
|
6
|
+
# Receives a domain and parses it into a URI
|
7
|
+
#
|
8
|
+
# @param domain [String] the domain of the API
|
9
|
+
def initialize(domain)
|
10
|
+
@base_uri = URI.parse(domain)
|
11
|
+
@base_uri = URI.parse('https://' + domain) if @base_uri.scheme.nil?
|
12
|
+
@base_uri.path << '/' unless @base_uri.path.end_with?('/')
|
13
|
+
end
|
14
|
+
|
15
|
+
# Defines a full API route and interpolates parameters into the route
|
16
|
+
# provided that the parameter sent in has a key that matches the parameter
|
17
|
+
# that is defined by the route.
|
18
|
+
#
|
19
|
+
# @param route [String] defines the route endpoint
|
20
|
+
# @param params [Hash] the optional params to be interpolated into the route
|
21
|
+
#
|
22
|
+
# @raise [ArgumentError] if route defined param is not provided
|
23
|
+
#
|
24
|
+
# @return [URI] the fully built route
|
25
|
+
def build_route(route, **params)
|
26
|
+
string_params = route.split('/').select{|param| param.start_with?(':')}
|
27
|
+
symboled_params = string_params.map{|param| param.tr(':', '').to_sym}
|
28
|
+
|
29
|
+
new_route = route.clone
|
30
|
+
symboled_params.each do |param|
|
31
|
+
value = params[param]
|
32
|
+
raise ArgumentError, "Param :#{param} is required" unless value
|
33
|
+
new_route.gsub!(":#{param}", value.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
@base_uri.merge(new_route)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api_client_builder
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jayce Higgins
|
@@ -61,8 +61,27 @@ executables: []
|
|
61
61
|
extensions: []
|
62
62
|
extra_rdoc_files: []
|
63
63
|
files:
|
64
|
+
- .dockerignore
|
65
|
+
- .gitignore
|
66
|
+
- .travis.yml
|
67
|
+
- Dockerfile
|
68
|
+
- Gemfile
|
69
|
+
- LICENSE.txt
|
70
|
+
- README.md
|
71
|
+
- api_client_builder-1.0.0.gem
|
64
72
|
- api_client_builder.gemspec
|
65
|
-
-
|
73
|
+
- build.sh
|
74
|
+
- docker-compose.yml
|
75
|
+
- lib/api_client_builder.rb
|
76
|
+
- lib/api_client_builder/api_client.rb
|
77
|
+
- lib/api_client_builder/get_collection_request.rb
|
78
|
+
- lib/api_client_builder/get_item_request.rb
|
79
|
+
- lib/api_client_builder/post_request.rb
|
80
|
+
- lib/api_client_builder/put_request.rb
|
81
|
+
- lib/api_client_builder/request.rb
|
82
|
+
- lib/api_client_builder/response.rb
|
83
|
+
- lib/api_client_builder/url_generator.rb
|
84
|
+
- lib/api_client_builder/version.rb
|
66
85
|
- spec/lib/api_client_builder/api_client_spec.rb
|
67
86
|
- spec/lib/api_client_builder/get_collection_request_spec.rb
|
68
87
|
- spec/lib/api_client_builder/get_item_request_spec.rb
|