client-api-builder 0.5.6 → 0.6.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.
data/README.md CHANGED
@@ -1,18 +1,22 @@
1
1
  # Client API Builder
2
2
 
3
- A Ruby gem that provides a simple and elegant way to create API clients through configuration. It allows you to define API endpoints and their behavior declaratively, making it easy to create and maintain API clients.
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)
6
+
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.
4
8
 
5
9
  ## Features
6
10
 
7
- - Declarative API client configuration
8
- - Support for different request body formats (JSON, query params)
9
- - Customizable headers
10
- - Nested routing support
11
- - ActiveSupport integration for logging and notifications
12
- - Error handling with detailed response information
13
- - Flexible parameter handling
14
- - Automatic HTTP method detection based on method names
15
- - Streaming support for handling large payloads
11
+ - **Declarative Configuration** - Define API endpoints with a clean DSL
12
+ - **Security by Default** - SSL/TLS verification, path traversal protection, SSRF prevention
13
+ - **Automatic HTTP Method Detection** - Intelligently determines HTTP methods from route names
14
+ - **Flexible Request Building** - Support for JSON, query params, and custom body builders
15
+ - **Nested Routing** - Organize complex APIs with hierarchical route structures
16
+ - **Retry Logic** - Configurable automatic retries for transient network failures
17
+ - **Streaming Support** - Handle large payloads efficiently with streaming to files or IO
18
+ - **ActiveSupport Integration** - Optional logging and instrumentation
19
+ - **Comprehensive Error Handling** - Detailed error information for debugging
16
20
 
17
21
  ## Installation
18
22
 
@@ -28,181 +32,512 @@ And then execute:
28
32
  $ bundle install
29
33
  ```
30
34
 
31
- Or install it yourself as:
35
+ Or install it yourself:
32
36
 
33
37
  ```bash
34
38
  $ gem install client-api-builder
35
39
  ```
36
40
 
37
- ## Usage
41
+ ## Quick Start
42
+
43
+ ```ruby
44
+ class GitHubClient
45
+ include ClientApiBuilder::Router
46
+
47
+ base_url 'https://api.github.com'
48
+
49
+ header 'Accept', 'application/vnd.github.v3+json'
50
+ header 'User-Agent', 'MyApp/1.0'
51
+
52
+ # Authentication header from instance method
53
+ header 'Authorization' do
54
+ "Bearer #{access_token}"
55
+ end
56
+
57
+ attr_accessor :access_token
58
+
59
+ # GET /users/:username
60
+ route :get_user, '/users/:username'
61
+
62
+ # GET /users/:username/repos
63
+ route :get_repos, '/users/:username/repos', query: { per_page: :per_page }
64
+
65
+ # POST /user/repos
66
+ route :create_repo, '/user/repos', body: { name: :name, private: :private }
67
+ end
68
+
69
+ client = GitHubClient.new
70
+ client.access_token = 'ghp_xxxxxxxxxxxx'
71
+
72
+ # Fetch a user
73
+ user = client.get_user(username: 'octocat')
74
+
75
+ # List repositories with pagination
76
+ repos = client.get_repos(username: 'octocat', per_page: 10)
77
+
78
+ # Create a new repository
79
+ new_repo = client.create_repo(name: 'my-new-repo', private: true)
80
+ ```
81
+
82
+ ## Usage Guide
83
+
84
+ ### Defining Routes
85
+
86
+ Routes are defined using the `route` class method:
87
+
88
+ ```ruby
89
+ route :method_name, '/path/:param', options
90
+ ```
91
+
92
+ **Options:**
93
+
94
+ | Option | Description |
95
+ |--------|-------------|
96
+ | `method:` | HTTP method (`:get`, `:post`, `:put`, `:patch`, `:delete`). Auto-detected if omitted. |
97
+ | `query:` | Hash defining query parameters. Use symbols for dynamic values. |
98
+ | `body:` | Hash defining request body. Use symbols for dynamic values. |
99
+ | `expected_response_code:` | Single expected HTTP status code |
100
+ | `expected_response_codes:` | Array of expected HTTP status codes |
101
+ | `stream:` | Enable streaming (`:file`, `:io`, `:block`, or `true`) |
102
+ | `return:` | Return type (`:response`, `:body`, or parsed JSON by default) |
103
+
104
+ ### Automatic HTTP Method Detection
38
105
 
39
- ### Basic Usage
106
+ The Router automatically detects HTTP methods based on route names:
40
107
 
41
- Create an API client by including the `ClientApiBuilder::Router` module and defining your endpoints:
108
+ | Prefix | HTTP Method |
109
+ |--------|-------------|
110
+ | `get_`, `find_`, `fetch_`, `list_`, `search_` | GET |
111
+ | `post_`, `create_`, `add_`, `insert_` | POST |
112
+ | `put_`, `update_`, `modify_`, `change_` | PUT |
113
+ | `patch_` | PATCH |
114
+ | `delete_`, `remove_`, `destroy_` | DELETE |
42
115
 
43
116
  ```ruby
