api_adaptor 0.0.1 → 0.1.0

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
  SHA256:
3
- metadata.gz: 15a3973e6718c891d082915a0306fec878eaadbaaad013a25dcaba91ce0cb92f
4
- data.tar.gz: d8da61b6f522276dcd74d569673bb6d4a0dd899ab43f9cb1ad85a85eec303c28
3
+ metadata.gz: ec2d6e2bc7c54d07e88bee9945a772f952076ba24b3f1fdf176e951bae3f8dad
4
+ data.tar.gz: c0d9d319cd02061d462eaba8bdc8cfd5a8dda94df5973e7ba8ef78fc3dada248
5
5
  SHA512:
6
- metadata.gz: 24586320847f202c0913f4612a246c2e9e8fef9a380090b773101a8c9fd2204811051c9551fbd7a9c76ab6e25cea655185c213735f937e7a9278b30e120fbc54
7
- data.tar.gz: 8c884ad4796aa32f2623c07d58aa15b8727f5b8dc22549fac2e7a9e2d61849643ce9059fb56edc3cc42c0f8ae0d19389377b20083d34afdf301e72f263c2f899
6
+ metadata.gz: 7ac1bd16754ce9ae420ad5ba9d3fc9d92ad280f896afcb5312688b880b228c179ca75a8edb6a16811064f6c3d71805045aae22312137b325b067a0339c801acf
7
+ data.tar.gz: 9dd42bfacca8f947ac4977c3f1a4970db143118dd101c851b61ecae8a4462cde109f4157e542020e57f147f01cc83973c11de3a8e9d0093dd7991241b1669485
data/.rubocop.yml CHANGED
@@ -1,5 +1,7 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.6
2
+ TargetRubyVersion: 3.2
3
+ NewCops: disable
4
+ SuggestExtensions: false
3
5
 
4
6
  Style/StringLiterals:
5
7
  Enabled: true
@@ -10,4 +12,10 @@ Style/StringLiteralsInInterpolation:
10
12
  EnforcedStyle: double_quotes
11
13
 
12
14
  Layout/LineLength:
13
- Max: 120
15
+ Enabled: false
16
+
17
+ Metrics:
18
+ Enabled: false
19
+
20
+ Style/Documentation:
21
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,4 +1,17 @@
1
- ## [Unreleased]
1
+ ## [0.1.0] - 2025-31-01
2
+
3
+ - Improvements to 307, 308 redirect behaviour
4
+ - Greater configuration over redirect behaviour
5
+ - Add CI/CD to Rubygems
6
+ - Test against Ruby v4.0
7
+
8
+ ## [0.0.2] - 2024-14-01
9
+
10
+ - Improvements to CI/CD
11
+ - Enable CodeQL
12
+ - Enable Dependabot
13
+ - Enable Rubocop Testing
14
+ - Enable unit testing against Ruby v3.2, v3.3, v3.4
2
15
 
3
16
  ## [0.0.1] - 2023-06-01
4
17
 
data/Gemfile.lock CHANGED
@@ -1,75 +1,96 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- api_adaptor (0.0.0)
4
+ api_adaptor (0.1.0)
5
5
  addressable (~> 2.8)
6
+ base64 (~> 0.3)
7
+ bigdecimal (>= 3.3, < 5.0)
8
+ link_header (~> 0.0.8)
9
+ logger (>= 1.6, < 2.0)
6
10
  rest-client (~> 2.1)
7
11
 
8
12
  GEM
9
13
  remote: https://rubygems.org/
10
14
  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
+ addressable (2.8.8)
16
+ public_suffix (>= 2.0.2, < 8.0)
17
+ ast (2.4.3)
18
+ base64 (0.3.0)
19
+ bigdecimal (4.0.1)
20
+ crack (1.0.1)
21
+ bigdecimal
15
22
  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)
23
+ diff-lcs (1.6.2)
24
+ docile (1.4.1)
25
+ domain_name (0.6.20240107)
26
+ hashdiff (1.2.1)
20
27
  http-accept (1.7.0)
21
28
  http-cookie (1.0.5)
22
29
  domain_name (~> 0.5)
23
- json (2.6.3)
24
- mime-types (3.4.1)
30
+ json (2.18.0)
31
+ language_server-protocol (3.17.0.5)
32
+ link_header (0.0.8)
33
+ lint_roller (1.1.0)
34
+ logger (1.7.0)
35
+ mime-types (3.5.2)
25
36
  mime-types-data (~> 3.2015)
26
- mime-types-data (3.2023.0218.1)
37
+ mime-types-data (3.2023.1205)
27
38
  netrc (0.11.0)
28
- parallel (1.23.0)
29
- parser (3.2.2.1)
39
+ parallel (1.27.0)
40
+ parser (3.3.10.1)
30
41
  ast (~> 2.4.1)
31
- public_suffix (5.0.1)
42
+ racc
43
+ prism (1.9.0)
44
+ public_suffix (7.0.2)
45
+ racc (1.8.1)
32
46
  rainbow (3.1.1)
33
- rake (13.0.6)
34
- regexp_parser (2.8.0)
47
+ rake (13.3.1)
48
+ regexp_parser (2.11.3)
35
49
  rest-client (2.1.0)
36
50
  http-accept (>= 1.7.0, < 2.0)
37
51
  http-cookie (>= 1.0.2, < 2.0)
38
52
  mime-types (>= 1.16, < 4.0)
39
53
  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)
