api_client_builder 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|