fsdk-2237-ruby-e2e 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 +7 -0
- data/README.md +107 -0
- data/lib/petstore/client.rb +45 -0
- data/lib/petstore/config.rb +54 -0
- data/lib/petstore/environment.rb +7 -0
- data/lib/petstore/error.rb +40 -0
- data/lib/petstore/errors/api_error.rb +9 -0
- data/lib/petstore/errors/client_error.rb +43 -0
- data/lib/petstore/errors/connection_error.rb +11 -0
- data/lib/petstore/errors/redirect_error.rb +9 -0
- data/lib/petstore/errors/response_error.rb +92 -0
- data/lib/petstore/errors/server_error.rb +25 -0
- data/lib/petstore/errors/timeout_error.rb +9 -0
- data/lib/petstore/http/auth.rb +19 -0
- data/lib/petstore/http/oauth_token_manager.rb +91 -0
- data/lib/petstore/internal/errors/constraint_error.rb +10 -0
- data/lib/petstore/internal/errors/type_error.rb +10 -0
- data/lib/petstore/internal/form/request.rb +110 -0
- data/lib/petstore/internal/http/base_request.rb +64 -0
- data/lib/petstore/internal/http/event_stream.rb +106 -0
- data/lib/petstore/internal/http/raw_client.rb +426 -0
- data/lib/petstore/internal/http/response_body.rb +35 -0
- data/lib/petstore/internal/iterators/cursor_item_iterator.rb +28 -0
- data/lib/petstore/internal/iterators/cursor_page_iterator.rb +72 -0
- data/lib/petstore/internal/iterators/item_iterator.rb +72 -0
- data/lib/petstore/internal/iterators/offset_item_iterator.rb +30 -0
- data/lib/petstore/internal/iterators/offset_page_iterator.rb +110 -0
- data/lib/petstore/internal/json/request.rb +61 -0
- data/lib/petstore/internal/json/serializable.rb +25 -0
- data/lib/petstore/internal/multipart/multipart_encoder.rb +141 -0
- data/lib/petstore/internal/multipart/multipart_form_data.rb +78 -0
- data/lib/petstore/internal/multipart/multipart_form_data_part.rb +51 -0
- data/lib/petstore/internal/multipart/multipart_request.rb +49 -0
- data/lib/petstore/internal/raw/request.rb +88 -0
- data/lib/petstore/internal/types/array.rb +47 -0
- data/lib/petstore/internal/types/boolean.rb +34 -0
- data/lib/petstore/internal/types/enum.rb +56 -0
- data/lib/petstore/internal/types/hash.rb +36 -0
- data/lib/petstore/internal/types/model/field.rb +38 -0
- data/lib/petstore/internal/types/model.rb +246 -0
- data/lib/petstore/internal/types/type.rb +35 -0
- data/lib/petstore/internal/types/union.rb +193 -0
- data/lib/petstore/internal/types/unknown.rb +15 -0
- data/lib/petstore/internal/types/utils.rb +137 -0
- data/lib/petstore/models/open_model.rb +30 -0
- data/lib/petstore/models/serializable.rb +63 -0
- data/lib/petstore/models/union_type.rb +12 -0
- data/lib/petstore/pet/client.rb +196 -0
- data/lib/petstore/pet/types/delete_pet_request.rb +13 -0
- data/lib/petstore/pet/types/find_pets_by_status_request.rb +11 -0
- data/lib/petstore/pet/types/find_pets_by_status_request_status.rb +24 -0
- data/lib/petstore/pet/types/find_pets_by_tags_request.rb +11 -0
- data/lib/petstore/pet/types/get_pet_by_id_request.rb +11 -0
- data/lib/petstore/pet/types/pet.rb +25 -0
- data/lib/petstore/pet/types/update_pet_with_form_request.rb +15 -0
- data/lib/petstore/serializers/form_serializer.rb +41 -0
- data/lib/petstore/serializers/json_serializer.rb +41 -0
- data/lib/petstore/serializers/xml_serializer.rb +56 -0
- data/lib/petstore/store/client.rb +89 -0
- data/lib/petstore/store/types/delete_order_request.rb +11 -0
- data/lib/petstore/store/types/get_order_by_id_request.rb +11 -0
- data/lib/petstore/store/types/order.rb +25 -0
- data/lib/petstore/types/api_response.rb +20 -0
- data/lib/petstore/types/category.rb +19 -0
- data/lib/petstore/types/order_status.rb +22 -0
- data/lib/petstore/types/pet_status.rb +22 -0
- data/lib/petstore/types/tag.rb +19 -0
- data/lib/petstore/user/client.rb +149 -0
- data/lib/petstore/user/types/delete_user_request.rb +11 -0
- data/lib/petstore/user/types/get_user_by_name_request.rb +11 -0
- data/lib/petstore/user/types/login_user_request.rb +13 -0
- data/lib/petstore/user/types/user.rb +27 -0
- data/lib/petstore/validator.rb +42 -0
- data/lib/petstore/version.rb +5 -0
- data/lib/petstore.rb +78 -0
- metadata +236 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8ce767b2edadc2f777a42dfa66fef226a51c2a5c570f1e189237aeca7af0a4fd
|
|
4
|
+
data.tar.gz: 4d7ffe98c283f2d621dc7074518df798b765343d625bb7d570f5ad1baf1c4d93
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aa142530404fff5df7f75017176159e0f5996fc9766c00a7f7a22e17438a37a38dcceff6640e48e0087cb4da12e66e5354916c05ba3895770749f142d618a2a3
|
|
7
|
+
data.tar.gz: 4faf1824d38721b134040739a3f8e6262bd58f67525c59b9071852f12f30c7e09e844babed6422b031201305ca0c3197a5b1af4ff28cc1bb0860d1afbebdbc7d
|
data/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Petstore Ruby Library
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
The Petstore Ruby library provides convenient access to the API from Ruby.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Reference](#reference)
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [Usage](#usage)
|
|
12
|
+
- [Errors](#errors)
|
|
13
|
+
- [Advanced](#advanced)
|
|
14
|
+
- [Retries](#retries)
|
|
15
|
+
- [Per-request Options](#per-request-options)
|
|
16
|
+
- [Contributing](#contributing)
|
|
17
|
+
|
|
18
|
+
## Reference
|
|
19
|
+
|
|
20
|
+
A full reference for this library is available [here](./reference.md).
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
gem install fsdk-2237-ruby-e2e
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require "petstore"
|
|
32
|
+
|
|
33
|
+
client = Petstore::PetstoreClient.new
|
|
34
|
+
# See reference.md for the available resources and methods, e.g.:
|
|
35
|
+
# client.some_resource.some_method(...)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Errors
|
|
39
|
+
|
|
40
|
+
When the API returns a non-success status code (4xx or 5xx), the SDK raises a subclass of
|
|
41
|
+
`Petstore::Errors::ResponseError`. The HTTP status code is
|
|
42
|
+
available via `#code` and the raw response body via `#message`.
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
begin
|
|
46
|
+
client.some_resource.some_method(...)
|
|
47
|
+
rescue Petstore::Errors::NotFoundError => e
|
|
48
|
+
# handle a 404 specifically
|
|
49
|
+
rescue Petstore::Errors::ResponseError => e
|
|
50
|
+
puts e.code
|
|
51
|
+
puts e.message
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Advanced
|
|
56
|
+
|
|
57
|
+
### Retries
|
|
58
|
+
|
|
59
|
+
Requests that fail with a retryable status code (408, 429, and 5xx) are automatically retried with
|
|
60
|
+
exponential backoff, up to `max_retries` times. A request that times out is retried on the same
|
|
61
|
+
terms. Configure the limit on the client:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
client = Petstore::PetstoreClient.new(max_retries: 3)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Timeouts
|
|
68
|
+
|
|
69
|
+
The open, read and write timeout defaults to 60 seconds. Set it on the client, or per call:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
client = Petstore::PetstoreClient.new(timeout: 10)
|
|
73
|
+
client.some_resource.some_method(request_options: { timeout: 2.5 })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A request that exhausts its retries after timing out raises
|
|
77
|
+
`Petstore::Errors::TimeoutError`, and one that never reached the
|
|
78
|
+
server at all — refused connection, unresolvable host, failed TLS handshake — raises
|
|
79
|
+
`Petstore::Errors::ConnectionError`. Both are subclasses of
|
|
80
|
+
`Petstore::Errors::ApiError`, carry no status code (there was no
|
|
81
|
+
response), and keep the underlying transport exception on `#cause`.
|
|
82
|
+
|
|
83
|
+
### Per-request Options
|
|
84
|
+
|
|
85
|
+
Every method accepts a `request_options:` hash to override behavior for a single call — a different
|
|
86
|
+
base URL, additional headers, or additional query parameters:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
client.some_resource.some_method(
|
|
90
|
+
request_options: {
|
|
91
|
+
base_url: "https://staging.example.com",
|
|
92
|
+
timeout: 5,
|
|
93
|
+
additional_headers: { "X-Custom-Header" => "value" },
|
|
94
|
+
additional_query_parameters: { "trace" => "true" }
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Contributing
|
|
100
|
+
|
|
101
|
+
While we value open-source contributions to this SDK, this library is generated programmatically.
|
|
102
|
+
Additions made directly to this library would have to be moved over to our generation code,
|
|
103
|
+
otherwise they would be overwritten upon the next generated release. Feel free to open a PR as
|
|
104
|
+
a proof of concept, but know that we will not be able to merge it as-is. We suggest opening
|
|
105
|
+
an issue first to discuss with us!
|
|
106
|
+
|
|
107
|
+
On the other hand, contributions to the README are always very welcome!
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
class PetstoreClient
|
|
5
|
+
DEFAULT_BASE_URL = Environment::DEFAULT
|
|
6
|
+
TOKEN_URL = ''
|
|
7
|
+
|
|
8
|
+
def initialize(client_id:, client_secret:, base_url: nil, max_retries: 2, timeout: nil, token_url: TOKEN_URL)
|
|
9
|
+
@token_manager = HTTP::OAuthTokenManager.new(
|
|
10
|
+
client_id: client_id,
|
|
11
|
+
client_secret: client_secret,
|
|
12
|
+
token_url: token_url,
|
|
13
|
+
base_url: base_url || DEFAULT_BASE_URL,
|
|
14
|
+
)
|
|
15
|
+
headers = { 'X-Fern-Language' => 'Ruby' }
|
|
16
|
+
@raw_client = Internal::Http::RawClient.new(
|
|
17
|
+
base_url: base_url || DEFAULT_BASE_URL,
|
|
18
|
+
headers: headers,
|
|
19
|
+
max_retries: max_retries,
|
|
20
|
+
timeout: timeout,
|
|
21
|
+
token_provider: -> { @token_manager.headers['Authorization'] },
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def pet
|
|
26
|
+
@pet ||= Pet::Client.new(client: @raw_client)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def store
|
|
30
|
+
@store ||= Store::Client.new(client: @raw_client)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def user
|
|
34
|
+
@user ||= User::Client.new(client: @raw_client)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def client_id=(client_id)
|
|
38
|
+
@token_manager.client_id = client_id
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def client_secret=(client_secret)
|
|
42
|
+
@token_manager.client_secret = client_secret
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
class Config
|
|
5
|
+
attr_reader :base_url, :timeout, :enable_response_validation, :return_raw, :additional_headers, :additional_query_parameters, :retry_config, :token, :api_key
|
|
6
|
+
|
|
7
|
+
def initialize(
|
|
8
|
+
base_url: nil,
|
|
9
|
+
timeout: nil,
|
|
10
|
+
timeout_ms: nil,
|
|
11
|
+
enable_response_validation: false,
|
|
12
|
+
return_raw: false,
|
|
13
|
+
additional_headers: nil,
|
|
14
|
+
additional_query_parameters: nil,
|
|
15
|
+
retry_config: nil,
|
|
16
|
+
token: nil,
|
|
17
|
+
api_key: nil
|
|
18
|
+
)
|
|
19
|
+
unless timeout_ms.nil?
|
|
20
|
+
warn '[DEPRECATION] `timeout_ms` is deprecated; use `timeout` (seconds) instead.', uplevel: 1
|
|
21
|
+
timeout = timeout_ms / 1000.0 if timeout.nil?
|
|
22
|
+
end
|
|
23
|
+
@base_url = base_url
|
|
24
|
+
@timeout = timeout
|
|
25
|
+
@enable_response_validation = enable_response_validation
|
|
26
|
+
@return_raw = return_raw
|
|
27
|
+
@additional_headers = additional_headers
|
|
28
|
+
@additional_query_parameters = additional_query_parameters
|
|
29
|
+
@retry_config = retry_config
|
|
30
|
+
@token = token
|
|
31
|
+
@api_key = api_key
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def with(**overrides)
|
|
35
|
+
base = to_h
|
|
36
|
+
base.delete(:timeout) if overrides.key?(:timeout_ms) && !overrides.key?(:timeout)
|
|
37
|
+
self.class.new(**base, **overrides)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
base_url: @base_url,
|
|
43
|
+
timeout: @timeout,
|
|
44
|
+
enable_response_validation: @enable_response_validation,
|
|
45
|
+
return_raw: @return_raw,
|
|
46
|
+
additional_headers: @additional_headers,
|
|
47
|
+
additional_query_parameters: @additional_query_parameters,
|
|
48
|
+
retry_config: @retry_config,
|
|
49
|
+
token: @token,
|
|
50
|
+
api_key: @api_key,
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class APIError < Error
|
|
7
|
+
attr_reader :status, :body, :headers
|
|
8
|
+
|
|
9
|
+
def initialize(status, body, headers = {})
|
|
10
|
+
@status = status
|
|
11
|
+
@body = body
|
|
12
|
+
@headers = headers || {}
|
|
13
|
+
super("HTTP #{status}: #{body}")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class TimeoutError < Error
|
|
18
|
+
def initialize(msg = 'Request timed out')
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The request never reached the server: connection refused, host unresolvable, socket closed by
|
|
24
|
+
# the peer, or a failed TLS handshake. The underlying transport exception is on `#cause`.
|
|
25
|
+
class ConnectionError < Error
|
|
26
|
+
def initialize(msg = 'Failed to connect')
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class ValidationError < Error
|
|
32
|
+
attr_reader :field, :validation_message
|
|
33
|
+
|
|
34
|
+
def initialize(field, message)
|
|
35
|
+
@field = field
|
|
36
|
+
@validation_message = message
|
|
37
|
+
super("Validation failed for #{field}: #{message}")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
module Errors
|
|
5
|
+
# A 4xx response.
|
|
6
|
+
class ClientError < ResponseError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# One class per status a spec commonly declares, so a caller can rescue the exact response
|
|
10
|
+
# rather than the whole 4xx range. A 400 and a 422 are different failures — a malformed
|
|
11
|
+
# request against a well-formed one the server refuses — and collapsing them onto ClientError
|
|
12
|
+
# left no way to branch on which had happened. Any other 4xx still falls back to ClientError.
|
|
13
|
+
class BadRequestError < ClientError
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class UnauthorizedError < ClientError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class PaymentRequiredError < ClientError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class ForbiddenError < ClientError
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class NotFoundError < ClientError
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class MethodNotAllowedError < ClientError
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class ConflictError < ClientError
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class UnsupportedMediaTypeError < ClientError
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class UnprocessableEntityError < ClientError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class TooManyRequestsError < ClientError
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
module Errors
|
|
5
|
+
# Raised when the request never reached the server: the connection was refused, the host could
|
|
6
|
+
# not be resolved, the peer closed the socket, or the TLS handshake failed. The underlying
|
|
7
|
+
# transport exception is available on `#cause`.
|
|
8
|
+
class ConnectionError < ApiError
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
module Errors
|
|
5
|
+
# An error returned by the API. The raw response body is the exception message and the HTTP
|
|
6
|
+
# status code is available via #code, as before; #body adds the body deserialized into the
|
|
7
|
+
# type the spec declares for that status, so a field the spec declares on an error response is
|
|
8
|
+
# reachable as `error.body.retry_after_seconds` instead of only by parsing #message by hand.
|
|
9
|
+
#
|
|
10
|
+
# #body is nil when the response carried no body, or a body that is not the declared shape.
|
|
11
|
+
class ResponseError < ApiError
|
|
12
|
+
attr_reader :code, :body
|
|
13
|
+
|
|
14
|
+
# @param msg [String] The raw response body.
|
|
15
|
+
# @param code [Integer] The HTTP status code.
|
|
16
|
+
# @param body [Object, nil] The deserialized error body.
|
|
17
|
+
def initialize(msg, code:, body: nil)
|
|
18
|
+
@code = code
|
|
19
|
+
@body = body
|
|
20
|
+
super(msg)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Deserializes a raw error body with +type+, or nil when there is nothing to deserialize or
|
|
24
|
+
# the body is not the declared shape.
|
|
25
|
+
#
|
|
26
|
+
# An error response is the one place a server is most likely to answer off-spec — an HTML
|
|
27
|
+
# page from a proxy, an empty 500, a plain-text gateway message — and a raise site must not
|
|
28
|
+
# turn that into a parse failure that hides the status code the caller needs. So this
|
|
29
|
+
# rescues and degrades to nil rather than propagating.
|
|
30
|
+
#
|
|
31
|
+
# @param raw [String, nil] The raw response body.
|
|
32
|
+
# @param type [Class, nil] The model declared for this status, or nil if none is.
|
|
33
|
+
# @return [Object, nil]
|
|
34
|
+
def self.load_body(raw, type)
|
|
35
|
+
return nil if type.nil? || raw.nil? || raw.to_s.strip.empty?
|
|
36
|
+
|
|
37
|
+
parsed = type.load(raw)
|
|
38
|
+
deserialized?(parsed) ? parsed : nil
|
|
39
|
+
rescue StandardError
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# True when +load+ actually deserialized the body into the declared type, rather than handing
|
|
44
|
+
# back the raw parsed JSON — which is what it does for an off-spec body, since a model's
|
|
45
|
+
# #coerce returns a non-Hash untouched and a union's returns a value matching no member
|
|
46
|
+
# untouched.
|
|
47
|
+
#
|
|
48
|
+
# This deliberately does NOT ask +is_a?(type)+. A oneOf/anyOf error model is generated as a
|
|
49
|
+
# MODULE and its +load+ returns the matching MEMBER, whose class does not include that
|
|
50
|
+
# module, so +is_a?+ is false for every VALID union body; an enum's +load+ returns a Symbol,
|
|
51
|
+
# which is likewise not an instance of the enum. Both would have silently nil'd #body.
|
|
52
|
+
# Everything successfully deserialized is a model instance or an enum's Symbol, and neither
|
|
53
|
+
# is a shape ::JSON.parse can produce on its own.
|
|
54
|
+
private_class_method def self.deserialized?(value)
|
|
55
|
+
value.is_a?(Internal::Types::Model) || value.is_a?(::Symbol)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def inspect
|
|
59
|
+
"#<#{self.class.name} @code=#{code} @body=#{message}>"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Maps an HTTP status code to the most specific error subclass. Status-specific classes come
|
|
63
|
+
# first so a caller can rescue the exact response the spec declares; the range classes are
|
|
64
|
+
# the fallback for a status without one.
|
|
65
|
+
#
|
|
66
|
+
# Resolved inside the method body rather than from a constant map, so the subclasses only
|
|
67
|
+
# have to be loaded by the time a request actually fails, not by the time this file is read.
|
|
68
|
+
def self.subclass_for_code(code)
|
|
69
|
+
case code
|
|
70
|
+
when 300..399 then RedirectError
|
|
71
|
+
when 400 then BadRequestError
|
|
72
|
+
when 401 then UnauthorizedError
|
|
73
|
+
when 402 then PaymentRequiredError
|
|
74
|
+
when 403 then ForbiddenError
|
|
75
|
+
when 404 then NotFoundError
|
|
76
|
+
when 405 then MethodNotAllowedError
|
|
77
|
+
when 409 then ConflictError
|
|
78
|
+
when 415 then UnsupportedMediaTypeError
|
|
79
|
+
when 422 then UnprocessableEntityError
|
|
80
|
+
when 429 then TooManyRequestsError
|
|
81
|
+
when 400..499 then ClientError
|
|
82
|
+
when 500 then InternalServerError
|
|
83
|
+
when 502 then BadGatewayError
|
|
84
|
+
when 503 then ServiceUnavailableError
|
|
85
|
+
when 504 then GatewayTimeoutError
|
|
86
|
+
when 500..599 then ServerError
|
|
87
|
+
else ResponseError
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
module Errors
|
|
5
|
+
# A 5xx response.
|
|
6
|
+
class ServerError < ResponseError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class InternalServerError < ServerError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class BadGatewayError < ServerError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Was `< ApiError`, which is not a ResponseError and so takes no `code:` — a 503 therefore
|
|
16
|
+
# raised `ArgumentError: wrong number of arguments` from the raise site itself, and the typed
|
|
17
|
+
# error was unreachable. It is also now inside the 5xx subtree, so `rescue ServerError`
|
|
18
|
+
# catches it.
|
|
19
|
+
class ServiceUnavailableError < ServerError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class GatewayTimeoutError < ServerError
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
module HTTP
|
|
5
|
+
class Auth
|
|
6
|
+
def initialize(token: nil, api_key: nil)
|
|
7
|
+
@token = token
|
|
8
|
+
@api_key = api_key
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def headers
|
|
12
|
+
result = {}
|
|
13
|
+
result['Authorization'] = "Bearer #{@token}" unless @token.nil?
|
|
14
|
+
result['api_key'] = @api_key unless @api_key.nil?
|
|
15
|
+
result
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module Petstore
|
|
8
|
+
module HTTP
|
|
9
|
+
class OAuthError < StandardError; end
|
|
10
|
+
|
|
11
|
+
# Manages OAuth 2.0 client credentials tokens: fetches, caches, and refreshes them.
|
|
12
|
+
# Token refresh is triggered when the token is absent or within REFRESH_BUFFER_SECS
|
|
13
|
+
# of expiry. A Mutex ensures thread-safe access to the cached token.
|
|
14
|
+
# Note: token refresh blocks all threads calling `headers` until the HTTP request
|
|
15
|
+
# completes. This is intentional — only one fetch occurs and others reuse the result.
|
|
16
|
+
class OAuthTokenManager
|
|
17
|
+
REFRESH_BUFFER_SECS = 30
|
|
18
|
+
|
|
19
|
+
# @param token_url [String] The token endpoint; a relative URL is joined to `base_url`.
|
|
20
|
+
# @param base_url [String, nil] The URL a relative `token_url` is resolved against.
|
|
21
|
+
def initialize(client_id:, client_secret:, token_url:, base_url: nil)
|
|
22
|
+
@client_id = client_id
|
|
23
|
+
@client_secret = client_secret
|
|
24
|
+
@token_url = token_url
|
|
25
|
+
@base_url = base_url
|
|
26
|
+
@access_token = nil
|
|
27
|
+
@expires_at = nil
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
attr_writer :client_id, :client_secret
|
|
32
|
+
|
|
33
|
+
# @return [Boolean] Whether both a client ID and a client secret are set.
|
|
34
|
+
def credentials?
|
|
35
|
+
[@client_id, @client_secret].none? { |value| value.nil? || value.to_s.empty? }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def headers
|
|
39
|
+
{ 'Authorization' => "Bearer #{token}" }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Force the next call to re-fetch a fresh token.
|
|
43
|
+
def invalidate
|
|
44
|
+
@mutex.synchronize { @access_token = nil }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def token
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
fetch_token if token_expired?
|
|
52
|
+
@access_token
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def token_expired?
|
|
57
|
+
@access_token.nil? || (@expires_at && Time.now + REFRESH_BUFFER_SECS >= @expires_at)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def token_uri
|
|
61
|
+
url = @token_url.to_s
|
|
62
|
+
return URI.parse(url) if @base_url.nil? || url.match?(%r{\A[a-z][a-z0-9+.-]*://}i)
|
|
63
|
+
|
|
64
|
+
URI.parse("#{@base_url.to_s.chomp('/')}/#{url.delete_prefix('/')}")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def fetch_token
|
|
68
|
+
uri = token_uri
|
|
69
|
+
req = Net::HTTP::Post.new(uri.request_uri)
|
|
70
|
+
req.set_form_data(
|
|
71
|
+
grant_type: 'client_credentials',
|
|
72
|
+
client_id: @client_id,
|
|
73
|
+
client_secret: @client_secret,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Net::HTTP.start with a block opens and closes the connection automatically.
|
|
77
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
78
|
+
http.request(req)
|
|
79
|
+
end
|
|
80
|
+
raise OAuthError, "token request failed (HTTP #{response.code}): #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
81
|
+
|
|
82
|
+
body = JSON.parse(response.body)
|
|
83
|
+
raise OAuthError, 'access_token missing from token response' unless body.key?('access_token')
|
|
84
|
+
|
|
85
|
+
@access_token = body['access_token']
|
|
86
|
+
expires_in = body['expires_in']
|
|
87
|
+
@expires_at = expires_in ? Time.now + expires_in.to_i : nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|