54
+ rexml (3.4.4)
55
+ rspec (3.13.2)
56
+ rspec-core (~> 3.13.0)
57
+ rspec-expectations (~> 3.13.0)
58
+ rspec-mocks (~> 3.13.0)
59
+ rspec-core (3.13.6)
60
+ rspec-support (~> 3.13.0)
61
+ rspec-expectations (3.13.5)
48
62
  diff-lcs (>= 1.2.0, < 2.0)
49
- rspec-support (~> 3.12.0)
50
- rspec-mocks (3.12.5)
63
+ rspec-support (~> 3.13.0)
64
+ rspec-mocks (3.13.7)
51
65
  diff-lcs (>= 1.2.0, < 2.0)
52
- rspec-support (~> 3.12.0)
53
- rspec-support (3.12.0)
54
- rubocop (1.51.0)
66
+ rspec-support (~> 3.13.0)
67
+ rspec-support (3.13.6)
68
+ rubocop (1.84.0)
55
69
  json (~> 2.3)
70
+ language_server-protocol (~> 3.17.0.2)
71
+ lint_roller (~> 1.1.0)
56
72
  parallel (~> 1.10)
57
- parser (>= 3.2.0.0)
73
+ parser (>= 3.3.0.2)
58
74
  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)
75
+ regexp_parser (>= 2.9.3, < 3.0)
76
+ rubocop-ast (>= 1.49.0, < 2.0)
62
77
  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)
78
+ unicode-display_width (>= 2.4.0, < 4.0)
79
+ rubocop-ast (1.49.0)
80
+ parser (>= 3.3.7.2)
81
+ prism (~> 1.7)
66
82
  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)
83
+ simplecov (0.22.0)
84
+ docile (~> 1.1)
85
+ simplecov-html (~> 0.11)
86
+ simplecov_json_formatter (~> 0.1)
87
+ simplecov-html (0.13.1)
88
+ simplecov_json_formatter (0.1.4)
89
+ timecop (0.9.10)
90
+ unicode-display_width (3.2.0)
91
+ unicode-emoji (~> 4.1)
92
+ unicode-emoji (4.2.0)
93
+ webmock (3.26.1)
73
94
  addressable (>= 2.8.0)
74
95
  crack (>= 0.3.2)
75
96
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -82,6 +103,7 @@ DEPENDENCIES
82
103
  rake (~> 13.0)
83
104
  rspec (~> 3.0)
84
105
  rubocop (~> 1.21)
106
+ simplecov (~> 0.22)
85
107
  timecop (~> 0.9)
86
108
  webmock (~> 3.18)
87
109
 
data/README.md CHANGED
@@ -7,90 +7,198 @@ Intended to bootstrap the quick writing of Adaptors for specific APIs, without h
7
7
 
8
8
  Install the gem and add to the application's Gemfile by executing:
9
9
 
10
- $ bundle add api_adaptor
10
+ ```shell
11
+ bundle add api_adaptor
12
+ ```
11
13
 
12
14
  If bundler is not being used to manage dependencies, install the gem by executing:
13
15
 
14
- $ gem install api_adaptor
16
+ ```shell
17
+ gem install api_adaptor
18
+ ```
19
+
20
+ ## Releasing
21
+
22
+ Publishing is handled by GitHub Actions when you push a version tag.
23
+
24
+ - RubyGems publishing uses **Trusted Publishing (OIDC)** via `rubygems/release-gem`
25
+ - Ensure `api_adaptor` is configured on RubyGems.org with this repository/workflow as a trusted publisher.
26
+ - Bump `ApiAdaptor::VERSION` in `lib/api_adaptor/version.rb`.
27
+ - Tag the release as `vX.Y.Z` (must match `ApiAdaptor::VERSION`) and push the tag:
28
+
29
+ ```shell
30
+ git tag v0.1.1
31
+ git push origin v0.1.1
32
+ ```
15
33
 
16
34
  ## Usage
17
35
 
18
36
  Use the ApiAdaptor as a base class for your API wrapper, for example:
19
37
 
20
- ```
21
- class MyApi < ApiAdaptor::Base
22
- def base_url
23
- endpoint
24
- end
25
- end
38
+ ```ruby
39
+ class MyApi < ApiAdaptor::Base; end
26
40
  ```
27
41
 
28
42
  Use your new class to create a client that can make HTTP requests to JSON APIs for:
29
43
 
30
- ### GET JSON
31
-
32
- ```
33
- client = MyApi.new
34
- response = client.get_json("http://some.endpoint/json")
44
+ ```ruby
45
+ client = MyApi.new
46
+ client.get_json("http://some.endpoint/json")
47
+ client.post_json("http://some.endpoint/json", { "foo": "bar" })
48
+ client.put_json("http://some.endpoint/json", { "foo": "bar" })
49
+ client.patch_json("http://some.endpoint/json", { "foo": "bar" })
50
+ client.delete_json("http://some.endpoint/json", { "foo": "bar" })
35
51
  ```
36
52
 
37
- ### POST JSON
53
+ You can also get a raw response from the API
38
54
 
