resteze 0.3.1 → 1.0.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +38 -0
  4. data/CLAUDE.md +83 -0
  5. data/README.md +75 -3
  6. data/docs/ACTIONS.md +329 -0
  7. data/docs/ADVANCED_USAGE.md +744 -0
  8. data/docs/API.md +404 -0
  9. data/docs/CONFIGURATION.md +962 -0
  10. data/docs/ERROR_HANDLING.md +676 -0
  11. data/docs/TESTING.md +789 -0
  12. data/lib/resteze/action.rb +99 -0
  13. data/lib/resteze/actions/validation.rb +56 -0
  14. data/lib/resteze/actions.rb +117 -0
  15. data/lib/resteze/api_module.rb +11 -5
  16. data/lib/resteze/api_resource.rb +28 -3
  17. data/lib/resteze/authenticating.rb +28 -0
  18. data/lib/resteze/authenticator/api_key.rb +45 -0
  19. data/lib/resteze/authenticator/basic.rb +40 -0
  20. data/lib/resteze/authenticator/bearer.rb +37 -0
  21. data/lib/resteze/authenticator/mtls.rb +73 -0
  22. data/lib/resteze/authenticator/strategy.rb +133 -0
  23. data/lib/resteze/authenticator.rb +187 -0
  24. data/lib/resteze/client.rb +60 -11
  25. data/lib/resteze/errors.rb +125 -4
  26. data/lib/resteze/instrumentation.rb +49 -0
  27. data/lib/resteze/list.rb +2 -2
  28. data/lib/resteze/middleware/raise_error.rb +38 -6
  29. data/lib/resteze/rate_limit.rb +44 -0
  30. data/lib/resteze/rate_limiter/fixed_window.rb +107 -0
  31. data/lib/resteze/rate_limiter/leaky_bucket.rb +40 -0
  32. data/lib/resteze/rate_limiter/sliding_window.rb +37 -0
  33. data/lib/resteze/rate_limiter/strategy.rb +58 -0
  34. data/lib/resteze/rate_limiter/token_bucket.rb +39 -0
  35. data/lib/resteze/rate_limiter.rb +229 -0
  36. data/lib/resteze/rate_limiting.rb +30 -0
  37. data/lib/resteze/response.rb +37 -1
  38. data/lib/resteze/result.rb +18 -0
  39. data/lib/resteze/save.rb +25 -3
  40. data/lib/resteze/synchronized_state.rb +29 -0
  41. data/lib/resteze/testing/minitest/configuration.rb +3 -1
  42. data/lib/resteze/testing/minitest/object.rb +30 -0
  43. data/lib/resteze/testing/rspec/configure.rb +3 -1
  44. data/lib/resteze/testing/rspec/object.rb +32 -2
  45. data/lib/resteze/version.rb +1 -1
  46. data/lib/resteze.rb +102 -10
  47. metadata +30 -2