44
117
  class MyApiClient
45
118
  include ClientApiBuilder::Router
46
119
 
47
- # Set the base URL for all requests
48
120
  base_url 'https://api.example.com'
49
121
 
50
- # Set default headers
51
- header 'Content-Type', 'application/json'
52
- header 'Accept', 'application/json'
53
-
54
- # Define an endpoint
55
- route :get_user, '/users/:id', method: :get, expected_response_code: 200
56
- route :create_user, '/users', method: :post, expected_response_code: 201, body: { name: :name, email: :email }
122
+ # Automatically uses appropriate HTTP methods
123
+ route :get_users, '/users' # GET
124
+ route :create_user, '/users', body: { name: :name } # POST
125
+ route :update_user, '/users/:id', body: { name: :name } # PUT
126
+ route :patch_user, '/users/:id', body: { name: :name } # PATCH
127
+ route :delete_user, '/users/:id' # DELETE
57
128
  end
129
+ ```
58
130
 
59
- # Use the client
60
- client = MyApiClient.new
61
- user = client.get_user(id: 123)
62
- new_user = client.create_user(name: 'John', email: 'john@example.com')
131
+ ### Dynamic Parameters
132
+
133
+ Parameters can be defined in three ways:
134
+
135
+ **1. Path Parameters** (using `:param` or `{param}` syntax):
136
+
137
+ ```ruby
138
+ route :get_user, '/users/:id'
139
+ route :get_post, '/users/{user_id}/posts/{post_id}'
63
140
  ```
64
141
 
65
- ### Automatic HTTP Method Detection
142
+ **2. Query Parameters:**
66
143
 
67
- The Router automatically detects the HTTP method based on the method name if not explicitly specified. This makes your API client code more intuitive and reduces boilerplate. The detection rules are:
144
+ ```ruby
145
+ route :search_users, '/users', query: { q: :query, page: :page, limit: :limit }
146
+ # Generates: GET /users?q=...&page=...&limit=...
147
+ ```
68
148
 
69
- - Methods starting with `post`, `create`, `add`, or `insert` → `POST`
70
- - Methods starting with `put`, `update`, `modify`, or `change` → `PUT`
71
- - Methods starting with `patch` → `PATCH`
72
- - Methods starting with `delete` or `remove` → `DELETE`
73
- - All other methods → `GET`
149
+ **3. Body Parameters:**
74
150
 
75
- Example:
151
+ ```ruby
152
+ route :create_user, '/users', body: { user: { name: :name, email: :email } }
153
+ # Sends JSON: {"user": {"name": "...", "email": "..."}}
154
+ ```
155
+
156
+ ### Headers
157
+
158
+ Define headers at the class level or dynamically:
76
159
 
77
160
  ```ruby
78
161
  class MyApiClient
79
162
  include ClientApiBuilder::Router
80
-
163
+
81
164
  base_url 'https://api.example.com'