39
- ```
40
- client = MyApi.new
41
- response = client.post_json("http://some.endpoint/json", { "foo": "bar" })
55
+ ```ruby
56
+ client.get_raw("http://some.endpoint/json")
42
57
  ```
43
58
 
44
- ### PUT JSON
59
+ ### Redirects (3xx)
45
60
 
46
- ```
47
- client = MyApi.new
48
- response = client.put_json("http://some.endpoint/json", { "foo": "bar" })
49
- ```
61
+ Some APIs return a `3xx` response (commonly `307`/`308`) with a `Location` header that points to the “real” URL.
62
+ `ApiAdaptor::JsonClient` follows these redirects in a controlled way so callers consistently receive a final JSON response.
63
+
64
+ #### Defaults
65
+
66
+ - Redirects are followed for `GET`/`HEAD` when the status is `301`, `302`, `303`, `307`, or `308`.
67
+ - `max_redirects` defaults to `3`.
68
+ - Cross-origin redirects (scheme/host/port change) are allowed by default, but credentials are **not** forwarded.
69
+ - Non-GET requests (`POST`, `PUT`, `PATCH`, `DELETE`) do **not** follow redirects by default.
70
+
71
+ #### Configuration
50
72
 
51
- ### PATCH JSON
73
+ You can configure redirect behaviour by passing options into your `ApiAdaptor::Base` subclass (they are forwarded to `ApiAdaptor::JsonClient`):
52
74
 
75
+ ```ruby
76
+ client = MyApi.new(
77
+ "https://example.com",
78
+ max_redirects: 3,
79
+ allow_cross_origin_redirects: true,
80
+ forward_auth_on_cross_origin_redirects: false,
81
+ follow_non_get_redirects: false
82
+ )
83
+
84
+ client.get_json("https://example.com/some/endpoint.json")
53
85
  ```
54
- client = MyApi.new
55
- response = client.patch_json("http://some.endpoint/json", { "foo": "bar" })
86
+
87
+ Or, if you are using `ApiAdaptor::JsonClient` directly:
88
+
89
+ ```ruby
90
+ client = ApiAdaptor::JsonClient.new(
91
+ bearer_token: "SOME_BEARER_TOKEN",
92
+ max_redirects: 3,
93
+ allow_cross_origin_redirects: true,
94
+ forward_auth_on_cross_origin_redirects: false
95
+ )
96
+
97
+ client.get_json("https://example.com/some/endpoint.json")
56
98
  ```
57
99
 
58
- ### DELETE JSON
100
+ #### Security: auth and cross-origin redirects
101
+
102
+ Following redirects across origins can accidentally leak credentials (for example `Authorization` bearer tokens) to an unexpected host.
103
+ To reduce risk:
104
+
105
+ - By default, when the redirect target is cross-origin, `Authorization` is stripped and basic auth is not applied.
106
+ - To completely prevent cross-origin redirects, set `allow_cross_origin_redirects: false`.
107
+ - Only set `forward_auth_on_cross_origin_redirects: true` if you fully trust the redirect target.
59
108
 
109
+ #### Non-GET redirects (risk of replay)
110
+
111
+ Redirects for non-GET requests are risky because they may cause a request to be replayed (and potentially create duplicate side effects).
112
+ For that reason, redirect-following is disabled for non-GET requests by default.
113
+
114
+ If you do want to follow `307`/`308` for non-GET requests, you can opt in:
115
+
116
+ ```ruby
117
+ client = MyApi.new(
118
+ "https://example.com",
119
+ follow_non_get_redirects: true
120
+ )
121
+
122
+ client.post_json("https://example.com/some/endpoint.json", { "a" => 1 })
60
123
  ```
61
- client = MyApi.new
62
- response = client.delete_json("http://some.endpoint/json", { "foo": "bar" })
124
+
125
+ #### Handling redirect failures
126
+
127
+ You can rescue these redirect-specific exceptions:
128
+
129
+ - `ApiAdaptor::TooManyRedirects` (exceeded `max_redirects`)
130
+ - `ApiAdaptor::RedirectLocationMissing` (a redirect response without a usable `Location`)
131
+
132
+ Example:
133
+
134
+ ```ruby
135
+ begin
136
+ client.get_json("https://example.com/some/endpoint.json")
137
+ rescue ApiAdaptor::TooManyRedirects, ApiAdaptor::RedirectLocationMissing => e
138
+ # handle / log / retry / surface a friendly message
139
+ raise e
140
+ end
63
141
  ```
64
142
 
65
- ### GET raw requests
143
+ ### Conventional usage
66
144
 
67
- you can also get a raw response from the API
145
+ An example of how to use this repository to bootstrap an API can be found in the [WikiData REST adaptor](https://github.com/huwd/wikidata_adaptor) it was built for.
68
146
 
147
+ A REST API module can be created with:
148
+
149
+ ```ruby
150
+ module MyApiAdaptor
151
+ # Wikidata REST API class
152
+ class RestApi < ApiAdaptor::Base
153
+ def get_foo(foo_id)
154
+ get_json("#{endpoint}/foo/#{CGI.escape(foo_id)}")
155
+ end
156
+ end
157
+ end
69
158
  ```
70
- client = MyApi.new
71
- response = client.get_raw("http://some.endpoint/json")
159
+
160
+ and can be wrapped in a top level module:
161
+
162
+ ```ruby
163
+ module MyApiAdaptor
164
+ class Error < StandardError; end
165
+
166
+ def self.rest_endpoint
167
+ ENV["MYAPI_REST_ENDPOINT"] || "https://example.com"
168
+ end
169
+
170
+ def self.rest_api
171
+ MyApiAdaptor::RestApi.new(rest_endpoint)
172
+ end
173
+ end
72
174
  ```
73
175
 
176
+ The intended convention is to have test helpers ship alongside the actual Adaptor code.
177
+ See [WikiData examples here](https://github.com/huwd/wikidata_adaptor/blob/main/lib/wikidata_adaptor/test_helpers/rest_api.rb).
178
+ This allows other applications that integrate the API Adaptor to easily mock out calls and receive representative data back.
179
+
74
180
  ## Environment variables
75
181
 
76
182
  User Agent is populated with a default string.
77
183
  See .env.example.
78
184
 
79
185
  For instance if you provide:
80
- ```
186
+
187
+ ```bash
81
188
  APP_NAME=test_app
82
189
  APP_VERSION=1.0.0
83
190
  APP_CONTACT=contact@example.com
84
191
  ```
