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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9cdddd966fbf038b00396e20049392a342e48a6a8aa31529981c8f0460dbe30c
4
+ data.tar.gz: 858ca6f56b21bd5feda55cce625e7af381337b32d9cc0be93a6e69bf84d2569d
5
+ SHA512:
6
+ metadata.gz: 31f3dd0ff7313db365ec54eac740c35eb442363ab535446ad2b480039daab08c2caa6351c902a83411f439d7c24b6774397d37cfae995dd56736ef1fc820d63a
7
+ data.tar.gz: e3d16f80907c70b37b248646e666f803d87e7264f9c3a6ecc676a65539841c769685d9cb95e5674bec5e9b88808ad8bd9d7cf4a3257e9a06984914684c99948b
data/.yardopts ADDED
@@ -0,0 +1,8 @@
1
+ --markup markdown
2
+ --readme README.md
3
+ --hide-api private
4
+ --embed-mixins
5
+ lib/**/*.rb
6
+ -
7
+ CHANGELOG.md
8
+ LICENSE.txt
data/CHANGELOG.md ADDED
@@ -0,0 +1,353 @@
1
+ # Changelog
2
+
3
+ All notable changes to `x-core` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ `x-core` is released in lockstep with the other gems of the [x-ruby](https://github.com/sferik/x-ruby) repository, at one version across `x-core`, `x-uploads`, `x-streams`, `x-resources`, and `x`. This file holds the changes to the HTTP layer; [the changelog of the repository](https://github.com/sferik/x-ruby/blob/main/CHANGELOG.md) holds the changes to every gem.
9
+
10
+ ## [1.0.0] - 2026-10-06
11
+
12
+ The first release of `x-core`, which 1.0.0 split out of the `x` gem. The entries below are the changes since `x` 0.19, the last release before the split; see [UPGRADING.md](https://github.com/sferik/x-ruby/blob/main/UPGRADING.md) for the changes that code written for 0.19 needs.
13
+
14
+ ### Added
15
+ * Split the `x` gem into five gems released in lockstep, at one version
16
+ * `x-core` (the HTTP client), `x-uploads` (uploads), `x-streams` (streams and their rules), `x-resources` (resources).
17
+ * `x` is a meta-gem that depends on all four and mixes their object, upload, and streaming methods into `X::Client`.
18
+ * `x-core` declares `X::Error`, the base class of every error the gems raise.
19
+ * Public classes sit directly under `X`; `X::Resources::Error`, `X::Uploads::Error`, and `X::Streams::Error` catch one gem's.
20
+ * `x` depends on exactly its own version of the other four; `x-uploads`, `x-streams`, and `x-resources` each depend on `x-core` with `>= 1.0.0, < 2`, so a later 1.x of `x-core` installs beside them, and never an earlier one.
21
+ * Add `X::Client#get_stream`, which opens a GET request whose body its block reads as it arrives
22
+ * It sends the client's credentials, headers, timeouts, and proxy, and refreshes a rejected token, as any request does.
23
+ * It asks for a body that is not compressed (`Accept-Encoding: identity`), which Net::HTTP would hold back in blocks, unless a header of the request or client names another.
24
+ * A failed response raises its `X::HTTPError`, once `on_response` is passed it, without reaching the block.
25
+ * The block is passed an `X::StreamResponse` before its body is read, and `get_stream` returns what the block returns.
26
+ * `X::StreamResponse` reads the `status`, `headers`, `uri`, `rate_limits`, and `rate_limit` of the response.
27
+ * Its `read_body` passes a block each chunk of the body as it arrives, in binary, since a chunk can end within a character.
28
+ * Without a block, `read_body` returns the body whole, as a UTF-8 String as `X::Response#body` is, or nil for a response without one.
29
+ * Its `http_response` is an escape hatch, the transport's response, whose class 1.x does not promise.
30
+ * `read_body` raises `X::NetworkError` for an error of the socket, such as a dropped connection or a read timeout, inside the block, which can rescue it.
31
+ * It names the request, its `cause` is the socket's error, and `get_stream` raises it as it is; any other error of the block is raised as it was.
32
+ * The body is read once, inside the block: a second `read_body`, unless both read it whole, or one after the block returns, raises `X::Error`, which `with_retries` does not retry.
33
+ * A `dup` or `clone` of the response reads the same body, so its reads count with the response's, and a frozen response is read as any other.
34
+ * One frozen deeply, as by `Ractor.make_shareable` on a Ruby before 4.1, which refuses to share the socket of one, reads its status, headers, and rate limits; `read_body` raises `X::Error` for a body not yet read, since its connection is frozen with it, and `get_stream` then raises the `FrozenError` of `Net::HTTP`, which cannot close it.
35
+ * `x-streams` reads the streams of the API with it.
36
+ * Add `X::Client#with_retries`, `memoized`, and `memoize`, kept throughout 1.x for the gems that extend a client
37
+ * `with_retries` sends a request again after a failure, as `x-uploads` sends each chunk of an upload.
38
+ * `memoize` keeps a value under a key for the authenticator of the client, and `memoized` reads it, as `x-resources` keeps the authenticated user.
39
+ * Pass a block to `get`, `post`, `put`, or `delete` to receive the `X::Response` of each response that request gets
40
+ * It is passed what the client's `on_response` is passed, after it, failed responses and each retry among them.
41
+ * Add `X::Core.gem_version`, which returns `X::Core::VERSION` as a `Gem::Version`
42
+ * Add the `headers:` option of `X::Client.new`, headers sent with every request and stream the client makes
43
+ * They are a Hash of String or Symbol names to String values; anything else raises `ArgumentError`.
44
+ * In any `headers:`, a Symbol's underscores become hyphens: `{content_type: "text/plain"}` sends `content-type`.
45
+ * A request's or `get_stream`'s header replaces the client's of the same name, which replaces the gem's default.
46
+ * Header names are case-insensitive, and a redirect to another origin drops the ones that carry credentials.
47
+ * `X::Client#headers` reads them back frozen, each named by a String: `user_agent:` reads as `{"user-agent" => ...}`.
48
+ * Token requests send them too, with the gem's `User-Agent`, beneath the token request's own credentials; a client's `Authorization` header is never sent with one.
49
+ * Raise `ArgumentError` from `X::Client.new` for a `base_url` that is not an absolute http or https URL
50
+ * A `base_url` with a user, password, query, or fragment raises too.
51
+ * Pass a resource class, such as `X::User`, as the `object_class` of a request to build objects from the response
52
+ * A list builds an `X::Page`, which holds the response's `meta`, such as its `next_token`, and the problems it reported.
53
+ * A list answered with no `data`, or a lookup by ids that finds none, builds an empty `X::Page` rather than nil.
54
+ * An `object_class` that responds to `from_response` is passed the parsed body and `client:`, and builds the result.
55
+ * `from_response` must take the keywords it does not read with `**`; the signatures type it as `X::_ResponseBuilder`.
56
+ * Pass query parameters to `get`, `post`, `put`, `delete`, and `get_stream` as `params:`
57
+ * nil values are dropped, Arrays are joined with commas, and a `Time` is sent in UTC as ISO 8601.
58
+ * Encode a `post` or `put` body that is a Hash or an Array as JSON
59
+ * A body that is not a String, a Hash, or an Array, such as an IO or a Symbol, raises `ArgumentError` before any request.
60
+ * Send form fields as a form-encoded body with the `form:` option of `post` and `put`
61
+ * Fields are encoded as `params:` are.
62
+ * A request given both a body and `form:` raises `ArgumentError`.
63
+ * Authenticate as the app given only an `api_key` and `api_key_secret`, with `X::AppOnlyAuthenticator`
64
+ * It fetches a bearer token with the client credentials grant on the first request, and keeps it.
65
+ * A token the base URL's origin rejects with 401 is fetched again, and the request or stream is sent once more.
66
+ * Add `X::Client#with`, which copies a client with some options of `X::Client.new` changed
67
+ * For example `client.with(base_url: "https://api.x.com/1.1/")`, or `with(access_token: nil, access_token_secret: nil)`.
68
+ * A copy given no new OAuth 2.0 credentials shares the client's OAuth 2.0 authenticator, and so its refreshes.
69
+ * Such a copy given `expires_at` raises `ArgumentError`, since the expiration belongs to the shared token.
70
+ * A copy that does not share it holds no `refresh_token`, `expires_at`, `scopes`, or token hooks unless given them.
71
+ * A copy with the same app credentials and base URL shares the app-only bearer token the client fetched.
72
+ * Add the `authenticator:` option of `X::Client.new` and `X::Client#with`, to authenticate with an `X::Authenticator`
73
+ * It raises `ArgumentError` beside credentials, `expires_at`, or `scopes`, or for what is not an `X::Authenticator`.
74
+ * A custom one subclasses `X::Authenticator` and overrides `headers`, reading what `X::_AuthenticatorRequest` types.
75
+ * A client sends its token requests over its own connection, but for an authenticator given to several clients, which sends them over the first's, apart from a refresh another one's request makes; refreshes reach the `save_tokens` of every client.
76
+ * A client given an `X::OAuth2Authenticator` holds no app credentials, so its `app_only` raises `X::UnsupportedOperation`.
77
+ * Add `inspect` to `X::Client` and the authenticators, which never reveals credentials
78
+ * Keep the headers passed to `get`, `post`, `put`, and `delete` across redirects
79
+ * Pass an `X::Response` to the client's `on_response` after every request, and for each object a stream delivers
80
+ * It reads rate limits with `rate_limits` and `rate_limit`, and counts the resources returned with `resource_counts`.
81
+ * A frozen response counts its resources as any other, and parses its body once, as a `dup` or `clone` of it does.
82
+ * So does one frozen deeply, as by `Ractor.make_shareable`, which parses its body for each count unless it was counted before.
83
+ * An `on_response` that is neither nil nor responds to `call` raises `ArgumentError` when the client is built.
84
+ * Add `X::Response#headers` and `X::HTTPError#headers`, a frozen Hash of the response headers with lowercase names
85
+ * Repeated fields are joined with a comma; read each cookie with `http_response.get_fields("set-cookie")`.
86
+ * Retry a request refused for a rate limit up to `max_rate_limit_retries` times, 0 by default, after the wait it asks for
87
+ * It waits no longer than `max_rate_limit_wait` seconds, 900 by default; a later reset raises at once.
88
+ * A refusal that names no wait waits a minute, doubling for each retry after; each wait adds up to 5 random seconds.
89
+ * A refusal for the project's usage cap, which `X::Problem#usage_capped?` tells apart, raises at once.
90
+ * Refresh an OAuth 2.0 access token when it expires, or when the base URL's origin rejects it with 401, and send again
91
+ * The `expires_at:` of `X::Client.new` must be a `Time`; anything else raises `ArgumentError`.
92
+ * A token refreshed less than a minute ago is not refreshed again, and a 401 from another origin refreshes nothing.
93
+ * Requests on several threads refresh once; `X::Client#expires_at` reads the expiration of the last refresh.
94
+ * A refresh that reports no lifetime forgets the expiration, rather than refresh again on every request.
95
+ * Store the tokens of each refresh with the `save_tokens:` callable of `X::Client.new`, passed an `X::OAuth2Tokens`
96
+ * Refreshes are reported one at a time, in order, leaving out one already replaced.
97
+ * Each hook runs even when another raises; then `X::TokenReportFailed` is raised, holding the `tokens` and `client`.
98
+ * A `Timeout::Error` a hook raises, even one `Timeout.timeout(5, Timeout::Error)` raises around the request, raises `X::TokenReportFailed` too.
99
+ * A `save_tokens` that is neither nil nor responds to `call` raises `ArgumentError` when the client is built.
100
+ * An authenticator reports to no hook of its own; its `refresh!` returns the frozen `X::OAuth2Tokens` of the refresh.
101
+ * Add `X::OAuth2Tokens`, the frozen `access_token`, `refresh_token`, `expires_at`, and `scopes` of a refresh or authorization
102
+ * It writes itself with Marshal, YAML, and `to_json` in a versioned format that every 1.x release reads back.
103
+ * `X::OAuth2Tokens.from_json` reads its JSON, or that JSON's Hash, back; `inspect` names neither token.
104
+ * `X::OAuth2Tokens.new` raises `ArgumentError` for an empty or non-String token, or an `expires_at` that is not a `Time`.
105
+ * Share a user's tokens among processes with a `load_tokens:` callable that returns the stored `X::OAuth2Tokens`, or nil
106
+ * `X::Client.new`, `X::Client#with`, `X::OAuth2Authorization#client`, and `X::OAuth2Authenticator.new` take it.
107
+ * A refresh reads the store under its lock first, and takes tokens another process refreshed in place of its own.
108
+ * A refresh X refuses with `invalid_request` or `invalid_grant` reads the store again, and takes the tokens there rather than raise once the other process has stored them.
109
+ * Tokens it loads are not passed to `save_tokens`.
110
+ * A `load_tokens` that does not respond to `call` raises `ArgumentError`; one returning anything else raises `TypeError`.
111
+ * Read the scopes X granted an OAuth 2.0 token with `scopes` on `X::OAuth2Tokens`, `X::OAuth2Authenticator`, and `X::Client`
112
+ * They are a frozen Array of Strings, or nil when unknown, and may be fewer than the app asked for.
113
+ * `X::Client.new`, `with`, and `X::OAuth2Authenticator.new` take `scopes:`, as in `X::Client.new(client_id:, **tokens.to_h)`.
114
+ * `scopes` that are not an Array of scope Strings, or that are given without OAuth 2.0 credentials, raise `ArgumentError`.
115
+ * Add `X::HTTPError#status`, `#body`, `#problems`, and `#problem`, to read what the API said without its response
116
+ * `status` is an Integer, as `X::Response#status` is.
117
+ * `problems` are the `X::Problem`s the body names; `problem` is the one it describes itself, or the first it names.
118
+ * `http_response` holds the response itself on `X::HTTPError` and `X::InvalidResponse`, as on `X::Response`.
119
+ * It is an escape hatch: the transport's object, a `Net::HTTPResponse` today, whose class 1.x does not promise.
120
+ * Raise an `X::ClientError` subclass of its own for the statuses 405, 408, 415, and 451, which raised `X::HTTPError` itself
121
+ * They are `X::MethodNotAllowed`, `X::RequestTimeout`, `X::UnsupportedMediaType`, and `X::UnavailableForLegalReasons`.
122
+ * Raise `X::PaymentRequired`, an `X::ClientError`, for a 402, which X sends when the paying account has no credit left
123
+ * Name the request in the errors `X::HTTPError`, `X::NetworkError`, `X::InvalidResponse`, and `X::TooManyRedirects`
124
+ * Each reads its `http_method` and `uri`, and its message names it, as in `GET /2/users/1: Could not find user`.
125
+ * The message leaves out the query, which `uri` keeps, and a redirected request is named by the last request sent.
126
+ * Add `X::Authenticator#user_id`, the user an OAuth 1.0a access token acts for, or nil for credentials that name none
127
+ * Keep connections open between requests to the same host, up to 16 idle per host
128
+ * The `keep_alive_timeout:` of `X::Client` and `X::OAuth2Authorization` keeps them for 30 seconds by default.
129
+ * A connection whose request fails is closed, and a forked process opens its own.
130
+ * Copies made with `X::Client#with` that connect as the client does share its connections, as its app-only copy does.
131
+ * A copy given another `debug_output` object opens its own, though the two are equal, as one empty Array is to another, so each writes to its own.
132
+ * `X::Client#close` closes them, and those of the copies that share them; a later request opens one again.
133
+ * Add `X::Client#app_only`, a copy of a client that authenticates as a user which authenticates as the app instead
134
+ * It holds the app's bearer token: the one the client was given, or one fetched once with the app's API key and secret.
135
+ * It returns the same copy each time, and a copy made with `with` that holds the same app credentials shares its token.
136
+ * An OAuth 2.0 user client without app credentials raises `X::UnsupportedOperation`, an `X::Error` of `x-core`.
137
+ * The gems send its requests to app-only endpoints, such as streams, as the user, so X's 403 raises `X::Forbidden`.
138
+ * Refresh the tokens of a public OAuth 2.0 client, given a `client_id`, `access_token`, and `refresh_token` without a secret
139
+ * The refresh sends the client ID in its body rather than authenticate with a secret.
140
+ * Authenticate as a user with an OAuth 2.0 access token that is not refreshed, given no `refresh_token`
141
+ * `refresh_token:` is optional for `X::Client.new`, `X::Client#with`, and `X::OAuth2Authenticator.new`.
142
+ * A rejected access token raises `X::Unauthorized`, and `refresh!` raises `X::UnsupportedOperation`.
143
+ * Authorize an app to act for a user with the OAuth 2.0 authorization code flow and PKCE, with `X::OAuth2Authorization`
144
+ * It builds the URL that asks the user, with a state and code verifier to store until X redirects back.
145
+ * It exchanges the code of the redirect for `tokens`, an `X::OAuth2Tokens` that holds no app secret, or for a `client`.
146
+ * A declined authorization, a mismatched state, or an invalid redirect raises `X::AuthorizationDenied`.
147
+ * A code X refuses raises `X::AuthorizationError`; invalid options raise `ArgumentError` before the code is spent.
148
+ * `X::OAuth2Authorization.new` raises `ArgumentError` for a client ID, redirect URI, or state that is nil, empty, or not a String.
149
+ * `client` passes the tokens of the exchange to its `save_tokens` before it returns the client.
150
+ * It takes `headers:`, which it exchanges the code with and gives the client it builds, as a gateway may require.
151
+ * Add `X::InvalidResponse#body`, `#status`, and `#headers`, to read a successful response, or stream line, that is not JSON
152
+ * `body` falls back to the body of the response once it has been read whole, and never reads a stream.
153
+ * Add `X::TooManyRequests#exhausted_rate_limits` and `#limiting_rate_limit`, the used-up limits and the one to wait for
154
+ * Add a constant of `X::Client` for the default of each setting
155
+ * New: `DEFAULT_MAX_REDIRECTS`, `DEFAULT_MAX_RATE_LIMIT_RETRIES`, `DEFAULT_MAX_RATE_LIMIT_WAIT`, and `DEFAULT_MAX_RETRIES`.
156
+ * `DEFAULT_KEEP_ALIVE_TIMEOUT` is new too, and the `DEFAULT_*_TIMEOUT`s of `X::Connection` move to `X::Client`.
157
+ * Ship this changelog with `x-core`, which the `changelog_uri` of its gemspec names
158
+ * Ship a `.yardopts` with `x-core`, so its documentation on rubydoc.info leaves out the private API
159
+ * Send a request again when the API fails to answer it, up to `max_retries` times, 2 by default
160
+ * A `GET`, `PUT`, or `DELETE` is sent again after an `X::ServerError`, `X::RequestTimeout`, or unsent `X::NetworkError`.
161
+ * A `POST`, and any other 4xx, raises at once; `max_retries: 0` raises at once for any of these failures. A 429 and a rejected OAuth 2.0 token are handled apart, as above.
162
+ * The wait starts at up to a second and doubles to at most a minute, with jitter, or follows a longer `Retry-After`.
163
+ * A `Retry-After` of more than a minute raises at once, where a 429 waits up to `max_rate_limit_wait`.
164
+ * An error that `on_response`, a request's block, or `from_response` raises is never sent again.
165
+ * Raise `ArgumentError` from `X::Client.new` and `X::Client#with` for an invalid setting, naming it and the value given
166
+ * `max_redirects`, `max_rate_limit_retries`, and `max_retries` must be Integers of at least 0.
167
+ * `max_rate_limit_wait` must be a number of seconds of at least 0, not NaN, and `keep_alive_timeout` a finite one.
168
+ * `open_timeout`, `read_timeout`, and `write_timeout` must be finite numbers of seconds of at least 0, or nil for none.
169
+ * `default_array_class` must be a Class, and `default_object_class` a Class or respond to `from_response`.
170
+ * `debug_output` must be nil or take a String with `<<`, as an IO, a StringIO, or a Logger does, where `true` raised `NoMethodError` from the first request.
171
+ * An Integer, a Proc, and a Method are refused as well: each responds to `<<`, to shift or to compose, and raised `TypeError` from the first request.
172
+ * A `debug_output` that is a String, such as `"debug.log"`, is refused too: every request, with its Authorization header, was appended to the String, not to a file. Pass an IO, such as `$stderr` or `File.open("debug.log", "a")`, or a StringIO to collect it.
173
+ * `X::OAuth2Authorization.new` checks its timeouts and `debug_output`, and a request its `array_class` and `object_class`, the same way.
174
+ * Add `X::Problem`, which describes a problem the API reported of a request
175
+ * It reads `title`, `detail`, `type`, `resource_type`, `resource_id`, `parameter`, `value`, and `message`.
176
+ * It compares by value, writes itself with Marshal and YAML in a versioned format every 1.x reads, and as JSON with `to_json`.
177
+ * `about?` tells whether it names a resource, or an Integer or String identifier, comparing them as Strings.
178
+ * A format it does not read raises `X::UnsupportedFormat`, an `X::Error` that `x-core` declares for every gem.
179
+ * `X::HTTPError#problem` returns one, and `x-resources` reports them for a successful response.
180
+
181
+ ### Changed
182
+ * Hold `X::Core::VERSION` in a String rather than a `Gem::Version`; compare it with `X::Core.gem_version`
183
+ * Require Ruby 3.4 or later
184
+ * Keep frozen copies of the credentials, tokens, header values, base URL, and proxy URL a client, an authenticator, an `X::OAuth2Authorization`, or `X::OAuth2Tokens` is given, so neither the caller nor what a reader returns can change what is sent, or where
185
+ * Keep secret credentials private on a client and its authenticator, so no reader, `inspect`, or serialization reveals them
186
+ * Gone from `X::Client`: `api_key_secret`, `access_token`, `access_token_secret`, `bearer_token`, and `client_secret`.
187
+ * `refresh_token` is gone too: store tokens from the `X::OAuth2Tokens` that `save_tokens` is passed.
188
+ * Authenticators no longer read their secrets or tokens; `api_key`, `client_id`, and `expires_at` stay public.
189
+ * An authenticator's `headers` still returns the `Authorization` header it sends, which holds a bearer or OAuth 2.0 token.
190
+ * Rename `X::Authenticator#header` to `headers`, which a custom authenticator overrides
191
+ * The request it is passed answers `http_method` (a Symbol such as `:post`), `uri`, `body`, and `[]` for a header, where 0.19 passed the `Net::HTTPRequest`.
192
+ * Raise `TypeError` when a client, an authenticator, or an `X::OAuth2Authorization` is written out
193
+ * That is `Marshal.dump`, `YAML.dump`, `as_json`, and `to_json`, which ActiveSupport's `render json:` calls.
194
+ * They wrote credentials in the clear in 0.19; keep credentials in a secret store, and tokens as `X::OAuth2Tokens`.
195
+ * Keep a client's proxy URL private, since it can hold the proxy's user and password
196
+ * `X::Client` no longer reads `proxy_url`, and `inspect` leaves it out.
197
+ * Send a client's credentials to the origin of its `base_url` alone
198
+ * An endpoint or stream naming a URL of another origin is sent without `Authorization`, `Cookie`, or `Proxy-Authorization`.
199
+ * Another API version at that origin, such as `https://api.x.com/1.1/account/settings.json`, still carries them.
200
+ * Open connections with a 10-second timeout rather than 60; `read_timeout` and `write_timeout` stay at 60 seconds
201
+ * Read only the rate limits a response reports in full, with a limit, remaining count, and reset time, in base 10
202
+ * `X::TooManyRequests#retry_after` no longer raises `KeyError` or `ArgumentError` for a missing or malformed header.
203
+ * Read the `Retry-After` header of a refused response with `X::HTTPError#retry_after`, in seconds or as an HTTP date
204
+ * It is nil for a response without one.
205
+ * `X::TooManyRequests#retry_after` reads it, then falls back on `#reset_in`, which it was an alias of.
206
+ * The retries of a client and the reconnects of a stream wait for it.
207
+ * Send requests to `api.x.com` rather than `api.twitter.com` by default
208
+ * Rename `X::ConnectionException`, the error for 409 Conflict, to `X::Conflict`, which X sends a filtered stream with no rules
209
+ * Rename `X::HTTPError#response` to `#http_response`, the name `X::Response` reads it by
210
+ * The `response` of `X::RateLimit` is now private.
211
+ * Rename `X::OAuthAuthenticator` to `X::OAuth1Authenticator`
212
+ * Move the HTTP client into `x-core`, under `lib/x/core`, and the uploaders into `x-uploads`, under `lib/x/uploads`
213
+ * Wrap every network failure in `X::NetworkError`, so a stream of `x-streams` reconnects after it
214
+ * That is `IOError`, `SystemCallError`, Net::HTTP's open, read, and write timeouts, `Net::ProtocolError`, and `Zlib::Error`; a `Timeout::Error` that `Timeout.timeout` raises around a request is raised as it is, but one raised with its class, as by `Timeout.timeout(5, Timeout::Error)`, that lands in `save_tokens` is an `X::TokenReportFailed`, holding the tokens.
215
+ * It is also `Net::HTTPBadResponse`, `OpenSSL::SSL::SSLError`, and the `SocketError` of a host that cannot be resolved.
216
+ * Stop following redirects after exactly `max_redirects` hops, rather than one more
217
+ * `max_redirects: 0` follows none, and raises `X::TooManyRedirects` for each redirect that could be followed.
218
+ * Sign OAuth 1.0a requests with the [simple_oauth](https://github.com/laserlemon/simple_oauth) gem, which sends the same header
219
+ * Build and parse the OAuth 2.0 token refresh with simple_oauth, which sends the same request
220
+ * Rename `X::OAuth2Authenticator#refresh_token!` to `#refresh!`, which returns the frozen `X::OAuth2Tokens` of the refresh
221
+ * It returned the Hash of the token response.
222
+ * It raises `X::UnsupportedOperation` for an authenticator that holds no refresh token.
223
+ * Raise `ArgumentError` from `X::Client.new` and `X::Client#with` for credentials that do not form a complete set
224
+ * Such a client sent requests without credentials, or as the app when an access token lacked its secret.
225
+ * A credential of no complete set, such as a `client_id` beside a `bearer_token`, raises rather than be ignored.
226
+ * OAuth 2.0 credentials beside a complete set of OAuth 1.0a credentials raise.
227
+ * An empty String credential, as `ENV.fetch("X_BEARER_TOKEN", "")` reads, or one that is not a String, raises.
228
+ * `expires_at` raises beside anything but the OAuth 2.0 `client_id` and `access_token` it describes.
229
+ * Raise `ArgumentError` from an authenticator's constructor for a missing, empty, or non-String credential
230
+ * An `expires_at` that is not a `Time`, such as a String read back from JSON, raises it too.
231
+ * Raise `X::InvalidResponse` for a successful response whose body is not JSON, such as a captive portal's page
232
+ * It returned nil; a successful response without a body still returns nil.
233
+ * `X::InvalidResponse` is an `X::HTTPError` whose `problem` is nil and whose `problems` are empty.
234
+ * Tag the body of a response UTF-8 rather than binary, so it no longer raises `Encoding::CompatibilityError`
235
+ * That is the `body` of `X::Response`, `X::HTTPError`, and `X::InvalidResponse`, and each line of a stream.
236
+ * A body that is not valid UTF-8 keeps its bytes, which `valid_encoding?` tells apart.
237
+ * Raise `X::AuthorizationError`, an `X::ClientError`, when X refuses to issue or refresh a token or to exchange a code
238
+ * It replaces a bare `X::Error`, and holds the OAuth 2.0 `error_code` and the `status`, `headers`, and `body`.
239
+ * It is not an `X::Unauthorized`, so code that asks the user to authorize the app again rescues both.
240
+ * A 429, server error, redirect, or non-JSON answer from a token endpoint raises the `X::HTTPError` of that response.
241
+ * A declined authorization raises `X::AuthorizationDenied`, an `X::Error` with the `error_code` of the redirect.
242
+ * Return nil from `X::TooManyRequests#reset_at`, `#reset_in`, and `#retry_after` for a response that names no reset time
243
+ * They returned `Time.at(0)` and 0, which told a caller to retry at once.
244
+ * Make the connection of a client internal, as `X::Core::Connection`, in place of `X::Connection`
245
+ * The authenticators take no `connection:`, since the client that takes one sends its token requests over the client's own.
246
+ * `X::OAuth2Authorization` takes `base_url`, `proxy_url`, the timeouts, `debug_output`, and `headers` in place of `connection:`.
247
+ * Resolve an endpoint with a leading slash against the base URL, so `client.get("/users/me")` requests `/2/users/me`
248
+ * Pass a whole URL to reach another path of the host.
249
+ * Raise `ArgumentError` from `get`, `post`, `put`, `delete`, and `get_stream` for an invalid endpoint, before any request
250
+ * An endpoint that is not a String, such as a Symbol or a URI, raises it naming its class, rather than `NoMethodError`.
251
+ * An invalid URL, or one that is not http or https with a host, raises it naming the endpoint and what is wrong.
252
+ * It raised `URI::InvalidURIError`, or an `ArgumentError` of `Net::HTTP` that named no endpoint.
253
+ * Send each request once, turning off the retry `Net::HTTP` makes of a GET, PUT, or DELETE after a timeout or drop
254
+ * That retry sent the OAuth 1.0a nonce and signature again, and could repeat a read the API bills.
255
+ * Such a failure raises `X::NetworkError`, which `max_retries` sends again only for a request that never reached the API.
256
+ * Make the token endpoint of `X::OAuth2Authenticator` private, in place of the public `TOKEN_HOST` and `TOKEN_PATH`
257
+ * Report every rate limit a response names from `X::TooManyRequests#rate_limits`, not only the exhausted ones
258
+ * `#rate_limit` reads the 15-minute limit, as on `X::Response`, rather than the exhausted one that resets last.
259
+ * That exhausted limit, which `reset_at`, `reset_in`, and `retry_after` still wait for, is `#limiting_rate_limit`.
260
+ * Make `X::HTTPError::JSON_CONTENT_TYPE_REGEXP` and `X::OAuth2Authenticator::EXPIRATION_BUFFER` private
261
+ * Make `X::RateLimit.new` and `X::RateLimit.reported?` private
262
+ * Build the errors of `x-core` and `X::Response` with public constructors, so code that rescues one can be tested
263
+ * `X::HTTPError.new(status:, headers:, body:, http_method: nil, uri: nil)`, or with `http_response:`, and a message first.
264
+ * A status error such as `X::NotFound` needs neither, so `raise X::TooManyRequests, "slow down"` works.
265
+ * `X::NetworkError`, `X::TooManyRedirects`, and `X::AuthorizationDenied` take a message, or none.
266
+ * `X::Response.new(http_method:, uri:, status:, headers:, body:)` takes `http_response:` in their place too.
267
+ * A response beside a status, a status outside 100 to 599, or headers that are not a Hash raise `ArgumentError`.
268
+ * Name the internals of `x-core` under `X::Core`, as private constants marked `@api private`, so they can change in 1.x
269
+ * Among them are `RequestBuilder`, `RedirectHandler`, `Connection`, `ConnectionPool`, and the client's mixins.
270
+ * The client, authenticators, `X::OAuth2Authorization`, `X::Response`, `X::RateLimit`, and errors keep their names.
271
+ * The signatures `x-core` ships declare its public interface alone.
272
+ * Name the gem in the `User-Agent` of every request, token requests included, as `x-ruby/1.0.0 ruby/3.4.0 (arm64-darwin24)`, rather than `X-Client`
273
+ * Send an idempotent request again on a new connection when the kept-open connection it took had gone stale
274
+ * That is an `EOFError`, `ECONNRESET`, `ECONNABORTED`, or `EPIPE` before the status and headers of its response are read.
275
+ * A response cut off after that is never sent again, as the API answered it.
276
+ * Raise `X::NetworkError` for a body cut off before its end, or shorter than its `Content-Length`, rather than read what arrived
277
+ * Any other failure, such as a timeout, is left to the retries of the client.
278
+ * Document every error class of `x-core` alike, and draw the whole hierarchy on `X::Error`
279
+ * Request the token endpoints at the origin of the client's base URL, rather than at `api.x.com` whatever the base URL
280
+ * They keep the path the base URL serves the API at, minus a trailing API version, which was dropped.
281
+ * Under `https://gateway.example/x/2/`, app-only tokens come from `/x/oauth2/token`, refreshes from `/x/2/oauth2/token`.
282
+ * `X::OAuth2Authorization` exchanges its code at the origin of the base URL of the client it builds.
283
+ * Raise `ArgumentError` from `post` and `put` for a keyword they do not take, such as `client.post("tweets", text: "Hello")`
284
+ * The message says to pass the body as a Hash: `post("tweets", {text: "Hello"})`.
285
+ * For `body:` alone, as `post("tweets", body: {text: "Hello"})`, it says to pass the body as the second argument instead.
286
+ * Keep the state of `X::Client` in an internal object, so no method another gem mixes in replaces one of its helpers
287
+
288
+ ### Removed
289
+ * Remove `X::HTTPError#code`; read the Integer `#status`, or the String `error.http_response.code`
290
+ * Remove `X::HTTPError#error_message` and `#message_from_json_response`, and make `#json?` private
291
+ * Remove `X::OAuthAuthenticator::OAUTH_SIGNATURE_ALGORITHM`, `OAUTH_VERSION`, and `OAUTH_SIGNATURE_METHOD`
292
+ * Remove `X::OAuth2Authenticator::REFRESH_GRANT_TYPE`
293
+ * Remove the `base64` dependency of `x-core`
294
+ * Remove `X::RateLimit#retry_after`, an alias for `#reset_in`; read `#reset_in`
295
+ * Remove the setters of `X::RateLimit`, `X::BearerTokenAuthenticator`, `X::OAuth1Authenticator`, and `X::OAuth2Authenticator`
296
+ * Derive a client that holds another credential with `X::Client#with`.
297
+ * Remove the setters of `X::Client`; derive a client that differs with `X::Client#with`
298
+ * Gone: `api_key=`, `api_key_secret=`, `access_token=`, `access_token_secret=`, `bearer_token=`, and `client_id=`.
299
+ * Gone: `client_secret=`, `refresh_token=`, `base_url=`, `default_array_class=`, `default_object_class=`, and `max_redirects=`.
300
+ * Gone: `open_timeout=`, `read_timeout=`, `write_timeout=`, `proxy_url=`, and `debug_output=`.
301
+ * Remove the setters of `X::Connection`: `open_timeout=`, `read_timeout=`, `write_timeout=`, `proxy_url=`, and `debug_output=`
302
+ * Remove the proxy readers of `X::Connection`: `proxy_url`, `proxy_uri`, `proxy_host`, `proxy_port`, `proxy_user`, `proxy_pass`
303
+ * Remove `X::Connection::DEFAULT_HOST` and `DEFAULT_PORT`, since every request names its host
304
+ * Remove `X::OAuth1Authenticator#access_token`, a credential; `user_id` reads the user the token acts for
305
+
306
+ ### Fixed
307
+ * Keep the method and body of a `PUT` or `DELETE` that a 301 or 302 redirects, as RFC 9110 has it
308
+ * A `delete` that a 301 answered read the resource at the new location, and returned it as though deleted.
309
+ * A `POST` that a 301 or 302 redirects, and any request a 303 redirects, is still followed with a `GET`.
310
+ * Declare `json`, `net-http`, and `uri` in `sig/manifest.yaml`, so `rbs collection` loads them for dependents
311
+ * Resolve a relative redirect against the URL of the request, not the base URL, so one from `upload.x.com` stays there
312
+ * Build the message of an `X::HTTPError` from a body that is not the JSON its content type claims
313
+ * It raised `JSON::ParserError`, `KeyError`, or `TypeError` in place of the error, so a stream did not reconnect.
314
+ * An error without a `message` gives its `detail` or `title`, once: `Unauthorized`, not `Unauthorized: Unauthorized`.
315
+ * Refresh an OAuth 2.0 token over the client's own connection, so its proxy, timeouts, and debug output apply
316
+ * Drop the credentials and any `Authorization`, `Cookie`, or `Proxy-Authorization` header on a redirect to another origin
317
+ * A header named by a String, or by a Symbol such as `proxy_authorization:`, in any case, is dropped.
318
+ * A 307 or 308, and a 301 or 302 of anything but a `POST`, still sends the request body to the new host.
319
+ * Send no `Authorization` header from a client without credentials, rather than an empty one
320
+ * Send a `Content-Type` header only with a request that carries a body
321
+ * A body is sent as JSON unless the request's headers, or its `form:`, name another type.
322
+ * A redirect followed with a `GET` sends no `Content-Type`.
323
+ * Raise `X::ClientError` or `X::ServerError`, rather than `X::HTTPError`, for a 4xx or 5xx no error class names
324
+ * So a stream reconnects, and a chunk upload retries, after any server error, such as a 501.
325
+ * Link each gem's `changelog_uri` to the `main` branch rather than `master`
326
+ * Send the credentials of the authenticator on every redirected request, not only the first
327
+ * Sign a form-encoded request body with OAuth 1.0a, which the signature left out
328
+ * Sign every value of a query parameter that repeats, rather than only one of them
329
+ * Sign the normalized URL, so a request to a host with no path signs the `/` the server sees
330
+ * Form-encode the client credentials before Basic authentication on a token refresh, as RFC 6749 Section 2.3.1 requires
331
+ * Leave the user and password of a proxy out of the message of an invalid proxy URL, and out of `inspect`
332
+ * A proxy URL that cannot be parsed raises `ArgumentError` rather than `URI::InvalidURIError`.
333
+ * Decode a percent-encoded proxy user and password, which were sent to the proxy still encoded
334
+ * Connect to an `https://` proxy over TLS, rather than send it the target host and credentials in plaintext
335
+ * Raise `X::HTTPError` for a redirect that cannot be followed, rather than `KeyError` or `URI::InvalidURIError`
336
+ * That is a 300, 304, or 305, which was followed, or one whose `Location` is missing, invalid, or not HTTP or HTTPS.
337
+ * Such a redirect raised `ArgumentError` too.
338
+ * End a `base_url` without a trailing slash with one, so `https://api.x.com/2` requests `/2/users/me`, not `/users/me`
339
+ * Raise an error of `on_response`, a request's block, or an `object_class` as it was raised, not as one of the request
340
+ * Such an error is not sent again, waited out, or refreshed for, even an `X::ServerError` or an `X::Unauthorized`.
341
+ * A stream stops on it rather than reconnect; a dropped socket or a line that is not JSON still reconnects.
342
+ * Send a query parameter without a value, as in `get("users?flag")`, as `flag` rather than `flag=`
343
+ * An empty parameter, as between the `&&` of `"a=1&&b"`, is kept, where it was sent as `=`.
344
+ * Connect to a host, or a proxy, named by an IPv6 literal, such as a `base_url` of `http://[::1]:8080/`
345
+ * `x-core` depends on net-http 0.9.1 or a later 0.x, since the net-http 0.6 of Ruby 3.4 cannot, and on Ruby 4.0 the net-http before 0.9.1 raises `IO::TimeoutError` for a connection that times out opening, which is then not retried.
346
+ * Take the proxy from `https_proxy` for HTTPS and `http_proxy` for HTTP, and honor `no_proxy`, without a `proxy_url`
347
+ * `Net::HTTP` read `http_proxy` alone, whatever the scheme.
348
+ * One of them that cannot be parsed raises `ArgumentError` from each request that opens a connection, and from `get_stream`, rather than `URI::InvalidURIError`.
349
+ * One that is not an `http` or `https` URL with a host, such as `proxy.example.com:8080`, which has no scheme, is refused the same way, as a `proxy_url` is, where the request was sent around the proxy. An empty one names no proxy.
350
+ * Its message names the variables read for the scheme of the request, as `https_proxy or HTTPS_PROXY`, and leaves out the value, which can hold the proxy's password, and it has no `cause`.
351
+ * Declare `X::BadGateway` and `X::GatewayTimeout` as `X::ServerError`s in the signatures, as the classes always were
352
+
353
+ [1.0.0]: https://github.com/sferik/x-ruby/releases/tag/v1.0.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Erik Berlin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # x-core
2
+
3
+ The HTTP layer of the [`x` gem](https://github.com/sferik/x-ruby): a small, dependency-light client for the [X API](https://developer.x.com) that returns parsed JSON.
4
+
5
+ It handles OAuth 1.0a, OAuth 2.0 with PKCE authorization and token refresh, and bearer tokens. It also handles redirects, proxies, timeouts, HTTP errors, and rate limits, and opens a GET whose body is read as it arrives, as a stream is. Its only dependency that is not a default gem is `simple_oauth`, which has no dependencies of its own; it also asks for net-http 0.9.1 or a later 0.x release, the default gem it sends its requests with, since the 0.6 that Ruby 3.4 ships cannot request a host named by an IPv6 literal, and on Ruby 4.0 an earlier net-http does not retry a connection that times out opening. Media uploads live in [`x-uploads`](https://github.com/sferik/x-ruby/tree/main/x-uploads), and the streams of the API, read a post at a time and reconnected as X recommends, in [`x-streams`](https://github.com/sferik/x-ruby/tree/main/x-streams).
6
+
7
+ Most applications should install [`x`](https://rubygems.org/gems/x), which adds resource objects from [`x-resources`](https://github.com/sferik/x-ruby/tree/main/x-resources). Install `x-core` alone when you only want raw JSON.
8
+
9
+ ## Installation
10
+
11
+ `x-core` requires Ruby 3.4 or later.
12
+
13
+ bundle add x-core
14
+
15
+ ## Usage
16
+
17
+ ```ruby
18
+ require "x/core"
19
+
20
+ client = X::Client.new(bearer_token: "INSERT YOUR BEARER TOKEN HERE")
21
+
22
+ client.get("users/by/username/sferik")
23
+ # {"data"=>{"id"=>"7505382", "name"=>"Erik Berlin", "username"=>"sferik"}}
24
+
25
+ # A block reads the response of that one request: its status, headers, rate limits, and the resources it
26
+ # returned. The on_response of a client receives the same summary, for every request the client makes.
27
+ client.get("users/by/username/sferik") { |response| puts response.rate_limit&.remaining }
28
+
29
+ # A GET whose body is read as it arrives, as a stream is; x-streams reads the streams of the API a post at a
30
+ # time with it, and reconnects them. Its block is passed an X::StreamResponse, which reads the status, headers,
31
+ # and rate limits of the response, and its body a chunk at a time, in binary, or whole, as a UTF-8 String,
32
+ # without a block. A connection that drops, or a read that times out, raises X::NetworkError from read_body,
33
+ # inside the block, which may rescue it. The body is read once, inside the block: a second read, unless both
34
+ # read it whole, or a read once the block has returned, raises X::Error, which is not sent again
35
+ client.get_stream("tweets/sample/stream") { |response| response.read_body { |chunk| print chunk } }
36
+ ```
37
+
38
+ `x-core` sends its requests with `Net::HTTP`, and keeps it out of what it promises: a block, a hook, an authenticator, and an error read the request and the response through objects of `x-core`'s own, `X::Response`, `X::StreamResponse`, the request an authenticator is passed, which the signatures type as `X::_AuthenticatorRequest`, and `X::HTTPError`, which every release of 1.x keeps. The `http_response` each of `X::Response`, `X::StreamResponse`, and `X::HTTPError` reads, and the `http_response:` an `X::Response` or an `X::HTTPError` is built with, are an escape hatch for what they do not read themselves: the object is the transport's, a `Net::HTTPResponse` today, and its class is not covered by the compatibility promise of 1.x.
39
+
40
+ See the [`x` README](https://github.com/sferik/x-ruby#readme) for more examples.
41
+
42
+ ## Development
43
+
44
+ This gem has its own `Gemfile`, `Steepfile`, signatures, test suite, and mutation config, and does not load the other gems in this repository:
45
+
46
+ bundle install
47
+ bundle exec rake test
48
+ bundle exec rake mutant
49
+ bundle exec rake steep
50
+ bundle exec rake yardstick
51
+
52
+ ## License
53
+
54
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).