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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +107 -0
  3. data/lib/petstore/client.rb +45 -0
  4. data/lib/petstore/config.rb +54 -0
  5. data/lib/petstore/environment.rb +7 -0
  6. data/lib/petstore/error.rb +40 -0
  7. data/lib/petstore/errors/api_error.rb +9 -0
  8. data/lib/petstore/errors/client_error.rb +43 -0
  9. data/lib/petstore/errors/connection_error.rb +11 -0
  10. data/lib/petstore/errors/redirect_error.rb +9 -0
  11. data/lib/petstore/errors/response_error.rb +92 -0
  12. data/lib/petstore/errors/server_error.rb +25 -0
  13. data/lib/petstore/errors/timeout_error.rb +9 -0
  14. data/lib/petstore/http/auth.rb +19 -0
  15. data/lib/petstore/http/oauth_token_manager.rb +91 -0
  16. data/lib/petstore/internal/errors/constraint_error.rb +10 -0
  17. data/lib/petstore/internal/errors/type_error.rb +10 -0
  18. data/lib/petstore/internal/form/request.rb +110 -0
  19. data/lib/petstore/internal/http/base_request.rb +64 -0
  20. data/lib/petstore/internal/http/event_stream.rb +106 -0
  21. data/lib/petstore/internal/http/raw_client.rb +426 -0
  22. data/lib/petstore/internal/http/response_body.rb +35 -0
  23. data/lib/petstore/internal/iterators/cursor_item_iterator.rb +28 -0
  24. data/lib/petstore/internal/iterators/cursor_page_iterator.rb +72 -0
  25. data/lib/petstore/internal/iterators/item_iterator.rb +72 -0
  26. data/lib/petstore/internal/iterators/offset_item_iterator.rb +30 -0
  27. data/lib/petstore/internal/iterators/offset_page_iterator.rb +110 -0
  28. data/lib/petstore/internal/json/request.rb +61 -0
  29. data/lib/petstore/internal/json/serializable.rb +25 -0
  30. data/lib/petstore/internal/multipart/multipart_encoder.rb +141 -0
  31. data/lib/petstore/internal/multipart/multipart_form_data.rb +78 -0
  32. data/lib/petstore/internal/multipart/multipart_form_data_part.rb +51 -0
  33. data/lib/petstore/internal/multipart/multipart_request.rb +49 -0
  34. data/lib/petstore/internal/raw/request.rb +88 -0
  35. data/lib/petstore/internal/types/array.rb +47 -0
  36. data/lib/petstore/internal/types/boolean.rb +34 -0
  37. data/lib/petstore/internal/types/enum.rb +56 -0
  38. data/lib/petstore/internal/types/hash.rb +36 -0
  39. data/lib/petstore/internal/types/model/field.rb +38 -0
  40. data/lib/petstore/internal/types/model.rb +246 -0
  41. data/lib/petstore/internal/types/type.rb +35 -0
  42. data/lib/petstore/internal/types/union.rb +193 -0
  43. data/lib/petstore/internal/types/unknown.rb +15 -0
  44. data/lib/petstore/internal/types/utils.rb +137 -0
  45. data/lib/petstore/models/open_model.rb +30 -0
  46. data/lib/petstore/models/serializable.rb +63 -0
  47. data/lib/petstore/models/union_type.rb +12 -0
  48. data/lib/petstore/pet/client.rb +196 -0
  49. data/lib/petstore/pet/types/delete_pet_request.rb +13 -0
  50. data/lib/petstore/pet/types/find_pets_by_status_request.rb +11 -0
  51. data/lib/petstore/pet/types/find_pets_by_status_request_status.rb +24 -0
  52. data/lib/petstore/pet/types/find_pets_by_tags_request.rb +11 -0
  53. data/lib/petstore/pet/types/get_pet_by_id_request.rb +11 -0
  54. data/lib/petstore/pet/types/pet.rb +25 -0
  55. data/lib/petstore/pet/types/update_pet_with_form_request.rb +15 -0
  56. data/lib/petstore/serializers/form_serializer.rb +41 -0
  57. data/lib/petstore/serializers/json_serializer.rb +41 -0
  58. data/lib/petstore/serializers/xml_serializer.rb +56 -0
  59. data/lib/petstore/store/client.rb +89 -0
  60. data/lib/petstore/store/types/delete_order_request.rb +11 -0
  61. data/lib/petstore/store/types/get_order_by_id_request.rb +11 -0
  62. data/lib/petstore/store/types/order.rb +25 -0
  63. data/lib/petstore/types/api_response.rb +20 -0
  64. data/lib/petstore/types/category.rb +19 -0
  65. data/lib/petstore/types/order_status.rb +22 -0
  66. data/lib/petstore/types/pet_status.rb +22 -0
  67. data/lib/petstore/types/tag.rb +19 -0
  68. data/lib/petstore/user/client.rb +149 -0
  69. data/lib/petstore/user/types/delete_user_request.rb +11 -0
  70. data/lib/petstore/user/types/get_user_by_name_request.rb +11 -0
  71. data/lib/petstore/user/types/login_user_request.rb +13 -0
  72. data/lib/petstore/user/types/user.rb +27 -0
  73. data/lib/petstore/validator.rb +42 -0
  74. data/lib/petstore/version.rb +5 -0
  75. data/lib/petstore.rb +78 -0
  76. 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
+ ![Generated by Postman](https://img.shields.io/badge/generated%20by-Postman-orange)
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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Environment
5
+ DEFAULT = '/api/v3'
6
+ end
7
+ 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Errors
5
+ # Base class for all errors raised by this SDK.
6
+ class ApiError < StandardError
7
+ end
8
+ end
9
+ 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Errors
5
+ # A 3xx response.
6
+ class RedirectError < ResponseError
7
+ end
8
+ end
9
+ 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Errors
5
+ # Raised when a request times out.
6
+ class TimeoutError < ApiError
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ module Errors
6
+ class ConstraintError < StandardError
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Petstore
4
+ module Internal
5
+ module Errors
6
+ class TypeError < StandardError
7
+ end
8
+ end
9
+ end
10
+ end