85
192
 
86
193
  User agent would read
87
- ```
194
+
195
+ ```text
88
196
  test_app/1.0.0 (contact@example.com)
89
197
  ```
90
198
 
91
199
  ## Contributing
92
200
 
93
- Bug reports and pull requests are welcome on GitHub at https://github.com/huwd/api_adaptor. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/huwd/api_adaptor/blob/main/CODE_OF_CONDUCT.md).
201
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/huwd/api_adaptor>. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/huwd/api_adaptor/blob/main/CODE_OF_CONDUCT.md).
94
202
 
95
203
  ## License
96
204
 
@@ -0,0 +1,3 @@
1
+ {
2
+ "foo": "bar"
3
+ }
@@ -1,86 +1,90 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "json_client"
2
4
  require "cgi"
3
5
  require_relative "null_logger"
4
6
  require_relative "list_response"
5
7
 
6
- class ApiAdaptor::Base
7
- class InvalidAPIURL < StandardError
8
- end
8
+ module ApiAdaptor
9
+ class Base
10
+ class InvalidAPIURL < StandardError
11
+ end
9
12
 
10
- extend Forwardable
13
+ extend Forwardable
11
14
 
12
- def client
13
- @client ||= create_client
14
- end
15
+ def client
16
+ @client ||= create_client
17
+ end
15
18
 
16
- def create_client
17
- ApiAdaptor::JsonClient.new(options)
18
- end
19
+ def create_client
20
+ ApiAdaptor::JsonClient.new(options)
21
+ end
19
22
 
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
23
+ def_delegators :client,
24
+ :get_json,
25
+ :post_json,
26
+ :put_json,
27
+ :patch_json,
28
+ :delete_json,
29
+ :get_raw,
30
+ :get_raw!,
31
+ :put_multipart,
32
+ :post_multipart
33
+
34
+ attr_reader :options
35
+
36
+ class << self
37
+ attr_writer :logger
38
+ attr_accessor :default_options
39
+ end
37
40
 
38
- def self.logger
39
- @logger ||= ApiAdaptor::NullLogger.new
40
- end
41
+ def self.logger
42
+ @logger ||= ApiAdaptor::NullLogger.new
43
+ end
41
44
 
42
- def initialize(endpoint_url, options = {})
43
- options[:endpoint_url] = endpoint_url
44
- raise InvalidAPIURL unless endpoint_url =~ URI::RFC3986_Parser::RFC3986_URI
45
+ def initialize(endpoint_url = nil, options = {})
46
+ options[:endpoint_url] = endpoint_url
47
+ raise InvalidAPIURL if !endpoint_url.nil? && endpoint_url !~ URI::RFC3986_Parser::RFC3986_URI
45
48
 
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
49
+ base_options = { logger: ApiAdaptor::Base.logger }
50
+ default_options = base_options.merge(ApiAdaptor::Base.default_options || {})
51
+ @options = default_options.merge(options)
52
+ self.endpoint = options[:endpoint_url]
53
+ end
51
54
 
52
- def url_for_slug(slug, options = {})
53
- "#{base_url}/#{slug}.json#{query_string(options)}"
54
- end
55
+ def url_for_slug(slug, options = {})
56
+ "#{base_url}/#{slug}.json#{query_string(options)}"
57
+ end
55
58
 
56
- def get_list(url)
57
- get_json(url) do |r|
58
- ApiAdaptor::ListResponse.new(r, self)
59
+ def get_list(url)
60
+ get_json(url) do |r|
61
+ ApiAdaptor::ListResponse.new(r, self)
62
+ end
59
63
  end
60
- end
61
64
 
62
- private
65
+ private
63
66
 
64
- attr_accessor :endpoint
67
+ attr_accessor :endpoint
65
68
 
66
- def query_string(params)
67
- return "" if params.empty?
69
+ def query_string(params)
70
+ return "" if params.empty?
68
71
 
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)}"
72
+ param_pairs = params.sort.map do |key, value|
73
+ case value
74
+ when Array
75
+ value.map do |v|
76
+ "#{CGI.escape("#{key}[]")}=#{CGI.escape(v.to_s)}"
77
+ end
78
+ else
79
+ "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
74
80
  end
75
- else
76
- "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
77
- end
78
- }.flatten
81
+ end.flatten
79
82
 
80
- "?#{param_pairs.join('&')}"
81
- end
83
+ "?#{param_pairs.join("&")}"
84
+ end
82
85
 
83
- def uri_encode(param)
84
- Addressable::URI.encode(param.to_s)
86
+ def uri_encode(param)
87
+ Addressable::URI.encode(param.to_s)
88
+ end
85
89
  end
86
- end
90
+ end
@@ -1,7 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiAdaptor
2
4
  # Abstract error class
3
5
  class BaseError < StandardError; end
4
6
 
7
+ class TooManyRedirects < BaseError; end
8
+
9
+ class RedirectLocationMissing < BaseError; end
10
+
5
11
  class EndpointNotFound < BaseError; end
6
12
 
7
13
  class TimedOutException < BaseError; end
@@ -41,6 +47,8 @@ module ApiAdaptor
41
47
 
42
48
  class HTTPUnprocessableEntity < HTTPClientError; end
43
49
 
50
+ class HTTPUnprocessableContent < HTTPClientError; end
51
+
44
52
  class HTTPBadRequest < HTTPClientError; end
45
53
 
46
54
  class HTTPTooManyRequests < HTTPIntermittentClientError; end
@@ -102,4 +110,4 @@ module ApiAdaptor
102
110
  end
103
111
  end
104
112
  end
105
- end
113
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiAdaptor
2
4
  class Headers
3
5
  class << self
@@ -6,18 +8,18 @@ module ApiAdaptor
6
8
  end
7
9
 
8
10
  def headers
9
- header_data.reject { |_k, v| (v.nil? || v.empty?) }
11
+ header_data.reject { |_k, v| v.nil? || v.empty? }
10
12
  end
11
13
 
12
14
  def clear_headers
13
15
  Thread.current[:headers] = {}
14
16
  end
15
17
 
16
- private
18
+ private
17
19
 
18
20
  def header_data
19
21
  Thread.current[:headers] ||= {}
20
22
  end
21
23
  end
22
24
  end
23
- end
25
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "exceptions"
2
4
  require_relative "variables"
3
5
  require_relative "null_logger"
@@ -13,9 +15,7 @@ module ApiAdaptor
13
15
  attr_accessor :logger, :options
14
16
 
15
17
  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
18
+ raise "It is no longer possible to disable the timeout." if options[:disable_timeout] || options[:timeout].to_i.negative?
19
19
 
20
20
  @logger = options[:logger] || NullLogger.new
21
21
  @options = options
@@ -24,7 +24,7 @@ module ApiAdaptor
24
24
  def self.default_request_headers
25
25
  {
26
26
  "Accept" => "application/json",
27
- "User-Agent" => "#{Variables.app_name}/#{Variables.app_version} (#{Variables. app_contact}",
27
+ "User-Agent" => "#{Variables.app_name}/#{Variables.app_version} (#{Variables.app_contact})"
28
28
  }
29
29
  end
30
30
 
@@ -34,11 +34,12 @@ module ApiAdaptor
34
34
 
35
35
  def self.json_body_headers
36
36
  {
37
- "Content-Type" => "application/json",
37
+ "Content-Type" => "application/json"
38
38
  }
39
39
  end
40
40
 
41
41
  DEFAULT_TIMEOUT_IN_SECONDS = 4
42
+ DEFAULT_MAX_REDIRECTS = 3
42
43
 
43
44
  def get_raw!(url)
44
45
  do_raw_request(:get, url)
@@ -78,7 +79,7 @@ module ApiAdaptor
78
79
  Response.new(r)
79
80
  end
80
81
 
81
- private
82
+ private
82
83
 
83
84
  def do_raw_request(method, url, params = nil)
84
85
  do_request(method, url, params)
@@ -94,9 +95,7 @@ module ApiAdaptor
94
95
  # from the Net::HTTPResponse
95
96
  def do_json_request(method, url, params = nil, additional_headers = {}, &create_response)
96
97
  begin
97
- if params
98
- additional_headers.merge!(self.class.json_body_headers)
99
- end
98
+ additional_headers.merge!(self.class.json_body_headers) if params
100
99
  response = do_request(method, url, (params.to_json if params), additional_headers)
101
100
  rescue RestClient::Exception => e
102
101
  # Attempt to parse the body as JSON if possible
@@ -119,25 +118,24 @@ module ApiAdaptor
119
118
  if @options[:bearer_token]
120
119
  headers = method_params[:headers] || {}
121
120
  method_params.merge(headers: headers.merge(
122
- "Authorization" => "Bearer #{@options[:bearer_token]}",
121
+ "Authorization" => "Bearer #{@options[:bearer_token]}"
123
122
  ))
124
123
  elsif @options[:basic_auth]
125
124
  method_params.merge(
126
125
  user: @options[:basic_auth][:user],
127
- password: @options[:basic_auth][:password],
126
+ password: @options[:basic_auth][:password]
128
127
  )
129
128
  else
130
129
  method_params
131
130
  end
132
131
  end
133
132
 
134
-
135
133
  # Take a hash of parameters for Request#execute; return a hash of
136
134
  # parameters with timeouts included
137
135
  def with_timeout(method_params)
138
136
  method_params.merge(
139
137
  timeout: options[:timeout] || DEFAULT_TIMEOUT_IN_SECONDS,
140
- open_timeout: options[:timeout] || DEFAULT_TIMEOUT_IN_SECONDS,
138
+ open_timeout: options[:timeout] || DEFAULT_TIMEOUT_IN_SECONDS
141
139
  )
142
140
  end
143
141
 
@@ -146,57 +144,184 @@ module ApiAdaptor
146
144
  headers: default_headers
147
145
  .merge(method_params[:headers] || {})
148
146
  .merge(ApiAdaptor::Headers.headers)
149
- .merge(additional_headers),
147
+ .merge(additional_headers)
150
148
  )
151
149
  end
152
150
 
153
151
  def with_ssl_options(method_params)
154
152
  method_params.merge(
155
153
  # This is the default value anyway, but we should probably be explicit
156
- verify_ssl: OpenSSL::SSL::VERIFY_NONE,
154
+ verify_ssl: OpenSSL::SSL::VERIFY_NONE
157
155
  )
158
156
  end
159
157
 
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
158
+ def max_redirects
159
+ value = options.fetch(:max_redirects, DEFAULT_MAX_REDIRECTS)
160
+ value = value.to_i
161
+ value.negative? ? 0 : value
162
+ end
164
163
 
165
- method_params = {
166
- method: method,
167
- url: url,
168
- }
164
+ def follow_non_get_redirects?
165
+ options.fetch(:follow_non_get_redirects, false)
166
+ end
167
+
168
+ def allow_cross_origin_redirects?
169
+ options.fetch(:allow_cross_origin_redirects, true)
170
+ end
171
+
172
+ def forward_auth_on_cross_origin_redirects?
173
+ options.fetch(:forward_auth_on_cross_origin_redirects, false)
174
+ end
175
+
176
+ def redirect_status_code?(code)
177
+ code.to_i >= 300 && code.to_i <= 399
178
+ end
179
+
180
+ def follow_redirect_code?(method, code)
181
+ code = code.to_i
182
+ return false unless redirect_status_code?(code)
183
+ return false if code == 304
184
+ return false if [305, 306].include?(code)
185
+
186
+ if %i[get head].include?(method)
187
+ [301, 302, 303, 307, 308].include?(code)
188
+ else
189
+ return true if follow_non_get_redirects? && [307, 308].include?(code)
169
190
 
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)
191
+ false
176
192
  end
193
+ end
177
194
 
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
195
+ def response_location(response)
196
+ return nil unless response
197
+
198
+ headers = response.headers || {}
199
+ headers[:location] || headers["location"] || headers["Location"]
200
+ end
201
+
202
+ def resolve_location(current_url, location)
203
+ URI.join(current_url, location.to_s).to_s
204
+ rescue URI::Error
205
+ location.to_s
206
+ end
207
+
208
+ def origin_for(url)
209
+ uri = URI.parse(url)
210
+ [uri.scheme, uri.host, uri.port]
211
+ end
212
+
213
+ def do_request(method, url, params = nil, additional_headers = {})
214
+ current_method = method
215
+ current_url = url
216
+ current_params = params
217
+ redirects_followed = 0
218
+
219
+ initial_origin = begin
220
+ origin_for(url)
221
+ rescue URI::InvalidURIError => e
222
+ raise ApiAdaptor::InvalidUrl, e.message
223
+ end
224
+
225
+ loop do
226
+ loggable = { request_uri: current_url, start_time: Time.now.to_f }
227
+ start_logging = loggable.merge(action: "start")
228
+ logger.debug start_logging.to_json
229
+
230
+ method_params = {
231
+ method: current_method,
232
+ url: current_url,
233
+ max_redirects: 0
234
+ }
235
+ method_params[:payload] = current_params
236
+ method_params = with_timeout(method_params)
237
+ method_params = with_headers(method_params, self.class.default_request_headers, additional_headers)
238
+
239
+ begin
240
+ current_origin = origin_for(current_url)
241
+ cross_origin = current_origin != initial_origin
242
+ include_auth = !cross_origin || forward_auth_on_cross_origin_redirects?
243
+ method_params = with_auth_options(method_params) if include_auth
244
+ unless include_auth
245
+ if method_params[:headers]
246
+ method_params[:headers].delete("Authorization")
247
+ method_params[:headers].delete("Proxy-Authorization")
248
+ end
249
+ method_params.delete(:user)
250
+ method_params.delete(:password)
251
+ end
252
+
253
+ method_params = with_ssl_options(method_params) if URI.parse(current_url).is_a? URI::HTTPS
254
+ return ::RestClient::Request.execute(method_params)
255
+ rescue RestClient::ExceptionWithResponse => e
256
+ if e.is_a?(RestClient::Exceptions::Timeout)
257
+ logger.error loggable.merge(status: "timeout", error_message: e.message, error_class: e.class.name,
258
+ end_time: Time.now.to_f).to_json
259
+ raise ApiAdaptor::TimedOutException, e.message
260
+ end
261
+
262
+ status_code = (e.http_code || e.response&.code).to_i
263
+
264
+ raise ApiAdaptor::TimedOutException, e.message if status_code == 408
265
+
266
+ if follow_redirect_code?(current_method.to_sym, status_code)
267
+ location = response_location(e.response)
268
+ raise ApiAdaptor::RedirectLocationMissing, "Redirect response missing Location header for #{current_url}" if location.to_s.strip.empty?
269
+
270
+ next_url = resolve_location(current_url, location)
271
+ begin
272
+ next_origin = origin_for(next_url)
273
+ rescue URI::InvalidURIError => e
274
+ logger.error loggable.merge(status: "invalid_uri", error_message: e.message, error_class: e.class.name,
275
+ end_time: Time.now.to_f).to_json
276
+ raise ApiAdaptor::InvalidUrl, e.message
277
+ end
278
+ if next_origin != initial_origin && !allow_cross_origin_redirects?
279
+ loggable.merge!(status: status_code, end_time: Time.now.to_f, body: e.http_body)
280
+ logger.warn loggable.to_json
281
+ raise
282
+ end
283
+
284
+ raise ApiAdaptor::TooManyRedirects, "Too many redirects (max #{max_redirects}) while requesting #{url}" if redirects_followed >= max_redirects
285
+
286
+ redirects_followed += 1
287
+ current_url = next_url
288
+
289
+ next
290
+ end
291
+
292
+ loggable.merge!(status: status_code, end_time: Time.now.to_f, body: e.http_body)
293
+ logger.warn loggable.to_json
294
+ raise
295
+ rescue Errno::ECONNREFUSED => e
296
+ logger.error loggable.merge(status: "refused", error_message: e.message, error_class: e.class.name,
297
+ end_time: Time.now.to_f).to_json
298
+ raise ApiAdaptor::EndpointNotFound, "Could not connect to #{current_url}"
299
+ rescue Timeout::Error => e
300
+ logger.error loggable.merge(status: "timeout", error_message: e.message, error_class: e.class.name,
301
+ end_time: Time.now.to_f).to_json
302
+ raise ApiAdaptor::TimedOutException, e.message
303
+ rescue RestClient::Exceptions::Timeout => e
304
+ logger.error loggable.merge(status: "timeout", error_message: e.message, error_class: e.class.name,
305
+ end_time: Time.now.to_f).to_json
306
+ raise ApiAdaptor::TimedOutException, e.message
307
+ rescue URI::InvalidURIError => e
308
+ logger.error loggable.merge(status: "invalid_uri", error_message: e.message, error_class: e.class.name,
309
+ end_time: Time.now.to_f).to_json
310
+ raise ApiAdaptor::InvalidUrl, e.message
311
+ rescue RestClient::Exception => e
312
+ loggable.merge!(status: e.http_code, end_time: Time.now.to_f, body: e.http_body)
313
+ logger.warn loggable.to_json
314
+ raise
315
+ rescue Errno::ECONNRESET => e
316
+ logger.error loggable.merge(status: "connection_reset", error_message: e.message, error_class: e.class.name,
317
+ end_time: Time.now.to_f).to_json
318
+ raise ApiAdaptor::TimedOutException, e.message
319
+ rescue SocketError => e
320
+ logger.error loggable.merge(status: "socket_error", error_message: e.message, error_class: e.class.name,
321
+ end_time: Time.now.to_f).to_json
322
+ raise ApiAdaptor::SocketErrorException, e.message
323
+ end
324
+ end
200
325
  end
201
326
  end
202
327
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require "api_adaptor/response"
3
5
  require "link_header"
@@ -24,7 +26,7 @@ module ApiAdaptor
24
26
  to_hash["results"]
25
27
  end
26
28
 
27
- def has_next_page?
29
+ def next_page?
28
30
  !page_link("next").nil?
29
31
  end
30
32
 
@@ -33,20 +35,16 @@ module ApiAdaptor
33
35
  # avoid us making multiple requests for the same page, but we shouldn't
34
36
  # allow the data to change once it's already been loaded, so long as we
35
37
  # 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
38
+ @next_page ||= (@api_client.get_list page_link("next").href if next_page?)
39
39
  end
40
40
 
41
- def has_previous_page?
41
+ def previous_page?
42
42
  !page_link("previous").nil?
43
43
  end
44
44
 
45
45
  def previous_page
46
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
47
+ @previous_page ||= (@api_client.get_list(page_link("previous").href) if previous_page?)
50
48
  end
51
49
 
52
50
  # Transparently get all results across all pages. Compare this with #each
@@ -70,13 +68,11 @@ module ApiAdaptor
70
68
  def with_subsequent_pages
71
69
  Enumerator.new do |yielder|
72
70
  each { |i| yielder << i }
73
- if has_next_page?
74
- next_page.with_subsequent_pages.each { |i| yielder << i }
75
- end
71
+ next_page.with_subsequent_pages.each { |i| yielder << i } if next_page?
76
72
  end
77
73
  end
78
74
 
79
- private
75
+ private
80
76
 
81
77
  def link_header
82
78
  @link_header ||= LinkHeader.parse @http_response.headers[:link]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require "forwardable"
3
5
 
@@ -8,13 +10,12 @@ module ApiAdaptor
8
10
  # API endpoints should return absolute URLs so that they make sense outside of the
9
11
  # domain context. However on systems within an API we want to present relative URLs.
10
12
  # 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
  #
13
14
  # Example:
14
15
  #
15
- # r = Response.new(response, web_urls_relative_to: "https://www.gov.uk")
16
+ # r = Response.new(response, web_urls_relative_to: "https://www.example.com")
16
17
  # r['results'][0]['web_url']
17
- # => "/bank-holidays"
18
+ # => "/foo"
18
19
  class Response
19
20
  extend Forwardable
20
21
  include Enumerable
@@ -58,12 +59,12 @@ module ApiAdaptor
58
59
  def reverse_max_age
59
60
  self["r-maxage"].to_i if key?("r-maxage")
60
61
  end
61
- alias_method :r_maxage, :reverse_max_age
62
+ alias r_maxage reverse_max_age
62
63
 
63
64
  def shared_max_age
64
65
  self["s-maxage"].to_i if key?("r-maxage")
65
66
  end
66
- alias_method :s_maxage, :shared_max_age
67
+ alias s_maxage shared_max_age
67
68
 
68
69
  def to_s
69
70
  directives = []
@@ -152,7 +153,7 @@ module ApiAdaptor
152
153
  false
153
154
  end
154
155
 
155
- private
156
+ private
156
157
 
157
158
  def transform_parsed(value)
158
159
  return value if @web_urls_relative_to.nil?
@@ -180,4 +181,4 @@ module ApiAdaptor
180
181
  end
181
182
  end
182
183
  end
183
- end
184
+ end
@@ -1,15 +1,17 @@
1
- module ApiAdaptor
2
- module Variables
3
- def self.app_name
4
- ENV['APP_NAME'] || "Ruby ApiAdaptor App"
5
- end
1
+ # frozen_string_literal: true
6
2
 
7
- def self.app_version
8
- ENV['APP_VERSION'] || "Version not stated"
9
- end
3
+ module ApiAdaptor
4
+ module Variables
5
+ def self.app_name
6
+ ENV["APP_NAME"] || "Ruby ApiAdaptor App"
7
+ end
8
+
9
+ def self.app_version
10
+ ENV["APP_VERSION"] || "Version not stated"
11
+ end
10
12
 
11
- def self.app_contact
12
- ENV['APP_CONTACT'] || "Contact not stated"
13
- end
13
+ def self.app_contact
14
+ ENV["APP_CONTACT"] || "Contact not stated"
14
15
  end
15
- end
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ApiAdaptor
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
metadata CHANGED
@@ -1,43 +1,62 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: api_adaptor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Huw Diprose
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-06-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: rest-client
13
+ name: addressable
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: '2.1'
18
+ version: '2.8'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
- version: '2.1'
25
+ version: '2.8'
27
26
  - !ruby/object:Gem::Dependency
28
- name: addressable
27
+ name: base64
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - "~>"
32
31
  - !ruby/object:Gem::Version
33
- version: '2.8'
32
+ version: '0.3'
34
33
  type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - "~>"
39
38
  - !ruby/object:Gem::Version
40
- version: '2.8'
39
+ version: '0.3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: bigdecimal
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '3.3'
47
+ - - "<"
48
+ - !ruby/object:Gem::Version
49
+ version: '5.0'
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '3.3'
57
+ - - "<"
58
+ - !ruby/object:Gem::Version
59
+ version: '5.0'
41
60
  - !ruby/object:Gem::Dependency
42
61
  name: link_header
43
62
  requirement: !ruby/object:Gem::Requirement
@@ -53,33 +72,39 @@ dependencies:
53
72
  - !ruby/object:Gem::Version
54
73
  version: 0.0.8
55
74
  - !ruby/object:Gem::Dependency
56
- name: webmock
75
+ name: logger
57
76
  requirement: !ruby/object:Gem::Requirement
58
77
  requirements:
59
- - - "~>"
78
+ - - ">="
60
79
  - !ruby/object:Gem::Version
61
- version: '3.18'
62
- type: :development
80
+ version: '1.6'
81
+ - - "<"
82
+ - !ruby/object:Gem::Version
83
+ version: '2.0'
84
+ type: :runtime
63
85
  prerelease: false
64
86
  version_requirements: !ruby/object:Gem::Requirement
65
87
  requirements:
66
- - - "~>"
88
+ - - ">="
67
89
  - !ruby/object:Gem::Version
68
- version: '3.18'
90
+ version: '1.6'
91
+ - - "<"
92
+ - !ruby/object:Gem::Version
93
+ version: '2.0'
69
94
  - !ruby/object:Gem::Dependency
70
- name: timecop
95
+ name: rest-client
71
96
  requirement: !ruby/object:Gem::Requirement
72
97
  requirements:
73
98
  - - "~>"
74
99
  - !ruby/object:Gem::Version
75
- version: '0.9'
76
- type: :development
100
+ version: '2.1'
101
+ type: :runtime
77
102
  prerelease: false
78
103
  version_requirements: !ruby/object:Gem::Requirement
79
104
  requirements:
80
105
  - - "~>"
81
106
  - !ruby/object:Gem::Version
82
- version: '0.9'
107
+ version: '2.1'
83
108
  - !ruby/object:Gem::Dependency
84
109
  name: rake
85
110
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +147,48 @@ dependencies:
122
147
  - - "~>"
123
148
  - !ruby/object:Gem::Version
124
149
  version: '1.21'
150
+ - !ruby/object:Gem::Dependency
151
+ name: simplecov
152
+ requirement: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - "~>"
155
+ - !ruby/object:Gem::Version
156
+ version: '0.22'
157
+ type: :development
158
+ prerelease: false
159
+ version_requirements: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - "~>"
162
+ - !ruby/object:Gem::Version
163
+ version: '0.22'
164
+ - !ruby/object:Gem::Dependency
165
+ name: timecop
166
+ requirement: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - "~>"
169
+ - !ruby/object:Gem::Version
170
+ version: '0.9'
171
+ type: :development
172
+ prerelease: false
173
+ version_requirements: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - "~>"
176
+ - !ruby/object:Gem::Version
177
+ version: '0.9'
178
+ - !ruby/object:Gem::Dependency
179
+ name: webmock
180
+ requirement: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - "~>"
183
+ - !ruby/object:Gem::Version
184
+ version: '3.18'
185
+ type: :development
186
+ prerelease: false
187
+ version_requirements: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - "~>"
190
+ - !ruby/object:Gem::Version
191
+ version: '3.18'
125
192
  description: A basic adaptor to send HTTP requests and parse the responses. Intended
126
193
  to bootstrap the quick writing of Adaptors for specific APIs, without having to
127
194
  write the same old JSON request and processing time and time again.
@@ -141,6 +208,7 @@ files:
141
208
  - LICENSE.txt
142
209
  - README.md
143
210
  - Rakefile
211
+ - fixtures/v1/integration/foo.json
144
212
  - lib/api_adaptor.rb
145
213
  - lib/api_adaptor/base.rb
146
214
  - lib/api_adaptor/exceptions.rb
@@ -160,7 +228,6 @@ metadata:
160
228
  homepage_uri: https://github.com/huwd/api_adaptor
161
229
  source_code_uri: https://github.com/huwd/api_adaptor
162
230
  changelog_uri: https://github.com/huwd/api_adaptor/blob/main/CHANGELOG.md
163
- post_install_message:
164
231
  rdoc_options: []
165
232
  require_paths:
166
233
  - lib
@@ -168,15 +235,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
168
235
  requirements:
169
236
  - - ">="
170
237
  - !ruby/object:Gem::Version
171
- version: 2.6.0
238
+ version: 3.2.0
172
239
  required_rubygems_version: !ruby/object:Gem::Requirement
173
240
  requirements:
174
241
  - - ">="
175
242
  - !ruby/object:Gem::Version
176
243
  version: '0'
177
244
  requirements: []
178
- rubygems_version: 3.4.13
179
- signing_key:
245
+ rubygems_version: 4.0.3
180
246
  specification_version: 4
181
247
  summary: A basic adaptor to send HTTP requests and parse the responses.
182
248
  test_files: []