x-core 1.0.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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +8 -0
  3. data/CHANGELOG.md +353 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +54 -0
  6. data/lib/x/core/app_only_authenticator.rb +198 -0
  7. data/lib/x/core/authenticator.rb +74 -0
  8. data/lib/x/core/authenticator_request.rb +56 -0
  9. data/lib/x/core/bearer_token_authenticator.rb +46 -0
  10. data/lib/x/core/built_response.rb +100 -0
  11. data/lib/x/core/client.rb +728 -0
  12. data/lib/x/core/client_app_only.rb +115 -0
  13. data/lib/x/core/client_credentials.rb +193 -0
  14. data/lib/x/core/client_internals.rb +315 -0
  15. data/lib/x/core/client_memo.rb +58 -0
  16. data/lib/x/core/client_settings.rb +166 -0
  17. data/lib/x/core/client_token_refresh.rb +281 -0
  18. data/lib/x/core/connection.rb +328 -0
  19. data/lib/x/core/connection_pool.rb +123 -0
  20. data/lib/x/core/connection_proxy.rb +151 -0
  21. data/lib/x/core/connection_request.rb +90 -0
  22. data/lib/x/core/credential_holder.rb +78 -0
  23. data/lib/x/core/credential_validator.rb +286 -0
  24. data/lib/x/core/errors/authorization_denied.rb +55 -0
  25. data/lib/x/core/errors/authorization_error.rb +41 -0
  26. data/lib/x/core/errors/bad_gateway.rb +9 -0
  27. data/lib/x/core/errors/bad_request.rb +10 -0
  28. data/lib/x/core/errors/callback_error.rb +88 -0
  29. data/lib/x/core/errors/client_error.rb +17 -0
  30. data/lib/x/core/errors/conflict.rb +9 -0
  31. data/lib/x/core/errors/error.rb +66 -0
  32. data/lib/x/core/errors/forbidden.rb +10 -0
  33. data/lib/x/core/errors/gateway_timeout.rb +9 -0
  34. data/lib/x/core/errors/gone.rb +9 -0
  35. data/lib/x/core/errors/http_error.rb +333 -0
  36. data/lib/x/core/errors/internal_server_error.rb +9 -0
  37. data/lib/x/core/errors/invalid_response.rb +96 -0
  38. data/lib/x/core/errors/method_not_allowed.rb +10 -0
  39. data/lib/x/core/errors/network_error.rb +59 -0
  40. data/lib/x/core/errors/not_acceptable.rb +10 -0
  41. data/lib/x/core/errors/not_found.rb +15 -0
  42. data/lib/x/core/errors/payload_too_large.rb +10 -0
  43. data/lib/x/core/errors/payment_required.rb +10 -0
  44. data/lib/x/core/errors/request_timeout.rb +9 -0
  45. data/lib/x/core/errors/server_error.rb +15 -0
  46. data/lib/x/core/errors/service_unavailable.rb +9 -0
  47. data/lib/x/core/errors/token_report_failed.rb +59 -0
  48. data/lib/x/core/errors/too_many_redirects.rb +53 -0
  49. data/lib/x/core/errors/too_many_requests.rb +80 -0
  50. data/lib/x/core/errors/unauthorized.rb +15 -0
  51. data/lib/x/core/errors/unavailable_for_legal_reasons.rb +10 -0
  52. data/lib/x/core/errors/unprocessable_entity.rb +10 -0
  53. data/lib/x/core/errors/unsupported_format.rb +22 -0
  54. data/lib/x/core/errors/unsupported_media_type.rb +10 -0
  55. data/lib/x/core/errors/unsupported_operation.rb +18 -0
  56. data/lib/x/core/oauth1_authenticator.rb +142 -0
  57. data/lib/x/core/oauth2_authenticator.rb +296 -0
  58. data/lib/x/core/oauth2_authorization.rb +405 -0
  59. data/lib/x/core/oauth2_refresh.rb +272 -0
  60. data/lib/x/core/oauth2_tokens.rb +277 -0
  61. data/lib/x/core/origin.rb +95 -0
  62. data/lib/x/core/problem.rb +302 -0
  63. data/lib/x/core/proxy_setting.rb +22 -0
  64. data/lib/x/core/rate_limit.rb +137 -0
  65. data/lib/x/core/rate_limit_handler.rb +166 -0
  66. data/lib/x/core/redirect_handler.rb +208 -0
  67. data/lib/x/core/refresh_reporter.rb +92 -0
  68. data/lib/x/core/request_builder.rb +155 -0
  69. data/lib/x/core/request_context.rb +85 -0
  70. data/lib/x/core/request_encoding.rb +135 -0
  71. data/lib/x/core/response.rb +196 -0
  72. data/lib/x/core/response_headers.rb +31 -0
  73. data/lib/x/core/response_parser.rb +155 -0
  74. data/lib/x/core/retry_handler.rb +149 -0
  75. data/lib/x/core/setting_validator.rb +399 -0
  76. data/lib/x/core/stream_body.rb +77 -0
  77. data/lib/x/core/stream_response.rb +267 -0
  78. data/lib/x/core/token_endpoint.rb +238 -0
  79. data/lib/x/core/version.rb +25 -0
  80. data/lib/x/core.rb +6 -0
  81. data/sig/manifest.yaml +8 -0
  82. data/sig/x-core.rbs +396 -0
  83. metadata +162 -0
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client_error"
4
+
5
+ module X
6
+ # Raised for a 404 Not Found response
7
+ #
8
+ # The API sends one for an endpoint it does not serve. It answers a lookup of a resource that does not exist with
9
+ # 200 OK and no data, or with a 404 that reports the resource as not found, and a request of x-core raises that
10
+ # 404 as this error too. The finders of x-resources read either answer as a missing resource; see
11
+ # X::MissingResource.
12
+ #
13
+ # @api public
14
+ class NotFound < ClientError; end
15
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client_error"
4
+
5
+ module X
6
+ # Raised for a 413 Payload Too Large response, which the API sends for a body larger than the endpoint takes, such
7
+ # as a media chunk above the size it accepts
8
+ # @api public
9
+ class PayloadTooLarge < ClientError; end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client_error"
4
+
5
+ module X
6
+ # Raised for a 402 Payment Required response, which the API sends for a request the account that pays for the app
7
+ # has no credit left to pay for, and which is refused again until credit is added
8
+ # @api public
9
+ class PaymentRequired < ClientError; end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client_error"
4
+
5
+ module X
6
+ # Raised for a 408 Request Timeout response, which the API sends when it gave up waiting for the rest of a request
7
+ # @api public
8
+ class RequestTimeout < ClientError; end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "http_error"
4
+
5
+ module X
6
+ # The base class of the errors of a 5xx response, which the API failed to answer
7
+ #
8
+ # The request is not the reason it failed, so the same request may pass later. A client given a max_retries above
9
+ # zero sends an idempotent request again after one, waiting a little longer before each attempt.
10
+ #
11
+ # @api public
12
+ # @example Retry a request the API failed to answer
13
+ # client = X::Client.new(bearer_token: token, max_retries: 2)
14
+ class ServerError < HTTPError; end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "server_error"
4
+
5
+ module X
6
+ # Raised for a 503 Service Unavailable response, which the API sends when it is overloaded or down for maintenance
7
+ # @api public
8
+ class ServiceUnavailable < ServerError; end
9
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module X
6
+ # Error raised when save_tokens raised for the tokens of an exchange of a code or of a refresh
7
+ #
8
+ # An authorization code works once, and so does a refresh token, so the tokens X issues for either are held in
9
+ # memory alone until save_tokens stores them: the first refresh token of X::OAuth2Authorization#client by the
10
+ # client it builds, and the one a refresh issued by the authenticator that refreshed, since the one it replaced is
11
+ # spent. They are not lost to a failure to store them, such as a database that is briefly down: the error holds
12
+ # them, so they can be stored again, and the client, which the exchange builds, or whose request refreshed. The
13
+ # error save_tokens raised is the cause, whose message the message ends with.
14
+ #
15
+ # @api public
16
+ class TokenReportFailed < Error
17
+ # The client the authorization built, or whose request refreshed the tokens
18
+ # @api public
19
+ # @return [Client, nil] the client, or nil for a refresh made by an authenticator alone, as its refresh! makes one
20
+ # @example Act for the user once the tokens are stored
21
+ # rescue X::TokenReportFailed => e
22
+ # store(e.tokens.refresh_token)
23
+ # e.client.get("users/me")
24
+ attr_reader :client
25
+
26
+ # The tokens of the exchange or the refresh, which save_tokens raised for
27
+ # @api public
28
+ # @return [OAuth2Tokens, nil] the tokens, or nil if none were given
29
+ # @example Store the tokens again
30
+ # rescue X::TokenReportFailed => e
31
+ # store(e.tokens.refresh_token)
32
+ attr_reader :tokens
33
+
34
+ # Initialize the error with the client and the tokens it failed to report
35
+ #
36
+ # @api public
37
+ # @param message [String, nil] the message, or nil for one that says the tokens of an exchange were not stored
38
+ # @param client [Client, nil] the client the authorization built, or whose request refreshed the tokens
39
+ # @param tokens [OAuth2Tokens, nil] the tokens of the exchange or the refresh
40
+ # @return [TokenReportFailed] a new error
41
+ # @example Raise the error for tokens save_tokens raised for
42
+ # raise X::TokenReportFailed.new(client:, tokens:)
43
+ def initialize(message = nil, client: nil, tokens: nil)
44
+ @client = client
45
+ @tokens = tokens
46
+ super(message || "The code was exchanged for tokens, but save_tokens raised for them")
47
+ end
48
+
49
+ # The message, ending with why save_tokens raised
50
+ #
51
+ # It ends with the message of the error save_tokens raised, which is the cause, if there is one.
52
+ #
53
+ # @api public
54
+ # @return [String] the message
55
+ # @example Read why the tokens were not stored
56
+ # error.message # => "The code was exchanged for tokens, but save_tokens raised for them: connection refused"
57
+ def to_s = [super, cause&.message].compact.join(": ")
58
+ end
59
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+ require_relative "../request_context"
5
+
6
+ module X
7
+ module Core
8
+ # Raised for a response that redirected more times than the client's max_redirects allows
9
+ #
10
+ # The message names the request whose redirect was one too many, and {#http_method} and {#uri} read it: the
11
+ # request that redirect answered, which is the one a client sent last, rather than the one it was first asked for.
12
+ #
13
+ # @api public
14
+ # @example Tell an endpoint that redirects in a loop from the others
15
+ # rescue X::TooManyRedirects => e
16
+ # logger.warn("#{e.uri} keeps redirecting")
17
+ class ::X::TooManyRedirects < Error
18
+ include RequestContext
19
+
20
+ # @!method http_method
21
+ # The HTTP method the request whose redirect was one too many was sent with
22
+ # @api public
23
+ # @return [Symbol, nil] the method, as :get, :post, :put, or :delete, or nil for an error built without one
24
+ # @example Tell a read that redirected in a loop from a write
25
+ # writes_failed += 1 unless error.http_method.eql?(:get)
26
+ # @!method uri
27
+ # The URI the request whose redirect was one too many was sent to
28
+ # @api public
29
+ # @return [URI::Generic, nil] the URI, or nil for an error built without one
30
+ # @example Get the path that keeps redirecting
31
+ # error.uri.path # => "/2/users/me"
32
+
33
+ # Initialize a new TooManyRedirects
34
+ #
35
+ # Public, so that code that rescues a TooManyRedirects can be tested with one built by hand, as RedirectHandler
36
+ # builds one, or raised with no message, as any exception is. The error names the request, when given its method
37
+ # and URI, as x-core names the request whose redirect was one too many.
38
+ #
39
+ # @api public
40
+ # @param message [String, nil] what went wrong, or nil for the name of the class, as an exception raised with no
41
+ # message is named, which names no request
42
+ # @param http_method [Symbol, String, nil] the method of the request whose redirect was one too many, in any case
43
+ # @param uri [URI::Generic, nil] the URI of the request whose redirect was one too many
44
+ # @return [TooManyRedirects] a new instance
45
+ # @example Create an error
46
+ # error = X::TooManyRedirects.new("Too many redirects", http_method: :get, uri: URI("https://api.x.com/2/users/me"))
47
+ def initialize(message = nil, http_method: nil, uri: nil)
48
+ name_request(http_method, uri)
49
+ super(message && message_naming_request(message))
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client_error"
4
+ require_relative "../rate_limit"
5
+
6
+ module X
7
+ # Error raised when rate limit is exceeded (HTTP 429)
8
+ # @api public
9
+ class TooManyRequests < ClientError
10
+ # The rate limits the response reports in its headers, as X::Response reports them
11
+ #
12
+ # They are read once and frozen, since the other readers of the error, and the wait before a request is sent
13
+ # again, read them too, so that a caller changing them changes none of those.
14
+ #
15
+ # @api public
16
+ # @return [Array<RateLimit>] the 15-minute limit, and the 24-hour app and user limits when reported, frozen
17
+ # @example Print how many requests remain in each window
18
+ # error.rate_limits.each { |limit| puts "#{limit.type}: #{limit.remaining}" }
19
+ def rate_limits
20
+ @rate_limits ||= RateLimit.__send__(:all_from, http_response).freeze
21
+ end
22
+
23
+ # The 15-minute rate limit of the endpoint, which nearly every response reports
24
+ #
25
+ # @api public
26
+ # @return [RateLimit, nil] the rate limit, or nil if the response reports none
27
+ # @example Read how many of the 15-minute requests are left
28
+ # error.rate_limit&.remaining # => 3
29
+ def rate_limit = rate_limits.find { |limit| limit.type.eql?(RateLimit::RATE_LIMIT_TYPE) }
30
+
31
+ # The rate limits with no requests left, one of which refused the request
32
+ #
33
+ # @api public
34
+ # @return [Array<RateLimit>] the reported limits that are exhausted
35
+ # @example Name the windows that are used up
36
+ # error.exhausted_rate_limits.map(&:type) # => ["app-limit-24hour"]
37
+ def exhausted_rate_limits = rate_limits.select(&:exhausted?)
38
+
39
+ # The exhausted rate limit that resets last, which a request waits for
40
+ #
41
+ # @api public
42
+ # @return [RateLimit, nil] the limit, or nil if the response reports none as exhausted
43
+ # @example Name the window that refused the request
44
+ # error.limiting_rate_limit&.type # => "app-limit-24hour"
45
+ def limiting_rate_limit = exhausted_rate_limits.max_by(&:reset_at)
46
+
47
+ # Get the time when the rate limit resets
48
+ #
49
+ # @api public
50
+ # @return [Time, nil] the reset time, or nil if the response does not say when the limit resets
51
+ # @example Get the reset time
52
+ # error.reset_at
53
+ def reset_at
54
+ limiting_rate_limit&.reset_at
55
+ end
56
+
57
+ # Get the seconds until the rate limit resets
58
+ #
59
+ # @api public
60
+ # @return [Integer, nil] the seconds until reset, or nil if the response does not say when the limit resets
61
+ # @example Get the time until reset
62
+ # error.reset_in
63
+ def reset_in
64
+ limiting_rate_limit&.reset_in
65
+ end
66
+
67
+ # The seconds to wait before retrying, as the response asks
68
+ #
69
+ # A Retry-After header counts the seconds from when the response was sent, so it says the same thing however
70
+ # far this machine's clock is from the API's, where reset_in is off by the difference between the two. The
71
+ # header is preferred for that reason, and the limit that refused the request answers for a response that
72
+ # carries none. X recommends waiting a minute, doubling the wait for each retry after, when it says neither.
73
+ #
74
+ # @api public
75
+ # @return [Integer, nil] the seconds to wait before retrying, or nil if the response does not say
76
+ # @example Wait before retrying
77
+ # sleep(error.retry_after || 60)
78
+ def retry_after = super || reset_in
79
+ end
80
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client_error"
4
+
5
+ module X
6
+ # Raised for a 401 Unauthorized response, which the API sends for credentials it does not accept, such as a token
7
+ # that has expired or been revoked, or a request that carries none
8
+ #
9
+ # A client that authenticates with OAuth 2.0 refreshes a token the API rejects and sends the request again, so it
10
+ # raises this error for a token that is rejected once refreshed, and an AuthorizationError, whose cause is this
11
+ # error, for a refresh X refuses.
12
+ #
13
+ # @api public
14
+ class Unauthorized < ClientError; end
15
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client_error"
4
+
5
+ module X
6
+ # Raised for a 451 Unavailable For Legal Reasons response, which the API sends for a resource withheld in the
7
+ # country the request came from
8
+ # @api public
9
+ class UnavailableForLegalReasons < ClientError; end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client_error"
4
+
5
+ module X
6
+ # Raised for a 422 Unprocessable Entity response, which the API sends for a request it can read but will not act
7
+ # on, such as a stream rule it rejects
8
+ # @api public
9
+ class UnprocessableEntity < ClientError; end
10
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module X
6
+ # Raised when Marshal, YAML, or JSON reads what was written in a format this release does not read
7
+ #
8
+ # What the X gems write with Marshal or YAML, such as a resource, a page, or a problem, and the JSON of the tokens
9
+ # of OAuth 2.0, is plain data led by the number of
10
+ # its format, which every release of 1.x writes, each adding only what an earlier one ignores, so that a cache
11
+ # written by one release of 1.x is read by every other, and one written in another format, as a later major
12
+ # version may write, fails where it is read, rather than read as something it is not. Such a cache is written again.
13
+ #
14
+ # @api public
15
+ # @example Read a cached user again when it was written in another format
16
+ # begin
17
+ # Marshal.load(cached)
18
+ # rescue X::UnsupportedFormat
19
+ # Rails.cache.delete("user")
20
+ # end
21
+ class UnsupportedFormat < Error; end
22
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client_error"
4
+
5
+ module X
6
+ # Raised for a 415 Unsupported Media Type response, which the API sends for a body in a format the endpoint does
7
+ # not take, such as a form where it takes JSON
8
+ # @api public
9
+ class UnsupportedMediaType < ClientError; end
10
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module X
6
+ # Raised when there is no way to do what was asked with what the client holds
7
+ #
8
+ # The API may offer none, as it offers no lookup of lists in a batch, or the credentials a client holds may not
9
+ # reach it: a client that authenticates with OAuth 2.0 as a user, and holds no credentials of the app, has no
10
+ # app-only client for Client#app_only to return, and an OAuth 2.0 authenticator that holds no refresh token, as an
11
+ # authorization without the offline.access scope issues none, cannot refresh its access token. Either is raised
12
+ # before any request. A request the API refuses the credentials of a client for raises the error of its response
13
+ # instead, as a stream, or the count of the full archive, of a client with no app-only client raises the
14
+ # X::Forbidden of the 403 the API answers it with.
15
+ #
16
+ # @api public
17
+ class UnsupportedOperation < Error; end
18
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simple_oauth"
4
+ require "uri"
5
+ require_relative "authenticator"
6
+ require_relative "credential_validator"
7
+ require_relative "setting_validator"
8
+
9
+ module X
10
+ module Core
11
+ # Authenticator for OAuth 1.0a authentication
12
+ # @api public
13
+ class ::X::OAuth1Authenticator < Authenticator
14
+ # The media type whose body OAuth 1.0a signs as request parameters
15
+ FORM_CONTENT_TYPE = "application/x-www-form-urlencoded"
16
+ private_constant :FORM_CONTENT_TYPE
17
+
18
+ # The API key (consumer key)
19
+ # @api public
20
+ # @return [String] the API key (consumer key)
21
+ # @example Get the API key
22
+ # authenticator.api_key
23
+ attr_reader :api_key
24
+
25
+ # Initialize a new OAuth1Authenticator
26
+ #
27
+ # @api public
28
+ # @param api_key [String] the API key (consumer key)
29
+ # @param api_key_secret [String] the API key secret (consumer secret)
30
+ # @param access_token [String] the access token
31
+ # @param access_token_secret [String] the access token secret
32
+ # @return [OAuth1Authenticator] a new instance
33
+ # @raise [ArgumentError] if a credential is nil or empty
34
+ # @example Create an OAuth authenticator
35
+ # authenticator = X::OAuth1Authenticator.new(
36
+ # api_key: "key",
37
+ # api_key_secret: "secret",
38
+ # access_token: "token",
39
+ # access_token_secret: "token_secret"
40
+ # )
41
+ def initialize(api_key:, api_key_secret:, access_token:, access_token_secret:)
42
+ CredentialValidator.validate_required!({api_key:, api_key_secret:, access_token:, access_token_secret:})
43
+ @api_key, @api_key_secret = SettingValidator.frozen(api_key), SettingValidator.frozen(api_key_secret)
44
+ @access_token, @access_token_secret = SettingValidator.frozen(access_token), SettingValidator.frozen(access_token_secret)
45
+ end
46
+
47
+ # The identifier of the user the access token acts for
48
+ #
49
+ # An OAuth 1.0a access token begins with the identifier of the user who authorized it, so a client that signs
50
+ # with one knows the user it acts for without asking the API.
51
+ #
52
+ # @api public
53
+ # @return [Integer, nil] the identifier, or nil for a token that begins with none
54
+ # @example Read the user a client acts for without a request
55
+ # client.authenticator.user_id # => 7505382
56
+ def user_id
57
+ prefix = access_token[/\A(\d+)-/, 1]
58
+ Integer(prefix, 10) if prefix
59
+ end
60
+
61
+ # Generate the OAuth authentication headers for a request
62
+ #
63
+ # The signature covers the HTTP method, the URL, its query parameters, and a
64
+ # form-encoded body. Bodies of any other media type, such as the JSON and multipart
65
+ # bodies the X API takes, are not signed.
66
+ #
67
+ # @api public
68
+ # @param request [#http_method, #uri, #body, #[]] the request, whose method, uri, body, and Content-Type the signature reads
69
+ # @return [Hash{String => String}] the authentication header with OAuth signature
70
+ # @example Generate an OAuth authentication header
71
+ # authenticator.headers(request)
72
+ def headers(request)
73
+ oauth_header = SimpleOAuth::Header.new(request.http_method, request.uri, form_params(request), credentials)
74
+ {AUTHENTICATION_HEADER => oauth_header.to_s}
75
+ end
76
+
77
+ private
78
+
79
+ # The API key secret (consumer secret), which signs a request
80
+ # @api private
81
+ # @return [String] the API key secret (consumer secret)
82
+ # @example Sign with the API key secret
83
+ # api_key_secret
84
+ attr_reader :api_key_secret
85
+
86
+ # The access token, which signs each request and names the user it acts for
87
+ #
88
+ # It is a credential, so it is private, as the access token of a client is: the user it acts for is read with
89
+ # {#user_id}.
90
+ #
91
+ # @api private
92
+ # @return [String] the access token
93
+ # @example Sign with the access token
94
+ # access_token
95
+ attr_reader :access_token
96
+
97
+ # The access token secret, which signs a request
98
+ # @api private
99
+ # @return [String] the access token secret
100
+ # @example Sign with the access token secret
101
+ # access_token_secret
102
+ attr_reader :access_token_secret
103
+
104
+ # The credentials, under the names simple_oauth gives them
105
+ # @api private
106
+ # @return [Hash{Symbol => String}] the credentials for signing
107
+ def credentials
108
+ {consumer_key: api_key, consumer_secret: api_key_secret, token: access_token,
109
+ token_secret: access_token_secret}
110
+ end
111
+
112
+ # The parameters a form-encoded body contributes to the signature
113
+ #
114
+ # A parameter that repeats is signed once per value, as RFC 5849 Section 3.4.1.3.2 requires,
115
+ # which the key-value pairs preserve.
116
+ #
117
+ # @api private
118
+ # @param request [#http_method, #uri, #body, #[]] the request
119
+ # @return [Array<Array(String, String)>] the body parameters, empty unless the body is form-encoded
120
+ def form_params(request)
121
+ URI.decode_www_form(form_body(request))
122
+ end
123
+
124
+ # The body whose parameters take part in the signature
125
+ #
126
+ # @api private
127
+ # @param request [#http_method, #uri, #body, #[]] the request
128
+ # @return [String] the body, or an empty String when it is not form-encoded
129
+ def form_body(request)
130
+ form_encoded?(request) ? request.body.to_s : ""
131
+ end
132
+
133
+ # Check whether a request carries a form-encoded body
134
+ # @api private
135
+ # @param request [#http_method, #uri, #body, #[]] the request
136
+ # @return [Boolean] true if the body is form-encoded
137
+ def form_encoded?(request)
138
+ request["Content-Type"].to_s.split(";").first.to_s.strip.downcase.eql?(FORM_CONTENT_TYPE)
139
+ end
140
+ end
141
+ end
142
+ end