resteze 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +31 -0
- data/CLAUDE.md +74 -0
- data/README.md +33 -0
- data/docs/ADVANCED_USAGE.md +760 -0
- data/docs/API.md +410 -0
- data/docs/CONFIGURATION.md +681 -0
- data/docs/ERROR_HANDLING.md +609 -0
- data/docs/TESTING.md +768 -0
- data/lib/resteze/client.rb +1 -0
- data/lib/resteze/instrumentation.rb +14 -0
- data/lib/resteze/testing/minitest/configuration.rb +18 -0
- data/lib/resteze/testing/minitest/object.rb +58 -0
- data/lib/resteze/testing/minitest.rb +10 -0
- data/lib/resteze/testing/rspec/configure.rb +16 -0
- data/lib/resteze/testing/rspec/object.rb +49 -0
- data/lib/resteze/testing/rspec.rb +10 -0
- data/lib/resteze/version.rb +1 -1
- data/lib/resteze.rb +2 -0
- metadata +16 -2
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
# Configuration Guide
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Basic Configuration](#basic-configuration)
|
|
5
|
+
- [Environment-based Configuration](#environment-based-configuration)
|
|
6
|
+
- [Dynamic Configuration](#dynamic-configuration)
|
|
7
|
+
- [Connection Configuration](#connection-configuration)
|
|
8
|
+
- [Logging Configuration](#logging-configuration)
|
|
9
|
+
- [Proxy Configuration](#proxy-configuration)
|
|
10
|
+
- [Advanced Configuration Patterns](#advanced-configuration-patterns)
|
|
11
|
+
|
|
12
|
+
## Basic Configuration
|
|
13
|
+
|
|
14
|
+
### Minimal Setup
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require 'resteze'
|
|
18
|
+
|
|
19
|
+
module MyApi
|
|
20
|
+
include Resteze
|
|
21
|
+
|
|
22
|
+
configure do |config|
|
|
23
|
+
config.api_base = 'https://api.example.com/'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Standard Configuration
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
module MyApi
|
|
32
|
+
include Resteze
|
|
33
|
+
|
|
34
|
+
configure do |config|
|
|
35
|
+
# API endpoint
|
|
36
|
+
config.api_base = 'https://api.example.com/'
|
|
37
|
+
|
|
38
|
+
# Timeouts (in seconds)
|
|
39
|
+
config.open_timeout = 30 # Connection timeout
|
|
40
|
+
config.read_timeout = 60 # Read timeout
|
|
41
|
+
|
|
42
|
+
# Logging
|
|
43
|
+
config.logger = Logger.new($stdout)
|
|
44
|
+
|
|
45
|
+
# Proxy (optional)
|
|
46
|
+
config.proxy = ENV['HTTP_PROXY']
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Custom Configuration Properties
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
module MyApi
|
|
55
|
+
include Resteze
|
|
56
|
+
|
|
57
|
+
# Define custom properties
|
|
58
|
+
class << self
|
|
59
|
+
attr_accessor :api_key,
|
|
60
|
+
:api_secret,
|
|
61
|
+
:environment,
|
|
62
|
+
:rate_limit,
|
|
63
|
+
:retry_count,
|
|
64
|
+
:cache_ttl
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
configure do |config|
|
|
68
|
+
# Standard Resteze config
|
|
69
|
+
config.api_base = 'https://api.example.com/'
|
|
70
|
+
config.open_timeout = 30
|
|
71
|
+
config.read_timeout = 60
|
|
72
|
+
|
|
73
|
+
# Custom config
|
|
74
|
+
config.api_key = ENV['API_KEY']
|
|
75
|
+
config.api_secret = ENV['API_SECRET']
|
|
76
|
+
config.environment = ENV['API_ENV'] || 'production'
|
|
77
|
+
config.rate_limit = 100 # requests per minute
|
|
78
|
+
config.retry_count = 3
|
|
79
|
+
config.cache_ttl = 300 # 5 minutes
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Environment-based Configuration
|
|
85
|
+
|
|
86
|
+
### Using Rails Environments
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
module MyApi
|
|
90
|
+
include Resteze
|
|
91
|
+
|
|
92
|
+
configure do |config|
|
|
93
|
+
case Rails.env
|
|
94
|
+
when 'production'
|
|
95
|
+
config.api_base = 'https://api.example.com/'
|
|
96
|
+
config.logger = Rails.logger
|
|
97
|
+
config.open_timeout = 30
|
|
98
|
+
config.read_timeout = 60
|
|
99
|
+
when 'staging'
|
|
100
|
+
config.api_base = 'https://staging-api.example.com/'
|
|
101
|
+
config.logger = Rails.logger
|
|
102
|
+
config.open_timeout = 45
|
|
103
|
+
config.read_timeout = 90
|
|
104
|
+
when 'development'
|
|
105
|
+
config.api_base = 'http://localhost:3000/'
|
|
106
|
+
config.logger = Logger.new($stdout)
|
|
107
|
+
config.logger.level = Logger::DEBUG
|
|
108
|
+
config.open_timeout = 60
|
|
109
|
+
config.read_timeout = 120
|
|
110
|
+
when 'test'
|
|
111
|
+
config.api_base = 'http://test.local/'
|
|
112
|
+
config.logger = Logger.new(nil) # Disable logging in tests
|
|
113
|
+
config.open_timeout = 5
|
|
114
|
+
config.read_timeout = 5
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Using Environment Variables
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
module MyApi
|
|
124
|
+
include Resteze
|
|
125
|
+
|
|
126
|
+
configure do |config|
|
|
127
|
+
# Required environment variables
|
|
128
|
+
config.api_base = ENV.fetch('MY_API_URL') do
|
|
129
|
+
raise "MY_API_URL environment variable is required"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Optional with defaults
|
|
133
|
+
config.open_timeout = ENV.fetch('MY_API_OPEN_TIMEOUT', 30).to_i
|
|
134
|
+
config.read_timeout = ENV.fetch('MY_API_READ_TIMEOUT', 60).to_i
|
|
135
|
+
|
|
136
|
+
# Conditional configuration
|
|
137
|
+
if ENV['MY_API_DEBUG'] == 'true'
|
|
138
|
+
config.logger = Logger.new($stdout)
|
|
139
|
+
config.logger.level = Logger::DEBUG
|
|
140
|
+
else
|
|
141
|
+
config.logger = Logger.new('log/my_api.log')
|
|
142
|
+
config.logger.level = Logger::INFO
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Proxy configuration
|
|
146
|
+
config.proxy = ENV['HTTP_PROXY'] if ENV['HTTP_PROXY']
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Using Configuration Files
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# config/my_api.yml
|
|
155
|
+
production:
|
|
156
|
+
api_base: "https://api.example.com/"
|
|
157
|
+
api_key: <%= ENV['MY_API_KEY'] %>
|
|
158
|
+
open_timeout: 30
|
|
159
|
+
read_timeout: 60
|
|
160
|
+
|
|
161
|
+
development:
|
|
162
|
+
api_base: "http://localhost:3000/"
|
|
163
|
+
api_key: "development-key"
|
|
164
|
+
open_timeout: 60
|
|
165
|
+
read_timeout: 120
|
|
166
|
+
|
|
167
|
+
test:
|
|
168
|
+
api_base: "http://test.local/"
|
|
169
|
+
api_key: "test-key"
|
|
170
|
+
open_timeout: 5
|
|
171
|
+
read_timeout: 5
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
# Load configuration from YAML
|
|
176
|
+
require 'yaml'
|
|
177
|
+
require 'erb'
|
|
178
|
+
|
|
179
|
+
module MyApi
|
|
180
|
+
include Resteze
|
|
181
|
+
|
|
182
|
+
# Load config file
|
|
183
|
+
config_file = File.join(Rails.root, 'config', 'my_api.yml')
|
|
184
|
+
config_data = YAML.safe_load(ERB.new(File.read(config_file)).result)
|
|
185
|
+
settings = config_data[Rails.env]
|
|
186
|
+
|
|
187
|
+
configure do |config|
|
|
188
|
+
settings.each do |key, value|
|
|
189
|
+
config.send("#{key}=", value)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Dynamic Configuration
|
|
196
|
+
|
|
197
|
+
### Runtime Configuration Changes
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
module MyApi
|
|
201
|
+
include Resteze
|
|
202
|
+
|
|
203
|
+
class << self
|
|
204
|
+
def reconfigure
|
|
205
|
+
yield self if block_given?
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def reset_configuration!
|
|
209
|
+
configure do |config|
|
|
210
|
+
config.api_base = default_api_base
|
|
211
|
+
config.open_timeout = 30
|
|
212
|
+
config.read_timeout = 60
|
|
213
|
+
config.logger = Logger.new($stdout)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
def default_api_base
|
|
220
|
+
'https://api.example.com/'
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Usage
|
|
226
|
+
MyApi.reconfigure do |config|
|
|
227
|
+
config.api_base = 'https://new-api.example.com/'
|
|
228
|
+
config.api_key = 'new-key'
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Reset to defaults
|
|
232
|
+
MyApi.reset_configuration!
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Per-Request Configuration
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
module MyApi
|
|
239
|
+
class Client < Resteze::Client
|
|
240
|
+
def with_timeout(open: nil, read: nil)
|
|
241
|
+
old_open = api_module.open_timeout
|
|
242
|
+
old_read = api_module.read_timeout
|
|
243
|
+
|
|
244
|
+
api_module.open_timeout = open if open
|
|
245
|
+
api_module.read_timeout = read if read
|
|
246
|
+
|
|
247
|
+
yield
|
|
248
|
+
ensure
|
|
249
|
+
api_module.open_timeout = old_open
|
|
250
|
+
api_module.read_timeout = old_read
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
class User < ApiResource
|
|
255
|
+
# Use custom timeout for slow endpoints
|
|
256
|
+
def self.export_all
|
|
257
|
+
Client.active_client.with_timeout(read: 300) do
|
|
258
|
+
request(:get, "#{resource_path}/export")
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Multi-tenant Configuration
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
module MyApi
|
|
269
|
+
include Resteze
|
|
270
|
+
|
|
271
|
+
class << self
|
|
272
|
+
def for_tenant(tenant_id)
|
|
273
|
+
Thread.current[:my_api_tenant] = tenant_id
|
|
274
|
+
yield
|
|
275
|
+
ensure
|
|
276
|
+
Thread.current[:my_api_tenant] = nil
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def current_tenant
|
|
280
|
+
Thread.current[:my_api_tenant]
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
class Client < Resteze::Client
|
|
285
|
+
def request_headers
|
|
286
|
+
headers = super
|
|
287
|
+
if api_module.current_tenant
|
|
288
|
+
headers['X-Tenant-ID'] = api_module.current_tenant
|
|
289
|
+
end
|
|
290
|
+
headers
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def self.api_url(path = "")
|
|
294
|
+
base = if api_module.current_tenant
|
|
295
|
+
"https://#{api_module.current_tenant}.api.example.com/"
|
|
296
|
+
else
|
|
297
|
+
api_module.api_base
|
|
298
|
+
end
|
|
299
|
+
[base.chomp("/"), path].join
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Usage
|
|
305
|
+
MyApi.for_tenant('acme') do
|
|
306
|
+
user = MyApi::User.retrieve('123') # Uses acme.api.example.com
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Connection Configuration
|
|
311
|
+
|
|
312
|
+
### Faraday Connection Setup
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
module MyApi
|
|
316
|
+
class Client < Resteze::Client
|
|
317
|
+
def self.default_connection
|
|
318
|
+
@default_connection ||= Faraday.new do |conn|
|
|
319
|
+
# Request middleware (order matters!)
|
|
320
|
+
conn.request :json # Encode request bodies as JSON
|
|
321
|
+
conn.request :retry, max: 3, interval: 0.5 # Retry failed requests
|
|
322
|
+
conn.request :authorization, 'Bearer', -> { api_module.api_key }
|
|
323
|
+
|
|
324
|
+
# Response middleware
|
|
325
|
+
conn.response :json, content_type: /\bjson$/ # Parse JSON responses
|
|
326
|
+
conn.response :logger, api_module.logger, bodies: true # Log requests
|
|
327
|
+
|
|
328
|
+
# Error handling
|
|
329
|
+
conn.use Middleware::RaiseError
|
|
330
|
+
|
|
331
|
+
# Adapter (must be last)
|
|
332
|
+
conn.adapter :net_http_persistent # Use persistent connections
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### SSL/TLS Configuration
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
module MyApi
|
|
343
|
+
class Client < Resteze::Client
|
|
344
|
+
def self.default_connection
|
|
345
|
+
@default_connection ||= Faraday.new(ssl: ssl_options) do |conn|
|
|
346
|
+
conn.use Middleware::RaiseError
|
|
347
|
+
conn.adapter Faraday.default_adapter
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
private
|
|
352
|
+
|
|
353
|
+
def self.ssl_options
|
|
354
|
+
{
|
|
355
|
+
# Certificate verification
|
|
356
|
+
verify: true, # Verify SSL certificates
|
|
357
|
+
|
|
358
|
+
# Client certificates
|
|
359
|
+
client_cert: OpenSSL::X509::Certificate.new(File.read('client.crt')),
|
|
360
|
+
client_key: OpenSSL::PKey::RSA.new(File.read('client.key')),
|
|
361
|
+
|
|
362
|
+
# CA bundle
|
|
363
|
+
ca_file: '/path/to/ca_bundle.pem',
|
|
364
|
+
|
|
365
|
+
# Minimum TLS version
|
|
366
|
+
version: :TLSv1_2,
|
|
367
|
+
|
|
368
|
+
# Cipher suites
|
|
369
|
+
ciphers: 'HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA'
|
|
370
|
+
}
|
|
371
|
+
rescue => e
|
|
372
|
+
Rails.logger.error "Failed to load SSL certificates: #{e.message}"
|
|
373
|
+
{ verify: true } # Fall back to default verification
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Custom Adapters
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
module MyApi
|
|
383
|
+
class Client < Resteze::Client
|
|
384
|
+
def self.default_connection
|
|
385
|
+
@default_connection ||= Faraday.new do |conn|
|
|
386
|
+
conn.use Middleware::RaiseError
|
|
387
|
+
|
|
388
|
+
# Choose adapter based on environment
|
|
389
|
+
case Rails.env
|
|
390
|
+
when 'production'
|
|
391
|
+
# Use Typhoeus for better performance
|
|
392
|
+
conn.adapter :typhoeus
|
|
393
|
+
when 'test'
|
|
394
|
+
# Use test adapter for testing
|
|
395
|
+
conn.adapter :test do |stub|
|
|
396
|
+
stub.get('/users/123') { [200, {}, { id: 123 }.to_json] }
|
|
397
|
+
end
|
|
398
|
+
else
|
|
399
|
+
# Default adapter
|
|
400
|
+
conn.adapter Faraday.default_adapter
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Logging Configuration
|
|
409
|
+
|
|
410
|
+
### Custom Logger
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
require 'logger'
|
|
414
|
+
|
|
415
|
+
module MyApi
|
|
416
|
+
include Resteze
|
|
417
|
+
|
|
418
|
+
class ApiLogger < Logger
|
|
419
|
+
def format_message(severity, timestamp, progname, msg)
|
|
420
|
+
{
|
|
421
|
+
severity: severity,
|
|
422
|
+
timestamp: timestamp.iso8601,
|
|
423
|
+
service: 'my_api',
|
|
424
|
+
message: msg
|
|
425
|
+
}.to_json + "\n"
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
configure do |config|
|
|
430
|
+
config.logger = ApiLogger.new('log/my_api.log', 'daily')
|
|
431
|
+
config.logger.level = Rails.env.production? ? Logger::INFO : Logger::DEBUG
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Structured Logging
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
module MyApi
|
|
440
|
+
class Client < Resteze::Client
|
|
441
|
+
private
|
|
442
|
+
|
|
443
|
+
def log_request(context)
|
|
444
|
+
logger.info({
|
|
445
|
+
event: 'api_request',
|
|
446
|
+
method: context.method,
|
|
447
|
+
path: context.path,
|
|
448
|
+
headers: sanitize_headers(context.headers),
|
|
449
|
+
params: sanitize_params(context.query_params)
|
|
450
|
+
}.to_json)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def log_response(context, request_start, response)
|
|
454
|
+
logger.info({
|
|
455
|
+
event: 'api_response',
|
|
456
|
+
method: context.method,
|
|
457
|
+
path: context.path,
|
|
458
|
+
status: response.status,
|
|
459
|
+
duration_ms: ((Time.now - request_start) * 1000).round,
|
|
460
|
+
request_id: response.headers['x-request-id']
|
|
461
|
+
}.to_json)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def sanitize_headers(headers)
|
|
465
|
+
headers.transform_values do |value|
|
|
466
|
+
value.to_s.include?('Bearer') ? '[REDACTED]' : value
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def sanitize_params(params)
|
|
471
|
+
params.transform_values do |value|
|
|
472
|
+
sensitive_param?(value) ? '[REDACTED]' : value
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def sensitive_param?(value)
|
|
477
|
+
value.to_s.match?(/password|token|secret|key/i)
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
## Proxy Configuration
|
|
484
|
+
|
|
485
|
+
### Basic Proxy
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
module MyApi
|
|
489
|
+
include Resteze
|
|
490
|
+
|
|
491
|
+
configure do |config|
|
|
492
|
+
config.proxy = 'http://proxy.example.com:8080'
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Authenticated Proxy
|
|
498
|
+
|
|
499
|
+
```ruby
|
|
500
|
+
module MyApi
|
|
501
|
+
include Resteze
|
|
502
|
+
|
|
503
|
+
configure do |config|
|
|
504
|
+
config.proxy = {
|
|
505
|
+
uri: 'http://proxy.example.com:8080',
|
|
506
|
+
user: ENV['PROXY_USER'],
|
|
507
|
+
password: ENV['PROXY_PASSWORD']
|
|
508
|
+
}
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Conditional Proxy
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
module MyApi
|
|
517
|
+
include Resteze
|
|
518
|
+
|
|
519
|
+
configure do |config|
|
|
520
|
+
# Only use proxy in production
|
|
521
|
+
if Rails.env.production?
|
|
522
|
+
config.proxy = ENV['HTTP_PROXY']
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Or use different proxies per environment
|
|
526
|
+
config.proxy = case Rails.env
|
|
527
|
+
when 'production'
|
|
528
|
+
'http://prod-proxy.example.com:8080'
|
|
529
|
+
when 'staging'
|
|
530
|
+
'http://staging-proxy.example.com:8080'
|
|
531
|
+
else
|
|
532
|
+
nil # No proxy for development/test
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Advanced Configuration Patterns
|
|
539
|
+
|
|
540
|
+
### Configuration with Validation
|
|
541
|
+
|
|
542
|
+
```ruby
|
|
543
|
+
module MyApi
|
|
544
|
+
include Resteze
|
|
545
|
+
|
|
546
|
+
class Configuration
|
|
547
|
+
REQUIRED_SETTINGS = %i[api_base api_key].freeze
|
|
548
|
+
VALID_ENVIRONMENTS = %w[production staging development test].freeze
|
|
549
|
+
|
|
550
|
+
def self.validate!(config)
|
|
551
|
+
REQUIRED_SETTINGS.each do |setting|
|
|
552
|
+
value = config.send(setting)
|
|
553
|
+
if value.nil? || value.to_s.empty?
|
|
554
|
+
raise "Configuration error: #{setting} is required"
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Validate API base URL
|
|
559
|
+
unless config.api_base =~ URI::DEFAULT_PARSER.make_regexp
|
|
560
|
+
raise "Configuration error: api_base must be a valid URL"
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Validate environment
|
|
564
|
+
if config.respond_to?(:environment)
|
|
565
|
+
unless VALID_ENVIRONMENTS.include?(config.environment)
|
|
566
|
+
raise "Configuration error: invalid environment '#{config.environment}'"
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Validate timeouts
|
|
571
|
+
if config.open_timeout <= 0 || config.read_timeout <= 0
|
|
572
|
+
raise "Configuration error: timeouts must be positive"
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
configure do |config|
|
|
578
|
+
config.api_base = ENV['MY_API_URL']
|
|
579
|
+
config.api_key = ENV['MY_API_KEY']
|
|
580
|
+
config.open_timeout = 30
|
|
581
|
+
config.read_timeout = 60
|
|
582
|
+
|
|
583
|
+
# Validate configuration
|
|
584
|
+
Configuration.validate!(config)
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Feature Flags Configuration
|
|
590
|
+
|
|
591
|
+
```ruby
|
|
592
|
+
module MyApi
|
|
593
|
+
include Resteze
|
|
594
|
+
|
|
595
|
+
class << self
|
|
596
|
+
attr_accessor :features
|
|
597
|
+
|
|
598
|
+
def feature_enabled?(feature)
|
|
599
|
+
features && features[feature] == true
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
configure do |config|
|
|
604
|
+
config.api_base = 'https://api.example.com/'
|
|
605
|
+
|
|
606
|
+
# Feature flags
|
|
607
|
+
config.features = {
|
|
608
|
+
caching: Rails.env.production?,
|
|
609
|
+
retry: true,
|
|
610
|
+
circuit_breaker: Rails.env.production?,
|
|
611
|
+
detailed_logging: Rails.env.development?,
|
|
612
|
+
batch_requests: true
|
|
613
|
+
}
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
class Client < Resteze::Client
|
|
617
|
+
def execute_request(method, path, **options)
|
|
618
|
+
# Use feature flags
|
|
619
|
+
if api_module.feature_enabled?(:caching) && method == :get
|
|
620
|
+
cached_request(method, path, **options)
|
|
621
|
+
else
|
|
622
|
+
super
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
private
|
|
627
|
+
|
|
628
|
+
def cached_request(method, path, **options)
|
|
629
|
+
cache_key = "#{path}:#{options.hash}"
|
|
630
|
+
Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
|
|
631
|
+
super(method, path, **options)
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### Configuration Registry
|
|
639
|
+
|
|
640
|
+
```ruby
|
|
641
|
+
module MyApi
|
|
642
|
+
include Resteze
|
|
643
|
+
|
|
644
|
+
class ConfigurationRegistry
|
|
645
|
+
def self.configurations
|
|
646
|
+
@configurations ||= {}
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def self.register(name, &block)
|
|
650
|
+
configurations[name] = block
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def self.apply(name)
|
|
654
|
+
config_block = configurations[name]
|
|
655
|
+
raise "Unknown configuration: #{name}" unless config_block
|
|
656
|
+
|
|
657
|
+
MyApi.configure(&config_block)
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# Register configurations
|
|
662
|
+
ConfigurationRegistry.register(:production) do |config|
|
|
663
|
+
config.api_base = 'https://api.example.com/'
|
|
664
|
+
config.api_key = ENV['PROD_API_KEY']
|
|
665
|
+
config.open_timeout = 30
|
|
666
|
+
config.read_timeout = 60
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
ConfigurationRegistry.register(:development) do |config|
|
|
670
|
+
config.api_base = 'http://localhost:3000/'
|
|
671
|
+
config.api_key = 'dev-key'
|
|
672
|
+
config.open_timeout = 60
|
|
673
|
+
config.read_timeout = 120
|
|
674
|
+
config.logger = Logger.new($stdout)
|
|
675
|
+
config.logger.level = Logger::DEBUG
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Apply configuration
|
|
679
|
+
ConfigurationRegistry.apply(Rails.env.to_sym)
|
|
680
|
+
end
|
|
681
|
+
```
|