api_adaptor 0.0.2 → 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 +4 -4
- data/.rubocop.yml +4 -5
- data/CHANGELOG.md +14 -1
- data/Gemfile.lock +59 -39
- data/README.md +126 -24
- data/fixtures/v1/integration/foo.json +3 -0
- data/lib/api_adaptor/base.rb +2 -2
- data/lib/api_adaptor/exceptions.rb +6 -0
- data/lib/api_adaptor/json_client.rb +165 -40
- data/lib/api_adaptor/response.rb +3 -4
- data/lib/api_adaptor/version.rb +1 -1
- metadata +73 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec2d6e2bc7c54d07e88bee9945a772f952076ba24b3f1fdf176e951bae3f8dad
|
|
4
|
+
data.tar.gz: c0d9d319cd02061d462eaba8bdc8cfd5a8dda94df5973e7ba8ef78fc3dada248
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ac1bd16754ce9ae420ad5ba9d3fc9d92ad280f896afcb5312688b880b228c179ca75a8edb6a16811064f6c3d71805045aae22312137b325b067a0339c801acf
|
|
7
|
+
data.tar.gz: 9dd42bfacca8f947ac4977c3f1a4970db143118dd101c851b61ecae8a4462cde109f4157e542020e57f147f01cc83973c11de3a8e9d0093dd7991241b1669485
|
data/.rubocop.yml
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
AllCops:
|
|
2
|
-
TargetRubyVersion: 2
|
|
2
|
+
TargetRubyVersion: 3.2
|
|
3
|
+
NewCops: disable
|
|
4
|
+
SuggestExtensions: false
|
|
3
5
|
|
|
4
6
|
Style/StringLiterals:
|
|
5
7
|
Enabled: true
|
|
@@ -10,13 +12,10 @@ Style/StringLiteralsInInterpolation:
|
|
|
10
12
|
EnforcedStyle: double_quotes
|
|
11
13
|
|
|
12
14
|
Layout/LineLength:
|
|
13
|
-
|
|
15
|
+
Enabled: false
|
|
14
16
|
|
|
15
17
|
Metrics:
|
|
16
18
|
Enabled: false
|
|
17
19
|
|
|
18
20
|
Style/Documentation:
|
|
19
21
|
Enabled: false
|
|
20
|
-
|
|
21
|
-
Layout/LineLength:
|
|
22
|
-
Enabled: false
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
## [
|
|
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,77 +1,96 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
api_adaptor (0.0
|
|
4
|
+
api_adaptor (0.1.0)
|
|
5
5
|
addressable (~> 2.8)
|
|
6
|
+
base64 (~> 0.3)
|
|
7
|
+
bigdecimal (>= 3.3, < 5.0)
|
|
6
8
|
link_header (~> 0.0.8)
|
|
9
|
+
logger (>= 1.6, < 2.0)
|
|
7
10
|
rest-client (~> 2.1)
|
|
8
11
|
|
|
9
12
|
GEM
|
|
10
13
|
remote: https://rubygems.org/
|
|
11
14
|
specs:
|
|
12
|
-
addressable (2.8.
|
|
13
|
-
public_suffix (>= 2.0.2, <
|
|
14
|
-
ast (2.4.
|
|
15
|
-
|
|
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
|
|
16
22
|
rexml
|
|
17
|
-
diff-lcs (1.
|
|
23
|
+
diff-lcs (1.6.2)
|
|
24
|
+
docile (1.4.1)
|
|
18
25
|
domain_name (0.6.20240107)
|
|
19
|
-
hashdiff (1.1
|
|
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.
|
|
24
|
-
language_server-protocol (3.17.0.
|
|
30
|
+
json (2.18.0)
|
|
31
|
+
language_server-protocol (3.17.0.5)
|
|
25
32
|
link_header (0.0.8)
|
|
33
|
+
lint_roller (1.1.0)
|
|
34
|
+
logger (1.7.0)
|
|
26
35
|
mime-types (3.5.2)
|
|
27
36
|
mime-types-data (~> 3.2015)
|
|
28
37
|
mime-types-data (3.2023.1205)
|
|
29
38
|
netrc (0.11.0)
|
|
30
|
-
parallel (1.
|
|
31
|
-
parser (3.3.
|
|
39
|
+
parallel (1.27.0)
|
|
40
|
+
parser (3.3.10.1)
|
|
32
41
|
ast (~> 2.4.1)
|
|
33
42
|
racc
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
prism (1.9.0)
|
|
44
|
+
public_suffix (7.0.2)
|
|
45
|
+
racc (1.8.1)
|
|
36
46
|
rainbow (3.1.1)
|
|
37
|
-
rake (13.1
|
|
38
|
-
regexp_parser (2.
|
|
47
|
+
rake (13.3.1)
|
|
48
|
+
regexp_parser (2.11.3)
|
|
39
49
|
rest-client (2.1.0)
|
|
40
50
|
http-accept (>= 1.7.0, < 2.0)
|
|
41
51
|
http-cookie (>= 1.0.2, < 2.0)
|
|
42
52
|
mime-types (>= 1.16, < 4.0)
|
|
43
53
|
netrc (~> 0.8)
|
|
44
|
-
rexml (3.
|
|
45
|
-
rspec (3.
|
|
46
|
-
rspec-core (~> 3.
|
|
47
|
-
rspec-expectations (~> 3.
|
|
48
|
-
rspec-mocks (~> 3.
|
|
49
|
-
rspec-core (3.
|
|
50
|
-
rspec-support (~> 3.
|
|
51
|
-
rspec-expectations (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)
|
|
52
62
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
53
|
-
rspec-support (~> 3.
|
|
54
|
-
rspec-mocks (3.
|
|
63
|
+
rspec-support (~> 3.13.0)
|
|
64
|
+
rspec-mocks (3.13.7)
|
|
55
65
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
56
|
-
rspec-support (~> 3.
|
|
57
|
-
rspec-support (3.
|
|
58
|
-
rubocop (1.
|
|
66
|
+
rspec-support (~> 3.13.0)
|
|
67
|
+
rspec-support (3.13.6)
|
|
68
|
+
rubocop (1.84.0)
|
|
59
69
|
json (~> 2.3)
|
|
60
|
-
language_server-protocol (
|
|
70
|
+
language_server-protocol (~> 3.17.0.2)
|
|
71
|
+
lint_roller (~> 1.1.0)
|
|
61
72
|
parallel (~> 1.10)
|
|
62
|
-
parser (>= 3.
|
|
73
|
+
parser (>= 3.3.0.2)
|
|
63
74
|
rainbow (>= 2.2.2, < 4.0)
|
|
64
|
-
regexp_parser (>=
|
|
65
|
-
|
|
66
|
-
rubocop-ast (>= 1.30.0, < 2.0)
|
|
75
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
76
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
67
77
|
ruby-progressbar (~> 1.7)
|
|
68
|
-
unicode-display_width (>= 2.4.0, <
|
|
69
|
-
rubocop-ast (1.
|
|
70
|
-
parser (>= 3.
|
|
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)
|
|
71
82
|
ruby-progressbar (1.13.0)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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)
|
|
75
94
|
addressable (>= 2.8.0)
|
|
76
95
|
crack (>= 0.3.2)
|
|
77
96
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
@@ -84,6 +103,7 @@ DEPENDENCIES
|
|
|
84
103
|
rake (~> 13.0)
|
|
85
104
|
rspec (~> 3.0)
|
|
86
105
|
rubocop (~> 1.21)
|
|
106
|
+
simplecov (~> 0.22)
|
|
87
107
|
timecop (~> 0.9)
|
|
88
108
|
webmock (~> 3.18)
|
|
89
109
|
|
data/README.md
CHANGED
|
@@ -17,64 +17,166 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
|
17
17
|
gem install api_adaptor
|
|
18
18
|
```
|
|
19
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
|
+
```
|
|
33
|
+
|
|
20
34
|
## Usage
|
|
21
35
|
|
|
22
36
|
Use the ApiAdaptor as a base class for your API wrapper, for example:
|
|
23
37
|
|
|
24
38
|
```ruby
|
|
25
|
-
class MyApi < ApiAdaptor::Base
|
|
26
|
-
def base_url
|
|
27
|
-
endpoint
|
|
28
|
-
end
|
|
29
|
-
end
|
|
39
|
+
class MyApi < ApiAdaptor::Base; end
|
|
30
40
|
```
|
|
31
41
|
|
|
32
42
|
Use your new class to create a client that can make HTTP requests to JSON APIs for:
|
|
33
43
|
|
|
34
|
-
### GET JSON
|
|
35
|
-
|
|
36
44
|
```ruby
|
|
37
45
|
client = MyApi.new
|
|
38
|
-
|
|
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" })
|
|
39
51
|
```
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
You can also get a raw response from the API
|
|
42
54
|
|
|
43
55
|
```ruby
|
|
44
|
-
client
|
|
45
|
-
response = client.post_json("http://some.endpoint/json", { "foo": "bar" })
|
|
56
|
+
client.get_raw("http://some.endpoint/json")
|
|
46
57
|
```
|
|
47
58
|
|
|
48
|
-
###
|
|
59
|
+
### Redirects (3xx)
|
|
60
|
+
|
|
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
|
|
72
|
+
|
|
73
|
+
You can configure redirect behaviour by passing options into your `ApiAdaptor::Base` subclass (they are forwarded to `ApiAdaptor::JsonClient`):
|
|
49
74
|
|
|
50
75
|
```ruby
|
|
51
|
-
client = MyApi.new
|
|
52
|
-
|
|
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
86
|
|
|
55
|
-
|
|
87
|
+
Or, if you are using `ApiAdaptor::JsonClient` directly:
|
|
56
88
|
|
|
57
89
|
```ruby
|
|
58
|
-
client =
|
|
59
|
-
|
|
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")
|
|
98
|
+
```
|
|
99
|
+
|
|
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.
|
|
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
124
|
|
|
62
|
-
|
|
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:
|
|
63
133
|
|
|
64
134
|
```ruby
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
67
141
|
```
|
|
68
142
|
|
|
69
|
-
###
|
|
143
|
+
### Conventional usage
|
|
70
144
|
|
|
71
|
-
|
|
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.
|
|
146
|
+
|
|
147
|
+
A REST API module can be created with:
|
|
72
148
|
|
|
73
149
|
```ruby
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
76
158
|
```
|
|
77
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
|
|
174
|
+
```
|
|
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
|
+
|
|
78
180
|
## Environment variables
|
|
79
181
|
|
|
80
182
|
User Agent is populated with a default string.
|
data/lib/api_adaptor/base.rb
CHANGED
|
@@ -42,9 +42,9 @@ module ApiAdaptor
|
|
|
42
42
|
@logger ||= ApiAdaptor::NullLogger.new
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
def initialize(endpoint_url, options = {})
|
|
45
|
+
def initialize(endpoint_url = nil, options = {})
|
|
46
46
|
options[:endpoint_url] = endpoint_url
|
|
47
|
-
raise InvalidAPIURL
|
|
47
|
+
raise InvalidAPIURL if !endpoint_url.nil? && endpoint_url !~ URI::RFC3986_Parser::RFC3986_URI
|
|
48
48
|
|
|
49
49
|
base_options = { logger: ApiAdaptor::Base.logger }
|
|
50
50
|
default_options = base_options.merge(ApiAdaptor::Base.default_options || {})
|
|
@@ -4,6 +4,10 @@ module ApiAdaptor
|
|
|
4
4
|
# Abstract error class
|
|
5
5
|
class BaseError < StandardError; end
|
|
6
6
|
|
|
7
|
+
class TooManyRedirects < BaseError; end
|
|
8
|
+
|
|
9
|
+
class RedirectLocationMissing < BaseError; end
|
|
10
|
+
|
|
7
11
|
class EndpointNotFound < BaseError; end
|
|
8
12
|
|
|
9
13
|
class TimedOutException < BaseError; end
|
|
@@ -43,6 +47,8 @@ module ApiAdaptor
|
|
|
43
47
|
|
|
44
48
|
class HTTPUnprocessableEntity < HTTPClientError; end
|
|
45
49
|
|
|
50
|
+
class HTTPUnprocessableContent < HTTPClientError; end
|
|
51
|
+
|
|
46
52
|
class HTTPBadRequest < HTTPClientError; end
|
|
47
53
|
|
|
48
54
|
class HTTPTooManyRequests < HTTPIntermittentClientError; end
|
|
@@ -39,6 +39,7 @@ module ApiAdaptor
|
|
|
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)
|
|
@@ -154,49 +155,173 @@ module ApiAdaptor
|
|
|
154
155
|
)
|
|
155
156
|
end
|
|
156
157
|
|
|
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
|
|
163
|
+
|
|
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)
|
|
190
|
+
|
|
191
|
+
false
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
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
|
+
|
|
157
213
|
def do_request(method, url, params = nil, additional_headers = {})
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
214
|
+
current_method = method
|
|
215
|
+
current_url = url
|
|
216
|
+
current_params = params
|
|
217
|
+
redirects_followed = 0
|
|
161
218
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
219
|
+
initial_origin = begin
|
|
220
|
+
origin_for(url)
|
|
221
|
+
rescue URI::InvalidURIError => e
|
|
222
|
+
raise ApiAdaptor::InvalidUrl, e.message
|
|
223
|
+
end
|
|
166
224
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
data/lib/api_adaptor/response.rb
CHANGED
|
@@ -10,19 +10,18 @@ module ApiAdaptor
|
|
|
10
10
|
# API endpoints should return absolute URLs so that they make sense outside of the
|
|
11
11
|
# domain context. However on systems within an API we want to present relative URLs.
|
|
12
12
|
# By specifying a base URI, this will convert all matching web_urls into relative URLs
|
|
13
|
-
# This is useful on non-canonical frontends, such as those in staging environments.
|
|
14
13
|
#
|
|
15
14
|
# Example:
|
|
16
15
|
#
|
|
17
|
-
# r = Response.new(response, web_urls_relative_to: "https://www.
|
|
16
|
+
# r = Response.new(response, web_urls_relative_to: "https://www.example.com")
|
|
18
17
|
# r['results'][0]['web_url']
|
|
19
|
-
# => "/
|
|
18
|
+
# => "/foo"
|
|
20
19
|
class Response
|
|
21
20
|
extend Forwardable
|
|
22
21
|
include Enumerable
|
|
23
22
|
|
|
24
23
|
class CacheControl < Hash
|
|
25
|
-
PATTERN = /([-a-z]+)(?:\s*=\s*([^,\s]+))?,?+/i
|
|
24
|
+
PATTERN = /([-a-z]+)(?:\s*=\s*([^,\s]+))?,?+/i
|
|
26
25
|
|
|
27
26
|
def initialize(value = nil)
|
|
28
27
|
super()
|
data/lib/api_adaptor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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.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:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: addressable
|
|
@@ -24,6 +23,40 @@ dependencies:
|
|
|
24
23
|
- - "~>"
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
25
|
version: '2.8'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: base64
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
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'
|
|
27
60
|
- !ruby/object:Gem::Dependency
|
|
28
61
|
name: link_header
|
|
29
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -38,6 +71,26 @@ dependencies:
|
|
|
38
71
|
- - "~>"
|
|
39
72
|
- !ruby/object:Gem::Version
|
|
40
73
|
version: 0.0.8
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: logger
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '1.6'
|
|
81
|
+
- - "<"
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '2.0'
|
|
84
|
+
type: :runtime
|
|
85
|
+
prerelease: false
|
|
86
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '1.6'
|
|
91
|
+
- - "<"
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '2.0'
|
|
41
94
|
- !ruby/object:Gem::Dependency
|
|
42
95
|
name: rest-client
|
|
43
96
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -94,6 +147,20 @@ dependencies:
|
|
|
94
147
|
- - "~>"
|
|
95
148
|
- !ruby/object:Gem::Version
|
|
96
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'
|
|
97
164
|
- !ruby/object:Gem::Dependency
|
|
98
165
|
name: timecop
|
|
99
166
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -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.
|
|
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:
|
|
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: []
|