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