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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6a42589201e1aa43a64afa11738378eca1182f7a
4
- data.tar.gz: 6c5fea2364dc9e85db7241ea02f9a8d66dced538
3
+ metadata.gz: 325e44729f05185e293461182fd2f405824ef233
4
+ data.tar.gz: 44970894a7563a62609223a3efd9b7d38a0ce557
5
5
  SHA512:
6
- metadata.gz: e6362629e413db828fdfefc719be94de0c10a23a099edf33e136a4a9e2cb7394d7087eb999844a324d89e1b710ddeeb4570914a990a683490bc7659b51f16b17
7
- data.tar.gz: f201de0c2a4c149e2958cc772260f9fa0c17c1bdd29c102d3f370043427b647e229342d30a24053eccb88794c58c1656d079f50ff2903190bf85c1a99bd8e66a
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
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Declare your gem's dependencies in logging.gemspec.
4
+ # Bundler will treat runtime dependencies like base dependencies, and
5
+ # development dependencies will be added by default to the :development group.
6
+ gemspec
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.
File without changes
Binary file
@@ -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
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+
3
+ docker-compose build --pull
4
+ docker-compose run test
@@ -0,0 +1,4 @@
1
+ test:
2
+ build: .
3
+ volumes:
4
+ - "coverage:/app/coverage"
@@ -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
@@ -0,0 +1,3 @@
1
+ module APIClientBuilder
2
+ VERSION = '1.0.1' unless defined?(APIClientBuilder::VERSION)
3
+ 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.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
- - readme.md
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