client-api-builder 0.6.1 → 0.7.1
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 +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +129 -34
- data/lib/client-api-builder.rb +2 -0
- data/lib/client_api_builder/nested_router.rb +1 -1
- data/lib/client_api_builder/net_http_request.rb +9 -4
- data/lib/client_api_builder/router.rb +17 -10
- data/lib/client_api_builder/version.rb +6 -0
- metadata +13 -27
- data/.cursor.json +0 -29
- data/.github/workflows/ci.yml +0 -53
- data/.gitignore +0 -51
- data/.rubocop.yml +0 -79
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/ARCHITECTURE.md +0 -223
- data/CLAUDE.md +0 -92
- data/Gemfile +0 -15
- data/Gemfile.lock +0 -115
- data/client-api-builder.gemspec +0 -31
- data/examples/basic_auth_example_client.rb +0 -46
- data/examples/imdb_datasets_client.rb +0 -33
- data/examples/lorem_ipsum_client.rb +0 -18
- data/script/console +0 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00351abdc1f660fefd19687f75b696db8a1b9b16390ff6427706a8f5db64ee74
|
|
4
|
+
data.tar.gz: 34dac63d4090971accab88566f4c217ccdb5a1d21ba000281ad5e2ea68bd74cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b34e2623702b08c9ac8156e8f0fe9da060636823492ec49bc34832e4e5bff09bb8ab4b3244cbba38bc49ec1a01a80ae30a484c8ec85fb8ae7a032131156278f5
|
|
7
|
+
data.tar.gz: 4e164e2afd778cd2a6c2adb171c0ee6afec71eef925de9b61fe9b8c1b1d94a5faf2bd8fa624f6f691c4b7268d550e2e58dc82ef0c9b40470fe687026a5cef90f
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.7.1](https://github.com/dougyouch/client-api-builder/compare/v0.7.0...v0.7.1) (2026-10-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **router:** detect destroy_ routes as DELETE ([85604c4](https://github.com/dougyouch/client-api-builder/commit/85604c4d974c8483e292e700eb51a2c78e312b54))
|
|
9
|
+
* **router:** url-encode path values ([76d6fb4](https://github.com/dougyouch/client-api-builder/commit/76d6fb4fa246b342e263322d06262cba6760c278))
|
|
10
|
+
|
|
11
|
+
### Upgrade Notes
|
|
12
|
+
|
|
13
|
+
* Path values are now percent-encoded by `escape_path`, including `/`. A value like `'a/b'` that previously expanded into two path segments is now sent as one segment (`a%2Fb`). To keep `/` as a separator, override `escape_path` in your client, e.g. `value.to_s.split('/').map { |part| ERB::Util.url_encode(part) }.join('/')`.
|
|
14
|
+
|
|
15
|
+
## [0.7.0](https://github.com/dougyouch/client-api-builder/compare/v0.6.1...v0.7.0) (2026-10-03)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### ⚠ BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* client-api-builder now requires Ruby 3.2 or newer (previously 3.0). Ruby 3.0 and 3.1 are end-of-life. ([7c951e0](https://github.com/dougyouch/client-api-builder/commit/7c951e0))
|
|
21
|
+
|
|
22
|
+
### Packaging
|
|
23
|
+
|
|
24
|
+
* ship only `lib/`, `README.md`, `LICENSE` and `CHANGELOG.md` in the gem instead of every tracked file ([ca141a1](https://github.com/dougyouch/client-api-builder/commit/ca141a1))
|
|
25
|
+
* refresh the gem summary and description ([ca141a1](https://github.com/dougyouch/client-api-builder/commit/ca141a1))
|
|
26
|
+
* add `ClientApiBuilder::VERSION` ([7c951e0](https://github.com/dougyouch/client-api-builder/commit/7c951e0))
|
|
27
|
+
|
|
28
|
+
### Build
|
|
29
|
+
|
|
30
|
+
* automate versioning, changelog and RubyGems publishing with release-please ([7c951e0](https://github.com/dougyouch/client-api-builder/commit/7c951e0))
|
|
31
|
+
* replace Codecov with a GitHub-hosted coverage badge ([7c951e0](https://github.com/dougyouch/client-api-builder/commit/7c951e0))
|
|
32
|
+
* adopt the dynamic-active-model RuboCop config with rubocop-rspec ([f36ed6f](https://github.com/dougyouch/client-api-builder/commit/f36ed6f))
|
data/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Client API Builder
|
|
2
2
|
|
|
3
|
-
[](https://github.com/dougyouch/client-api-builder/actions/workflows/ci.yml)
|
|
5
|
-
[](https://rubygems.org/gems/client-api-builder)
|
|
4
|
+
[](https://github.com/dougyouch/client-api-builder/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/dougyouch/client-api-builder/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/dougyouch/client-api-builder/actions/workflows/ci.yml)
|
|
6
7
|
|
|
7
8
|
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
9
|
|
|
@@ -93,25 +94,54 @@ route :method_name, '/path/:param', options
|
|
|
93
94
|
|
|
94
95
|
| Option | Description |
|
|
95
96
|
|--------|-------------|
|
|
96
|
-
| `method:` | HTTP method
|
|
97
|
+
| `method:` | HTTP method. Auto-detected from the route name if omitted. Any of `:get`, `:post`, `:put`, `:patch`, `:delete`, `:head`, `:options`, `:trace`, `:copy`, `:lock`, `:unlock`, `:mkcol`, `:move`, `:propfind`, `:proppatch`. |
|
|
97
98
|
| `query:` | Hash defining query parameters. Use symbols for dynamic values. |
|
|
98
|
-
| `body:` |
|
|
99
|
-
| `
|
|
99
|
+
| `body:` | Request body: a Hash or Array (symbols become arguments) or a literal String. |
|
|
100
|
+
| `no_body:` | `true` to send no body, even for POST/PUT/PATCH. |
|
|
101
|
+
| `has_body:` | `true` to add a `body:` argument to any method, e.g. a GET with a body. |
|
|
102
|
+
| `expected_response_code:` | Single expected HTTP status code. Without one, any 2xx response is accepted. |
|
|
100
103
|
| `expected_response_codes:` | Array of expected HTTP status codes |
|
|
101
104
|
| `stream:` | Enable streaming (`:file`, `:io`, `:block`, or `true`) |
|
|
102
105
|
| `return:` | Return type (`:response`, `:body`, or parsed JSON by default) |
|
|
103
106
|
|
|
107
|
+
POST, PUT and PATCH routes without a `body:` option take the request body as a `body:` argument:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
route :create_user, '/users'
|
|
111
|
+
|
|
112
|
+
client.create_user(body: { name: 'Ann' })
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Per-Request Options
|
|
116
|
+
|
|
117
|
+
Every generated method also accepts options that apply to that call only:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
client.get_user(
|
|
121
|
+
id: 1,
|
|
122
|
+
headers: { 'X-Trace-Id' => 'abc' }, # merged over the class headers
|
|
123
|
+
query: { expand: 'teams' }, # merged over the route's query params
|
|
124
|
+
body: { name: 'Ann' }, # replaces the route's body
|
|
125
|
+
connection_options: { read_timeout: 5 },
|
|
126
|
+
retries: 3, # attempts for this call
|
|
127
|
+
sleep: 0.5, # seconds between attempts
|
|
128
|
+
return: :body # :body or :response instead of parsed JSON
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
104
132
|
### Automatic HTTP Method Detection
|
|
105
133
|
|
|
106
|
-
The Router
|
|
134
|
+
The Router detects the HTTP method from how the route name starts:
|
|
107
135
|
|
|
108
|
-
|
|
|
109
|
-
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
|
|
|
136
|
+
| Name starts with | HTTP Method |
|
|
137
|
+
|------------------|-------------|
|
|
138
|
+
| `post`, `create`, `add`, `insert` | POST |
|
|
139
|
+
| `put`, `update`, `modify`, `change` | PUT |
|
|
140
|
+
| `patch` | PATCH |
|
|
141
|
+
| `delete`, `remove`, `destroy` | DELETE |
|
|
142
|
+
| anything else | GET |
|
|
143
|
+
|
|
144
|
+
The match is on the start of the name only, so `address_lookup` is a POST. Pass `method:` when the name doesn't say it.
|
|
115
145
|
|
|
116
146
|
```ruby
|
|
117
147
|
class MyApiClient
|
|
@@ -132,11 +162,31 @@ end
|
|
|
132
162
|
|
|
133
163
|
Parameters can be defined in three ways:
|
|
134
164
|
|
|
135
|
-
**1. Path Parameters** (using `:param`
|
|
165
|
+
**1. Path Parameters** (using `:param` syntax):
|
|
136
166
|
|
|
137
167
|
```ruby
|
|
138
168
|
route :get_user, '/users/:id'
|
|
139
|
-
|
|
169
|
+
# client.get_user(id: 1)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`{name}` is filled from the client's own `name` method rather than an argument, which suits values like account IDs that are set once:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
attr_accessor :account_id
|
|
176
|
+
|
|
177
|
+
route :get_invoices, '/accounts/{account_id}/invoices'
|
|
178
|
+
# client.get_invoices
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The same `'{name}'` form works as a value inside `query:` and `body:`.
|
|
182
|
+
|
|
183
|
+
Path values, from arguments and `{name}` alike, are percent-encoded so each stays a single segment: `get_file(name: 'a/b c')` requests `/files/a%2Fb%20c`. Only RFC 3986 unreserved characters (`A-Z a-z 0-9 - . _ ~`) are left as is. To change this, override `escape_path`:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
# Allow '/' in values, e.g. for nested object keys
|
|
187
|
+
def escape_path(value)
|
|
188
|
+
value.to_s.split('/').map { |part| ERB::Util.url_encode(part) }.join('/')
|
|
189
|
+
end
|
|
140
190
|
```
|
|
141
191
|
|
|
142
192
|
**2. Query Parameters:**
|
|
@@ -193,7 +243,7 @@ class MyApiClient
|
|
|
193
243
|
# Default: JSON (using to_json)
|
|
194
244
|
body_builder :to_json
|
|
195
245
|
|
|
196
|
-
# URL-encoded form data (using to_query)
|
|
246
|
+
# URL-encoded form data (using to_query, requires ActiveSupport)
|
|
197
247
|
body_builder :to_query
|
|
198
248
|
|
|
199
249
|
# Custom query params builder (no ActiveSupport dependency)
|
|
@@ -213,6 +263,8 @@ class MyApiClient
|
|
|
213
263
|
end
|
|
214
264
|
```
|
|
215
265
|
|
|
266
|
+
String bodies are sent as is. Query strings are built the same way with `query_builder`, which accepts `:to_query`, `:query_params`, a method name or a block. It defaults to `:to_query` when ActiveSupport is loaded and to `:query_params` otherwise.
|
|
267
|
+
|
|
216
268
|
### Nested Routing (Sections)
|
|
217
269
|
|
|
218
270
|
Organize complex APIs with nested routes:
|
|
@@ -222,12 +274,17 @@ class MyApiClient
|
|
|
222
274
|
include ClientApiBuilder::Router
|
|
223
275
|
|
|
224
276
|
base_url 'https://api.example.com'
|
|
225
|
-
header 'Authorization', :
|
|
277
|
+
header 'Authorization', :authorization
|
|
226
278
|
|
|
227
279
|
attr_accessor :auth_token
|
|
228
280
|
|
|
281
|
+
def authorization
|
|
282
|
+
"Bearer #{auth_token}"
|
|
283
|
+
end
|
|
284
|
+
|
|
229
285
|
section :users do
|
|
230
286
|
base_url 'https://api.example.com/v2' # Override base URL
|
|
287
|
+
header 'Authorization', :authorization
|
|
231
288
|
|
|
232
289
|
route :list, '/users'
|
|
233
290
|
route :get, '/users/:id'
|
|
@@ -235,6 +292,8 @@ class MyApiClient
|
|
|
235
292
|
end
|
|
236
293
|
|
|
237
294
|
section :posts do
|
|
295
|
+
header 'Authorization', :authorization
|
|
296
|
+
|
|
238
297
|
route :list, '/posts'
|
|
239
298
|
route :get, '/posts/:id'
|
|
240
299
|
end
|
|
@@ -249,6 +308,8 @@ user = client.users.get(id: 123)
|
|
|
249
308
|
posts = client.posts.list
|
|
250
309
|
```
|
|
251
310
|
|
|
311
|
+
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, `{name}` path values, and response blocks are evaluated on the root client, so they can use its methods and state.
|
|
312
|
+
|
|
252
313
|
### Connection Options
|
|
253
314
|
|
|
254
315
|
Configure connection settings:
|
|
@@ -268,6 +329,8 @@ class MyApiClient
|
|
|
268
329
|
end
|
|
269
330
|
```
|
|
270
331
|
|
|
332
|
+
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`.
|
|
333
|
+
|
|
271
334
|
### Retry Configuration
|
|
272
335
|
|
|
273
336
|
Configure automatic retries for transient failures:
|
|
@@ -278,12 +341,14 @@ class MyApiClient
|
|
|
278
341
|
|
|
279
342
|
base_url 'https://api.example.com'
|
|
280
343
|
|
|
281
|
-
#
|
|
344
|
+
# Make up to 3 attempts in total, waiting 0.5 seconds between them
|
|
282
345
|
configure_retries 3, 0.5
|
|
283
346
|
end
|
|
284
347
|
```
|
|
285
348
|
|
|
286
|
-
|
|
349
|
+
The first argument is the total number of attempts, not extra retries. The default is 1, so requests are not retried unless you configure it. `retries:` and `sleep:` can also be passed per request.
|
|
350
|
+
|
|
351
|
+
Only these network errors are retried by default:
|
|
287
352
|
- `Net::OpenTimeout`, `Net::ReadTimeout`
|
|
288
353
|
- `Errno::ECONNRESET`, `Errno::ECONNREFUSED`, `Errno::ETIMEDOUT`
|
|
289
354
|
- `SocketError`, `EOFError`
|
|
@@ -345,6 +410,8 @@ client.process_stream do |response, chunk|
|
|
|
345
410
|
end
|
|
346
411
|
```
|
|
347
412
|
|
|
413
|
+
Streaming routes return the `Net::HTTPResponse`. Files are written in `wb` mode by default; pass `connection_options: { file_mode: 'ab' }` to append.
|
|
414
|
+
|
|
348
415
|
### Response Handling
|
|
349
416
|
|
|
350
417
|
Customize how responses are processed:
|
|
@@ -370,11 +437,16 @@ class MyApiClient
|
|
|
370
437
|
data
|
|
371
438
|
end
|
|
372
439
|
end
|
|
440
|
+
|
|
441
|
+
# A block passed to the call replaces the route's block
|
|
442
|
+
client.get_user(id: 1) { |data| data['name'] }
|
|
373
443
|
```
|
|
374
444
|
|
|
445
|
+
Blocks run on the client, so they can call its methods and set its state. Empty response bodies return `nil`.
|
|
446
|
+
|
|
375
447
|
### Error Handling
|
|
376
448
|
|
|
377
|
-
|
|
449
|
+
`ClientApiBuilder::UnexpectedResponse` is raised when the status code isn't expected (any non-2xx by default, or anything outside `expected_response_code(s)`) and when a response body isn't valid JSON. It carries the response:
|
|
378
450
|
|
|
379
451
|
```ruby
|
|
380
452
|
begin
|
|
@@ -412,12 +484,9 @@ puts client.request_attempts # Number of attempts (including retries)
|
|
|
412
484
|
|
|
413
485
|
### ActiveSupport Integration
|
|
414
486
|
|
|
415
|
-
When ActiveSupport is
|
|
487
|
+
When ActiveSupport is loaded before your client class includes `ClientApiBuilder::Router`, every request is instrumented as a `client_api_builder.request` event:
|
|
416
488
|
|
|
417
489
|
```ruby
|
|
418
|
-
# Set up logging
|
|
419
|
-
ClientApiBuilder.logger = Logger.new(STDOUT)
|
|
420
|
-
|
|
421
490
|
# Subscribe to request events
|
|
422
491
|
ActiveSupport::Notifications.subscribe('client_api_builder.request') do |*args|
|
|
423
492
|
event = ActiveSupport::Notifications::Event.new(*args)
|
|
@@ -433,9 +502,15 @@ subscriber = ClientApiBuilder::ActiveSupportLogSubscriber.new(Rails.logger)
|
|
|
433
502
|
subscriber.subscribe!
|
|
434
503
|
```
|
|
435
504
|
|
|
505
|
+
Separately, `ClientApiBuilder.logger` receives every exception raised during a request attempt, including ones that are retried:
|
|
506
|
+
|
|
507
|
+
```ruby
|
|
508
|
+
ClientApiBuilder.logger = Logger.new($stdout)
|
|
509
|
+
```
|
|
510
|
+
|
|
436
511
|
#### Production Logging
|
|
437
512
|
|
|
438
|
-
|
|
513
|
+
The built-in log subscriber already leaves out query strings, which may hold credentials. To customize the format, subscribe directly:
|
|
439
514
|
|
|
440
515
|
```ruby
|
|
441
516
|
ActiveSupport::Notifications.subscribe('client_api_builder.request') do |_, start_time, end_time, _, payload|
|
|
@@ -461,7 +536,7 @@ Client API Builder includes several security features enabled by default:
|
|
|
461
536
|
|
|
462
537
|
### SSL/TLS Verification
|
|
463
538
|
|
|
464
|
-
|
|
539
|
+
HTTPS connections verify SSL certificates using `OpenSSL::SSL::VERIFY_PEER` and default to a 30 second open timeout and 60 second read timeout. Plain HTTP connections use Net::HTTP's own defaults.
|
|
465
540
|
|
|
466
541
|
### SSRF Protection
|
|
467
542
|
|
|
@@ -478,9 +553,13 @@ class MyApiClient
|
|
|
478
553
|
end
|
|
479
554
|
```
|
|
480
555
|
|
|
556
|
+
### Path Value Encoding
|
|
557
|
+
|
|
558
|
+
Values inserted into a route's path are percent-encoded, so input such as `../admin` or `a/b?x=1` can't add path segments or a query string to the request.
|
|
559
|
+
|
|
481
560
|
### Path Traversal Protection
|
|
482
561
|
|
|
483
|
-
File streaming
|
|
562
|
+
File streaming rejects any path containing `..` or a null byte:
|
|
484
563
|
|
|
485
564
|
```ruby
|
|
486
565
|
# These will raise ArgumentError
|
|
@@ -522,12 +601,12 @@ end
|
|
|
522
601
|
| Method | Description |
|
|
523
602
|
|--------|-------------|
|
|
524
603
|
| `base_url(url)` | Set the base URL for all requests |
|
|
525
|
-
| `header(name, value)` | Add a header to all requests |
|
|
604
|
+
| `header(name, value = nil, &block)` | Add a header to all requests (value, method name symbol, or block) |
|
|
526
605
|
| `body_builder(builder)` | Configure request body serialization |
|
|
527
606
|
| `query_builder(builder)` | Configure query string serialization |
|
|
528
|
-
| `query_param(name, value)` | Add a query parameter to all requests |
|
|
607
|
+
| `query_param(name, value = nil, &block)` | Add a query parameter to all requests (value, method name symbol, or block) |
|
|
529
608
|
| `connection_option(name, value)` | Set Net::HTTP connection options |
|
|
530
|
-
| `configure_retries(
|
|
609
|
+
| `configure_retries(max_attempts, sleep = 0.05)` | Configure retry behavior |
|
|
531
610
|
| `route(name, path, options)` | Define an API endpoint |
|
|
532
611
|
| `section(name, options, &block)` | Define nested routes |
|
|
533
612
|
| `namespace(path, &block)` | Add path prefix to routes in block |
|
|
@@ -541,11 +620,27 @@ end
|
|
|
541
620
|
| `total_request_time` | Duration of last request in seconds |
|
|
542
621
|
| `request_attempts` | Number of attempts for last request |
|
|
543
622
|
| `root_router` | Returns the root router (for nested routers) |
|
|
623
|
+
| `base_url` | Base URL used for requests |
|
|
624
|
+
|
|
625
|
+
### Overridable Hooks
|
|
626
|
+
|
|
627
|
+
Define these in your client to change default behavior:
|
|
628
|
+
|
|
629
|
+
| Method | Default |
|
|
630
|
+
|--------|---------|
|
|
631
|
+
| `retry_request?(exception, options)` | `true` for the network errors listed under Retry Configuration |
|
|
632
|
+
| `escape_path(value)` | Percent-encodes path values (`ERB::Util.url_encode`) |
|
|
633
|
+
| `parse_response(response, options)` | Parses the body as JSON, `nil` when empty |
|
|
634
|
+
| `handle_response(response, options, &block)` | Applies `return:`, parsing and the response block |
|
|
635
|
+
| `expected_response_code!(response, codes, options)` | Raises `UnexpectedResponse` for unexpected codes |
|
|
636
|
+
| `get_retry_request_max_retries(options)` | `retries:` option, then `configure_retries`, then 1 |
|
|
637
|
+
| `get_retry_request_sleep_time(exception, options)` | `sleep:` option, then `configure_retries`, then 0.05 |
|
|
544
638
|
|
|
545
639
|
## Requirements
|
|
546
640
|
|
|
547
|
-
- Ruby 3.
|
|
641
|
+
- Ruby 3.2+
|
|
548
642
|
- `inheritance-helper` gem (>= 0.2.5)
|
|
643
|
+
- `activesupport` (optional) for `to_query` builders and instrumentation
|
|
549
644
|
|
|
550
645
|
## Contributing
|
|
551
646
|
|
|
@@ -554,9 +649,9 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/dougyo
|
|
|
554
649
|
1. Fork the repository
|
|
555
650
|
2. Create your feature branch (`git checkout -b feature/my-feature`)
|
|
556
651
|
3. Write tests for your changes
|
|
557
|
-
4. Ensure all tests pass (`bundle exec rspec`)
|
|
652
|
+
4. Ensure all tests pass with full line and branch coverage (`CI=true bundle exec rspec`)
|
|
558
653
|
5. Ensure code style compliance (`bundle exec rubocop`)
|
|
559
|
-
6. Commit your changes (`git commit -am '
|
|
654
|
+
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
655
|
7. Push to the branch (`git push origin feature/my-feature`)
|
|
561
656
|
8. Create a Pull Request
|
|
562
657
|
|
data/lib/client-api-builder.rb
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'erb'
|
|
3
4
|
require 'inheritance-helper'
|
|
4
5
|
require 'json'
|
|
5
6
|
|
|
@@ -75,13 +76,14 @@ module ClientApiBuilder
|
|
|
75
76
|
uri = URI.parse(url.to_s)
|
|
76
77
|
return if ALLOWED_URL_SCHEMES.include?(uri.scheme&.downcase)
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
allowed = ALLOWED_URL_SCHEMES.join(', ')
|
|
80
|
+
raise ArgumentError, "Invalid base_url scheme: #{uri.scheme.inspect}. Allowed: #{allowed}"
|
|
79
81
|
rescue URI::InvalidURIError => e
|
|
80
82
|
raise ArgumentError, "Invalid base_url: #{e.message}"
|
|
81
83
|
end
|
|
82
84
|
|
|
83
|
-
# set the builder to :to_json, :to_query, :query_params or specify a proc
|
|
84
|
-
# or get the body builder
|
|
85
|
+
# set the builder to :to_json, :to_query, :query_params or specify a proc
|
|
86
|
+
# to handle building the request body payload, or get the body builder
|
|
85
87
|
def body_builder(builder = nil, &block)
|
|
86
88
|
return default_options[:body_builder] if builder.nil? && block.nil?
|
|
87
89
|
|
|
@@ -176,7 +178,7 @@ module ClientApiBuilder
|
|
|
176
178
|
:put
|
|
177
179
|
when /^(?:patch)/i
|
|
178
180
|
:patch
|
|
179
|
-
when /^(?:delete|remove)/i
|
|
181
|
+
when /^(?:delete|remove|destroy)/i
|
|
180
182
|
:delete
|
|
181
183
|
else
|
|
182
184
|
:get
|
|
@@ -332,7 +334,7 @@ module ClientApiBuilder
|
|
|
332
334
|
end
|
|
333
335
|
|
|
334
336
|
def extract_expected_response_codes(options)
|
|
335
|
-
codes = options[:expected_response_codes] || (options[:expected_response_code]
|
|
337
|
+
codes = options[:expected_response_codes] || Array(options[:expected_response_code])
|
|
336
338
|
codes.map(&:to_s)
|
|
337
339
|
end
|
|
338
340
|
|
|
@@ -374,7 +376,9 @@ module ClientApiBuilder
|
|
|
374
376
|
|
|
375
377
|
def generate_route_code(method_name, path, options = {})
|
|
376
378
|
# Validate method_name to prevent code injection
|
|
377
|
-
|
|
379
|
+
unless method_name.to_s.match?(/\A[a-z_][a-z0-9_]*\z/i)
|
|
380
|
+
raise ArgumentError, "Invalid method name: #{method_name.inspect}"
|
|
381
|
+
end
|
|
378
382
|
|
|
379
383
|
http_method = options[:method] || auto_detect_http_method(method_name)
|
|
380
384
|
path, path_arguments = process_route_path(path)
|
|
@@ -410,7 +414,7 @@ module ClientApiBuilder
|
|
|
410
414
|
code += " @request_options = {method: #{ctx[:http_method].inspect}, uri: __uri__, body: __body__, " \
|
|
411
415
|
"headers: __headers__, connection_options: __connection_options__}\n"
|
|
412
416
|
code += generate_request_call_code(ctx[:options], ctx[:stream_param])
|
|
413
|
-
code
|
|
417
|
+
"#{code}end\n\n"
|
|
414
418
|
end
|
|
415
419
|
|
|
416
420
|
def generate_wrapper_method(ctx)
|
|
@@ -424,7 +428,7 @@ module ClientApiBuilder
|
|
|
424
428
|
code += " expected_response_code!(@response, __expected_response_codes__, __options__)\n"
|
|
425
429
|
code += generate_response_handling_code(ctx[:options])
|
|
426
430
|
code += " end\n"
|
|
427
|
-
code
|
|
431
|
+
"#{code}end\n"
|
|
428
432
|
end
|
|
429
433
|
|
|
430
434
|
def route(method_name, path, options = {}, &block)
|
|
@@ -549,8 +553,11 @@ module ClientApiBuilder
|
|
|
549
553
|
self
|
|
550
554
|
end
|
|
551
555
|
|
|
556
|
+
# Percent-encodes everything but RFC 3986 unreserved characters (A-Z a-z 0-9 - . _ ~),
|
|
557
|
+
# so a value inserted into the path, including any '/', stays one segment.
|
|
558
|
+
# Override to change how path values are encoded.
|
|
552
559
|
def escape_path(path)
|
|
553
|
-
path
|
|
560
|
+
ERB::Util.url_encode(path.to_s)
|
|
554
561
|
end
|
|
555
562
|
|
|
556
563
|
def instrument_request
|
|
@@ -577,7 +584,7 @@ module ClientApiBuilder
|
|
|
577
584
|
end
|
|
578
585
|
end
|
|
579
586
|
|
|
580
|
-
def get_retry_request_sleep_time(
|
|
587
|
+
def get_retry_request_sleep_time(_exception, options)
|
|
581
588
|
options[:sleep] || self.class.default_options[:sleep] || 0.05
|
|
582
589
|
end
|
|
583
590
|
|
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.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Doug Youch
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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.
|
|
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:
|
|
71
|
+
rubygems_version: 4.0.20
|
|
86
72
|
specification_version: 4
|
|
87
|
-
summary: Build
|
|
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
|
-
}
|