82
-
83
- # These will automatically use the appropriate HTTP methods
84
- route :get_users, '/users' # Uses GET
85
- route :create_user, '/users', body: { name: :name } # Uses POST
86
- route :update_user, '/users/:id', body: { name: :name } # Uses PUT
87
- route :delete_user, '/users/:id' # Uses DELETE
88
-
89
- # You can still explicitly specify the method if needed
90
- route :custom_action, '/custom', method: :post
165
+
166
+ # Static header
167
+ header 'Content-Type', 'application/json'
168
+
169
+ # Dynamic header from instance method
170
+ header 'Authorization', :auth_header
171
+
172
+ # Dynamic header from block
173
+ header 'X-Request-ID' do
174
+ SecureRandom.uuid
175
+ end
176
+
177
+ attr_accessor :api_key
178
+
179
+ def auth_header
180
+ "Bearer #{api_key}"
181
+ end
91
182
  end
92
183
  ```
93
184
 
94
185
  ### Request Body Formats
95
186
 
96
- By default, the client converts the body data to JSON. To use query parameters instead:
187
+ Configure how request bodies are serialized:
97
188
 
98
189
  ```ruby
99
190
  class MyApiClient
100
191
  include ClientApiBuilder::Router
101
-
102
- # Use query parameters for the body
192
+
193
+ # Default: JSON (using to_json)
194
+ body_builder :to_json
195
+
196
+ # URL-encoded form data (using to_query)
197
+ body_builder :to_query
198
+
199
+ # Custom query params builder (no ActiveSupport dependency)
103
200
  body_builder :query_params
104
-
105
- base_url 'https://api.example.com'
106
-
107
- route :search, '/search', body: { q: :query, page: :page }
201
+
202
+ # Custom builder method
203
+ body_builder :my_custom_builder
204
+
205
+ # Custom builder with block
206
+ body_builder do |data|
207
+ data.to_xml
208
+ end
209
+
210
+ def my_custom_builder(data)
211
+ # Custom serialization logic
212
+ end
108
213
  end
109
214
  ```
110
215
 
111
- ### Nested Routing
216
+ ### Nested Routing (Sections)
112
217
 
113
- For APIs with nested resources, you can use the `NestedRouter`:
218
+ Organize complex APIs with nested routes:
114
219
 
115
220
  ```ruby
116
221
  class MyApiClient
117
222
  include ClientApiBuilder::Router
118
-
223
+
119
224
  base_url 'https://api.example.com'
120
-
225
+ header 'Authorization', :auth_token
226
+
227
+ attr_accessor :auth_token
228
+
121
229
  section :users do
122
- route :list, '/', method: :get
123
- route :get, '/:id', method: :get
230
+ base_url 'https://api.example.com/v2' # Override base URL
231
+
232
+ route :list, '/users'
233
+ route :get, '/users/:id'
234
+ route :create, '/users', body: { name: :name, email: :email }
235
+ end
236
+
237
+ section :posts do
238
+ route :list, '/posts'
239
+ route :get, '/posts/:id'
124
240
  end
125
241
  end
126
242
 
127
243
  client = MyApiClient.new
244
+ client.auth_token = 'secret'
245
+
246
+ # Access nested routes
128
247
  users = client.users.list
129
248
  user = client.users.get(id: 123)
