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.
- checksums.yaml +7 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +353 -0
- data/LICENSE.txt +21 -0
- data/README.md +54 -0
- data/lib/x/core/app_only_authenticator.rb +198 -0
- data/lib/x/core/authenticator.rb +74 -0
- data/lib/x/core/authenticator_request.rb +56 -0
- data/lib/x/core/bearer_token_authenticator.rb +46 -0
- data/lib/x/core/built_response.rb +100 -0
- data/lib/x/core/client.rb +728 -0
- data/lib/x/core/client_app_only.rb +115 -0
- data/lib/x/core/client_credentials.rb +193 -0
- data/lib/x/core/client_internals.rb +315 -0
- data/lib/x/core/client_memo.rb +58 -0
- data/lib/x/core/client_settings.rb +166 -0
- data/lib/x/core/client_token_refresh.rb +281 -0
- data/lib/x/core/connection.rb +328 -0
- data/lib/x/core/connection_pool.rb +123 -0
- data/lib/x/core/connection_proxy.rb +151 -0
- data/lib/x/core/connection_request.rb +90 -0
- data/lib/x/core/credential_holder.rb +78 -0
- data/lib/x/core/credential_validator.rb +286 -0
- data/lib/x/core/errors/authorization_denied.rb +55 -0
- data/lib/x/core/errors/authorization_error.rb +41 -0
- data/lib/x/core/errors/bad_gateway.rb +9 -0
- data/lib/x/core/errors/bad_request.rb +10 -0
- data/lib/x/core/errors/callback_error.rb +88 -0
- data/lib/x/core/errors/client_error.rb +17 -0
- data/lib/x/core/errors/conflict.rb +9 -0
- data/lib/x/core/errors/error.rb +66 -0
- data/lib/x/core/errors/forbidden.rb +10 -0
- data/lib/x/core/errors/gateway_timeout.rb +9 -0
- data/lib/x/core/errors/gone.rb +9 -0
- data/lib/x/core/errors/http_error.rb +333 -0
- data/lib/x/core/errors/internal_server_error.rb +9 -0
- data/lib/x/core/errors/invalid_response.rb +96 -0
- data/lib/x/core/errors/method_not_allowed.rb +10 -0
- data/lib/x/core/errors/network_error.rb +59 -0
- data/lib/x/core/errors/not_acceptable.rb +10 -0
- data/lib/x/core/errors/not_found.rb +15 -0
- data/lib/x/core/errors/payload_too_large.rb +10 -0
- data/lib/x/core/errors/payment_required.rb +10 -0
- data/lib/x/core/errors/request_timeout.rb +9 -0
- data/lib/x/core/errors/server_error.rb +15 -0
- data/lib/x/core/errors/service_unavailable.rb +9 -0
- data/lib/x/core/errors/token_report_failed.rb +59 -0
- data/lib/x/core/errors/too_many_redirects.rb +53 -0
- data/lib/x/core/errors/too_many_requests.rb +80 -0
- data/lib/x/core/errors/unauthorized.rb +15 -0
- data/lib/x/core/errors/unavailable_for_legal_reasons.rb +10 -0
- data/lib/x/core/errors/unprocessable_entity.rb +10 -0
- data/lib/x/core/errors/unsupported_format.rb +22 -0
- data/lib/x/core/errors/unsupported_media_type.rb +10 -0
- data/lib/x/core/errors/unsupported_operation.rb +18 -0
- data/lib/x/core/oauth1_authenticator.rb +142 -0
- data/lib/x/core/oauth2_authenticator.rb +296 -0
- data/lib/x/core/oauth2_authorization.rb +405 -0
- data/lib/x/core/oauth2_refresh.rb +272 -0
- data/lib/x/core/oauth2_tokens.rb +277 -0
- data/lib/x/core/origin.rb +95 -0
- data/lib/x/core/problem.rb +302 -0
- data/lib/x/core/proxy_setting.rb +22 -0
- data/lib/x/core/rate_limit.rb +137 -0
- data/lib/x/core/rate_limit_handler.rb +166 -0
- data/lib/x/core/redirect_handler.rb +208 -0
- data/lib/x/core/refresh_reporter.rb +92 -0
- data/lib/x/core/request_builder.rb +155 -0
- data/lib/x/core/request_context.rb +85 -0
- data/lib/x/core/request_encoding.rb +135 -0
- data/lib/x/core/response.rb +196 -0
- data/lib/x/core/response_headers.rb +31 -0
- data/lib/x/core/response_parser.rb +155 -0
- data/lib/x/core/retry_handler.rb +149 -0
- data/lib/x/core/setting_validator.rb +399 -0
- data/lib/x/core/stream_body.rb +77 -0
- data/lib/x/core/stream_response.rb +267 -0
- data/lib/x/core/token_endpoint.rb +238 -0
- data/lib/x/core/version.rb +25 -0
- data/lib/x/core.rb +6 -0
- data/sig/manifest.yaml +8 -0
- data/sig/x-core.rbs +396 -0
- 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
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).
|