client-api-builder 0.7.2 → 0.8.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: aac435317feb174db101e201b00464222bfc2df0f10601ac88e7897c3694be27
4
- data.tar.gz: '08ad14efc0bff6c1f6e7fd4864e26c6f41853d72808fdc84b0e4781a0cfd229f'
3
+ metadata.gz: 3e2d55d531b568e6f1cb0bb718ee236ca44eac7951c133ca6f2d294460968a80
4
+ data.tar.gz: d617dc3983f1cde402095e2be735c804f9a657969007a1f601779fe90f01c110
5
5
  SHA512:
6
- metadata.gz: e28726cc8c9670553371ba62a3c2ea4d30a4edb5db413936b91810ab1b592b2b84e6dd0fd3bcac8ad47fa2fb2c1b5d57a6c0443cace95210b9cf203a5e783d43
7
- data.tar.gz: ed7628816de37ff4e8f4a27f67520f277abac439cb6183eca0c98ce5a072a1bd8c0413a3db5bda145d75208b4272c598b89514ad80f6692a6cb300c255330b64
6
+ metadata.gz: 288273782ec2072f360e56b3653a80510684fd05772560db6544b438cc558c56a58d8de179cf87636a80dc396789aed431c63aee4681e90ad9bbc11325b294ba
7
+ data.tar.gz: 5a8622d04ae326e393c1b104a79ff8ed30f1f22420b6b3a7f4b1f30aa39b4e29e7929b625d8ff665276dd6ad45e2ee96e327afceb082b9a6c1d7eb227dde2229
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0](https://github.com/dougyouch/client-api-builder/compare/v0.7.2...v0.8.0) (2026-10-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * **section:** let sections inherit root client headers, query params and connection options ([8849d98](https://github.com/dougyouch/client-api-builder/commit/8849d9875c86633639bf33c5e9b1eb486f67e921))
9
+
3
10
  ## [0.7.2](https://github.com/dougyouch/client-api-builder/compare/v0.7.1...v0.7.2) (2026-10-03)
4
11
 
5
12
 
data/README.md CHANGED
@@ -296,9 +296,9 @@ class MyApiClient
296
296
  "Bearer #{auth_token}"
297
297
  end
298
298
 
299
- section :users do
299
+ # Use the root client's headers (Authorization) beneath the section's own
300
+ section :users, inherit: :headers do
300
301
  base_url 'https://api.example.com/v2' # Override base URL
301
- header 'Authorization', :authorization
302
302
 
303
303
  route :list, '/users'
304
304
  route :get, '/users/:id'
@@ -306,7 +306,7 @@ class MyApiClient
306
306
  end
307
307
 
308
308
  section :posts do
309
- header 'Authorization', :authorization
309
+ header 'Authorization', :authorization # or declare what it needs itself
310
310
 
311
311
  route :list, '/posts'
312
312
  route :get, '/posts/:id'
@@ -322,7 +322,17 @@ user = client.users.get(id: 123)
322
322
  posts = client.posts.list
323
323
  ```
324
324
 
325
- A section is its own router class. It uses the parent's `base_url` unless it sets one, but headers, query params, connection options, retries and builders are not inherited, so declare the ones it needs inside the section. 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.
325
+ A section is its own router class. It uses the parent's `base_url` unless it sets one, but by default nothing else is inherited: declare the headers, query params and connection options it needs inside the section, or opt into the root client's with `inherit:`:
326
+
327
+ ```ruby
328
+ section :users, inherit: %i[headers query_params connection_options] do
329
+ # or, inside the block: inherit_from_root :headers
330
+ end
331
+ ```
332
+
333
+ 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`.
334
+
335
+ 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.
326
336
 
327
337
  ### Connection Options
328
338
 
@@ -643,7 +653,7 @@ end
643
653
  | `connection_option(name, value)` | Set Net::HTTP connection options |
644
654
  | `configure_retries(max_attempts, sleep = 0.05)` | Configure retry behavior |
645
655
  | `route(name, path, options)` | Define an API endpoint |
646
- | `section(name, options, &block)` | Define nested routes |
656
+ | `section(name, options, &block)` | Define nested routes; `inherit:` opts into the root client's `:headers`, `:query_params` and/or `:connection_options` |
647
657
  | `namespace(path, &block)` | Add path prefix to routes in block |
648
658
 
649
659
  ### Instance Methods
@@ -8,6 +8,9 @@ module ClientApiBuilder
8
8
  class NestedRouter
9
9
  include ::ClientApiBuilder::Router
10
10
 
11
+ # Root client settings a section can opt into with inherit: or inherit_from_root
12
+ INHERITABLE_SETTINGS = %i[headers query_params connection_options].freeze
13
+
11
14
  attr_reader :root_router,
12
15
  :nested_router_options
13
16
 
@@ -20,6 +23,39 @@ module ClientApiBuilder
20
23
  "\#{escape_path(root_router.#{var})}"
21
24
  end
22
25
 
26
+ # The root client settings this section uses beneath its own; none by default
27
+ def self.inherited_root_settings
28
+ [].freeze
29
+ end
30
+
31
+ # Opts this section into the root client's class-level headers, query params and/or
32
+ # connection options. They are read per request and the section's own values take precedence.
33
+ def self.inherit_from_root(*settings)
34
+ settings = normalize_inherited_settings(settings)
35
+ redefine_class_method(:inherited_root_settings, (inherited_root_settings | settings).freeze)
36
+ end
37
+
38
+ def self.normalize_inherited_settings(settings)
39
+ settings = settings.flatten.map(&:to_sym)
40
+ unknown = settings - INHERITABLE_SETTINGS
41
+ return settings if unknown.empty?
42
+
43
+ raise ArgumentError, "Unknown inherit setting(s): #{unknown.map(&:inspect).join(', ')}. " \
44
+ "Allowed: #{INHERITABLE_SETTINGS.map(&:inspect).join(', ')}"
45
+ end
46
+
47
+ def configured_headers
48
+ inherits_from_root?(:headers) ? root_router.configured_headers.merge(super) : super
49
+ end
50
+
51
+ def configured_query_params
52
+ inherits_from_root?(:query_params) ? root_router.configured_query_params.merge(super) : super
53
+ end
54
+
55
+ def configured_connection_options
56
+ inherits_from_root?(:connection_options) ? root_router.configured_connection_options.merge(super) : super
57
+ end
58
+
23
59
  def base_url
24
60
  self.class.base_url || root_router.base_url
25
61
  end
@@ -27,5 +63,11 @@ module ClientApiBuilder
27
63
  def handle_response(response, options, &)
28
64
  root_router.handle_response(response, options, &)
29
65
  end
66
+
67
+ private
68
+
69
+ def inherits_from_root?(setting)
70
+ self.class.inherited_root_settings.include?(setting)
71
+ end
30
72
  end
31
73
  end
@@ -484,29 +484,41 @@ module ClientApiBuilder
484
484
  # Class-level headers may be method names or blocks; per-request headers are used as given.
485
485
  # Values are converted to strings, as Net::HTTP requires; nil values are left out of the request.
486
486
  def build_headers(options)
487
- headers = self.class.default_headers.transform_values { |value| resolve_config_value(value) }
487
+ headers = configured_headers
488
488
  headers.merge!(options[:headers]) if options[:headers]
489
489
  headers.transform_values { |value| value&.to_s }
490
490
  end
491
491
 
492
+ # Class-level headers with symbols and blocks resolved; sections may add the root client's
493
+ def configured_headers
494
+ self.class.default_headers.transform_values { |value| resolve_config_value(value) }
495
+ end
496
+
492
497
  def build_connection_options(options)
493
- if options[:connection_options]
494
- self.class.default_connection_options.merge(options[:connection_options])
495
- else
496
- self.class.default_connection_options
497
- end
498
+ connection_options = configured_connection_options
499
+ options[:connection_options] ? connection_options.merge(options[:connection_options]) : connection_options
500
+ end
501
+
502
+ # Class-level connection options; sections may add the root client's
503
+ def configured_connection_options
504
+ self.class.default_connection_options
498
505
  end
499
506
 
500
507
  # Class-level query params may be method names or blocks; values from route arguments
501
508
  # and per-request options are sent as given.
502
509
  def build_query(query, options)
503
- query_params = self.class.default_query_params.transform_values { |value| resolve_config_value(value) }
510
+ query_params = configured_query_params
504
511
  query_params.merge!(query) if query
505
512
  query_params.merge!(options[:query]) if options[:query]
506
513
 
507
514
  query_params.empty? ? nil : self.class.build_query(self, query_params)
508
515
  end
509
516
 
517
+ # Class-level query params with symbols and blocks resolved; sections may add the root client's
518
+ def configured_query_params
519
+ self.class.default_query_params.transform_values { |value| resolve_config_value(value) }
520
+ end
521
+
510
522
  # Resolves a class-level header or query_param value: a Symbol calls that method and a
511
523
  # Proc is evaluated, both on the root router; anything else is used as is.
512
524
  def resolve_config_value(value)
@@ -12,17 +12,23 @@ module ClientApiBuilder
12
12
 
13
13
  # Defines <name>_router (the section's NestedRouter class) and <name> (its router for a
14
14
  # client instance) with closures, so anonymous client classes and any option values work.
15
- def section(name, nested_router_options = {}, &)
15
+ # inherit: opts the section into root client settings (see NestedRouter.inherit_from_root);
16
+ # the remaining options are passed to the section as nested_router_options.
17
+ def section(name, nested_router_options = {}, &block)
16
18
  raise ArgumentError, "Invalid section name: #{name.inspect}" unless name.to_s.match?(SECTION_NAME)
17
19
 
20
+ nested_router_options = nested_router_options.dup
21
+ inherit = ::ClientApiBuilder::NestedRouter.normalize_inherited_settings(Array(nested_router_options.delete(:inherit)))
22
+
18
23
  kls = InheritanceHelper::ClassBuilder::Utils.create_class(
19
24
  self,
20
25
  name,
21
26
  ::ClientApiBuilder::NestedRouter,
22
27
  nil,
23
- 'NestedRouter',
24
- &
28
+ 'NestedRouter'
25
29
  )
30
+ kls.inherit_from_root(inherit)
31
+ kls.class_eval(&block) if block
26
32
 
27
33
  define_singleton_method(:"#{name}_router") { kls }
28
34
  define_section_accessor(name, nested_router_options)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ClientApiBuilder
4
4
  # Gem version, bumped by release-please
5
- VERSION = '0.7.2'
5
+ VERSION = '0.8.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.7.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch