client-api-builder 0.11.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5a8b4e5fcad31bffb2d4234fb2d73205ae0e3aed33ba75ec32defa778ed456f
4
- data.tar.gz: ff349350509add94a7108af7dddf9416b98265752735a510d46d35bc97897849
3
+ metadata.gz: 7d553ae987e278ac4a41b6185531e02292cc2f3b57cd4af1332552d827ad74b5
4
+ data.tar.gz: 50cd54be44249bf9a366929c73abe839bab6c2594bf299bc6a0ebb5db4278db3
5
5
  SHA512:
6
- metadata.gz: cea030b2bb67c167c3dc4f4a798cadd2e4a8c873fe71d19bed944b4263446f0befb3cbdbc6dccbd9741814aeedb52f0b0b7b2aff155f7800c221601d52383fbd
7
- data.tar.gz: 62d3ccbfc3ad52500a29070c6bad1961ba06539e131fc53e5eb775616c8ca179d91c8bd4f0e3bfa150cb3c40a2f8897f3e8bb957d4ae867400c7ec6a872db42e
6
+ metadata.gz: 8dfbfc94f744e4d7f91386d550d6d33b98049c872b4324f3dffd9a49e3d42d52ea805d93c2e9cd0c97c556826b6a9aaa2415689d019a49790be14af677adc936
7
+ data.tar.gz: cd6170e890b619dbac6ec5fe7be55fb9f092fcde346597a201390ee351204a51f8d483a627e26ae3bffae0d50329d63d2e93f81192047b7d56f897c1bc187711
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.0](https://github.com/dougyouch/client-api-builder/compare/v1.0.0...v1.1.0) (2026-10-23)
4
+
5
+
6
+ ### Features
7
+
8
+ * **transports:** choose how requests are sent per environment ([2193768](https://github.com/dougyouch/client-api-builder/commit/2193768ce8b916ffb9064ccedbd09b87b47def5e))
9
+
10
+ ## [1.0.0](https://github.com/dougyouch/client-api-builder/compare/v0.11.0...v1.0.0) (2026-10-05)
11
+
12
+
13
+ ### ⚠ BREAKING CHANGES
14
+
15
+ * **gem:** Ruby 3.2 reached end of life in March 2026 and is no longer supported. CI now also runs the specs on Ruby 3.3, the minimum, with Gemfile.lock and the coverage gate.
16
+ * **deps:** section router classes are named after the section even when ActiveSupport is loaded. inheritance-helper 0.2 used String#classify there, so section :users created UserNestedRouter and collided with section :user; it is now UsersNestedRouter. Use <name>_router rather than the constant name.
17
+
18
+ ### Build System
19
+
20
+ * **deps:** require inheritance-helper 1.x ([60525c4](https://github.com/dougyouch/client-api-builder/commit/60525c4e21d1eb202f7f49ebf246c057bc9a6cb6))
21
+ * **gem:** require ruby 3.3 ([a6f79da](https://github.com/dougyouch/client-api-builder/commit/a6f79da0abb0986e711fce17f7f6428b7a1b73e1))
22
+
3
23
  ## [0.11.0](https://github.com/dougyouch/client-api-builder/compare/v0.10.0...v0.11.0) (2026-10-04)
4
24
 
5
25
 
data/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  [![Coverage](https://raw.githubusercontent.com/dougyouch/client-api-builder/badges/coverage.svg)](https://github.com/dougyouch/client-api-builder/actions/workflows/ci.yml)
6
6
  [![Branch Coverage](https://raw.githubusercontent.com/dougyouch/client-api-builder/badges/branches.svg)](https://github.com/dougyouch/client-api-builder/actions/workflows/ci.yml)
7
7
 
8
+ [Changelog](CHANGELOG.md) · [Upgrading to 1.0](UPGRADING.md)
9
+
8
10
  A Ruby gem for building robust, secure API clients through declarative configuration. Define your API endpoints and their behavior with minimal boilerplate while benefiting from built-in security features, automatic retries, and comprehensive error handling.
9
11
 
10
12
  ## Features
@@ -15,6 +17,7 @@ A Ruby gem for building robust, secure API clients through declarative configura
15
17
  - **Flexible Request Building** - Support for JSON, query params, and custom body builders
16
18
  - **Nested Routing** - Organize complex APIs with hierarchical route structures
17
19
  - **Retry Logic** - Configurable automatic retries for transient network failures
20
+ - **Transports** - Choose how requests reach the network per environment at boot, so one client class can open a connection per request in a web server and keep persistent connections in a worker
18
21
  - **Connection Pooling** - Opt-in persistent connections shared across threads, with no extra gems
19
22
  - **Per-Thread Connections** - Opt-in persistent connections dedicated to each thread, with no pool to size
20
23
  - **HTTP/2** - Opt-in HTTP/2 over TLS, multiplexing concurrent requests on one connection per host (with the `http-2` gem)
@@ -336,7 +339,7 @@ section :users, inherit: %i[headers query_params connection_options] do
336
339
  end
337
340
  ```
338
341
 
339
- Inherited settings are the root client's class-level ones, read on every request (so ones declared after the section, or in a subclass of the client, apply too). The section's own settings override them, and per-request options override both; a per-request `nil` header still drops an inherited one. Retries and body/query builders are always the section's own. Any other options passed to `section` are available to it as `nested_router_options`.
342
+ Inherited settings are the root client's class-level ones, read on every request (so ones declared after the section, or in a subclass of the client, apply too). The section's own settings override them, and per-request options override both; a per-request `nil` header still drops an inherited one. Retries and body/query builders are always the section's own. Any other options passed to `section` are available to it as `nested_router_options`. Get a section's class with `<name>_router` (`MyApiClient.users_router`) rather than its constant name; see [UPGRADING.md](UPGRADING.md#section-class-names-are-no-longer-singularized).
340
343
 
341
344
  Symbol and block values given to `header` and `query_param`, `{name}` path values, and response blocks are evaluated on the root client, so they can use its methods and state.
342
345
 
@@ -361,9 +364,67 @@ end
361
364
 
362
365
  Any `Net::HTTP.start` option can be set this way. Your options are applied over the secure HTTPS defaults, so setting `verify_mode` yourself replaces `VERIFY_PEER`.
363
366
 
367
+ ### Transports
368
+
369
+ How requests reach the network is up to a transport, chosen for the environment at boot rather than in the client class. The same `GitHubClient` can open a connection per request inside a web server and keep per-thread connections inside a background worker:
370
+
371
+ ```ruby
372
+ # config/puma.rb, or any web server's boot code: the default, shown for clarity
373
+ ClientApiBuilder.use_transport(ClientApiBuilder::Transports::OneShot.new)
374
+
375
+ # config/initializers/sidekiq.rb
376
+ Sidekiq.configure_server do |config|
377
+ ClientApiBuilder.use_transport(ClientApiBuilder::Transports::ThreadLocal.new(ttl: 30))
378
+ config.on(:shutdown) { ClientApiBuilder.close_transports }
379
+ end
380
+ ```
381
+
382
+ | Transport | Connections | Suits |
383
+ |---|---|---|
384
+ | `Transports::OneShot.new` (default) | A new one per request, closed once the response is read | Web servers, forking process managers, low request rates |
385
+ | `Transports::Pooled.new(max_connections:, ttl:, checkout_timeout:, idle_timeout:)` | Pools shared by every thread, one per scheme, host, port and connection options | Many threads, including short-lived ones ([details](#persistent-connections)) |
386
+ | `Transports::ThreadLocal.new(ttl:, idle_timeout:)` | One per thread and host, reused by that thread's requests | A fixed set of worker threads ([details](#per-thread-connections)) |
387
+ | `Transports::HTTP2.new(fallback: Transports::OneShot.new)` | One HTTP/2 connection per origin, carrying concurrent requests as streams | Many threads waiting on one slow https API ([details](#http2)) |
388
+
389
+ Register a transport for every client, for one client class (and its subclasses), or for one client instance:
390
+
391
+ ```ruby
392
+ ClientApiBuilder.use_transport(ClientApiBuilder::Transports::ThreadLocal.new) # every client
393
+ ClientApiBuilder.use_transport(ClientApiBuilder::Transports::Pooled.new(max_connections: 10), GitHubClient)
394
+ client.transport = ClientApiBuilder::Transports::OneShot.new # this instance only
395
+
396
+ ClientApiBuilder.close_transports # closes every registered transport's connections, e.g. at shutdown
397
+ ```
398
+
399
+ A request uses the first transport it finds:
400
+
401
+ 1. The one set on the client instance with `transport=`.
402
+ 2. The one registered with `use_transport` for the client's class or its nearest ancestor (a module works too, for every class including it).
403
+ 3. The one the class itself includes (`ConnectionPools`, `ThreadConnections` or `HTTP2`, below), or its nearest ancestor's.
404
+ 4. For a section, its root client's transport.
405
+ 5. The process default set with `use_transport(transport)`, else `OneShot`.
406
+
407
+ So a class can include a sensible default for its API, and the environment still decides: `use_transport` for the class wins whether it runs before or after the class is loaded. Sections use their root client's transport, so they share its connections, unless a transport is set or registered for the section, or it calls `connection_pool` or `connection_per_thread`.
408
+
409
+ Every transport is fork-safe (after a fork, the child opens its own connections and leaves the parent's alone) and closes a connection whose request raised rather than reusing it.
410
+
411
+ A transport is any object with `with_connection(uri, connection_options) { |http| ... }`, which yields something answering `request(net_request) { |response| ... }` like a started `Net::HTTP`, and `close`. `connection_options` are the `Net::HTTP.start` options, with the [secure HTTPS defaults](#connection-options) applied.
412
+
364
413
  ### Persistent Connections
365
414
 
366
- By default each request opens and closes its own connection. Include `ClientApiBuilder::ConnectionPools` (after `Router`) to keep connections open and reuse them:
415
+ `Transports::Pooled` keeps connections open and reuses them:
416
+
417
+ ```ruby
418
+ # Optional settings; these are the defaults
419
+ ClientApiBuilder.use_transport(
420
+ ClientApiBuilder::Transports::Pooled.new(max_connections: 5, # connections per host
421
+ ttl: 30, # seconds before a connection is closed and replaced
422
+ checkout_timeout: 5, # seconds to wait for a free connection
423
+ idle_timeout: 2) # seconds idle before Net::HTTP reconnects (keep_alive_timeout)
424
+ )
425
+ ```
426
+
427
+ Or give a client class its own pools, as its default, by including `ClientApiBuilder::ConnectionPools` (after `Router`):
367
428
 
368
429
  ```ruby
369
430
  class MyApiClient
@@ -372,21 +433,17 @@ class MyApiClient
372
433
 
373
434
  base_url 'https://api.example.com'
374
435
 
375
- # Optional; these are the defaults
376
- connection_pool max_connections: 5, # connections per host
377
- ttl: 30, # seconds before a connection is closed and replaced
378
- checkout_timeout: 5, # seconds to wait for a free connection
379
- idle_timeout: 2 # seconds idle before Net::HTTP reconnects (keep_alive_timeout)
436
+ connection_pool max_connections: 10, ttl: 60 # optional; takes the same settings
380
437
  end
381
438
  ```
382
439
 
383
- The pools live on the class, like ActiveRecord's, so every instance shares them: keep one client instance per thread and the threads share the connections. A connection is checked out for one request (or one streamed response) and returned straight after, so `max_connections` can be lower than your thread count. There is one pool per scheme, host, port and connection options.
440
+ Every client using the transport shares its pools: keep one client instance per thread and the threads share the connections. A connection is checked out for one request (or one streamed response) and returned straight after, so `max_connections` can be lower than your thread count. There is one pool per scheme, host, port and connection options.
384
441
 
385
442
  - Sockets the server has closed, or that sat idle past `idle_timeout`, are reopened automatically.
386
443
  - A connection whose request raised is closed rather than reused.
387
444
  - When no connection frees up within `checkout_timeout`, the request raises `ClientApiBuilder::ConnectionPools::TimeoutError`.
388
- - Sections use their root client's pools, getting a separate pool for any other host in their `base_url`. Subclasses share their parent's pools unless they call `connection_pool` themselves.
389
- - A section can call `connection_pool` in its block to get its own pools and settings. This works whether or not the root client includes `ConnectionPools`:
445
+ - With `ConnectionPools`, sections use their root client's pools, getting a separate pool for any other host in their `base_url`. Subclasses share their parent's pools unless they call `connection_pool` themselves.
446
+ - A section can call `connection_pool` in its block to get its own pools and settings. This works whatever transport the root client uses:
390
447
 
391
448
  ```ruby
392
449
  section :uploads do
@@ -397,25 +454,20 @@ The pools live on the class, like ActiveRecord's, so every instance shares them:
397
454
 
398
455
  Sections nested inside it still fall back to the root client, as they do for `base_url`.
399
456
  - After a fork (Puma, Sidekiq, Resque), the child process opens its own connections.
400
- - `MyApiClient.close_connections` closes idle connections now and in-use ones when they're returned, including the pools of sections that have their own.
457
+ - `transport.close` (or `MyApiClient.close_connections`, with `ConnectionPools`) closes idle connections now and in-use ones when they're returned. `close_connections` also closes the pools of sections that have their own.
401
458
 
402
459
  Pooling uses only `Net::HTTP` from the standard library.
403
460
 
404
461
  ### Per-Thread Connections
405
462
 
406
- When a program runs a fixed set of worker threads, each making its own requests, include `ClientApiBuilder::ThreadConnections` (after `Router`, instead of `ConnectionPools`) to give every thread its own persistent connections:
463
+ When a program runs a fixed set of worker threads, each making its own requests, `Transports::ThreadLocal` gives every thread its own persistent connections:
407
464
 
408
465
  ```ruby
409
- class MyApiClient
410
- include ClientApiBuilder::Router
411
- include ClientApiBuilder::ThreadConnections
412
-
413
- base_url 'https://api.example.com'
414
-
415
- # Optional; these are the defaults
416
- connection_per_thread ttl: 30, # seconds before a connection is closed and replaced
417
- idle_timeout: 2 # seconds idle before Net::HTTP reconnects (keep_alive_timeout)
418
- end
466
+ # Optional settings; these are the defaults
467
+ ClientApiBuilder.use_transport(
468
+ ClientApiBuilder::Transports::ThreadLocal.new(ttl: 30, # seconds before a connection is closed and replaced
469
+ idle_timeout: 2) # seconds idle before Net::HTTP reconnects (keep_alive_timeout)
470
+ )
419
471
 
420
472
  workers = Array.new(20) do
421
473
  Thread.new do
@@ -425,23 +477,35 @@ workers = Array.new(20) do
425
477
  end
426
478
  ```
427
479
 
480
+ To make it a client class's default, include `ClientApiBuilder::ThreadConnections` (after `Router`, instead of `ConnectionPools`) and optionally call `connection_per_thread ttl: 30, idle_timeout: 2` in the class.
481
+
428
482
  A thread opens a connection per host (scheme, port and connection options) on its first request and reuses it for every later one. There's no pool size to keep in step with the thread count, and no thread ever waits for another's connection.
429
483
 
430
484
  - Sockets the server has closed, or that sat idle past `idle_timeout`, are reopened automatically. A connection whose request raised is closed rather than reused.
431
485
  - A request made while the thread's connection is busy (from inside a streaming block, or from another fiber on the thread) opens a connection of its own; the thread keeps one of the two afterwards.
432
- - Connections of threads that have finished are closed when a thread makes its first request. With short-lived threads, prefer `ConnectionPools`.
433
- - Sections and subclasses share the class's connections the same way they share pools, and a section can call `connection_per_thread` in its block to get its own.
434
- - After a fork, the child process opens its own connections. `MyApiClient.close_connections` closes every thread's idle connections now and in-use ones when their request ends.
486
+ - Connections of threads that have finished are closed when a thread makes its first request. With short-lived threads, prefer `Transports::Pooled`.
487
+ - With `ThreadConnections`, sections and subclasses share the class's connections the same way they share pools, and a section can call `connection_per_thread` in its block to get its own.
488
+ - After a fork, the child process opens its own connections. `transport.close` (or `MyApiClient.close_connections`) closes every thread's idle connections now and in-use ones when their request ends.
435
489
 
436
490
  ### HTTP/2
437
491
 
438
- Include `ClientApiBuilder::HTTP2` (after `Router`, and after `ConnectionPools` or `ThreadConnections` if you use either) to send https requests over HTTP/2. It needs the [`http-2`](https://rubygems.org/gems/http-2) gem, which isn't installed with this one:
492
+ `Transports::HTTP2` sends https requests over HTTP/2. It needs the [`http-2`](https://rubygems.org/gems/http-2) gem, which isn't installed with this one:
439
493
 
440
494
  ```ruby
441
495
  # Gemfile
442
496
  gem 'http-2'
443
497
  ```
444
498
 
499
+ ```ruby
500
+ # fallback: is used for http:// URLs and servers that only speak HTTP/1.1 (default: OneShot)
501
+ ClientApiBuilder.use_transport(
502
+ ClientApiBuilder::Transports::HTTP2.new(fallback: ClientApiBuilder::Transports::Pooled.new),
503
+ MyApiClient
504
+ )
505
+ ```
506
+
507
+ Or make it a client class's default by including `ClientApiBuilder::HTTP2` (after `Router`, and after `ConnectionPools` or `ThreadConnections` to use them as the fallback):
508
+
445
509
  ```ruby
446
510
  class MyApiClient
447
511
  include ClientApiBuilder::Router
@@ -452,15 +516,15 @@ class MyApiClient
452
516
  end
453
517
  ```
454
518
 
455
- Every instance of the class shares one connection per host (and connection options), and concurrent requests from different threads travel over it as separate streams. Responses are ordinary `Net::HTTPResponse` objects with `http_version` `'2.0'`, so `handle_response`, `UnexpectedResponse#response` and streaming routes work unchanged.
519
+ Every client using the transport shares one connection per host (and connection options), and concurrent requests from different threads travel over it as separate streams. Responses are ordinary `Net::HTTPResponse` objects with `http_version` `'2.0'`, so `handle_response`, `UnexpectedResponse#response` and streaming routes work unchanged.
456
520
 
457
- - The protocol is negotiated during the TLS handshake (ALPN). When a server picks HTTP/1.1, that host is remembered and its requests go through `Net::HTTP`, or the pooled or per-thread connections if the class includes `ConnectionPools` or `ThreadConnections`. `http://` URLs always use HTTP/1.1.
521
+ - The protocol is negotiated during the TLS handshake (ALPN). When a server picks HTTP/1.1, that host is remembered and its requests go through the fallback transport. `http://` URLs always use the fallback.
458
522
  - The usual connection options apply: `open_timeout`, `read_timeout` (per response header and body chunk), and the SSL options (`verify_mode`, `ca_file`, `cert_store`, `cert`, `key`, ...). Proxies aren't supported.
459
523
  - When the server's limit on concurrent streams is reached, a request waits up to `read_timeout` for a stream to free up.
460
524
  - Gzip and deflate responses are inflated, as `Net::HTTP` does, unless you set `Accept-Encoding` yourself.
461
525
  - A stream the server refused, or never processed before closing the connection (GOAWAY), raises `ClientApiBuilder::HTTP2::StreamRefused`; a dropped connection raises `ClientApiBuilder::HTTP2::ConnectionLost`. Both are retried by `configure_retries`. A stream the server reset raises `ClientApiBuilder::HTTP2::StreamError`.
462
526
  - Response data is acknowledged to the server as it arrives, so a streaming consumer slower than the server buffers the difference in memory.
463
- - Sections use their root client's connections. After a fork, the child opens its own. `MyApiClient.close_connections` closes the HTTP/2 connections (and the pooled or per-thread ones).
527
+ - After a fork, the child opens its own connections. `transport.close` (or `MyApiClient.close_connections`, with `include ClientApiBuilder::HTTP2`) closes the HTTP/2 connections and the fallback's.
464
528
 
465
529
  **When it's faster.** `script/benchmark_http2.rb` compares a new connection per request, `ConnectionPools` and HTTP/2 against a local TLS server. On one machine (Ruby 4.0.7, http-2 1.2.3):
466
530
 
@@ -470,7 +534,7 @@ Every instance of the class shares one connection per host (and connection optio
470
534
  | 50 threads, 20ms delay, one pooled connection per thread | 628 req/s | 1,904 req/s | 1,765 req/s |
471
535
  | 50 threads, 20ms delay, default pool of 5 | 640 req/s | 208 req/s | 1,706 req/s |
472
536
 
473
- HTTP/2 avoids a TLS handshake per request, like the pools, and isn't limited by a connection count: many threads waiting on a slow API share one connection. Per request, it costs more client CPU than pooled HTTP/1.1 (the protocol layer is pure Ruby), which shows most with large bodies. If you can give each thread its own connection (a pool as large as the thread count, or `ThreadConnections`), HTTP/1.1 is usually as fast or faster.
537
+ HTTP/2 avoids a TLS handshake per request, like the pools, and isn't limited by a connection count: many threads waiting on a slow API share one connection. Per request, it costs more client CPU than pooled HTTP/1.1 (the protocol layer is pure Ruby), which shows most with large bodies. If you can give each thread its own connection (a pool as large as the thread count, or `Transports::ThreadLocal`), HTTP/1.1 is usually as fast or faster.
474
538
 
475
539
  ### Retry Configuration
476
540
 
@@ -756,7 +820,7 @@ Only safe file modes are allowed for streaming to files: `w`, `wb`, `a`, `ab`, `
756
820
 
757
821
  ## Thread Safety
758
822
 
759
- Client instances are **not thread-safe**. Create a separate client instance per thread. To share connections between those instances, see [Persistent Connections](#persistent-connections).
823
+ Client instances are **not thread-safe**. Create a separate client instance per thread. To share connections between those instances, see [Transports](#transports).
760
824
 
761
825
  ```ruby
762
826
  # Correct: Create a new client for each thread
@@ -817,7 +881,7 @@ Define these in your client to change default behavior:
817
881
  | Method | Default |
818
882
  |--------|---------|
819
883
  | `retry_request?(exception, options)` | `true` for the network errors listed under Retry Configuration |
820
- | `with_http_connection(uri, connection_options, &block)` | Yields a started `Net::HTTP`: a new connection per request, a pooled one with `ConnectionPools`, or the thread's own with `ThreadConnections` |
884
+ | `with_http_connection(uri, connection_options, &block)` | Yields a started `Net::HTTP` (or an HTTP/2 connection) from the client's [transport](#transports) |
821
885
  | `escape_path(value)` | Percent-encodes path values (`ERB::Util.url_encode`) |
822
886
  | `parse_response(response, options)` | Parses the body as JSON, `nil` when empty |
823
887
  | `handle_response(response, options, &block)` | Applies `return:`, parsing and the response block |
@@ -827,8 +891,8 @@ Define these in your client to change default behavior:
827
891
 
828
892
  ## Requirements
829
893
 
830
- - Ruby 3.2+
831
- - `inheritance-helper` gem (>= 0.2.5)
894
+ - Ruby 3.3+
895
+ - `inheritance-helper` gem (1.x)
832
896
  - `activesupport` (optional) for `to_query` builders and instrumentation
833
897
 
834
898
  ## Contributing
data/UPGRADING.md ADDED
@@ -0,0 +1,139 @@
1
+ # Upgrading
2
+
3
+ ## From 1.0 to 1.1: transports (optional)
4
+
5
+ 1.1 has no breaking changes. Clients that include `ClientApiBuilder::ConnectionPools`, `ThreadConnections` or `HTTP2`
6
+ keep working as before, with the same `connection_pool`, `connection_per_thread` and `close_connections` class methods.
7
+
8
+ What's new is that how requests reach the network can be chosen per environment at boot, with
9
+ [transports](README.md#transports), instead of only in the client class. Move to them when one client class needs
10
+ different connection handling in different processes, for example a connection per request in a web server and
11
+ persistent connections in a background worker.
12
+
13
+ Before, the class decided for every process:
14
+
15
+ ```ruby
16
+ class GitHubClient
17
+ include ClientApiBuilder::Router
18
+ include ClientApiBuilder::ThreadConnections
19
+
20
+ base_url 'https://api.github.com'
21
+ connection_per_thread ttl: 30
22
+ end
23
+ ```
24
+
25
+ After, the class describes the API and each process picks its transport:
26
+
27
+ ```ruby
28
+ class GitHubClient
29
+ include ClientApiBuilder::Router
30
+
31
+ base_url 'https://api.github.com'
32
+ end
33
+
34
+ # Sidekiq boot code; the web server keeps the default, a new connection per request
35
+ ClientApiBuilder.use_transport(ClientApiBuilder::Transports::ThreadLocal.new(ttl: 30), GitHubClient)
36
+ at_exit { ClientApiBuilder.close_transports }
37
+ ```
38
+
39
+ You can also keep the `include` as the class's default and override it only where needed: a transport registered for
40
+ the class with `use_transport` wins over the one it includes, whether it's registered before or after the class loads.
41
+
42
+ | Include style | Transport |
43
+ |---|---|
44
+ | `include ClientApiBuilder::ConnectionPools` + `connection_pool(**settings)` | `Transports::Pooled.new(**settings)` |
45
+ | `include ClientApiBuilder::ThreadConnections` + `connection_per_thread(**settings)` | `Transports::ThreadLocal.new(**settings)` |
46
+ | `include ClientApiBuilder::HTTP2` | `Transports::HTTP2.new` |
47
+ | `include ClientApiBuilder::ConnectionPools` + `include ClientApiBuilder::HTTP2` | `Transports::HTTP2.new(fallback: Transports::Pooled.new)` |
48
+ | `MyClient.close_connections` | `transport.close`, or `ClientApiBuilder.close_transports` for all of them |
49
+
50
+ One behaviour to know: if your client overrides `with_http_connection`, the override still runs and still takes
51
+ precedence over every transport.
52
+
53
+ ## From 0.11 to 1.0
54
+
55
+ 1.0 has two breaking changes. Most clients need no code changes; check whether these apply to you.
56
+
57
+ | Change | Who is affected |
58
+ |---|---|
59
+ | [Ruby 3.3 or newer is required](#ruby-33-or-newer-is-required) | Apps on Ruby 3.2 |
60
+ | [Section class names are no longer singularized](#section-class-names-are-no-longer-singularized) | Apps with ActiveSupport loaded (any Rails app) that refer to a section's class by its constant name |
61
+
62
+ 1.0 also requires [inheritance-helper 1.x](https://github.com/dougyouch/inheritance-helper/blob/master/UPGRADING.md).
63
+ If your own code uses inheritance-helper directly, check its upgrade guide too.
64
+
65
+ ### Ruby 3.3 or newer is required
66
+
67
+ Ruby 3.2 reached end of life in March 2026. On Ruby 3.2, Bundler and RubyGems keep resolving to 0.11.0.
68
+
69
+ ### Section class names are no longer singularized
70
+
71
+ Each `section` creates a router class and assigns it to a constant on the client class. The name came from
72
+ inheritance-helper, which used `String#classify` when ActiveSupport was loaded, and `classify` singularizes. 1.0
73
+ requires inheritance-helper 1.x, which builds the name the same way everywhere:
74
+
75
+ | Section | 0.11 with ActiveSupport | 0.11 without ActiveSupport | 1.0 |
76
+ |---|---|---|---|
77
+ | `section :users` | `UserNestedRouter` | `UsersNestedRouter` | `UsersNestedRouter` |
78
+ | `section :news_items` | `NewsItemNestedRouter` | `NewsItemsNestedRouter` | `NewsItemsNestedRouter` |
79
+ | `section :user` | `UserNestedRouter` | `UserNestedRouter` | `UserNestedRouter` |
80
+
81
+ **What doesn't change:** `client.users`, `MyClient.users_router` and `MyClient.section_routers` return the same
82
+ classes as before. Without ActiveSupport, nothing changes at all.
83
+
84
+ **What changes:** with ActiveSupport loaded, code that names the constant, such as `MyClient::UserNestedRouter`
85
+ for `section :users`, gets a `NameError` (uninitialized constant).
86
+
87
+ **Watch for a singular and a plural section in the same client.** On 0.11 with ActiveSupport, `section :user`
88
+ and `section :users` both became `UserNestedRouter`, and whichever was declared last replaced the other (with an
89
+ "already initialized constant" warning). On 1.0 each gets its own class:
90
+
91
+ ```ruby
92
+ class MyClient
93
+ include ClientApiBuilder::Router
94
+
95
+ section(:user) { route :me, '/me' }
96
+ section(:users) { route :list_users, '/users' }
97
+ end
98
+
99
+ MyClient::UserNestedRouter # 0.11: the :users section (declared last) 1.0: the :user section
100
+ MyClient::UsersNestedRouter # 0.11: NameError 1.0: the :users section
101
+ ```
102
+
103
+ Code that used `UserNestedRouter` to mean the `:users` section doesn't fail on 1.0. It gets the `:user` section
104
+ instead, so check for this case.
105
+
106
+ #### Finding affected code
107
+
108
+ Search for the generated constants:
109
+
110
+ ```bash
111
+ grep -rnE '[A-Za-z]+NestedRouter\b' app lib spec | grep -v 'ClientApiBuilder::NestedRouter'
112
+ ```
113
+
114
+ #### Fixing it
115
+
116
+ Use the `<name>_router` class method, which is the same on every version:
117
+
118
+ ```ruby
119
+ # before
120
+ MyClient::UserNestedRouter
121
+ # after
122
+ MyClient.users_router
123
+ ```
124
+
125
+ If you can't change every reference at once, alias the old name to the section's class after it's defined:
126
+
127
+ ```ruby
128
+ class MyClient
129
+ include ClientApiBuilder::Router
130
+
131
+ section(:users) { route :list_users, '/users' }
132
+
133
+ # temporary: keeps 0.11's name working until callers use users_router
134
+ UserNestedRouter = users_router
135
+ end
136
+ ```
137
+
138
+ Don't add an alias that clashes with a section of that name. In the example above with both `:user` and
139
+ `:users`, `UserNestedRouter` already belongs to `:user`.
@@ -20,11 +20,26 @@ module ClientApiBuilder
20
20
 
21
21
  class << self
22
22
  attr_accessor :logger
23
+
24
+ # The process-wide Transports::Registry
25
+ attr_reader :transports
26
+
27
+ # Sends requests through transport: for every client, or for client_class and its
28
+ # subclasses when given. Meant to be called once at boot for the environment.
29
+ def use_transport(transport, client_class = nil)
30
+ transports.use(transport, client_class)
31
+ end
32
+
33
+ # Closes the connections of every registered transport, e.g. at shutdown
34
+ def close_transports
35
+ transports.close_all
36
+ end
23
37
  end
24
38
 
25
39
  autoload :ActiveSupportNotifications, 'client_api_builder/active_support_notifications'
26
40
  autoload :ActiveSupportLogSubscriber, 'client_api_builder/active_support_log_subscriber'
27
41
  autoload :ConnectionPools, 'client_api_builder/connection_pools'
42
+ autoload :DeclaredTransport, 'client_api_builder/declared_transport'
28
43
  autoload :HTTP2, 'client_api_builder/http2'
29
44
  autoload :NestedRouter, 'client_api_builder/nested_router'
30
45
  autoload :QueryParams, 'client_api_builder/query_params'
@@ -32,8 +47,11 @@ module ClientApiBuilder
32
47
  autoload :Router, 'client_api_builder/router'
33
48
  autoload :Section, 'client_api_builder/section'
34
49
  autoload :ThreadConnections, 'client_api_builder/thread_connections'
50
+ autoload :Transports, 'client_api_builder/transports'
35
51
 
36
52
  module NetHTTP
37
53
  autoload :Request, 'client_api_builder/net_http_request'
38
54
  end
55
+
56
+ @transports = Transports::Registry.new
39
57
  end
@@ -12,6 +12,8 @@
12
12
  #
13
13
  # Each thread uses its own client instance; the instances check connections out of the
14
14
  # class's pools (one pool per host and connection options) for the length of one request.
15
+ # The pools are a Transports::Pooled declared as the class's transport, so
16
+ # ClientApiBuilder.use_transport can still pick another one for the class at boot.
15
17
  module ClientApiBuilder
16
18
  module ConnectionPools
17
19
  autoload :Connection, 'client_api_builder/connection_pools/connection'
@@ -34,6 +36,7 @@ module ClientApiBuilder
34
36
  raise ArgumentError, 'include ClientApiBuilder::ConnectionPools before ClientApiBuilder::HTTP2'
35
37
  end
36
38
 
39
+ base.extend ::ClientApiBuilder::DeclaredTransport
37
40
  base.extend ClassMethods
38
41
  base.connection_pool
39
42
  end
@@ -42,7 +45,12 @@ module ClientApiBuilder
42
45
  # Configures this class's pools, replacing any it already had. Subclasses share their
43
46
  # parent's pools unless they call connection_pool themselves.
44
47
  def connection_pool(**settings)
45
- redefine_class_method(:connection_pools, PoolSet.new(Settings.new(**settings)))
48
+ redefine_class_method(:http1_transport, ::ClientApiBuilder::Transports::Pooled.new(**settings))
49
+ declare_transport
50
+ end
51
+
52
+ def connection_pools
53
+ http1_transport.pool_set
46
54
  end
47
55
 
48
56
  # Closes idle connections now and in-use ones when they are returned, including the
@@ -52,9 +60,5 @@ module ClientApiBuilder
52
60
  section_routers.each_value(&:close_connections)
53
61
  end
54
62
  end
55
-
56
- def with_http_connection(uri, connection_options, &)
57
- self.class.connection_pools.with_connection(uri, connection_options, &)
58
- end
59
63
  end
60
64
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClientApiBuilder
4
+ # Class methods shared by the ConnectionPools, ThreadConnections and HTTP2 mixins. Each
5
+ # stores its transport on the class with redefine_class_method, so subclasses share it,
6
+ # and declare_transport registers their combination as the class's declared transport
7
+ # (see Transports::Registry): the HTTP/2 transport falling back to the HTTP/1.1 one.
8
+ module DeclaredTransport
9
+ # The transport for http URLs, and https origins without HTTP/2
10
+ def http1_transport
11
+ Transports::OneShot.new
12
+ end
13
+
14
+ # The class's HTTP/2 transport, when it includes HTTP2
15
+ def http2_transport
16
+ nil
17
+ end
18
+
19
+ def declare_transport
20
+ transport = http2_transport ? http2_transport.with_fallback(http1_transport) : http1_transport
21
+ ClientApiBuilder.transports.declare(transport, self)
22
+ end
23
+ end
24
+ end
@@ -15,7 +15,8 @@
15
15
  # as streams. TLS negotiates the protocol (ALPN): when the server picks HTTP/1.1, the origin is
16
16
  # remembered and its requests go through Net::HTTP (or the pooled or per-thread connections)
17
17
  # instead. http URLs always use HTTP/1.1. Responses are Net::HTTPResponse objects with
18
- # http_version '2.0'.
18
+ # http_version '2.0'. The class's transport becomes a Transports::HTTP2 falling back to its
19
+ # HTTP/1.1 transport; ClientApiBuilder.use_transport can still pick another one at boot.
19
20
  module ClientApiBuilder
20
21
  module HTTP2
21
22
  autoload :Connection, 'client_api_builder/http2/connection'
@@ -43,9 +44,10 @@ module ClientApiBuilder
43
44
  raise ArgumentError, 'include ClientApiBuilder::Router before ClientApiBuilder::HTTP2'
44
45
  end
45
46
 
46
- require_http2_gem
47
+ base.extend ::ClientApiBuilder::DeclaredTransport
47
48
  base.extend ClassMethods
48
- base.redefine_class_method(:http2_connections, ConnectionSet.new)
49
+ base.redefine_class_method(:http2_transport, ::ClientApiBuilder::Transports::HTTP2.new)
50
+ base.declare_transport
49
51
  end
50
52
 
51
53
  def self.require_http2_gem
@@ -55,6 +57,10 @@ module ClientApiBuilder
55
57
  end
56
58
 
57
59
  module ClassMethods
60
+ def http2_connections
61
+ http2_transport.connections
62
+ end
63
+
58
64
  # Closes this class's HTTP/2 connections, and its pooled or per-thread connections when it
59
65
  # has them
60
66
  def close_connections
@@ -62,10 +68,5 @@ module ClientApiBuilder
62
68
  super if defined?(super)
63
69
  end
64
70
  end
65
-
66
- def with_http_connection(uri, connection_options, &)
67
- connection = uri.scheme == 'https' && self.class.http2_connections.connection_for(uri, connection_options)
68
- connection ? yield(connection) : super
69
- end
70
71
  end
71
72
  end
@@ -87,10 +87,10 @@ module ClientApiBuilder
87
87
  root_router.handle_response(response, options, &)
88
88
  end
89
89
 
90
- # Uses the root client's connections unless this section calls connection_pool or
91
- # connection_per_thread
92
- def with_http_connection(uri, connection_options, &)
93
- root_router.with_http_connection(uri, connection_options, &)
90
+ # Uses the root client's transport unless one is set on this section, registered for its
91
+ # class, or declared by connection_pool or connection_per_thread
92
+ def transport
93
+ @transport || ClientApiBuilder.transports.registered_for(self.class) || root_router.transport
94
94
  end
95
95
 
96
96
  private
@@ -50,10 +50,19 @@ module ClientApiBuilder
50
50
  end
51
51
  end
52
52
 
53
- # Yields a started Net::HTTP session for one request. By default each request opens
54
- # and closes its own connection; ConnectionPools overrides this to reuse pooled ones.
53
+ # Sends this client's requests through transport instead of its class's
54
+ attr_writer :transport
55
+
56
+ # The transport set on this client, else the one registered for its class (see
57
+ # Transports::Registry)
58
+ def transport
59
+ @transport || ClientApiBuilder.transports.for_class(self.class)
60
+ end
61
+
62
+ # Yields a started Net::HTTP session (or something answering request like one) for one
63
+ # request, from the client's transport
55
64
  def with_http_connection(uri, connection_options, &)
56
- Net::HTTP.start(uri.hostname, uri.port, connection_options, &)
65
+ transport.with_connection(uri, connection_options, &)
57
66
  end
58
67
 
59
68
  # validate_response, when given, is called with the response before its body is streamed
@@ -12,7 +12,8 @@
12
12
  #
13
13
  # Each thread opens its own connection per host on its first request and reuses it for its
14
14
  # later ones, so there's no pool size to match to the thread count and no waiting for a
15
- # connection to free up.
15
+ # connection to free up. The connections are a Transports::ThreadLocal declared as the class's
16
+ # transport, so ClientApiBuilder.use_transport can still pick another one for the class at boot.
16
17
  module ClientApiBuilder
17
18
  module ThreadConnections
18
19
  autoload :ConnectionSet, 'client_api_builder/thread_connections/connection_set'
@@ -30,6 +31,7 @@ module ClientApiBuilder
30
31
  raise ArgumentError, 'include ClientApiBuilder::ThreadConnections before ClientApiBuilder::HTTP2'
31
32
  end
32
33
 
34
+ base.extend ::ClientApiBuilder::DeclaredTransport
33
35
  base.extend ClassMethods
34
36
  base.connection_per_thread
35
37
  end
@@ -38,7 +40,12 @@ module ClientApiBuilder
38
40
  # Configures this class's per-thread connections, replacing any it already had.
39
41
  # Subclasses share their parent's unless they call connection_per_thread themselves.
40
42
  def connection_per_thread(**settings)
41
- redefine_class_method(:thread_connections, ConnectionSet.new(Settings.new(**settings)))
43
+ redefine_class_method(:http1_transport, ::ClientApiBuilder::Transports::ThreadLocal.new(**settings))
44
+ declare_transport
45
+ end
46
+
47
+ def thread_connections
48
+ http1_transport.connection_set
42
49
  end
43
50
 
44
51
  # Closes every thread's idle connections now and in-use ones when their request ends,
@@ -48,9 +55,5 @@ module ClientApiBuilder
48
55
  section_routers.each_value(&:close_connections)
49
56
  end
50
57
  end
51
-
52
- def with_http_connection(uri, connection_options, &)
53
- self.class.thread_connections.with_connection(uri, connection_options, &)
54
- end
55
58
  end
56
59
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClientApiBuilder
4
+ module Transports
5
+ # HTTP/2 for https, one connection per origin and connection options carrying concurrent
6
+ # requests as streams (needs the http-2 gem). http URLs, and origins whose server chose
7
+ # HTTP/1.1 during the TLS handshake, go through the fallback transport.
8
+ #
9
+ # connections: shares the HTTP/2 connections of another transport (see with_fallback).
10
+ class HTTP2
11
+ attr_reader :fallback, :connections
12
+
13
+ def initialize(fallback: OneShot.new, connections: nil)
14
+ ::ClientApiBuilder::HTTP2.require_http2_gem
15
+ @fallback = fallback
16
+ @connections = connections || ::ClientApiBuilder::HTTP2::ConnectionSet.new
17
+ end
18
+
19
+ # A transport over the same HTTP/2 connections with a different fallback
20
+ def with_fallback(fallback)
21
+ self.class.new(fallback: fallback, connections: connections)
22
+ end
23
+
24
+ def with_connection(uri, connection_options, &)
25
+ connection = uri.scheme == 'https' && connections.connection_for(uri, connection_options)
26
+ connection ? yield(connection) : fallback.with_connection(uri, connection_options, &)
27
+ end
28
+
29
+ # Closes the HTTP/2 connections and the fallback's
30
+ def close
31
+ connections.close
32
+ fallback.close
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ module ClientApiBuilder
6
+ module Transports
7
+ # A new connection for every request, closed once the response is read. Nothing outlives
8
+ # a request, so it suits web servers and forking process managers. This is the default.
9
+ class OneShot
10
+ def with_connection(uri, connection_options, &)
11
+ Net::HTTP.start(uri.hostname, uri.port, connection_options, &)
12
+ end
13
+
14
+ # Nothing stays open between requests
15
+ def close; end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClientApiBuilder
4
+ module Transports
5
+ # Persistent connections shared by every thread, checked out of a pool per scheme, host,
6
+ # port and connection options for the length of one request. Takes the
7
+ # ConnectionPools::Settings options (max_connections, ttl, checkout_timeout, idle_timeout).
8
+ # A connection whose request raised is closed, and after a fork the child opens its own.
9
+ class Pooled
10
+ attr_reader :pool_set
11
+
12
+ def initialize(**settings)
13
+ @pool_set = ConnectionPools::PoolSet.new(ConnectionPools::Settings.new(**settings))
14
+ end
15
+
16
+ def settings
17
+ pool_set.settings
18
+ end
19
+
20
+ def with_connection(uri, connection_options, &)
21
+ pool_set.with_connection(uri, connection_options, &)
22
+ end
23
+
24
+ # Closes idle connections now and in-use ones when they're returned
25
+ def close
26
+ pool_set.close
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClientApiBuilder
4
+ module Transports
5
+ # Which transport each client class uses. A class (or module) can have two kinds of entry:
6
+ # one registered with use, normally at boot for the environment, and one declared by its
7
+ # class body through ConnectionPools, ThreadConnections or HTTP2. A class uses the
8
+ # registered transport of the nearest class in its ancestry, else the declared one of the
9
+ # nearest, else the process default (OneShot unless use set another), so the environment's
10
+ # choice wins whichever loads first.
11
+ #
12
+ # Entries are held weakly by class, so anonymous client classes can still be collected.
13
+ class Registry
14
+ def initialize(default_factory: -> { OneShot.new })
15
+ @default_factory = default_factory
16
+ @mutex = Mutex.new
17
+ @default = nil
18
+ @registered = ObjectSpace::WeakKeyMap.new
19
+ @declared = ObjectSpace::WeakKeyMap.new
20
+ @transports = ObjectSpace::WeakMap.new
21
+ end
22
+
23
+ # Sends requests through transport: for every client, or for client_class and its
24
+ # subclasses (and the classes including it, for a module) when given
25
+ def use(transport, client_class = nil)
26
+ @mutex.synchronize do
27
+ client_class ? @registered[client_class] = transport : @default = transport
28
+ @transports[transport] = true
29
+ end
30
+ end
31
+
32
+ # The transport a client class's own body chose; use takes precedence over it
33
+ def declare(transport, client_class)
34
+ @mutex.synchronize do
35
+ @declared[client_class] = transport
36
+ @transports[transport] = true
37
+ end
38
+ end
39
+
40
+ # The transport registered or declared for the class or its nearest ancestor, or nil
41
+ def registered_for(client_class)
42
+ ancestors = client_class.ancestors
43
+ @mutex.synchronize { nearest(@registered, ancestors) || nearest(@declared, ancestors) }
44
+ end
45
+
46
+ def for_class(client_class)
47
+ registered_for(client_class) || default
48
+ end
49
+
50
+ def default
51
+ @mutex.synchronize { @default ||= @default_factory.call }
52
+ end
53
+
54
+ # Closes every transport registered or declared, and the default
55
+ def close_all
56
+ transports = @mutex.synchronize { [*@transports.keys, @default].compact.uniq }
57
+ transports.each(&:close)
58
+ end
59
+
60
+ # Forgets the transports set with use, and the default, closing nothing; declarations
61
+ # belong to class bodies and stay. Meant for specs.
62
+ def reset
63
+ @mutex.synchronize do
64
+ @registered = ObjectSpace::WeakKeyMap.new
65
+ @default = nil
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def nearest(entries, ancestors)
72
+ ancestors.each do |ancestor|
73
+ transport = entries[ancestor]
74
+ return transport if transport
75
+ end
76
+ nil
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClientApiBuilder
4
+ module Transports
5
+ # Persistent connections dedicated to each thread: one per scheme, host, port and
6
+ # connection options, opened on the thread's first request. Takes the
7
+ # ThreadConnections::Settings options (ttl, idle_timeout). Suits a fixed set of worker
8
+ # threads; with many short-lived threads, prefer Pooled. A connection whose request
9
+ # raised is closed, and after a fork the child opens its own.
10
+ class ThreadLocal
11
+ attr_reader :connection_set
12
+
13
+ def initialize(**settings)
14
+ @connection_set = ThreadConnections::ConnectionSet.new(ThreadConnections::Settings.new(**settings))
15
+ end
16
+
17
+ def settings
18
+ connection_set.settings
19
+ end
20
+
21
+ def with_connection(uri, connection_options, &)
22
+ connection_set.with_connection(uri, connection_options, &)
23
+ end
24
+
25
+ # Closes every thread's idle connections now and in-use ones when their request ends
26
+ def close
27
+ connection_set.close
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Purpose: objects that own connections and send requests, chosen per environment at boot
4
+ # rather than baked into a client class:
5
+ #
6
+ # ClientApiBuilder.use_transport(ClientApiBuilder::Transports::ThreadLocal.new) # every client
7
+ # ClientApiBuilder.use_transport(ClientApiBuilder::Transports::Pooled.new, GitHubClient) # one class
8
+ #
9
+ # A transport answers with_connection(uri, connection_options) { |http| ... }, yielding
10
+ # something that responds to request(net_request) { |response| } like a started Net::HTTP,
11
+ # and close, which closes its connections.
12
+ module ClientApiBuilder
13
+ module Transports
14
+ autoload :HTTP2, 'client_api_builder/transports/http2'
15
+ autoload :OneShot, 'client_api_builder/transports/one_shot'
16
+ autoload :Pooled, 'client_api_builder/transports/pooled'
17
+ autoload :Registry, 'client_api_builder/transports/registry'
18
+ autoload :ThreadLocal, 'client_api_builder/transports/thread_local'
19
+ end
20
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ClientApiBuilder
4
4
  # Gem version, bumped by release-please
5
- VERSION = '0.11.0'
5
+ VERSION = '1.1.0'
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: client-api-builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
@@ -15,14 +15,20 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 0.2.5
18
+ version: '1.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
26
  - - ">="
24
27
  - !ruby/object:Gem::Version
25
- version: 0.2.5
28
+ version: '1.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2'
26
32
  description: Client API Builder generates HTTP client methods from a declarative route
27
33
  DSL. It infers HTTP methods from route names, builds query strings and request bodies,
28
34
  and supports nested routers, configurable retries, and streaming responses to files
@@ -37,6 +43,7 @@ files:
37
43
  - CHANGELOG.md
38
44
  - LICENSE
39
45
  - README.md
46
+ - UPGRADING.md
40
47
  - lib/client-api-builder.rb
41
48
  - lib/client_api_builder/active_support_log_subscriber.rb
42
49
  - lib/client_api_builder/active_support_notifications.rb
@@ -45,6 +52,7 @@ files:
45
52
  - lib/client_api_builder/connection_pools/pool.rb
46
53
  - lib/client_api_builder/connection_pools/pool_set.rb
47
54
  - lib/client_api_builder/connection_pools/settings.rb
55
+ - lib/client_api_builder/declared_transport.rb
48
56
  - lib/client_api_builder/http2.rb
49
57
  - lib/client_api_builder/http2/connection.rb
50
58
  - lib/client_api_builder/http2/connection_set.rb
@@ -62,6 +70,12 @@ files:
62
70
  - lib/client_api_builder/thread_connections.rb
63
71
  - lib/client_api_builder/thread_connections/connection_set.rb
64
72
  - lib/client_api_builder/thread_connections/settings.rb
73
+ - lib/client_api_builder/transports.rb
74
+ - lib/client_api_builder/transports/http2.rb
75
+ - lib/client_api_builder/transports/one_shot.rb
76
+ - lib/client_api_builder/transports/pooled.rb
77
+ - lib/client_api_builder/transports/registry.rb
78
+ - lib/client_api_builder/transports/thread_local.rb
65
79
  - lib/client_api_builder/version.rb
66
80
  homepage: https://github.com/dougyouch/client-api-builder
67
81
  licenses:
@@ -78,7 +92,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
78
92
  requirements:
79
93
  - - ">="
80
94
  - !ruby/object:Gem::Version
81
- version: '3.2'
95
+ version: '3.3'
82
96
  required_rubygems_version: !ruby/object:Gem::Requirement
83
97
  requirements:
84
98
  - - ">="