api_adaptor 0.0.0 → 0.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/.env.example +3 -0
- data/CHANGELOG.md +5 -1
- data/Gemfile +0 -6
- data/Gemfile.lock +89 -0
- data/README.md +72 -1
- data/lib/api_adaptor/base.rb +86 -0
- data/lib/api_adaptor/exceptions.rb +105 -0
- data/lib/api_adaptor/headers.rb +23 -0
- data/lib/api_adaptor/json_client.rb +202 -0
- data/lib/api_adaptor/list_response.rb +89 -0
- data/lib/api_adaptor/null_logger.rb +94 -0
- data/lib/api_adaptor/response.rb +183 -0
- data/lib/api_adaptor/variables.rb +15 -0
- data/lib/api_adaptor/version.rb +1 -1
- data/lib/api_adaptor.rb +1 -0
- metadata +125 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15a3973e6718c891d082915a0306fec878eaadbaaad013a25dcaba91ce0cb92f
|
4
|
+
data.tar.gz: d8da61b6f522276dcd74d569673bb6d4a0dd899ab43f9cb1ad85a85eec303c28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 24586320847f202c0913f4612a246c2e9e8fef9a380090b773101a8c9fd2204811051c9551fbd7a9c76ab6e25cea655185c213735f937e7a9278b30e120fbc54
|
7
|
+
data.tar.gz: 8c884ad4796aa32f2623c07d58aa15b8727f5b8dc22549fac2e7a9e2d61849643ce9059fb56edc3cc42c0f8ae0d19389377b20083d34afdf301e72f263c2f899
|
data/.env.example
ADDED
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
api_adaptor (0.0.0)
|
5
|
+
addressable (~> 2.8)
|
6
|
+
rest-client (~> 2.1)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
addressable (2.8.4)
|
12
|
+
public_suffix (>= 2.0.2, < 6.0)
|
13
|
+
ast (2.4.2)
|
14
|
+
crack (0.4.5)
|
15
|
+
rexml
|
16
|
+
diff-lcs (1.5.0)
|
17
|
+
domain_name (0.5.20190701)
|
18
|
+
unf (>= 0.0.5, < 1.0.0)
|
19
|
+
hashdiff (1.0.1)
|
20
|
+
http-accept (1.7.0)
|
21
|
+
http-cookie (1.0.5)
|
22
|
+
domain_name (~> 0.5)
|
23
|
+
json (2.6.3)
|
24
|
+
mime-types (3.4.1)
|
25
|
+
mime-types-data (~> 3.2015)
|
26
|
+
mime-types-data (3.2023.0218.1)
|
27
|
+
netrc (0.11.0)
|
28
|
+
parallel (1.23.0)
|
29
|
+
parser (3.2.2.1)
|
30
|
+
ast (~> 2.4.1)
|
31
|
+
public_suffix (5.0.1)
|
32
|
+
rainbow (3.1.1)
|
33
|
+
rake (13.0.6)
|
34
|
+
regexp_parser (2.8.0)
|
35
|
+
rest-client (2.1.0)
|
36
|
+
http-accept (>= 1.7.0, < 2.0)
|
37
|
+
http-cookie (>= 1.0.2, < 2.0)
|
38
|
+
mime-types (>= 1.16, < 4.0)
|
39
|
+
netrc (~> 0.8)
|
40
|
+
rexml (3.2.5)
|
41
|
+
rspec (3.12.0)
|
42
|
+
rspec-core (~> 3.12.0)
|
43
|
+
rspec-expectations (~> 3.12.0)
|
44
|
+
rspec-mocks (~> 3.12.0)
|
45
|
+
rspec-core (3.12.2)
|
46
|
+
rspec-support (~> 3.12.0)
|
47
|
+
rspec-expectations (3.12.3)
|
48
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
49
|
+
rspec-support (~> 3.12.0)
|
50
|
+
rspec-mocks (3.12.5)
|
51
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
52
|
+
rspec-support (~> 3.12.0)
|
53
|
+
rspec-support (3.12.0)
|
54
|
+
rubocop (1.51.0)
|
55
|
+
json (~> 2.3)
|
56
|
+
parallel (~> 1.10)
|
57
|
+
parser (>= 3.2.0.0)
|
58
|
+
rainbow (>= 2.2.2, < 4.0)
|
59
|
+
regexp_parser (>= 1.8, < 3.0)
|
60
|
+
rexml (>= 3.2.5, < 4.0)
|
61
|
+
rubocop-ast (>= 1.28.0, < 2.0)
|
62
|
+
ruby-progressbar (~> 1.7)
|
63
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
64
|
+
rubocop-ast (1.28.1)
|
65
|
+
parser (>= 3.2.1.0)
|
66
|
+
ruby-progressbar (1.13.0)
|
67
|
+
timecop (0.9.6)
|
68
|
+
unf (0.1.4)
|
69
|
+
unf_ext
|
70
|
+
unf_ext (0.0.8.2)
|
71
|
+
unicode-display_width (2.4.2)
|
72
|
+
webmock (3.18.1)
|
73
|
+
addressable (>= 2.8.0)
|
74
|
+
crack (>= 0.3.2)
|
75
|
+
hashdiff (>= 0.4.0, < 2.0.0)
|
76
|
+
|
77
|
+
PLATFORMS
|
78
|
+
x86_64-linux
|
79
|
+
|
80
|
+
DEPENDENCIES
|
81
|
+
api_adaptor!
|
82
|
+
rake (~> 13.0)
|
83
|
+
rspec (~> 3.0)
|
84
|
+
rubocop (~> 1.21)
|
85
|
+
timecop (~> 0.9)
|
86
|
+
webmock (~> 3.18)
|
87
|
+
|
88
|
+
BUNDLED WITH
|
89
|
+
2.4.13
|
data/README.md
CHANGED
@@ -15,7 +15,78 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
15
15
|
|
16
16
|
## Usage
|
17
17
|
|
18
|
-
|
18
|
+
Use the ApiAdaptor as a base class for your API wrapper, for example:
|
19
|
+
|
20
|
+
```
|
21
|
+
class MyApi < ApiAdaptor::Base
|
22
|
+
def base_url
|
23
|
+
endpoint
|
24
|
+
end
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
Use your new class to create a client that can make HTTP requests to JSON APIs for:
|
29
|
+
|
30
|
+
### GET JSON
|
31
|
+
|
32
|
+
```
|
33
|
+
client = MyApi.new
|
34
|
+
response = client.get_json("http://some.endpoint/json")
|
35
|
+
```
|
36
|
+
|
37
|
+
### POST JSON
|
38
|
+
|
39
|
+
```
|
40
|
+
client = MyApi.new
|
41
|
+
response = client.post_json("http://some.endpoint/json", { "foo": "bar" })
|
42
|
+
```
|
43
|
+
|
44
|
+
### PUT JSON
|
45
|
+
|
46
|
+
```
|
47
|
+
client = MyApi.new
|
48
|
+
response = client.put_json("http://some.endpoint/json", { "foo": "bar" })
|
49
|
+
```
|
50
|
+
|
51
|
+
### PATCH JSON
|
52
|
+
|
53
|
+
```
|
54
|
+
client = MyApi.new
|
55
|
+
response = client.patch_json("http://some.endpoint/json", { "foo": "bar" })
|
56
|
+
```
|
57
|
+
|
58
|
+
### DELETE JSON
|
59
|
+
|
60
|
+
```
|
61
|
+
client = MyApi.new
|
62
|
+
response = client.delete_json("http://some.endpoint/json", { "foo": "bar" })
|
63
|
+
```
|
64
|
+
|
65
|
+
### GET raw requests
|
66
|
+
|
67
|
+
you can also get a raw response from the API
|
68
|
+
|
69
|
+
```
|
70
|
+
client = MyApi.new
|
71
|
+
response = client.get_raw("http://some.endpoint/json")
|
72
|
+
```
|
73
|
+
|
74
|
+
## Environment variables
|
75
|
+
|
76
|
+
User Agent is populated with a default string.
|
77
|
+
See .env.example.
|
78
|
+
|
79
|
+
For instance if you provide:
|
80
|
+
```
|
81
|
+
APP_NAME=test_app
|
82
|
+
APP_VERSION=1.0.0
|
83
|
+
APP_CONTACT=contact@example.com
|
84
|
+
```
|
85
|
+
|
86
|
+
User agent would read
|
87
|
+
```
|
88
|
+
test_app/1.0.0 (contact@example.com)
|
89
|
+
```
|
19
90
|
|
20
91
|
## Contributing
|
21
92
|
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require_relative "json_client"
|
2
|
+
require "cgi"
|
3
|
+
require_relative "null_logger"
|
4
|
+
require_relative "list_response"
|
5
|
+
|
6
|
+
class ApiAdaptor::Base
|
7
|
+
class InvalidAPIURL < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
def client
|
13
|
+
@client ||= create_client
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_client
|
17
|
+
ApiAdaptor::JsonClient.new(options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def_delegators :client,
|
21
|
+
:get_json,
|
22
|
+
:post_json,
|
23
|
+
:put_json,
|
24
|
+
:patch_json,
|
25
|
+
:delete_json,
|
26
|
+
:get_raw,
|
27
|
+
:get_raw!,
|
28
|
+
:put_multipart,
|
29
|
+
:post_multipart
|
30
|
+
|
31
|
+
attr_reader :options
|
32
|
+
|
33
|
+
class << self
|
34
|
+
attr_writer :logger
|
35
|
+
attr_accessor :default_options
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.logger
|
39
|
+
@logger ||= ApiAdaptor::NullLogger.new
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(endpoint_url, options = {})
|
43
|
+
options[:endpoint_url] = endpoint_url
|
44
|
+
raise InvalidAPIURL unless endpoint_url =~ URI::RFC3986_Parser::RFC3986_URI
|
45
|
+
|
46
|
+
base_options = { logger: ApiAdaptor::Base.logger }
|
47
|
+
default_options = base_options.merge(ApiAdaptor::Base.default_options || {})
|
48
|
+
@options = default_options.merge(options)
|
49
|
+
self.endpoint = options[:endpoint_url]
|
50
|
+
end
|
51
|
+
|
52
|
+
def url_for_slug(slug, options = {})
|
53
|
+
"#{base_url}/#{slug}.json#{query_string(options)}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_list(url)
|
57
|
+
get_json(url) do |r|
|
58
|
+
ApiAdaptor::ListResponse.new(r, self)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
attr_accessor :endpoint
|
65
|
+
|
66
|
+
def query_string(params)
|
67
|
+
return "" if params.empty?
|
68
|
+
|
69
|
+
param_pairs = params.sort.map { |key, value|
|
70
|
+
case value
|
71
|
+
when Array
|
72
|
+
value.map do |v|
|
73
|
+
"#{CGI.escape("#{key}[]")}=#{CGI.escape(v.to_s)}"
|
74
|
+
end
|
75
|
+
else
|
76
|
+
"#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
|
77
|
+
end
|
78
|
+
}.flatten
|
79
|
+
|
80
|
+
"?#{param_pairs.join('&')}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def uri_encode(param)
|
84
|
+
Addressable::URI.encode(param.to_s)
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module ApiAdaptor
|
2
|
+
# Abstract error class
|
3
|
+
class BaseError < StandardError; end
|
4
|
+
|
5
|
+
class EndpointNotFound < BaseError; end
|
6
|
+
|
7
|
+
class TimedOutException < BaseError; end
|
8
|
+
|
9
|
+
class InvalidUrl < BaseError; end
|
10
|
+
|
11
|
+
class SocketErrorException < BaseError; end
|
12
|
+
|
13
|
+
# Superclass for all 4XX and 5XX errors
|
14
|
+
class HTTPErrorResponse < BaseError
|
15
|
+
attr_accessor :code, :error_details, :http_body
|
16
|
+
|
17
|
+
def initialize(code, message = nil, error_details = nil, http_body = nil)
|
18
|
+
super(message)
|
19
|
+
@code = code
|
20
|
+
@error_details = error_details
|
21
|
+
@http_body = http_body
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Superclass & fallback for all 4XX errors
|
26
|
+
class HTTPClientError < HTTPErrorResponse; end
|
27
|
+
|
28
|
+
class HTTPIntermittentClientError < HTTPClientError; end
|
29
|
+
|
30
|
+
class HTTPNotFound < HTTPClientError; end
|
31
|
+
|
32
|
+
class HTTPGone < HTTPClientError; end
|
33
|
+
|
34
|
+
class HTTPPayloadTooLarge < HTTPClientError; end
|
35
|
+
|
36
|
+
class HTTPUnauthorized < HTTPClientError; end
|
37
|
+
|
38
|
+
class HTTPForbidden < HTTPClientError; end
|
39
|
+
|
40
|
+
class HTTPConflict < HTTPClientError; end
|
41
|
+
|
42
|
+
class HTTPUnprocessableEntity < HTTPClientError; end
|
43
|
+
|
44
|
+
class HTTPBadRequest < HTTPClientError; end
|
45
|
+
|
46
|
+
class HTTPTooManyRequests < HTTPIntermittentClientError; end
|
47
|
+
|
48
|
+
# Superclass & fallback for all 5XX errors
|
49
|
+
class HTTPServerError < HTTPErrorResponse; end
|
50
|
+
|
51
|
+
class HTTPIntermittentServerError < HTTPServerError; end
|
52
|
+
|
53
|
+
class HTTPInternalServerError < HTTPServerError; end
|
54
|
+
|
55
|
+
class HTTPBadGateway < HTTPIntermittentServerError; end
|
56
|
+
|
57
|
+
class HTTPUnavailable < HTTPIntermittentServerError; end
|
58
|
+
|
59
|
+
class HTTPGatewayTimeout < HTTPIntermittentServerError; end
|
60
|
+
|
61
|
+
module ExceptionHandling
|
62
|
+
def build_specific_http_error(error, url, details = nil)
|
63
|
+
message = "URL: #{url}\nResponse body:\n#{error.http_body}"
|
64
|
+
code = error.http_code
|
65
|
+
error_class_for_code(code).new(code, message, details, error.http_body)
|
66
|
+
end
|
67
|
+
|
68
|
+
def error_class_for_code(code)
|
69
|
+
case code
|
70
|
+
when 400
|
71
|
+
ApiAdaptor::HTTPBadRequest
|
72
|
+
when 401
|
73
|
+
ApiAdaptor::HTTPUnauthorized
|
74
|
+
when 403
|
75
|
+
ApiAdaptor::HTTPForbidden
|
76
|
+
when 404
|
77
|
+
ApiAdaptor::HTTPNotFound
|
78
|
+
when 409
|
79
|
+
ApiAdaptor::HTTPConflict
|
80
|
+
when 410
|
81
|
+
ApiAdaptor::HTTPGone
|
82
|
+
when 413
|
83
|
+
ApiAdaptor::HTTPPayloadTooLarge
|
84
|
+
when 422
|
85
|
+
ApiAdaptor::HTTPUnprocessableEntity
|
86
|
+
when 429
|
87
|
+
ApiAdaptor::HTTPTooManyRequests
|
88
|
+
when (400..499)
|
89
|
+
ApiAdaptor::HTTPClientError
|
90
|
+
when 500
|
91
|
+
ApiAdaptor::HTTPInternalServerError
|
92
|
+
when 502
|
93
|
+
ApiAdaptor::HTTPBadGateway
|
94
|
+
when 503
|
95
|
+
ApiAdaptor::HTTPUnavailable
|
96
|
+
when 504
|
97
|
+
ApiAdaptor::HTTPGatewayTimeout
|
98
|
+
when (500..599)
|
99
|
+
ApiAdaptor::HTTPServerError
|
100
|
+
else
|
101
|
+
ApiAdaptor::HTTPErrorResponse
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ApiAdaptor
|
2
|
+
class Headers
|
3
|
+
class << self
|
4
|
+
def set_header(header_name, value)
|
5
|
+
header_data[header_name] = value
|
6
|
+
end
|
7
|
+
|
8
|
+
def headers
|
9
|
+
header_data.reject { |_k, v| (v.nil? || v.empty?) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def clear_headers
|
13
|
+
Thread.current[:headers] = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def header_data
|
19
|
+
Thread.current[:headers] ||= {}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require_relative "exceptions"
|
2
|
+
require_relative "variables"
|
3
|
+
require_relative "null_logger"
|
4
|
+
require_relative "headers"
|
5
|
+
require_relative "response"
|
6
|
+
|
7
|
+
require "rest-client"
|
8
|
+
|
9
|
+
module ApiAdaptor
|
10
|
+
class JsonClient
|
11
|
+
include ApiAdaptor::ExceptionHandling
|
12
|
+
|
13
|
+
attr_accessor :logger, :options
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
if options[:disable_timeout] || options[:timeout].to_i.negative?
|
17
|
+
raise "It is no longer possible to disable the timeout."
|
18
|
+
end
|
19
|
+
|
20
|
+
@logger = options[:logger] || NullLogger.new
|
21
|
+
@options = options
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.default_request_headers
|
25
|
+
{
|
26
|
+
"Accept" => "application/json",
|
27
|
+
"User-Agent" => "#{Variables.app_name}/#{Variables.app_version} (#{Variables. app_contact}",
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.default_request_with_json_body_headers
|
32
|
+
default_request_headers.merge(json_body_headers)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.json_body_headers
|
36
|
+
{
|
37
|
+
"Content-Type" => "application/json",
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
DEFAULT_TIMEOUT_IN_SECONDS = 4
|
42
|
+
|
43
|
+
def get_raw!(url)
|
44
|
+
do_raw_request(:get, url)
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_raw(url)
|
48
|
+
get_raw!(url)
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_json(url, additional_headers = {}, &create_response)
|
52
|
+
do_json_request(:get, url, nil, additional_headers, &create_response)
|
53
|
+
end
|
54
|
+
|
55
|
+
def post_json(url, params = {}, additional_headers = {})
|
56
|
+
do_json_request(:post, url, params, additional_headers)
|
57
|
+
end
|
58
|
+
|
59
|
+
def put_json(url, params, additional_headers = {})
|
60
|
+
do_json_request(:put, url, params, additional_headers)
|
61
|
+
end
|
62
|
+
|
63
|
+
def patch_json(url, params, additional_headers = {})
|
64
|
+
do_json_request(:patch, url, params, additional_headers)
|
65
|
+
end
|
66
|
+
|
67
|
+
def delete_json(url, params = {}, additional_headers = {})
|
68
|
+
do_json_request(:delete, url, params, additional_headers)
|
69
|
+
end
|
70
|
+
|
71
|
+
def post_multipart(url, params)
|
72
|
+
r = do_raw_request(:post, url, params.merge(multipart: true))
|
73
|
+
Response.new(r)
|
74
|
+
end
|
75
|
+
|
76
|
+
def put_multipart(url, params)
|
77
|
+
r = do_raw_request(:put, url, params.merge(multipart: true))
|
78
|
+
Response.new(r)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def do_raw_request(method, url, params = nil)
|
84
|
+
do_request(method, url, params)
|
85
|
+
rescue RestClient::Exception => e
|
86
|
+
raise build_specific_http_error(e, url, nil)
|
87
|
+
end
|
88
|
+
|
89
|
+
# method: the symbolic name of the method to use, e.g. :get, :post
|
90
|
+
# url: the request URL
|
91
|
+
# params: the data to send (JSON-serialised) in the request body
|
92
|
+
# additional_headers: headers to set on the request (in addition to the default ones)
|
93
|
+
# create_response: optional block to instantiate a custom response object
|
94
|
+
# from the Net::HTTPResponse
|
95
|
+
def do_json_request(method, url, params = nil, additional_headers = {}, &create_response)
|
96
|
+
begin
|
97
|
+
if params
|
98
|
+
additional_headers.merge!(self.class.json_body_headers)
|
99
|
+
end
|
100
|
+
response = do_request(method, url, (params.to_json if params), additional_headers)
|
101
|
+
rescue RestClient::Exception => e
|
102
|
+
# Attempt to parse the body as JSON if possible
|
103
|
+
error_details = begin
|
104
|
+
e.http_body ? JSON.parse(e.http_body) : nil
|
105
|
+
rescue JSON::ParserError
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
raise build_specific_http_error(e, url, error_details)
|
109
|
+
end
|
110
|
+
|
111
|
+
# If no custom response is given, just instantiate Response
|
112
|
+
create_response ||= proc { |r| Response.new(r) }
|
113
|
+
create_response.call(response)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Take a hash of parameters for Request#execute; return a hash of
|
117
|
+
# parameters with authentication information included
|
118
|
+
def with_auth_options(method_params)
|
119
|
+
if @options[:bearer_token]
|
120
|
+
headers = method_params[:headers] || {}
|
121
|
+
method_params.merge(headers: headers.merge(
|
122
|
+
"Authorization" => "Bearer #{@options[:bearer_token]}",
|
123
|
+
))
|
124
|
+
elsif @options[:basic_auth]
|
125
|
+
method_params.merge(
|
126
|
+
user: @options[:basic_auth][:user],
|
127
|
+
password: @options[:basic_auth][:password],
|
128
|
+
)
|
129
|
+
else
|
130
|
+
method_params
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
# Take a hash of parameters for Request#execute; return a hash of
|
136
|
+
# parameters with timeouts included
|
137
|
+
def with_timeout(method_params)
|
138
|
+
method_params.merge(
|
139
|
+
timeout: options[:timeout] || DEFAULT_TIMEOUT_IN_SECONDS,
|
140
|
+
open_timeout: options[:timeout] || DEFAULT_TIMEOUT_IN_SECONDS,
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
def with_headers(method_params, default_headers, additional_headers)
|
145
|
+
method_params.merge(
|
146
|
+
headers: default_headers
|
147
|
+
.merge(method_params[:headers] || {})
|
148
|
+
.merge(ApiAdaptor::Headers.headers)
|
149
|
+
.merge(additional_headers),
|
150
|
+
)
|
151
|
+
end
|
152
|
+
|
153
|
+
def with_ssl_options(method_params)
|
154
|
+
method_params.merge(
|
155
|
+
# This is the default value anyway, but we should probably be explicit
|
156
|
+
verify_ssl: OpenSSL::SSL::VERIFY_NONE,
|
157
|
+
)
|
158
|
+
end
|
159
|
+
|
160
|
+
def do_request(method, url, params = nil, additional_headers = {})
|
161
|
+
loggable = { request_uri: url, start_time: Time.now.to_f }
|
162
|
+
start_logging = loggable.merge(action: "start")
|
163
|
+
logger.debug start_logging.to_json
|
164
|
+
|
165
|
+
method_params = {
|
166
|
+
method: method,
|
167
|
+
url: url,
|
168
|
+
}
|
169
|
+
|
170
|
+
method_params[:payload] = params
|
171
|
+
method_params = with_timeout(method_params)
|
172
|
+
method_params = with_headers(method_params, self.class.default_request_headers, additional_headers)
|
173
|
+
method_params = with_auth_options(method_params)
|
174
|
+
if URI.parse(url).is_a? URI::HTTPS
|
175
|
+
method_params = with_ssl_options(method_params)
|
176
|
+
end
|
177
|
+
|
178
|
+
::RestClient::Request.execute(method_params)
|
179
|
+
rescue Errno::ECONNREFUSED => e
|
180
|
+
logger.error loggable.merge(status: "refused", error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
181
|
+
raise ApiAdaptor::EndpointNotFound, "Could not connect to #{url}"
|
182
|
+
rescue RestClient::Exceptions::Timeout => e
|
183
|
+
logger.error loggable.merge(status: "timeout", error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
184
|
+
raise ApiAdaptor::TimedOutException, e.message
|
185
|
+
rescue URI::InvalidURIError => e
|
186
|
+
logger.error loggable.merge(status: "invalid_uri", error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
187
|
+
raise ApiAdaptor::InvalidUrl, e.message
|
188
|
+
rescue RestClient::Exception => e
|
189
|
+
# Log the error here, since we have access to loggable, but raise the
|
190
|
+
# exception up to the calling method to deal with
|
191
|
+
loggable.merge!(status: e.http_code, end_time: Time.now.to_f, body: e.http_body)
|
192
|
+
logger.warn loggable.to_json
|
193
|
+
raise
|
194
|
+
rescue Errno::ECONNRESET => e
|
195
|
+
logger.error loggable.merge(status: "connection_reset", error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
196
|
+
raise ApiAdaptor::TimedOutException, e.message
|
197
|
+
rescue SocketError => e
|
198
|
+
logger.error loggable.merge(status: "socket_error", error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
199
|
+
raise ApiAdaptor::SocketErrorException, e.message
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "json"
|
2
|
+
require "api_adaptor/response"
|
3
|
+
require "link_header"
|
4
|
+
|
5
|
+
module ApiAdaptor
|
6
|
+
# Response class for lists of multiple items.
|
7
|
+
#
|
8
|
+
# This expects responses to be in a common format, with the list of results
|
9
|
+
# contained under the `results` key. The response may also have previous and
|
10
|
+
# subsequent pages, indicated by entries in the response's `Link` header.
|
11
|
+
class ListResponse < Response
|
12
|
+
# The ListResponse is instantiated with a reference back to the API client,
|
13
|
+
# so it can make requests for the subsequent pages
|
14
|
+
def initialize(response, api_client, options = {})
|
15
|
+
super(response, options)
|
16
|
+
@api_client = api_client
|
17
|
+
end
|
18
|
+
|
19
|
+
# Pass calls to `self.each` to the `results` sub-object, so we can iterate
|
20
|
+
# over the response directly
|
21
|
+
def_delegators :results, :each, :to_ary
|
22
|
+
|
23
|
+
def results
|
24
|
+
to_hash["results"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_next_page?
|
28
|
+
!page_link("next").nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
def next_page
|
32
|
+
# This shouldn't be a performance problem, since the cache will generally
|
33
|
+
# avoid us making multiple requests for the same page, but we shouldn't
|
34
|
+
# allow the data to change once it's already been loaded, so long as we
|
35
|
+
# retain a reference to any one page in the sequence
|
36
|
+
@next_page ||= if has_next_page?
|
37
|
+
@api_client.get_list page_link("next").href
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def has_previous_page?
|
42
|
+
!page_link("previous").nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
def previous_page
|
46
|
+
# See the note in `next_page` for why this is memoised
|
47
|
+
@previous_page ||= if has_previous_page?
|
48
|
+
@api_client.get_list(page_link("previous").href)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Transparently get all results across all pages. Compare this with #each
|
53
|
+
# or #results which only iterate over the current page.
|
54
|
+
#
|
55
|
+
# Example:
|
56
|
+
#
|
57
|
+
# list_response.with_subsequent_pages.each do |result|
|
58
|
+
# ...
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# or:
|
62
|
+
#
|
63
|
+
# list_response.with_subsequent_pages.count
|
64
|
+
#
|
65
|
+
# Pages of results are fetched on demand. When iterating, that means
|
66
|
+
# fetching pages as results from the current page are exhausted. If you
|
67
|
+
# invoke a method such as #count, this method will fetch all pages at that
|
68
|
+
# point. Note that the responses are stored so subsequent pages will not be
|
69
|
+
# loaded multiple times.
|
70
|
+
def with_subsequent_pages
|
71
|
+
Enumerator.new do |yielder|
|
72
|
+
each { |i| yielder << i }
|
73
|
+
if has_next_page?
|
74
|
+
next_page.with_subsequent_pages.each { |i| yielder << i }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def link_header
|
82
|
+
@link_header ||= LinkHeader.parse @http_response.headers[:link]
|
83
|
+
end
|
84
|
+
|
85
|
+
def page_link(rel)
|
86
|
+
link_header.find_link(["rel", rel])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Null logger class. This is essentially the same as sending data down the
|
4
|
+
# `/dev/null` black hole.
|
5
|
+
#
|
6
|
+
# @example Basic Usage
|
7
|
+
#
|
8
|
+
# logger = NullLogger.new
|
9
|
+
# Rails.logger = logger
|
10
|
+
#
|
11
|
+
#
|
12
|
+
# @example Basic Pattern Usage
|
13
|
+
# class SomeService
|
14
|
+
# def initialize(options = {})
|
15
|
+
# @logger = options[:logger] || NullLogger.new
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# def perform
|
19
|
+
# @logger.debug -> { "do some work here" }
|
20
|
+
# # .. ..
|
21
|
+
# @logger.info -> { "finished working" }
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# service = SomeService.new(logger: Logger.new(STDOUT))
|
26
|
+
# service.perform
|
27
|
+
#
|
28
|
+
# silent = SomeService.new(logger: NullLogger.new
|
29
|
+
# silent.perform
|
30
|
+
#
|
31
|
+
module ApiAdaptor
|
32
|
+
class NullLogger
|
33
|
+
# @param _args Anything that we want to ignore
|
34
|
+
# @return [nil]
|
35
|
+
def unknown(*_args)
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param _args Anything that we want to ignore
|
40
|
+
# @return [nil]
|
41
|
+
def fatal(*_args)
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [FALSE]
|
46
|
+
def fatal?
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param _args Anything that we want to ignore
|
51
|
+
# @return [nil]
|
52
|
+
def error(*_args)
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [FALSE]
|
57
|
+
def error?
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param _args Anything that we want to ignore
|
62
|
+
# @return [nil]
|
63
|
+
def warn(*_args)
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [FALSE]
|
68
|
+
def warn?
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
# @param _args Anything that we want to ignore
|
73
|
+
# @return [nil]
|
74
|
+
def info(*_args)
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [FALSE]
|
79
|
+
def info?
|
80
|
+
false
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param _args Anything that we want to ignore
|
84
|
+
# @return [nil]
|
85
|
+
def debug(*_args)
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [FALSE]
|
90
|
+
def debug?
|
91
|
+
false
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require "json"
|
2
|
+
require "forwardable"
|
3
|
+
|
4
|
+
module ApiAdaptor
|
5
|
+
# This wraps an HTTP response with a JSON body.
|
6
|
+
#
|
7
|
+
# Responses can be configured to use relative URLs for `web_url` properties.
|
8
|
+
# API endpoints should return absolute URLs so that they make sense outside of the
|
9
|
+
# domain context. However on systems within an API we want to present relative URLs.
|
10
|
+
# By specifying a base URI, this will convert all matching web_urls into relative URLs
|
11
|
+
# This is useful on non-canonical frontends, such as those in staging environments.
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
#
|
15
|
+
# r = Response.new(response, web_urls_relative_to: "https://www.gov.uk")
|
16
|
+
# r['results'][0]['web_url']
|
17
|
+
# => "/bank-holidays"
|
18
|
+
class Response
|
19
|
+
extend Forwardable
|
20
|
+
include Enumerable
|
21
|
+
|
22
|
+
class CacheControl < Hash
|
23
|
+
PATTERN = /([-a-z]+)(?:\s*=\s*([^,\s]+))?,?+/i
|
24
|
+
|
25
|
+
def initialize(value = nil)
|
26
|
+
super()
|
27
|
+
parse(value)
|
28
|
+
end
|
29
|
+
|
30
|
+
def public?
|
31
|
+
self["public"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def private?
|
35
|
+
self["private"]
|
36
|
+
end
|
37
|
+
|
38
|
+
def no_cache?
|
39
|
+
self["no-cache"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def no_store?
|
43
|
+
self["no-store"]
|
44
|
+
end
|
45
|
+
|
46
|
+
def must_revalidate?
|
47
|
+
self["must-revalidate"]
|
48
|
+
end
|
49
|
+
|
50
|
+
def proxy_revalidate?
|
51
|
+
self["proxy-revalidate"]
|
52
|
+
end
|
53
|
+
|
54
|
+
def max_age
|
55
|
+
self["max-age"].to_i if key?("max-age")
|
56
|
+
end
|
57
|
+
|
58
|
+
def reverse_max_age
|
59
|
+
self["r-maxage"].to_i if key?("r-maxage")
|
60
|
+
end
|
61
|
+
alias_method :r_maxage, :reverse_max_age
|
62
|
+
|
63
|
+
def shared_max_age
|
64
|
+
self["s-maxage"].to_i if key?("r-maxage")
|
65
|
+
end
|
66
|
+
alias_method :s_maxage, :shared_max_age
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
directives = []
|
70
|
+
values = []
|
71
|
+
|
72
|
+
each do |key, value|
|
73
|
+
if value == true
|
74
|
+
directives << key
|
75
|
+
elsif value
|
76
|
+
values << "#{key}=#{value}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
(directives.sort + values.sort).join(", ")
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def parse(header)
|
86
|
+
return if header.nil? || header.empty?
|
87
|
+
|
88
|
+
header.scan(PATTERN).each do |name, value|
|
89
|
+
self[name.downcase] = value || true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def_delegators :to_hash, :[], :"<=>", :each, :dig
|
95
|
+
|
96
|
+
def initialize(http_response, options = {})
|
97
|
+
@http_response = http_response
|
98
|
+
@web_urls_relative_to = options[:web_urls_relative_to] ? URI.parse(options[:web_urls_relative_to]) : nil
|
99
|
+
end
|
100
|
+
|
101
|
+
def raw_response_body
|
102
|
+
@http_response.body
|
103
|
+
end
|
104
|
+
|
105
|
+
def code
|
106
|
+
# Return an integer code for consistency with HTTPErrorResponse
|
107
|
+
@http_response.code
|
108
|
+
end
|
109
|
+
|
110
|
+
def headers
|
111
|
+
@http_response.headers
|
112
|
+
end
|
113
|
+
|
114
|
+
def expires_at
|
115
|
+
if headers[:date] && cache_control.max_age
|
116
|
+
response_date = Time.parse(headers[:date])
|
117
|
+
response_date + cache_control.max_age
|
118
|
+
elsif headers[:expires]
|
119
|
+
Time.parse(headers[:expires])
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def expires_in
|
124
|
+
return unless headers[:date]
|
125
|
+
|
126
|
+
age = Time.now.utc - Time.parse(headers[:date])
|
127
|
+
|
128
|
+
if cache_control.max_age
|
129
|
+
cache_control.max_age - age.to_i
|
130
|
+
elsif headers[:expires]
|
131
|
+
Time.parse(headers[:expires]).to_i - Time.now.utc.to_i
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def cache_control
|
136
|
+
@cache_control ||= CacheControl.new(headers[:cache_control])
|
137
|
+
end
|
138
|
+
|
139
|
+
def to_hash
|
140
|
+
parsed_content
|
141
|
+
end
|
142
|
+
|
143
|
+
def parsed_content
|
144
|
+
@parsed_content ||= transform_parsed(JSON.parse(@http_response.body))
|
145
|
+
end
|
146
|
+
|
147
|
+
def present?
|
148
|
+
true
|
149
|
+
end
|
150
|
+
|
151
|
+
def blank?
|
152
|
+
false
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def transform_parsed(value)
|
158
|
+
return value if @web_urls_relative_to.nil?
|
159
|
+
|
160
|
+
case value
|
161
|
+
when Hash
|
162
|
+
Hash[value.map do |k, v|
|
163
|
+
# NOTE: Don't bother transforming if the value is nil
|
164
|
+
if k == "web_url" && v
|
165
|
+
# Use relative URLs to route when the web_url value is on the
|
166
|
+
# same domain as the site root. Note that we can't just use the
|
167
|
+
# `route_to` method, as this would give us technically correct
|
168
|
+
# but potentially confusing `//host/path` URLs for URLs with the
|
169
|
+
# same scheme but different hosts.
|
170
|
+
relative_url = @web_urls_relative_to.route_to(v)
|
171
|
+
[k, relative_url.host ? v : relative_url.to_s]
|
172
|
+
else
|
173
|
+
[k, transform_parsed(v)]
|
174
|
+
end
|
175
|
+
end]
|
176
|
+
when Array
|
177
|
+
value.map { |v| transform_parsed(v) }
|
178
|
+
else
|
179
|
+
value
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ApiAdaptor
|
2
|
+
module Variables
|
3
|
+
def self.app_name
|
4
|
+
ENV['APP_NAME'] || "Ruby ApiAdaptor App"
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.app_version
|
8
|
+
ENV['APP_VERSION'] || "Version not stated"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.app_contact
|
12
|
+
ENV['APP_CONTACT'] || "Contact not stated"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/api_adaptor/version.rb
CHANGED
data/lib/api_adaptor.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,127 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api_adaptor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Huw Diprose
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
12
|
-
dependencies:
|
11
|
+
date: 2023-06-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rest-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: addressable
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.8'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.8'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: link_header
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.0.8
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.0.8
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: webmock
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.18'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.18'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: timecop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.9'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.9'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '13.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '13.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.21'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.21'
|
13
125
|
description: A basic adaptor to send HTTP requests and parse the responses. Intended
|
14
126
|
to bootstrap the quick writing of Adaptors for specific APIs, without having to
|
15
127
|
write the same old JSON request and processing time and time again.
|
@@ -19,15 +131,25 @@ executables: []
|
|
19
131
|
extensions: []
|
20
132
|
extra_rdoc_files: []
|
21
133
|
files:
|
134
|
+
- ".env.example"
|
22
135
|
- ".rspec"
|
23
136
|
- ".rubocop.yml"
|
24
137
|
- CHANGELOG.md
|
25
138
|
- CODE_OF_CONDUCT.md
|
26
139
|
- Gemfile
|
140
|
+
- Gemfile.lock
|
27
141
|
- LICENSE.txt
|
28
142
|
- README.md
|
29
143
|
- Rakefile
|
30
144
|
- lib/api_adaptor.rb
|
145
|
+
- lib/api_adaptor/base.rb
|
146
|
+
- lib/api_adaptor/exceptions.rb
|
147
|
+
- lib/api_adaptor/headers.rb
|
148
|
+
- lib/api_adaptor/json_client.rb
|
149
|
+
- lib/api_adaptor/list_response.rb
|
150
|
+
- lib/api_adaptor/null_logger.rb
|
151
|
+
- lib/api_adaptor/response.rb
|
152
|
+
- lib/api_adaptor/variables.rb
|
31
153
|
- lib/api_adaptor/version.rb
|
32
154
|
- sig/api_adaptor.rbs
|
33
155
|
homepage: https://github.com/huwd/api_adaptor
|