249
+ posts = client.posts.list
250
+ ```
251
+
252
+ ### Connection Options
253
+
254
+ Configure connection settings:
255
+
256
+ ```ruby
257
+ class MyApiClient
258
+ include ClientApiBuilder::Router
259
+
260
+ base_url 'https://api.example.com'
261
+
262
+ # Set timeouts
263
+ connection_option :open_timeout, 10
264
+ connection_option :read_timeout, 30
265
+
266
+ # SSL options (verify_mode is enabled by default)
267
+ connection_option :ssl_timeout, 10
268
+ end
269
+ ```
270
+
271
+ ### Retry Configuration
272
+
273
+ Configure automatic retries for transient failures:
274
+
275
+ ```ruby
276
+ class MyApiClient
277
+ include ClientApiBuilder::Router
278
+
279
+ base_url 'https://api.example.com'
280
+
281
+ # Retry up to 3 times with 0.5 second delay between attempts
282
+ configure_retries 3, 0.5
283
+ end
284
+ ```
285
+
286
+ By default, retries are performed only for network-related errors:
287
+ - `Net::OpenTimeout`, `Net::ReadTimeout`
288
+ - `Errno::ECONNRESET`, `Errno::ECONNREFUSED`, `Errno::ETIMEDOUT`
289
+ - `SocketError`, `EOFError`
290
+
291
+ Customize retry behavior by overriding `retry_request?`:
292
+
293
+ ```ruby
294
+ class MyApiClient
295
+ include ClientApiBuilder::Router
296
+
297
+ def retry_request?(exception, options)
298
+ case exception
299
+ when Net::OpenTimeout, Net::ReadTimeout
300
+ true
301
+ when ClientApiBuilder::UnexpectedResponse
302
+ # Retry on 503 Service Unavailable
303
+ exception.response.code == '503'
304
+ else
305
+ false
306
+ end
307
+ end
308
+ end
309
+ ```
310
+
311
+ ### Streaming Support
312
+
313
+ Handle large responses efficiently:
314
+
315
+ ```ruby
316
+ class MyApiClient
317
+ include ClientApiBuilder::Router
318
+
319
+ base_url 'https://api.example.com'
320
+
321
+ # Stream directly to a file
322
+ route :download_file, '/files/:id/download', stream: :file
323
+
324
+ # Stream to an IO object
325
+ route :stream_to_io, '/files/:id/stream', stream: :io
326
+
327
+ # Stream with block processing
328
+ route :process_stream, '/events/stream', stream: :block
329
+ end
330
+
331
+ client = MyApiClient.new
332
+
333
+ # Download to file
334
+ client.download_file(id: 123, file: '/path/to/output.zip')
335
+
336
+ # Stream to IO
337
+ File.open('/path/to/output.dat', 'wb') do |file|
338
+ client.stream_to_io(id: 123, io: file)
339
+ end
340
+
341
+ # Process stream in chunks
342
+ client.process_stream do |response, chunk|
343
+ puts "Received #{chunk.bytesize} bytes"
344
+ process_data(chunk)
345
+ end
346
+ ```
347
+
348
+ ### Response Handling
349
+
350
+ Customize how responses are processed:
351
+
352
+ ```ruby
353
+ class MyApiClient
354
+ include ClientApiBuilder::Router
355
+
356
+ base_url 'https://api.example.com'
357
+
358
+ # Return parsed JSON (default)
359
+ route :get_user, '/users/:id'
360
+
361
+ # Return raw response body
362
+ route :get_raw, '/raw/:id', return: :body
363
+
364
+ # Return Net::HTTPResponse object
365
+ route :get_response, '/data/:id', return: :response
366
+
367
+ # Custom response handling with block
368
+ route :get_token, '/auth/token' do |data|
369
+ self.auth_token = data['access_token']
370
+ data
371
+ end
372
+ end
130
373
  ```
131
374
 
132
375
  ### Error Handling
133
376
 
134
- The gem provides custom error classes for better error handling:
377
+ The gem provides detailed error information:
135
378
 
136
379
  ```ruby
137
380
  begin
138
- client.get_user(id: 123)
381
+ client.get_user(id: 999)
139
382
  rescue ClientApiBuilder::UnexpectedResponse => e
140
- puts "Request failed with status #{e.response.status}"
141
- puts "Response body: #{e.response.body}"
383
+ puts "HTTP Status: #{e.response.code}"
384
+ puts "Response Body: #{e.response.body}"
385
+ puts "Error Message: #{e.message}"
142
386
  end
143
387
  ```
144
388
 
389
+ ### Debugging
390
+
391
+ Access request and response details after each call:
392
+
393
+ ```ruby
394
+ client = MyApiClient.new
395
+ client.get_user(id: 123)
396
+
397
+ # Response information
398
+ puts client.response.code # HTTP status code
399
+ puts client.response.body # Response body
400
+ puts client.response.to_hash # Response headers
401
+
402
+ # Request information
403
+ puts client.request_options[:method] # HTTP method used
404
+ puts client.request_options[:uri] # Full URI
405
+ puts client.request_options[:body] # Request body
406
+ puts client.request_options[:headers] # Request headers
407
+
408
+ # Performance metrics
409
+ puts client.total_request_time # Time in seconds
410
+ puts client.request_attempts # Number of attempts (including retries)
411
+ ```
412
+
145
413
  ### ActiveSupport Integration
146
414
 
147
- The gem integrates with ActiveSupport for logging and notifications:
415
+ When ActiveSupport is available, the gem provides instrumentation and logging:
148
416
 
149
417
  ```ruby
