resteze 0.3.1 → 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/CLAUDE.md +74 -0
- data/README.md +31 -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/version.rb +1 -1
- data/lib/resteze.rb +2 -0
- metadata +9 -2
data/docs/API.md
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# Resteze API Documentation
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Core Concepts](#core-concepts)
|
|
5
|
+
- [Basic Setup](#basic-setup)
|
|
6
|
+
- [Resource Definition](#resource-definition)
|
|
7
|
+
- [Making Requests](#making-requests)
|
|
8
|
+
- [Response Handling](#response-handling)
|
|
9
|
+
- [Advanced Features](#advanced-features)
|
|
10
|
+
|
|
11
|
+
## Core Concepts
|
|
12
|
+
|
|
13
|
+
Resteze provides a framework for building REST API client gems with the following core components:
|
|
14
|
+
|
|
15
|
+
### ApiModule
|
|
16
|
+
The foundation that gets included in your API namespace module. It sets up the infrastructure for your API client.
|
|
17
|
+
|
|
18
|
+
### ApiResource
|
|
19
|
+
Base class for all your API resources. Inherits from `Resteze::Object` (which extends `Hashie::Trash`) providing property management and data transformation capabilities.
|
|
20
|
+
|
|
21
|
+
### Client
|
|
22
|
+
Manages HTTP connections and request execution. Thread-safe and supports connection customization.
|
|
23
|
+
|
|
24
|
+
### Request
|
|
25
|
+
Module that provides request functionality to resources, delegating to the active client.
|
|
26
|
+
|
|
27
|
+
## Basic Setup
|
|
28
|
+
|
|
29
|
+
### Creating Your API Module
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require 'resteze'
|
|
33
|
+
|
|
34
|
+
module MyApi
|
|
35
|
+
include Resteze
|
|
36
|
+
|
|
37
|
+
# Basic configuration
|
|
38
|
+
configure do |config|
|
|
39
|
+
config.api_base = 'https://api.example.com/'
|
|
40
|
+
config.open_timeout = 30 # seconds
|
|
41
|
+
config.read_timeout = 60 # seconds
|
|
42
|
+
config.logger = Logger.new($stdout)
|
|
43
|
+
config.proxy = 'http://proxy.example.com:8080' # optional
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Custom Configuration Properties
|
|
49
|
+
|
|
50
|
+
You can add custom configuration properties to your API module:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
module MyApi
|
|
54
|
+
include Resteze
|
|
55
|
+
|
|
56
|
+
class << self
|
|
57
|
+
attr_accessor :api_key, :api_secret, :environment
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
configure do |config|
|
|
61
|
+
config.api_base = 'https://api.example.com/'
|
|
62
|
+
config.api_key = ENV['MY_API_KEY']
|
|
63
|
+
config.api_secret = ENV['MY_API_SECRET']
|
|
64
|
+
config.environment = :production
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Resource Definition
|
|
70
|
+
|
|
71
|
+
### Basic Resource
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
module MyApi
|
|
75
|
+
class User < ApiResource
|
|
76
|
+
# Define properties that map to API response fields
|
|
77
|
+
property :id
|
|
78
|
+
property :email
|
|
79
|
+
property :name
|
|
80
|
+
property :created_at
|
|
81
|
+
property :updated_at
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Resource with Custom Paths
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
module MyApi
|
|
90
|
+
class User < ApiResource
|
|
91
|
+
property :id
|
|
92
|
+
property :email
|
|
93
|
+
|
|
94
|
+
# Override the resource slug (defaults to pluralized class name)
|
|
95
|
+
def self.resource_slug
|
|
96
|
+
'accounts' # Use /accounts instead of /users
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Override the entire resource path
|
|
100
|
+
def self.resource_path(id = nil)
|
|
101
|
+
if id
|
|
102
|
+
"/v2/accounts/#{CGI.escape(id.to_s)}"
|
|
103
|
+
else
|
|
104
|
+
"/v2/accounts"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Custom service path
|
|
109
|
+
def self.service_path
|
|
110
|
+
'/api'
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# API version
|
|
114
|
+
def self.api_version
|
|
115
|
+
'v2'
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Nested Resources
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
module MyApi
|
|
125
|
+
class Comment < ApiResource
|
|
126
|
+
property :id
|
|
127
|
+
property :post_id
|
|
128
|
+
property :content
|
|
129
|
+
property :author
|
|
130
|
+
|
|
131
|
+
def self.resource_path(id = nil, post_id: nil)
|
|
132
|
+
if post_id
|
|
133
|
+
"/posts/#{post_id}/comments#{id ? "/#{id}" : ""}"
|
|
134
|
+
else
|
|
135
|
+
super(id)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Retrieve comments for a specific post
|
|
140
|
+
def self.list_by_post(post_id)
|
|
141
|
+
request(:get, resource_path(nil, post_id: post_id))
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Making Requests
|
|
148
|
+
|
|
149
|
+
### Retrieving Resources
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# Get a single resource by ID
|
|
153
|
+
user = MyApi::User.retrieve('123')
|
|
154
|
+
puts user.email
|
|
155
|
+
|
|
156
|
+
# With additional parameters
|
|
157
|
+
user = MyApi::User.new('123', values: { include: 'profile' })
|
|
158
|
+
user.refresh # Makes the API call
|
|
159
|
+
|
|
160
|
+
# Refresh existing resource
|
|
161
|
+
user.refresh # Re-fetches from API
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Custom Request Methods
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
module MyApi
|
|
168
|
+
class User < ApiResource
|
|
169
|
+
property :id
|
|
170
|
+
property :email
|
|
171
|
+
property :status
|
|
172
|
+
|
|
173
|
+
# Instance method for custom action
|
|
174
|
+
def activate!
|
|
175
|
+
response = request(
|
|
176
|
+
:post,
|
|
177
|
+
"#{resource_path}/activate",
|
|
178
|
+
params: { send_email: true }
|
|
179
|
+
)
|
|
180
|
+
initialize_from(response.data)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Class method for custom endpoint
|
|
184
|
+
def self.search(query)
|
|
185
|
+
response = request(
|
|
186
|
+
:get,
|
|
187
|
+
"#{resource_path}/search",
|
|
188
|
+
params: { q: query }
|
|
189
|
+
)
|
|
190
|
+
response.data.map { |attrs| construct_from(attrs) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Custom headers
|
|
194
|
+
def update_with_version(version, attributes)
|
|
195
|
+
request(
|
|
196
|
+
:patch,
|
|
197
|
+
resource_path,
|
|
198
|
+
params: attributes,
|
|
199
|
+
headers: { 'If-Match' => version }
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Direct Request Access
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# Using the request module directly
|
|
210
|
+
MyApi::User.request(:get, '/custom/endpoint', params: { foo: 'bar' })
|
|
211
|
+
|
|
212
|
+
# Using the client directly
|
|
213
|
+
client = MyApi::Client.active_client
|
|
214
|
+
response = client.execute_request(
|
|
215
|
+
:post,
|
|
216
|
+
'/api/v1/users',
|
|
217
|
+
headers: { 'X-Custom-Header' => 'value' },
|
|
218
|
+
params: { email: 'user@example.com' }
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Response Handling
|
|
223
|
+
|
|
224
|
+
### Response Object
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
response = MyApi::User.request(:get, '/users')
|
|
228
|
+
|
|
229
|
+
# Access response data
|
|
230
|
+
response.data # Parsed response body
|
|
231
|
+
response.status # HTTP status code
|
|
232
|
+
response.headers # Response headers
|
|
233
|
+
response.body # Raw response body
|
|
234
|
+
response.request_id # Request ID from headers (if present)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Working with Objects
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
user = MyApi::User.retrieve('123')
|
|
241
|
+
|
|
242
|
+
# Access properties
|
|
243
|
+
user.id
|
|
244
|
+
user.email
|
|
245
|
+
user[:email] # Hash-style access
|
|
246
|
+
|
|
247
|
+
# Check if persisted
|
|
248
|
+
user.persisted? # true if has an ID
|
|
249
|
+
|
|
250
|
+
# Access metadata (fields not defined as properties)
|
|
251
|
+
user.resteze_metadata # Hash of additional fields
|
|
252
|
+
user.property_bag # Hash of undefined properties
|
|
253
|
+
|
|
254
|
+
# Deep merge data
|
|
255
|
+
user.merge_from({ email: 'new@example.com', profile: { name: 'John' } })
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Property Transformation
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
module MyApi
|
|
262
|
+
class User < ApiResource
|
|
263
|
+
# Use Hashie transformations
|
|
264
|
+
property :email, from: :email_address
|
|
265
|
+
property :name, from: :full_name
|
|
266
|
+
property :active, from: :is_active, with: ->(v) { v == 'true' }
|
|
267
|
+
|
|
268
|
+
# Custom transformation method
|
|
269
|
+
property :created_at, transform_with: ->(v) { Time.parse(v) if v }
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Advanced Features
|
|
275
|
+
|
|
276
|
+
### Custom Object Keys
|
|
277
|
+
|
|
278
|
+
When API responses wrap data in a specific key:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
module MyApi
|
|
282
|
+
# Response format: { "user": { "id": 1, "email": "..." } }
|
|
283
|
+
|
|
284
|
+
def self.default_object_key(klass)
|
|
285
|
+
klass.name.demodulize.underscore
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
class User < ApiResource
|
|
289
|
+
property :id
|
|
290
|
+
property :email
|
|
291
|
+
|
|
292
|
+
# Or override per-class
|
|
293
|
+
def self.object_key
|
|
294
|
+
:account # Look for data in { "account": {...} }
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Custom API Keys
|
|
301
|
+
|
|
302
|
+
Transform property names between Ruby and API formats:
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
module MyApi
|
|
306
|
+
# Convert snake_case to camelCase
|
|
307
|
+
def self.default_api_key(attribute)
|
|
308
|
+
attribute.to_s.camelcase(:lower)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
class User < ApiResource
|
|
312
|
+
property :first_name # Maps to "firstName" in API
|
|
313
|
+
property :last_name # Maps to "lastName" in API
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Thread Safety
|
|
319
|
+
|
|
320
|
+
Resteze uses thread-local storage for client management:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
# Each thread gets its own client instance
|
|
324
|
+
Thread.new do
|
|
325
|
+
MyApi::Client.new(custom_connection).request do
|
|
326
|
+
# All requests in this block use the custom client
|
|
327
|
+
user = MyApi::User.retrieve('123')
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Default client is shared within a thread
|
|
332
|
+
MyApi::Client.active_client # Returns thread's active client
|
|
333
|
+
MyApi::Client.default_client # Returns thread's default client
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Custom Middleware
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
module MyApi
|
|
340
|
+
class Client < Resteze::Client
|
|
341
|
+
def self.default_connection
|
|
342
|
+
@default_connection ||= Faraday.new do |conn|
|
|
343
|
+
# Add custom middleware
|
|
344
|
+
conn.use MyCustomMiddleware
|
|
345
|
+
conn.request :json
|
|
346
|
+
conn.response :json
|
|
347
|
+
conn.use Faraday::Request::UrlEncoded
|
|
348
|
+
conn.use MyApi::Middleware::RaiseError
|
|
349
|
+
conn.adapter Faraday.default_adapter
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Connection Customization
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
# Create a custom connection
|
|
360
|
+
custom_conn = Faraday.new do |conn|
|
|
361
|
+
conn.request :retry, max: 3, interval: 0.5
|
|
362
|
+
conn.request :authorization, 'Bearer', -> { MyApi.api_key }
|
|
363
|
+
conn.adapter :typhoeus # Use Typhoeus adapter
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Use custom connection
|
|
367
|
+
client = MyApi::Client.new(custom_conn)
|
|
368
|
+
client.request do
|
|
369
|
+
user = MyApi::User.retrieve('123')
|
|
370
|
+
end
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Logging
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
module MyApi
|
|
377
|
+
configure do |config|
|
|
378
|
+
# Use a custom logger
|
|
379
|
+
config.logger = Logger.new('api.log')
|
|
380
|
+
config.logger.level = Logger::DEBUG # Show detailed request/response
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
class Client < Resteze::Client
|
|
384
|
+
# Override logging behavior
|
|
385
|
+
def log_request(context)
|
|
386
|
+
super
|
|
387
|
+
# Add custom logging
|
|
388
|
+
logger.info "Custom: #{context.inspect}"
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Proxy Support
|
|
395
|
+
|
|
396
|
+
```ruby
|
|
397
|
+
module MyApi
|
|
398
|
+
configure do |config|
|
|
399
|
+
# Simple proxy
|
|
400
|
+
config.proxy = 'http://proxy.example.com:8080'
|
|
401
|
+
|
|
402
|
+
# Proxy with authentication
|
|
403
|
+
config.proxy = {
|
|
404
|
+
uri: 'http://proxy.example.com:8080',
|
|
405
|
+
user: 'username',
|
|
406
|
+
password: 'password'
|
|
407
|
+
}
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
```
|