client-api-builder 0.6.1 → 0.7.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: a417fa853b68dc6e9e96eff4cf99d6a0e026d459e38ae57cbec7341e4735e291
4
- data.tar.gz: b2cb35125eb9cdea344051788aee903daea7762c1f0bbbfc13526e1a8815692b
3
+ metadata.gz: 6ee8b61c4583f46d39c0d502192fb713f502ece28b7ea98092b73572efc9ffd4
4
+ data.tar.gz: bc80ea2afb78cdacc5067ebb45005a4bbcc997d13e79e9c6a9cf6b7edc8d5696
5
5
  SHA512:
6
- metadata.gz: 7fbc5052d79571ea1b901067bbaa3feb9a5e0e3c29ccebe1279905b917c121ecda94284f583c52badc5f00ef2c8c55fcba062563e1e8446730ea496659ac5ba3
7
- data.tar.gz: f2521e526d45195ff795ad3663c7f6bf8140bb8316a8c3b04f170fe215c0b185745536b152ea0879a896d60ea82cc33a7e7e2e0a9086892c1ee1c5c6a82125eb
6
+ metadata.gz: 4fd43d2172214d83d6b235096bd17f6289a07243dae96f3911b66c783d11caf1695ee541c2a0d2b547acf305616819c749bb32168ca62b5fa98880b042050925
7
+ data.tar.gz: e2d29d72f3c076be3b24d0941f317745ce367342cc96157f7c8d2a8592bc2dea0a733da1f4d9d5d40f46e1bf2859a01023ebb57c9ca31be4efca07e18267751e
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## [0.7.0](https://github.com/dougyouch/client-api-builder/compare/v0.6.1...v0.7.0) (2026-10-03)
4
+
5
+
6
+ ### Build System
7
+
8
+ * **release:** release 0.7.0 ([fff1354](https://github.com/dougyouch/client-api-builder/commit/fff13543d5d8189e2bb7a84eda7e8ca3181f3469))
9
+
10
+ ## Changelog
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Client API Builder
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/client-api-builder.svg)](https://badge.fury.io/rb/client-api-builder)
4
- [![CI](https://github.com/dougyouch/client-api-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/dougyouch/client-api-builder/actions/workflows/ci.yml)
5
- [![codecov](https://codecov.io/gh/dougyouch/client-api-builder/branch/master/graph/badge.svg)](https://codecov.io/gh/dougyouch/client-api-builder)
4
+ [![CI](https://github.com/dougyouch/client-api-builder/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/dougyouch/client-api-builder/actions/workflows/ci.yml)
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
 
7
7
  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.
8
8
 
@@ -544,7 +544,7 @@ end
544
544
 
545
545
  ## Requirements
546
546
 
547
- - Ruby 3.0+
547
+ - Ruby 3.2+
548
548
  - `inheritance-helper` gem (>= 0.2.5)
549
549
 
550
550
  ## Contributing
@@ -556,7 +556,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/dougyo
556
556
  3. Write tests for your changes
557
557
  4. Ensure all tests pass (`bundle exec rspec`)
558
558
  5. Ensure code style compliance (`bundle exec rubocop`)
559
- 6. Commit your changes (`git commit -am 'Add my feature'`)
559
+ 6. Commit your changes using [conventional commits](https://www.conventionalcommits.org/) (`git commit -am 'feat(router): add my feature'`); release notes and version bumps are generated from them
560
560
  7. Push to the branch (`git push origin feature/my-feature`)
561
561
  8. Create a Pull Request
562
562
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'client_api_builder/version'
4
+
3
5
  module ClientApiBuilder
4
6
  class Error < StandardError; end
5
7
 
@@ -51,7 +51,8 @@ module ClientApiBuilder
51
51
  end
52
52
 
53
53
  def stream(method:, uri:, body:, headers:, connection_options:)
54
- request(method: method, uri: uri, body: body, headers: headers, connection_options: connection_options) do |response|
54
+ request(method: method, uri: uri, body: body, headers: headers,
55
+ connection_options: connection_options) do |response|
55
56
  response.read_body do |chunk|
56
57
  yield response, chunk
57
58
  end
@@ -59,7 +60,8 @@ module ClientApiBuilder
59
60
  end
60
61
 
61
62
  def stream_to_io(method:, uri:, body:, headers:, connection_options:, io:)
62
- stream(method: method, uri: uri, body: body, headers: headers, connection_options: connection_options) do |_, chunk|
63
+ stream(method: method, uri: uri, body: body, headers: headers,
64
+ connection_options: connection_options) do |_, chunk|
63
65
  io.write chunk
64
66
  end
65
67
  end
@@ -75,12 +77,15 @@ module ClientApiBuilder
75
77
  elsif ALLOWED_FILE_MODES.include?(mode.to_s)
76
78
  mode.to_s
77
79
  else
78
- raise ArgumentError, "Invalid file mode: #{mode.inspect}. Allowed modes: #{ALLOWED_FILE_MODES.join(', ')}"
80
+ raise ArgumentError,
81
+ "Invalid file mode: #{mode.inspect}. Allowed modes: #{ALLOWED_FILE_MODES.join(', ')}"
79
82
  end
80
83
 
81
84
  # Validate file path - expand to absolute path and check for path traversal
82
85
  expanded_path = File.expand_path(file)
83
- raise ArgumentError, 'Invalid file path: potential path traversal detected' if file.to_s.include?('..') || expanded_path.include?("\0")
86
+ if file.to_s.include?('..') || expanded_path.include?("\0")
87
+ raise ArgumentError, 'Invalid file path: potential path traversal detected'
88
+ end
84
89
 
85
90
  File.open(expanded_path, mode) do |io|
86
91
  stream_to_io(method: method, uri: uri, body: body, headers: headers, connection_options: opts, io: io)
@@ -75,13 +75,14 @@ module ClientApiBuilder
75
75
  uri = URI.parse(url.to_s)
76
76
  return if ALLOWED_URL_SCHEMES.include?(uri.scheme&.downcase)
77
77
 
78
- raise ArgumentError, "Invalid base_url scheme: #{uri.scheme.inspect}. Allowed: #{ALLOWED_URL_SCHEMES.join(', ')}"
78
+ allowed = ALLOWED_URL_SCHEMES.join(', ')
79
+ raise ArgumentError, "Invalid base_url scheme: #{uri.scheme.inspect}. Allowed: #{allowed}"
79
80
  rescue URI::InvalidURIError => e
80
81
  raise ArgumentError, "Invalid base_url: #{e.message}"
81
82
  end
82
83
 
83
- # set the builder to :to_json, :to_query, :query_params or specify a proc to handle building the request body payload
84
- # or get the body builder
84
+ # set the builder to :to_json, :to_query, :query_params or specify a proc
85
+ # to handle building the request body payload, or get the body builder
85
86
  def body_builder(builder = nil, &block)
86
87
  return default_options[:body_builder] if builder.nil? && block.nil?
87
88
 
@@ -332,7 +333,7 @@ module ClientApiBuilder
332
333
  end
333
334
 
334
335
  def extract_expected_response_codes(options)
335
- codes = options[:expected_response_codes] || (options[:expected_response_code] ? [options[:expected_response_code]] : [])
336
+ codes = options[:expected_response_codes] || Array(options[:expected_response_code])
336
337
  codes.map(&:to_s)
337
338
  end
338
339
 
@@ -374,7 +375,9 @@ module ClientApiBuilder
374
375
 
375
376
  def generate_route_code(method_name, path, options = {})
376
377
  # Validate method_name to prevent code injection
377
- raise ArgumentError, "Invalid method name: #{method_name.inspect}" unless method_name.to_s.match?(/\A[a-z_][a-z0-9_]*\z/i)
378
+ unless method_name.to_s.match?(/\A[a-z_][a-z0-9_]*\z/i)
379
+ raise ArgumentError, "Invalid method name: #{method_name.inspect}"
380
+ end
378
381
 
379
382
  http_method = options[:method] || auto_detect_http_method(method_name)
380
383
  path, path_arguments = process_route_path(path)
@@ -410,7 +413,7 @@ module ClientApiBuilder
410
413
  code += " @request_options = {method: #{ctx[:http_method].inspect}, uri: __uri__, body: __body__, " \
411
414
  "headers: __headers__, connection_options: __connection_options__}\n"
412
415
  code += generate_request_call_code(ctx[:options], ctx[:stream_param])
413
- code + "end\n\n"
416
+ "#{code}end\n\n"
414
417
  end
415
418
 
416
419
  def generate_wrapper_method(ctx)
@@ -424,7 +427,7 @@ module ClientApiBuilder
424
427
  code += " expected_response_code!(@response, __expected_response_codes__, __options__)\n"
425
428
  code += generate_response_handling_code(ctx[:options])
426
429
  code += " end\n"
427
- code + "end\n"
430
+ "#{code}end\n"
428
431
  end
429
432
 
430
433
  def route(method_name, path, options = {}, &block)
@@ -577,7 +580,7 @@ module ClientApiBuilder
577
580
  end
578
581
  end
579
582
 
580
- def get_retry_request_sleep_time(_e, options)
583
+ def get_retry_request_sleep_time(_exception, options)
581
584
  options[:sleep] || self.class.default_options[:sleep] || 0.05
582
585
  end
583
586
 
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClientApiBuilder
4
+ # Gem version, bumped by release-please
5
+ VERSION = '0.7.0'
6
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: client-api-builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-02-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: inheritance-helper
@@ -23,33 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: 0.2.5
26
- description: |
27
- A Ruby gem for building API clients through declarative configuration. Features include
28
- automatic HTTP method detection, nested routing, streaming support, configurable retries,
29
- and security features like SSL verification, SSRF protection, and path traversal prevention.
30
- Define your API endpoints with a clean DSL and get comprehensive error handling, debugging
31
- capabilities, and optional ActiveSupport integration for logging and instrumentation.
26
+ description: Client API Builder generates HTTP client methods from a declarative route
27
+ DSL. It infers HTTP methods from route names, builds query strings and request bodies,
28
+ and supports nested routers, configurable retries, and streaming responses to files
29
+ or IO. SSL verification, base URL scheme checks, and path traversal protection are
30
+ on by default. Optional ActiveSupport integration adds instrumentation and request
31
+ logging.
32
32
  email: dougyouch@gmail.com
33
33
  executables: []
34
34
  extensions: []
35
35
  extra_rdoc_files: []
36
36
  files:
37
- - ".cursor.json"
38
- - ".github/workflows/ci.yml"
39
- - ".gitignore"
40
- - ".rubocop.yml"
41
- - ".ruby-gemset"
42
- - ".ruby-version"
43
- - ARCHITECTURE.md
44
- - CLAUDE.md
45
- - Gemfile
46
- - Gemfile.lock
37
+ - CHANGELOG.md
47
38
  - LICENSE
48
39
  - README.md
49
- - client-api-builder.gemspec
50
- - examples/basic_auth_example_client.rb
51
- - examples/imdb_datasets_client.rb
52
- - examples/lorem_ipsum_client.rb
53
40
  - lib/client-api-builder.rb
54
41
  - lib/client_api_builder/active_support_log_subscriber.rb
55
42
  - lib/client_api_builder/active_support_notifications.rb
@@ -58,13 +45,12 @@ files:
58
45
  - lib/client_api_builder/query_params.rb
59
46
  - lib/client_api_builder/router.rb
60
47
  - lib/client_api_builder/section.rb
61
- - script/console
48
+ - lib/client_api_builder/version.rb
62
49
  homepage: https://github.com/dougyouch/client-api-builder
63
50
  licenses:
64
51
  - MIT
65
52
  metadata:
66
53
  rubygems_mfa_required: 'true'
67
- homepage_uri: https://github.com/dougyouch/client-api-builder
68
54
  source_code_uri: https://github.com/dougyouch/client-api-builder
69
55
  changelog_uri: https://github.com/dougyouch/client-api-builder/blob/master/CHANGELOG.md
70
56
  bug_tracker_uri: https://github.com/dougyouch/client-api-builder/issues
@@ -75,14 +61,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
75
61
  requirements:
76
62
  - - ">="
77
63
  - !ruby/object:Gem::Version
78
- version: '3.0'
64
+ version: '3.2'
79
65
  required_rubygems_version: !ruby/object:Gem::Requirement
80
66
  requirements:
81
67
  - - ">="
82
68
  - !ruby/object:Gem::Version
83
69
  version: '0'
84
70
  requirements: []
85
- rubygems_version: 3.6.2
71
+ rubygems_version: 4.0.20
86
72
  specification_version: 4
87
- summary: Build robust, secure API clients through declarative configuration
73
+ summary: Build Ruby HTTP API clients from declarative route definitions
88
74
  test_files: []
data/.cursor.json DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "rules": [
3
- {
4
- "name": "Ruby spec file",
5
- "pattern": "^lib/(.+)\\.rb$",
6
- "target": "spec/${1}_spec.rb"
7
- },
8
- {
9
- "name": "Ruby implementation file",
10
- "pattern": "^spec/(.+)_spec\\.rb$",
11
- "target": "lib/${1}.rb"
12
- },
13
- {
14
- "name": "Related client_api_builder files",
15
- "pattern": "^(?:lib|spec)/client_api_builder/(.+)\\.rb$",
16
- "related": [
17
- "lib/client_api_builder/${1}.rb",
18
- "spec/client_api_builder/${1}_spec.rb"
19
- ]
20
- },
21
- {
22
- "name": "Main library file",
23
- "pattern": "^(?:lib|spec)/client_api_builder/.+\\.rb$",
24
- "related": [
25
- "lib/client-api-builder.rb"
26
- ]
27
- }
28
- ]
29
- }
@@ -1,53 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [master]
6
- pull_request:
7
- branches: [master]
8
-
9
- jobs:
10
- lint:
11
- name: RuboCop
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v4
15
-
16
- - name: Set up Ruby
17
- uses: ruby/setup-ruby@v1
18
- with:
19
- ruby-version: '3.4'
20
- bundler-cache: true
21
-
22
- - name: Run RuboCop
23
- run: bundle exec rubocop --format github
24
-
25
- test:
26
- name: Tests (Ruby ${{ matrix.ruby }})
27
- runs-on: ubuntu-latest
28
- strategy:
29
- fail-fast: false
30
- matrix:
31
- ruby: ['3.2', '3.3', '3.4']
32
-
33
- steps:
34
- - uses: actions/checkout@v4
35
-
36
- - name: Set up Ruby ${{ matrix.ruby }}
37
- uses: ruby/setup-ruby@v1
38
- with:
39
- ruby-version: ${{ matrix.ruby }}
40
- bundler-cache: true
41
-
42
- - name: Run tests
43
- run: bundle exec rspec
44
-
45
- - name: Upload coverage to Codecov
46
- if: matrix.ruby == '3.4'
47
- uses: codecov/codecov-action@v4
48
- with:
49
- files: coverage/coverage.xml
50
- fail_ci_if_error: false
51
- verbose: true
52
- env:
53
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
data/.gitignore DELETED
@@ -1,51 +0,0 @@
1
- # rcov generated
2
- coverage
3
- coverage.data
4
-
5
- # rdoc generated
6
- rdoc
7
-
8
- # yard generated
9
- doc
10
- .yardoc
11
-
12
- # bundler
13
- .bundle
14
-
15
- # jeweler generated
16
- pkg
17
-
18
- # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
19
- #
20
- # * Create a file at ~/.gitignore
21
- # * Include files you want ignored
22
- # * Run: git config --global core.excludesfile ~/.gitignore
23
- #
24
- # After doing this, these files will be ignored in all your git projects,
25
- # saving you from having to 'pollute' every project you touch with them
26
- #
27
- # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
28
- #
29
- # For MacOS:
30
- #
31
- .DS_Store
32
-
33
- # For TextMate
34
- #*.tmproj
35
- #tmtags
36
-
37
- # For emacs:
38
- *~
39
- \#*
40
- .\#*
41
-
42
- # For vim:
43
- *.swp
44
-
45
- # For redcar:
46
- #.redcar
47
-
48
- # For rubinius:
49
- #*.rbc
50
-
51
- *.gem
data/.rubocop.yml DELETED
@@ -1,79 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 3.2
3
- NewCops: enable
4
- SuggestExtensions: false
5
- Exclude:
6
- - 'bin/**/*'
7
- - 'vendor/**/*'
8
- - 'coverage/**/*'
9
-
10
- # Relaxed metrics for existing codebase
11
- Metrics/MethodLength:
12
- Max: 100
13
-
14
- Metrics/AbcSize:
15
- Max: 80
16
-
17
- Metrics/ClassLength:
18
- Max: 200
19
-
20
- Metrics/ModuleLength:
21
- Max: 340
22
-
23
- Metrics/CyclomaticComplexity:
24
- Max: 30
25
-
26
- Metrics/PerceivedComplexity:
27
- Max: 30
28
-
29
- Metrics/BlockLength:
30
- Exclude:
31
- - 'spec/**/*'
32
- - '*.gemspec'
33
-
34
- Metrics/ParameterLists:
35
- Max: 7
36
-
37
- # Style preferences
38
- Style/Documentation:
39
- Enabled: false
40
-
41
- Style/FrozenStringLiteralComment:
42
- EnforcedStyle: always
43
-
44
- Layout/LineLength:
45
- Max: 170
46
- Exclude:
47
- - 'spec/**/*'
48
-
49
- # File naming - allow hyphenated gem name
50
- Naming/FileName:
51
- Exclude:
52
- - 'lib/client-api-builder.rb'
53
-
54
- # Allow short parameter names for unused exception variables
55
- Naming/MethodParameterName:
56
- AllowedNames:
57
- - e
58
- - _e
59
- - io
60
-
61
- # Gemspec settings
62
- Gemspec/RequiredRubyVersion:
63
- Enabled: false
64
-
65
- # Style relaxations for existing code patterns
66
- Style/OptionalBooleanParameter:
67
- Enabled: false
68
-
69
- Style/StringConcatenation:
70
- Enabled: false
71
-
72
- Style/FormatStringToken:
73
- Enabled: false
74
-
75
- # Note: Previously had exclusions for Style/ClassVars, Style/PerlBackrefs, and
76
- # Lint/RescueException in router.rb - these have been fixed:
77
- # - ClassVars: Changed to thread-local storage
78
- # - PerlBackrefs: Changed to Regexp.last_match
79
- # - RescueException: Changed to StandardError
data/.ruby-gemset DELETED
@@ -1 +0,0 @@
1
- client-api-builder
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 3.4.2
data/ARCHITECTURE.md DELETED
@@ -1,223 +0,0 @@
1
- # Client API Builder Architecture
2
-
3
- This document describes the internal architecture and design of the Client API Builder gem.
4
-
5
- ## Overview
6
-
7
- Client API Builder is a Ruby gem that provides a declarative way to create API clients. It uses a modular architecture with several key components working together to provide a flexible and extensible API client framework.
8
-
9
- ## File Structure
10
-
11
- ```
12
- lib/
13
- ├── client-api-builder.rb # Main entry point, autoloads, error classes
14
- └── client_api_builder/
15
- ├── router.rb # Core Router module with route DSL
16
- ├── nested_router.rb # NestedRouter class for hierarchical APIs
17
- ├── section.rb # Section module for creating nested routers
18
- ├── net_http_request.rb # Net::HTTP request execution and streaming
19
- ├── query_params.rb # Custom query parameter builder
20
- ├── active_support_notifications.rb # ActiveSupport instrumentation
21
- └── active_support_log_subscriber.rb # ActiveSupport logging
22
- ```
23
-
24
- ## Core Components
25
-
26
- ### 1. Router Module (`ClientApiBuilder::Router`)
27
-
28
- The `Router` module is the core component that provides the main functionality for defining and executing API requests.
29
-
30
- **Class Methods** (defined in `ClassMethods`):
31
- - `base_url`: Sets the base URL for all requests
32
- - `header`: Adds headers to requests (supports values, symbols, or procs)
33
- - `route`: Defines API endpoints with dynamic method generation
34
- - `body_builder`: Configures request body formatting (`:to_json`, `:to_query`, `:query_params`, or custom)
35
- - `query_builder`: Configures query parameter formatting
36
- - `query_param`: Adds query parameters to all requests
37
- - `connection_option`: Sets Net::HTTP connection options
38
- - `configure_retries`: Sets retry behavior (max_retries, sleep time)
39
- - `namespace`: Groups routes under a common path prefix
40
-
41
- **Instance Methods**:
42
- - `build_headers`: Constructs request headers, evaluating procs/symbols
43
- - `build_connection_options`: Merges default and request-specific options
44
- - `build_query`: Formats query parameters using configured builder
45
- - `build_body`: Formats request body using configured builder
46
- - `build_uri`: Constructs full URI with base_url, path, and query
47
- - `handle_response`: Processes API responses, parses JSON by default
48
- - `request_wrapper`: Manages request execution with retry and instrumentation
49
- - `root_router`: Returns self (overridden in NestedRouter)
50
-
51
- **Instance Attributes** (via `attr_reader`):
52
- - `response`: The last Net::HTTP response object
53
- - `request_options`: Hash of method, uri, body, headers, connection_options
54
- - `total_request_time`: Duration of last request in seconds
55
- - `request_attempts`: Number of attempts for last request
56
-
57
- ### 2. Route Code Generation
58
-
59
- The `route` class method dynamically generates two methods per endpoint using `generate_route_code`:
60
-
61
- ```ruby
62
- route :get_user, '/users/:id', expected_response_code: 200
63
- ```
64
-
65
- Generates:
66
- - `get_user_raw_response(id:, **options, &block)` - Makes HTTP request, sets `@response` and `@request_options`
67
- - `get_user(id:, **options, &block)` - Wraps raw_response with retry logic, response code validation, and response handling
68
-
69
- **Path Parameters**: Extracted from `:param` or `{param}` syntax in path
70
- **Body/Query Parameters**: Extracted from `body:` and `query:` options using symbol values
71
-
72
- ### 3. HTTP Method Auto-Detection
73
-
74
- When `method:` is not specified in route options, `auto_detect_http_method` infers it from the method name:
75
-
76
- | Prefix Pattern | HTTP Method |
77
- |---------------|-------------|
78
- | `post`, `create`, `add`, `insert` | POST |
79
- | `put`, `update`, `modify`, `change` | PUT |
80
- | `patch` | PATCH |
81
- | `delete`, `remove` | DELETE |
82
- | (default) | GET |
83
-
84
- ### 4. Nested Router (`ClientApiBuilder::NestedRouter`)
85
-
86
- Enables hierarchical API client organization:
87
-
88
- ```ruby
89
- section :users do
90
- route :list, '/'
91
- route :get, '/:id'
92
- end
93
- # Usage: client.users.get(id: 123)
94
- ```
95
-
96
- Key behaviors:
97
- - Includes `ClientApiBuilder::Router` module
98
- - Stores `root_router` reference to access shared state
99
- - Stores `nested_router_options` passed from section definition
100
- - Overrides `base_url` to fall back to root_router's base_url
101
- - Delegates `handle_response` to root_router
102
- - Overrides `get_instance_method` to access root_router's instance variables in paths
103
-
104
- ### 5. Section Module (`ClientApiBuilder::Section`)
105
-
106
- Creates nested routers dynamically using `InheritanceHelper::ClassBuilder::Utils.create_class`:
107
-
108
- ```ruby
109
- def section(name, nested_router_options={}, &block)
110
- # Creates: MyClient::UsersNestedRouter < ClientApiBuilder::NestedRouter
111
- # Defines: MyClient.users_router (class method)
112
- # Defines: MyClient#users (instance method, memoized)
113
- end
114
- ```
115
-
116
- ### 6. NetHTTP::Request Module
117
-
118
- Provides HTTP request execution using Net::HTTP:
119
-
120
- **Methods**:
121
- - `request(method:, uri:, body:, headers:, connection_options:)` - Standard request with optional block
122
- - `stream(...)` - Streams response body in chunks via `read_body`
123
- - `stream_to_io(..., io:)` - Writes streamed chunks to an IO object
124
- - `stream_to_file(..., file:)` - Opens file and streams to it
125
-
126
- **Supported HTTP Methods** (via `METHOD_TO_NET_HTTP_CLASS`):
127
- `copy`, `delete`, `get`, `head`, `lock`, `mkcol`, `move`, `options`, `patch`, `post`, `propfind`, `proppatch`, `put`, `trace`, `unlock`
128
-
129
- ### 7. QueryParams Class
130
-
131
- Standalone query parameter builder (used when ActiveSupport unavailable):
132
-
133
- - Handles nested hashes with bracket notation: `user[name]=John`
134
- - Handles arrays: `ids[]=1&ids[]=2`
135
- - Configurable separators: `name_value_separator` (default `=`), `param_separator` (default `&`)
136
- - Supports custom escape proc
137
-
138
- ### 8. ActiveSupport Integration
139
-
140
- **ActiveSupportNotifications** (conditionally included when ActiveSupport defined):
141
- - Overrides `instrument_request` to use `ActiveSupport::Notifications.instrument`
142
- - Event name: `client_api_builder.request`
143
- - Payload includes `client: self`
144
-
145
- **ActiveSupportLogSubscriber**:
146
- - Subscribes to `client_api_builder.request` events for logging
147
-
148
- ## Design Patterns
149
-
150
- ### Module Inclusion Pattern
151
-
152
- ```ruby
153
- module ClientApiBuilder
154
- module Router
155
- def self.included(base)
156
- base.extend InheritanceHelper::Methods
157
- base.extend ClassMethods
158
- base.include ::ClientApiBuilder::Section
159
- base.include ::ClientApiBuilder::NetHTTP::Request
160
- base.include(::ClientApiBuilder::ActiveSupportNotifications) if defined?(ActiveSupport)
161
- base.send(:attr_reader, :response, :request_options, :total_request_time, :request_attempts)
162
- end
163
- end
164
- end
165
- ```
166
-
167
- ### Builder Pattern
168
-
169
- Request components built separately then combined:
170
- ```ruby
171
- __uri__ = build_uri(__path__, __query__, __options__)
172
- __body__ = build_body(__body__, __options__)
173
- __headers__ = build_headers(__options__)
174
- __connection_options__ = build_connection_options(__options__)
175
- ```
176
-
177
- ### Configuration Inheritance
178
-
179
- Uses `inheritance-helper` gem's `add_value_to_class_method` for configuration that properly inherits to subclasses:
180
- ```ruby
181
- def base_url(url = nil)
182
- return default_options[:base_url] unless url
183
- add_value_to_class_method(:default_options, base_url: url)
184
- end
185
- ```
186
-
187
- ## Configuration Hierarchy
188
-
189
- 1. **Default Options**: `Router.default_options` returns frozen hash with defaults
190
- 2. **Class-level Configuration**: Set through DSL methods, stored via `add_value_to_class_method`
191
- 3. **Instance-level**: Access class config, can override in method calls
192
- 4. **Request-level**: `**__options__` parameter on generated methods
193
-
194
- ## Error Handling
195
-
196
- - `ClientApiBuilder::Error`: Base error class
197
- - `ClientApiBuilder::UnexpectedResponse`: Raised when response code doesn't match expected codes
198
- - Stores `response` for inspection
199
- - Response procs: Per-route custom response handling stored in `default_options[:response_procs]`
200
- - Retry on exception: `retry_request?` method (always returns true by default, override to customize)
201
-
202
- ## Streaming Support
203
-
204
- Routes can specify streaming behavior:
205
-
206
- ```ruby
207
- route :download, '/file', stream: :file # stream_to_file, requires file: argument
208
- route :stream, '/events', stream: :io # stream_to_io, requires io: argument
209
- route :process, '/data', stream: :block # stream with block for each chunk
210
- route :download, '/file', stream: true # alias for :file
211
- ```
212
-
213
- ## Dependencies
214
-
215
- - `inheritance-helper`: Class inheritance and configuration management
216
- - `json`: JSON parsing and serialization (stdlib)
217
- - `net/http`: HTTP request handling (stdlib)
218
- - `cgi`: URL encoding in QueryParams (stdlib)
219
- - `active_support` (optional): Enhanced query building and instrumentation
220
-
221
- ## Thread Safety
222
-
223
- The library is not thread-safe. Each client instance maintains state (`@response`, `@request_options`, etc.) that would cause race conditions if shared across threads. Create separate client instances per thread.
data/CLAUDE.md DELETED
@@ -1,92 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- Client API Builder is a Ruby gem for creating API clients through declarative configuration. It uses Ruby's module inclusion pattern with `ClientApiBuilder::Router` as the core component.
8
-
9
- ## Common Commands
10
-
11
- ```bash
12
- # Install dependencies
13
- bundle install
14
-
15
- # Run all tests
16
- bundle exec rspec
17
-
18
- # Run a single test file
19
- bundle exec rspec spec/client_api_builder/router_spec.rb
20
-
21
- # Run a specific test by line number
22
- bundle exec rspec spec/client_api_builder/router_spec.rb:42
23
-
24
- # Run linter
25
- bundle exec rubocop
26
-
27
- # Build the gem
28
- gem build client-api-builder.gemspec
29
- ```
30
-
31
- ## Architecture
32
-
33
- ### Core Components
34
-
35
- - **Router** (`lib/client_api_builder/router.rb`): Main module providing `route`, `base_url`, `header`, `body_builder`, `query_builder`, and `configure_retries` class methods. Uses `InheritanceHelper::Methods` for configuration inheritance.
36
-
37
- - **NestedRouter** (`lib/client_api_builder/nested_router.rb`): Enables hierarchical API organization. Maintains reference to `root_router` and shares configuration with parent.
38
-
39
- - **Section** (`lib/client_api_builder/section.rb`): Provides `section` class method for creating nested route groups via dynamically generated classes.
40
-
41
- - **NetHTTP::Request** (`lib/client_api_builder/net_http_request.rb`): HTTP request execution using `Net::HTTP`. Handles standard requests and streaming (`:file`, `:io`, `:block` modes).
42
-
43
- - **QueryParams** (`lib/client_api_builder/query_params.rb`): Custom query parameter builder used when ActiveSupport's `to_query` is unavailable.
44
-
45
- - **ActiveSupportNotifications/LogSubscriber**: Optional integration for logging and instrumentation when ActiveSupport is present.
46
-
47
- ### Route Code Generation
48
-
49
- The `route` class method in Router uses `generate_route_code` to dynamically create two methods per route:
50
- 1. `method_name_raw_response` - Makes the HTTP request
51
- 2. `method_name` - Wraps the request with retry logic and response handling
52
-
53
- ### HTTP Method Auto-Detection
54
-
55
- Methods are auto-detected from route names: `post/create/add/insert` → POST, `put/update/modify/change` → PUT, `patch` → PATCH, `delete/remove` → DELETE, others → GET.
56
-
57
- ### Configuration Hierarchy
58
-
59
- 1. `default_options` class method (base defaults)
60
- 2. Class-level configuration via DSL methods
61
- 3. Instance-level overrides
62
- 4. Request-level options (`**__options__`)
63
-
64
- ## Key Patterns
65
-
66
- - Module inclusion with `self.included(base)` extending ClassMethods and including InstanceMethods
67
- - `add_value_to_class_method` from `inheritance-helper` for configuration inheritance
68
- - Response procs stored per method name for custom response handling
69
- - `root_router` method for accessing the top-level router from nested routers
70
-
71
- ## Dependencies
72
-
73
- - `inheritance-helper` (runtime): Class inheritance and method management
74
- - `webmock` (test): HTTP request stubbing
75
- - `activesupport` (optional): Enhanced query param building and instrumentation
76
-
77
- ## Code Commits
78
-
79
- Format using angular formatting:
80
- ```
81
- <type>(<scope>): <short summary>
82
- ```
83
- - **type**: build|ci|docs|feat|fix|perf|refactor|test
84
- - **scope**: The feature or component of the service we're working on
85
- - **summary**: Summary in present tense. Not capitalized. No period at the end.
86
-
87
- ## Documentation Maintenance
88
-
89
- When modifying the codebase, keep documentation in sync:
90
- - **ARCHITECTURE.md** - Update when adding/removing classes, changing component relationships, or altering data flow patterns
91
- - **README.md** - Update when adding new features, changing public APIs, or modifying usage examples
92
- - **Code comments** - Update inline documentation when changing method signatures or behavior
data/Gemfile DELETED
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'http://rubygems.org'
4
-
5
- gem 'inheritance-helper'
6
-
7
- group :development do
8
- gem 'activesupport'
9
- gem 'rake'
10
- gem 'rspec'
11
- gem 'rubocop'
12
- gem 'simplecov'
13
- gem 'simplecov-cobertura'
14
- gem 'webmock'
15
- end
data/Gemfile.lock DELETED
@@ -1,115 +0,0 @@
1
- GEM
2
- remote: http://rubygems.org/
3
- specs:
4
- activesupport (8.1.2)
5
- base64
6
- bigdecimal
7
- concurrent-ruby (~> 1.0, >= 1.3.1)
8
- connection_pool (>= 2.2.5)
9
- drb
10
- i18n (>= 1.6, < 2)
11
- json
12
- logger (>= 1.4.2)
13
- minitest (>= 5.1)
14
- securerandom (>= 0.3)
15
- tzinfo (~> 2.0, >= 2.0.5)
16
- uri (>= 0.13.1)
17
- addressable (2.8.8)
18
- public_suffix (>= 2.0.2, < 8.0)
19
- ast (2.4.3)
20
- base64 (0.3.0)
21
- bigdecimal (4.0.1)
22
- concurrent-ruby (1.3.6)
23
- connection_pool (3.0.2)
24
- crack (1.0.1)
25
- bigdecimal
26
- rexml
27
- diff-lcs (1.6.2)
28
- docile (1.4.1)
29
- drb (2.2.3)
30
- hashdiff (1.2.1)
31
- i18n (1.14.8)
32
- concurrent-ruby (~> 1.0)
33
- inheritance-helper (0.2.5)
34
- json (2.18.0)
35
- language_server-protocol (3.17.0.5)
36
- lint_roller (1.1.0)
37
- logger (1.7.0)
38
- minitest (6.0.1)
39
- prism (~> 1.5)
40
- parallel (1.27.0)
41
- parser (3.3.10.1)
42
- ast (~> 2.4.1)
43
- racc
44
- prism (1.9.0)
45
- public_suffix (7.0.2)
46
- racc (1.8.1)
47
- rainbow (3.1.1)
48
- rake (13.3.1)
49
- regexp_parser (2.11.3)
50
- rexml (3.4.4)
51
- rspec (3.13.2)
52
- rspec-core (~> 3.13.0)
53
- rspec-expectations (~> 3.13.0)
54
- rspec-mocks (~> 3.13.0)
55
- rspec-core (3.13.6)
56
- rspec-support (~> 3.13.0)
57
- rspec-expectations (3.13.5)
58
- diff-lcs (>= 1.2.0, < 2.0)
59
- rspec-support (~> 3.13.0)
60
- rspec-mocks (3.13.7)
61
- diff-lcs (>= 1.2.0, < 2.0)
62
- rspec-support (~> 3.13.0)
63
- rspec-support (3.13.7)
64
- rubocop (1.84.0)
65
- json (~> 2.3)
66
- language_server-protocol (~> 3.17.0.2)
67
- lint_roller (~> 1.1.0)
68
- parallel (~> 1.10)
69
- parser (>= 3.3.0.2)
70
- rainbow (>= 2.2.2, < 4.0)
71
- regexp_parser (>= 2.9.3, < 3.0)
72
- rubocop-ast (>= 1.49.0, < 2.0)
73
- ruby-progressbar (~> 1.7)
74
- unicode-display_width (>= 2.4.0, < 4.0)
75
- rubocop-ast (1.49.0)
76
- parser (>= 3.3.7.2)
77
- prism (~> 1.7)
78
- ruby-progressbar (1.13.0)
79
- securerandom (0.4.1)
80
- simplecov (0.22.0)
81
- docile (~> 1.1)
82
- simplecov-html (~> 0.11)
83
- simplecov_json_formatter (~> 0.1)
84
- simplecov-cobertura (3.1.0)
85
- rexml
86
- simplecov (~> 0.19)
87
- simplecov-html (0.13.2)
88
- simplecov_json_formatter (0.1.4)
89
- tzinfo (2.0.6)
90
- concurrent-ruby (~> 1.0)
91
- unicode-display_width (3.2.0)
92
- unicode-emoji (~> 4.1)
93
- unicode-emoji (4.2.0)
94
- uri (1.1.1)
95
- webmock (3.26.1)
96
- addressable (>= 2.8.0)
97
- crack (>= 0.3.2)
98
- hashdiff (>= 0.4.0, < 2.0.0)
99
-
100
- PLATFORMS
101
- arm64-darwin-24
102
- ruby
103
-
104
- DEPENDENCIES
105
- activesupport
106
- inheritance-helper
107
- rake
108
- rspec
109
- rubocop
110
- simplecov
111
- simplecov-cobertura
112
- webmock
113
-
114
- BUNDLED WITH
115
- 2.6.2
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- Gem::Specification.new do |s|
4
- s.name = 'client-api-builder'
5
- s.version = '0.6.1'
6
- s.licenses = ['MIT']
7
- s.summary = 'Build robust, secure API clients through declarative configuration'
8
- s.description = <<~DESC
9
- A Ruby gem for building API clients through declarative configuration. Features include
10
- automatic HTTP method detection, nested routing, streaming support, configurable retries,
11
- and security features like SSL verification, SSRF protection, and path traversal prevention.
12
- Define your API endpoints with a clean DSL and get comprehensive error handling, debugging
13
- capabilities, and optional ActiveSupport integration for logging and instrumentation.
14
- DESC
15
- s.authors = ['Doug Youch']
16
- s.email = 'dougyouch@gmail.com'
17
- s.homepage = 'https://github.com/dougyouch/client-api-builder'
18
- s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
-
20
- s.required_ruby_version = '>= 3.0'
21
-
22
- s.add_dependency 'inheritance-helper', '>= 0.2.5'
23
-
24
- s.metadata = {
25
- 'rubygems_mfa_required' => 'true',
26
- 'homepage_uri' => s.homepage,
27
- 'source_code_uri' => 'https://github.com/dougyouch/client-api-builder',
28
- 'changelog_uri' => 'https://github.com/dougyouch/client-api-builder/blob/master/CHANGELOG.md',
29
- 'bug_tracker_uri' => 'https://github.com/dougyouch/client-api-builder/issues'
30
- }
31
- end
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'base64'
4
- require 'securerandom'
5
-
6
- BasicAuthExampleClient = Struct.new(
7
- :username,
8
- :password
9
- ) do
10
- include ClientApiBuilder::Router
11
- include ClientApiBuilder::Section
12
-
13
- base_url 'https://www.example.com'
14
-
15
- configure_retries(2)
16
-
17
- header 'Authorization', :basic_authorization
18
- query_param('cache_buster') { (Time.now.to_f * 1000).to_i }
19
-
20
- route :get_apps, '/apps'
21
- route :get_app, '/apps/:app_id'
22
-
23
- section :users do
24
- header 'Authorization', :bearer_authorization
25
-
26
- route :create_user, '/users?z={cache_buster}'
27
- end
28
-
29
- def cache_buster
30
- (Time.now.to_f * 1000).to_i
31
- end
32
-
33
- private
34
-
35
- def auth_token
36
- @auth_token ||= SecureRandom.uuid
37
- end
38
-
39
- def basic_authorization
40
- 'basic ' + Base64.strict_encode64(username + ':' + password)
41
- end
42
-
43
- def bearer_authorization
44
- 'bearer ' + auth_token
45
- end
46
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class IMDBDatesetsClient
4
- include ClientApiBuilder::Router
5
-
6
- base_url 'https://datasets.imdbws.com'
7
-
8
- route :get_name_basics, '/name.basics.tsv.gz', stream: :file
9
- route :get_title_akas, '/title.akas.tsv.gz', stream: :io
10
- route :get_title_basics, '/title.basics.tsv.gz', stream: :block
11
-
12
- def self.stream_to_file
13
- new.get_name_basics(file: 'name.basics.tsv.gz')
14
- end
15
-
16
- def self.stream_to_io
17
- File.open('title.akas.tsv.gz', 'wb') do |io|
18
- new.get_title_akas(io: io)
19
- end
20
- end
21
-
22
- def self.stream_with_block
23
- File.open('title.basics.tsv.gz', 'wb') do |io|
24
- total_read = 0.0
25
- new.get_title_basics do |response, chunk|
26
- total_read += chunk.bytesize
27
- percentage_complete = ((total_read / response.content_length) * 100).to_i
28
- puts "downloading title.basics.tsv.gz completed: #{percentage_complete}%"
29
- io.write chunk
30
- end
31
- end
32
- end
33
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class LoremIpsumClient
4
- include ClientApiBuilder::Router
5
-
6
- # by default it converts the body data to JSON
7
- # to convert the body to query params (x=1&y=2) use the following
8
- # if using active support change this to :to_query
9
- body_builder :query_params
10
-
11
- base_url 'https://www.lipsum.com'
12
-
13
- header 'Content-Type', 'application/x-www-form-urlencoded'
14
- header 'Accept', 'application/json'
15
-
16
- # this creates a method called create_lorem_ipsum with 2 named arguments amont and what
17
- route :create_lorem_ipsum, '/feed/json', body: { amount: :amount, what: :what, start: 'yes', generate: 'Generate Lorem Ipsum' }
18
- end
data/script/console DELETED
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- $LOAD_PATH << File.expand_path('../lib', __dir__)
5
- $LOAD_PATH << File.expand_path('../examples', __dir__)
6
- require 'client-api-builder'
7
- autoload :BasicAuthExampleClient, 'basic_auth_example_client'
8
- autoload :IMDBDatesetsClient, 'imdb_datasets_client'
9
- autoload :LoremIpsumClient, 'lorem_ipsum_client'
10
- require 'logger'
11
- LOG = Logger.new($stdout)
12
- ClientApiBuilder.logger = LOG
13
- ClientApiBuilder::ActiveSupportLogSubscriber.new(LOG).subscribe!
14
- require 'irb'
15
- IRB.start(__FILE__)