data/docs/API.md ADDED
@@ -0,0 +1,404 @@
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
+ Declare extra endpoints with the `action` DSL. See [ACTIONS.md](ACTIONS.md) for
167
+ the full option table, re-routing the built-ins, and API-wide defaults.
168
+
169
+ ```ruby
170
+ module MyApi
171
+ class User < ApiResource
172
+ property :id
173
+ property :email
174
+ property :status
175
+
176
+ # Member action: user.activate(send_email: true) => POST /users/:id/activate
177
+ action :activate, method: :post, on: :member, returns: :self
178
+
179
+ # Collection action: User.search(q: "jane") => GET /users/search?q=jane
180
+ action :search, method: :get, returns: :self
181
+
182
+ # Anything the DSL does not express is still one request call away
183
+ def update_with_version(version, attributes)
184
+ request(
185
+ :patch,
186
+ resource_path,
187
+ params: attributes,
188
+ headers: { 'If-Match' => version }
189
+ )
190
+ end
191
+ end
192
+ end
193
+ ```
194
+
195
+ ### Direct Request Access
196
+
197
+ ```ruby
198
+ # Using the request module directly
199
+ MyApi::User.request(:get, '/custom/endpoint', params: { foo: 'bar' })
200
+
201
+ # Using the client directly
202
+ client = MyApi::Client.active_client
203
+ response = client.execute_request(
204
+ :post,
205
+ '/api/v1/users',
206
+ headers: { 'X-Custom-Header' => 'value' },
207
+ params: { email: 'user@example.com' }
208
+ )
209
+ ```
210
+
211
+ ## Response Handling
212
+
213
+ ### Response Object
214
+
215
+ ```ruby
216
+ response = MyApi::User.request(:get, '/users')
217
+
218
+ # Access response data
219
+ response.data # Parsed response body
220
+ response.http_status # HTTP status code
221
+ response.http_headers # Response headers
222
+ response.http_body # Raw response body
223
+ response.request_id # Request ID from headers (if present)
224
+ response.success? # true for any 2xx status
225
+ response.ok? # true only for 200
226
+ response.rate_limit # MyApi.rate_limiter.rate_limit(response): the strategy's view, or nil
227
+ ```
228
+
229
+ For APIs that report failures inside a successful response, override `MyApi::Response.body_error` to return an error to raise; see [Errors Reported in a 2xx Body](ERROR_HANDLING.md#errors-reported-in-a-2xx-body).
230
+
231
+ ### Working with Objects
232
+
233
+ ```ruby
234
+ user = MyApi::User.retrieve('123')
235
+
236
+ # Access properties
237
+ user.id
238
+ user.email
239
+ user[:email] # Hash-style access
240
+
241
+ # Check if persisted
242
+ user.persisted? # true if has an ID
243
+
244
+ # Access metadata (fields not defined as properties)
245
+ user.resteze_metadata # Hash of additional fields
246
+ user.property_bag # Hash of undefined properties
247
+
248
+ # Deep merge data
249
+ user.merge_from({ email: 'new@example.com', profile: { name: 'John' } })
250
+ ```
251
+
252
+ ### Property Transformation
253
+
254
+ ```ruby
255
+ module MyApi
256
+ class User < ApiResource
257
+ # Use Hashie transformations
258
+ property :email, from: :email_address
259
+ property :name, from: :full_name
260
+ property :active, from: :is_active, with: ->(v) { v == 'true' }
261
+
262
+ # Custom transformation method
263
+ property :created_at, transform_with: ->(v) { Time.parse(v) if v }
264
+ end
265
+ end
266
+ ```
267
+
268
+ ## Advanced Features
269
+
270
+ ### Custom Object Keys
271
+
272
+ When API responses wrap data in a specific key:
273
+
274
+ ```ruby
275
+ module MyApi
276
+ # Response format: { "user": { "id": 1, "email": "..." } }
277
+
278
+ def self.default_object_key(klass)
279
+ klass.name.demodulize.underscore
280
+ end
281
+
282
+ class User < ApiResource
283
+ property :id
284
+ property :email
285
+
286
+ # Or override per-class
287
+ def self.object_key
288
+ :account # Look for data in { "account": {...} }
289
+ end
290
+ end
291
+ end
292
+ ```
293
+
294
+ ### Custom API Keys
295
+
296
+ Transform property names between Ruby and API formats:
297
+
298
+ ```ruby
299
+ module MyApi
300
+ # Convert snake_case to camelCase
301
+ def self.default_api_key(attribute)
302
+ attribute.to_s.camelcase(:lower)
303
+ end
304
+
305
+ class User < ApiResource
306
+ property :first_name # Maps to "firstName" in API
307
+ property :last_name # Maps to "lastName" in API
308
+ end
309
+ end
310
+ ```
311
+
312
+ ### Thread Safety
313
+
314
+ Resteze uses thread-local storage for client management:
315
+
316
+ ```ruby
317
+ # Each thread gets its own client instance
318
+ Thread.new do
319
+ MyApi::Client.new(custom_connection).request do
320
+ # All requests in this block use the custom client
321
+ user = MyApi::User.retrieve('123')
322
+ end
323
+ end
324
+
325
+ # Default client is shared within a thread
326
+ MyApi::Client.active_client # Returns thread's active client
327
+ MyApi::Client.default_client # Returns thread's default client
328
+ ```
329
+
330
+ ### Custom Middleware
331
+
332
+ ```ruby
333
+ module MyApi
334
+ class Client < Resteze::Client
335
+ def self.default_connection
336
+ @default_connection ||= Faraday.new do |conn|
337
+ # Add custom middleware
338
+ conn.use MyCustomMiddleware
339
+ conn.request :json
340
+ conn.response :json
341
+ conn.use Faraday::Request::UrlEncoded
342
+ conn.use MyApi::Middleware::RaiseError
343
+ conn.adapter Faraday.default_adapter
344
+ end
345
+ end
346
+ end
347
+ end
348
+ ```
349
+
350
+ ### Connection Customization
351
+
352
+ ```ruby
353
+ # Create a custom connection
354
+ custom_conn = Faraday.new do |conn|
355
+ conn.request :retry, max: 3, interval: 0.5
356
+ conn.request :authorization, 'Bearer', -> { MyApi.api_key }
357
+ conn.adapter :typhoeus # Use Typhoeus adapter
358
+ end
359
+
360
+ # Use custom connection
361
+ client = MyApi::Client.new(custom_conn)
362
+ client.request do
363
+ user = MyApi::User.retrieve('123')
364
+ end
365
+ ```
366
+
367
+ ### Logging
368
+
369
+ ```ruby
370
+ module MyApi
371
+ configure do |config|
372
+ # Use a custom logger
373
+ config.logger = Logger.new('api.log')
374
+ config.logger.level = Logger::DEBUG # Show detailed request/response
375
+ end
376
+
377
+ class Client < Resteze::Client
378
+ # Override logging behavior
379
+ def log_request(context)
380
+ super
381
+ # Add custom logging
382
+ logger.info "Custom: #{context.inspect}"
383
+ end
384
+ end
385
+ end
386
+ ```
387
+
388
+ ### Proxy Support
389
+
390
+ ```ruby
391
+ module MyApi
392
+ configure do |config|
393
+ # Simple proxy
394
+ config.proxy = 'http://proxy.example.com:8080'
395
+
396
+ # Proxy with authentication
397
+ config.proxy = {
398
+ uri: 'http://proxy.example.com:8080',
399
+ user: 'username',
400
+ password: 'password'
401
+ }
402
+ end
403
+ end
404
+ ```