150
- # Enable logging
418
+ # Set up logging
151
419
  ClientApiBuilder.logger = Logger.new(STDOUT)
152
420
 
153
- # Subscribe to notifications
154
- ActiveSupport::Notifications.subscribe('request.client_api_builder') do |*args|
421
+ # Subscribe to request events
422
+ ActiveSupport::Notifications.subscribe('client_api_builder.request') do |*args|
155
423
  event = ActiveSupport::Notifications::Event.new(*args)
156
- puts "Request took #{event.duration}ms"
424
+ client = event.payload[:client]
425
+
426
+ puts "#{client.request_options[:method]} #{client.request_options[:uri]}"
427
+ puts "Status: #{client.response&.code}"
428
+ puts "Duration: #{event.duration.round(2)}ms"
157
429
  end
430
+
431
+ # Or use the built-in log subscriber
432
+ subscriber = ClientApiBuilder::ActiveSupportLogSubscriber.new(Rails.logger)
433
+ subscriber.subscribe!
158
434
  ```
159
435
 
160
- ### Streaming Support
436
+ ## Security Features
161
437
 
162
- The library supports streaming responses, which is particularly useful for handling large payloads. You can stream responses directly to a file or process them in chunks:
438
+ Client API Builder includes several security features enabled by default:
439
+
440
+ ### SSL/TLS Verification
441
+
442
+ All HTTPS connections verify SSL certificates by default using `OpenSSL::SSL::VERIFY_PEER`. Default timeouts are also configured to prevent hanging connections.
443
+
444
+ ### SSRF Protection
445
+
446
+ Base URLs are validated to only allow `http` and `https` schemes, preventing Server-Side Request Forgery attacks:
163
447
 
164
448
  ```ruby
165
449
  class MyApiClient
166
450
  include ClientApiBuilder::Router
167
-
168
- base_url 'https://api.example.com'
169
-
170
- # Stream response directly to a file
171
- route :download_users, '/users', stream: :file
172
-
173
- # Stream response in chunks for custom processing
174
- route :stream_users, '/users', stream: true
451
+
452
+ base_url 'https://api.example.com' # Valid
453
+ base_url 'http://api.example.com' # Valid
454
+ base_url 'file:///etc/passwd' # Raises ArgumentError
455
+ base_url 'ftp://example.com' # Raises ArgumentError
175
456
  end
457
+ ```
176
458
 
177
- # Use the client
178
- client = MyApiClient.new
459
+ ### Path Traversal Protection
179
460
 
180
- # Stream to file
181
- client.download_users('users.json') # Saves response directly to users.json
461
+ File streaming operations validate paths to prevent directory traversal attacks:
182
462
 
183
- # Stream with custom processing
184
- client.stream_users do |chunk|
185
- # Process each chunk of the response
186
- puts "Received #{chunk.bytesize} bytes"
463
+ ```ruby
464
+ # These will raise ArgumentError
465
+ client.download_file(id: 1, file: '/tmp/../etc/passwd')
466
+ client.download_file(id: 1, file: "/tmp/file\0.txt")
467
+ ```
468
+
469
+ ### Safe File Modes
470
+
471
+ Only safe file modes are allowed for streaming to files: `w`, `wb`, `a`, `ab`, `w+`, `wb+`, `a+`, `ab+`.
472
+
473
+ ## Thread Safety
474
+
475
+ Client instances are **not thread-safe**. Create a separate client instance per thread:
476
+
477
+ ```ruby
478
+ # Correct: Create a new client for each thread
479
+ threads = 5.times.map do |i|
480
+ Thread.new do
481
+ client = MyApiClient.new
482
+ client.get_user(id: i)
483
+ end
484
+ end
485
+ threads.each(&:join)
486
+
487
+ # Incorrect: Do not share clients across threads
488
+ client = MyApiClient.new
489
+ threads = 5.times.map do |i|
490
+ Thread.new do
491
+ client.get_user(id: i) # Race condition!
492
+ end
187
493
  end
188
494
  ```
189
495
 
190
- When using `stream: :file`, the response is written directly to disk as it's received, which is memory-efficient for large responses. The file path is passed as an argument to the method.
496
+ ## Configuration Reference
497
+
498
+ ### Class-Level Methods
499
+
500
+ | Method | Description |
501
+ |--------|-------------|
502
+ | `base_url(url)` | Set the base URL for all requests |
503
+ | `header(name, value)` | Add a header to all requests |
504
+ | `body_builder(builder)` | Configure request body serialization |
505
+ | `query_builder(builder)` | Configure query string serialization |
506
+ | `query_param(name, value)` | Add a query parameter to all requests |
507
+ | `connection_option(name, value)` | Set Net::HTTP connection options |
508
+ | `configure_retries(max, sleep)` | Configure retry behavior |
509
+ | `route(name, path, options)` | Define an API endpoint |
510
+ | `section(name, options, &block)` | Define nested routes |
511
+ | `namespace(path, &block)` | Add path prefix to routes in block |
191
512
 
192
- For custom streaming, you can provide a block that will be called with each chunk of the response as it's received. This allows for custom processing of large responses without loading the entire response into memory.
513
+ ### Instance Methods
193
514
 
194
- ## Configuration Options
515
+ | Method | Description |
516
+ |--------|-------------|
517
+ | `response` | Last Net::HTTPResponse object |
518
+ | `request_options` | Options used for last request |
519
+ | `total_request_time` | Duration of last request in seconds |
520
+ | `request_attempts` | Number of attempts for last request |
521
+ | `root_router` | Returns the root router (for nested routers) |
195
522
 
196
- - `base_url`: Set the base URL for all requests
197
- - `header`: Add headers to all requests
198
- - `body_builder`: Configure how request bodies are formatted
199
- - `route`: Define API endpoints with their paths and parameters
200
- - `nested_route`: Define nested resource routes
523
+ ## Requirements
524
+
525
+ - Ruby 3.0+
526
+ - `inheritance-helper` gem (>= 0.2.5)
201
527
 
202
528
  ## Contributing
203
529
 
204
530
  Bug reports and pull requests are welcome on GitHub at https://github.com/dougyouch/client-api-builder.
205
531
 
532
+ 1. Fork the repository
533
+ 2. Create your feature branch (`git checkout -b feature/my-feature`)
534
+ 3. Write tests for your changes
535
+ 4. Ensure all tests pass (`bundle exec rspec`)
536
+ 5. Ensure code style compliance (`bundle exec rubocop`)
537
+ 6. Commit your changes (`git commit -am 'Add my feature'`)
538
+ 7. Push to the branch (`git push origin feature/my-feature`)
539
+ 8. Create a Pull Request
540
+
206
541
  ## License
207
542
 
208
543
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -2,14 +2,30 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'client-api-builder'
5
- s.version = '0.5.6'
5
+ s.version = '0.6.0'
6
6
  s.licenses = ['MIT']
7
- s.summary = 'Utility for creating API clients through configuration'
8
- s.description = 'Create API clients through configuration with complete transparency'
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
9
15
  s.authors = ['Doug Youch']
10
16
  s.email = 'dougyouch@gmail.com'
11
17
  s.homepage = 'https://github.com/dougyouch/client-api-builder'
12
18
  s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
13
19
 
14
- s.add_runtime_dependency 'inheritance-helper', '>= 0.2.5'
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
+ }
15
31
  end
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
  require 'securerandom'
3
5
 
4
- class BasicAuthExampleClient < Struct.new(
5
- :username,
6
- :password
7
- )
8
-
6
+ BasicAuthExampleClient = Struct.new(
7
+ :username,
8
+ :password
9
+ ) do
9
10
  include ClientApiBuilder::Router
10
11
  include ClientApiBuilder::Section
11
12
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class IMDBDatesetsClient
2
4
  include ClientApiBuilder::Router
3
5