respondo 1.0.0 → 2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +150 -0
- data/README.md +416 -188
- data/lib/respondo/controller_helpers.rb +232 -27
- data/lib/respondo/pagination.rb +141 -83
- data/lib/respondo/response_builder.rb +8 -12
- data/lib/respondo/version.rb +1 -1
- data/lib/respondo.rb +1 -1
- metadata +2 -2
data/README.md
CHANGED
|
@@ -60,23 +60,54 @@ Respondo auto-includes itself into `ActionController::Base` and `ActionControlle
|
|
|
60
60
|
|
|
61
61
|
Every single response — success or error — returns the same four keys:
|
|
62
62
|
|
|
63
|
-
| Key | Type | Description
|
|
64
|
-
|
|
65
|
-
| `success` | Boolean | `true` or `false`
|
|
66
|
-
| `message` | String | Human-readable description
|
|
67
|
-
| `data` | Object/Array/nil | The payload
|
|
68
|
-
| `meta` | Object | Timestamp + pagination + optional request_id |
|
|
63
|
+
| Key | Type | Description |
|
|
64
|
+
|-----------|------------------|------------------------------------------------------|
|
|
65
|
+
| `success` | Boolean | `true` or `false` |
|
|
66
|
+
| `message` | String | Human-readable description |
|
|
67
|
+
| `data` | Object/Array/nil | The payload |
|
|
68
|
+
| `meta` | Object | Timestamp + pagination (if passed) + optional request_id |
|
|
69
69
|
|
|
70
70
|
Error responses additionally include:
|
|
71
71
|
|
|
72
72
|
| Key | Type | Description |
|
|
73
73
|
|----------|------|--------------------------------------|
|
|
74
74
|
| `errors` | Hash | Field-level errors `{field: [msgs]}` |
|
|
75
|
+
| `errors` | Hash | Field-level errors `{field: msgs}` |
|
|
75
76
|
|
|
76
77
|
---
|
|
77
78
|
|
|
78
79
|
## Complete Helper Reference
|
|
79
80
|
|
|
81
|
+
### 1xx — Informational Helpers
|
|
82
|
+
|
|
83
|
+
> 1xx responses are protocol-level and carry no body in standard HTTP/1.1. Respondo still returns a JSON body for API logging consistency. Use with care.
|
|
84
|
+
|
|
85
|
+
#### `render_continue` — 100
|
|
86
|
+
```ruby
|
|
87
|
+
render_continue
|
|
88
|
+
render_continue(message: "Continue sending request body")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### `render_switching_protocols` — 101
|
|
92
|
+
```ruby
|
|
93
|
+
render_switching_protocols
|
|
94
|
+
render_switching_protocols(message: "Upgrading to WebSocket")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### `render_processing` — 102
|
|
98
|
+
```ruby
|
|
99
|
+
render_processing
|
|
100
|
+
render_processing(message: "Working on it — please wait")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### `render_early_hints` — 103
|
|
104
|
+
```ruby
|
|
105
|
+
render_early_hints
|
|
106
|
+
render_early_hints(message: "Early hints provided", meta: { link: "</style.css>; rel=preload" })
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
80
111
|
### 2xx — Success Helpers
|
|
81
112
|
|
|
82
113
|
#### `render_ok` — 200 OK
|
|
@@ -102,6 +133,13 @@ render_accepted(message: "Your export is being processed. You will receive an em
|
|
|
102
133
|
render_accepted(data: { job_id: "abc123" }, message: "Job queued")
|
|
103
134
|
```
|
|
104
135
|
|
|
136
|
+
#### `render_non_authoritative` — 203 Non-Authoritative Information
|
|
137
|
+
Use when data comes from a third-party or cache rather than the origin server.
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
render_non_authoritative(data: @user, message: "Data sourced from cache")
|
|
141
|
+
```
|
|
142
|
+
|
|
105
143
|
#### `render_no_content` — 200 OK
|
|
106
144
|
Use after DELETE or actions with no meaningful response body. Returns standard JSON structure for consistency.
|
|
107
145
|
|
|
@@ -110,6 +148,13 @@ render_no_content # "Deleted successfully"
|
|
|
110
148
|
render_no_content(message: "Account deactivated")
|
|
111
149
|
```
|
|
112
150
|
|
|
151
|
+
#### `render_reset_content` — 205 Reset Content
|
|
152
|
+
Tell the client to reset the document view (e.g. clear a form after submission).
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
render_reset_content(message: "Form submitted — please reset the view")
|
|
156
|
+
```
|
|
157
|
+
|
|
113
158
|
#### `render_partial_content` — 206 Partial Content
|
|
114
159
|
Use for chunked or range-based responses.
|
|
115
160
|
|
|
@@ -128,138 +173,216 @@ render_multi_status(
|
|
|
128
173
|
)
|
|
129
174
|
```
|
|
130
175
|
|
|
176
|
+
#### `render_already_reported` — 208 Already Reported
|
|
177
|
+
```ruby
|
|
178
|
+
render_already_reported(data: @resource, message: "Already reported in this binding")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### `render_im_used` — 226 IM Used
|
|
182
|
+
```ruby
|
|
183
|
+
render_im_used(data: @resource, message: "Delta encoding applied")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### 3xx — Redirect Helpers
|
|
189
|
+
|
|
190
|
+
> These return a JSON body so your API can communicate redirect intent with context.
|
|
191
|
+
> Pass the target URL via `meta: { redirect_url: "..." }`.
|
|
192
|
+
|
|
193
|
+
#### `render_multiple_choices` — 300
|
|
194
|
+
```ruby
|
|
195
|
+
render_multiple_choices(
|
|
196
|
+
data: [{ format: "json", url: "/resource.json" }, { format: "xml", url: "/resource.xml" }],
|
|
197
|
+
message: "Multiple representations available"
|
|
198
|
+
)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### `render_moved_permanently` — 301
|
|
202
|
+
```ruby
|
|
203
|
+
render_moved_permanently(message: "This endpoint has moved", meta: { redirect_url: new_url })
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `render_found` — 302
|
|
207
|
+
```ruby
|
|
208
|
+
render_found(message: "Resource temporarily at another URL", meta: { redirect_url: temp_url })
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### `render_see_other` — 303
|
|
212
|
+
```ruby
|
|
213
|
+
render_see_other(message: "See the canonical resource", meta: { redirect_url: canonical_url })
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### `render_not_modified` — 304
|
|
217
|
+
```ruby
|
|
218
|
+
render_not_modified(message: "Your cached version is still valid")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### `render_temporary_redirect` — 307
|
|
222
|
+
```ruby
|
|
223
|
+
render_temporary_redirect(message: "Repeat this request at the redirect URL", meta: { redirect_url: url })
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### `render_permanent_redirect` — 308
|
|
227
|
+
```ruby
|
|
228
|
+
render_permanent_redirect(message: "Permanently moved — update your bookmarks", meta: { redirect_url: url })
|
|
229
|
+
```
|
|
230
|
+
|
|
131
231
|
---
|
|
132
232
|
|
|
133
233
|
### 4xx — Client Error Helpers
|
|
134
234
|
|
|
135
235
|
#### `render_bad_request` — 400 Bad Request
|
|
136
|
-
Malformed request, missing required parameters, invalid format.
|
|
137
|
-
|
|
138
236
|
```ruby
|
|
139
|
-
render_bad_request
|
|
140
|
-
render_bad_request("
|
|
141
|
-
render_bad_request("Invalid input", errors: { date: ["must be a valid date"] })
|
|
142
|
-
render_bad_request("Invalid input", code: "INVALID_FORMAT") # custom error code
|
|
237
|
+
render_bad_request(message: "The 'date' parameter is required")
|
|
238
|
+
render_bad_request(message: "Invalid input", errors: { date: ["must be a valid date"] })
|
|
143
239
|
```
|
|
144
240
|
|
|
145
241
|
#### `render_unauthorized` — 401 Unauthorized
|
|
146
|
-
User is not authenticated. Use when no valid token/session is present.
|
|
147
|
-
|
|
148
242
|
```ruby
|
|
149
|
-
render_unauthorized
|
|
150
|
-
render_unauthorized("
|
|
151
|
-
render_unauthorized("Token has expired", code: "TOKEN_EXPIRED")
|
|
243
|
+
render_unauthorized(message: "Please log in to continue")
|
|
244
|
+
render_unauthorized(message: "Token has expired")
|
|
152
245
|
```
|
|
153
246
|
|
|
154
247
|
#### `render_payment_required` — 402 Payment Required
|
|
155
|
-
Feature is behind a paywall or subscription.
|
|
156
|
-
|
|
157
248
|
```ruby
|
|
158
|
-
render_payment_required
|
|
159
|
-
render_payment_required("Upgrade to Pro to access this feature")
|
|
160
|
-
render_payment_required("Subscription expired", code: "SUBSCRIPTION_EXPIRED")
|
|
249
|
+
render_payment_required(message: "Upgrade to Pro to access this feature")
|
|
161
250
|
```
|
|
162
251
|
|
|
163
252
|
#### `render_forbidden` — 403 Forbidden
|
|
164
|
-
User is authenticated but lacks permission for this action.
|
|
165
|
-
|
|
166
253
|
```ruby
|
|
167
|
-
render_forbidden
|
|
168
|
-
render_forbidden("You can only edit your own posts")
|
|
169
|
-
render_forbidden("Admin access required", code: "ADMIN_REQUIRED")
|
|
254
|
+
render_forbidden(message: "You can only edit your own posts")
|
|
170
255
|
```
|
|
171
256
|
|
|
172
257
|
#### `render_not_found` — 404 Not Found
|
|
173
|
-
|
|
258
|
+
```ruby
|
|
259
|
+
render_not_found(message: "User not found")
|
|
260
|
+
render_not_found(message: "Post ##{params[:id]} does not exist")
|
|
261
|
+
```
|
|
174
262
|
|
|
263
|
+
#### `render_method_not_allowed` — 405
|
|
175
264
|
```ruby
|
|
176
|
-
|
|
177
|
-
render_not_found("User not found")
|
|
178
|
-
render_not_found("Post ##{params[:id]} does not exist")
|
|
265
|
+
render_method_not_allowed(message: "This endpoint only accepts POST requests")
|
|
179
266
|
```
|
|
180
267
|
|
|
181
|
-
#### `
|
|
182
|
-
|
|
268
|
+
#### `render_not_acceptable` — 406
|
|
269
|
+
```ruby
|
|
270
|
+
render_not_acceptable(message: "Only application/json is supported")
|
|
271
|
+
```
|
|
183
272
|
|
|
273
|
+
#### `render_proxy_auth_required` — 407
|
|
184
274
|
```ruby
|
|
185
|
-
|
|
186
|
-
render_method_not_allowed("This endpoint only accepts POST requests")
|
|
275
|
+
render_proxy_auth_required(message: "Authenticate with the proxy first")
|
|
187
276
|
```
|
|
188
277
|
|
|
189
|
-
#### `
|
|
190
|
-
|
|
278
|
+
#### `render_request_timeout` — 408
|
|
279
|
+
```ruby
|
|
280
|
+
render_request_timeout(message: "The query took too long. Try a smaller date range.")
|
|
281
|
+
```
|
|
191
282
|
|
|
283
|
+
#### `render_conflict` — 409 Conflict
|
|
192
284
|
```ruby
|
|
193
|
-
|
|
194
|
-
|
|
285
|
+
render_conflict(message: "Email address is already registered")
|
|
286
|
+
render_conflict(message: "Duplicate entry", errors: { email: ["has already been taken"] })
|
|
195
287
|
```
|
|
196
288
|
|
|
197
|
-
#### `
|
|
198
|
-
|
|
289
|
+
#### `render_gone` — 410 Gone
|
|
290
|
+
```ruby
|
|
291
|
+
render_gone(message: "This account has been permanently deleted")
|
|
292
|
+
```
|
|
199
293
|
|
|
294
|
+
#### `render_length_required` — 411
|
|
200
295
|
```ruby
|
|
201
|
-
|
|
202
|
-
render_request_timeout("The query took too long. Try a smaller date range.")
|
|
296
|
+
render_length_required(message: "Content-Length header is required")
|
|
203
297
|
```
|
|
204
298
|
|
|
205
|
-
#### `
|
|
206
|
-
|
|
299
|
+
#### `render_precondition_failed` — 412
|
|
300
|
+
```ruby
|
|
301
|
+
render_precondition_failed(message: "Resource has been modified since your last request")
|
|
302
|
+
```
|
|
207
303
|
|
|
304
|
+
#### `render_payload_too_large` — 413
|
|
208
305
|
```ruby
|
|
209
|
-
|
|
210
|
-
render_conflict("Email address is already registered")
|
|
211
|
-
render_conflict("Cannot cancel a completed order", code: "INVALID_STATE_TRANSITION")
|
|
212
|
-
render_conflict("Duplicate entry", errors: { email: ["has already been taken"] })
|
|
306
|
+
render_payload_too_large(message: "File exceeds the 10 MB upload limit")
|
|
213
307
|
```
|
|
214
308
|
|
|
215
|
-
#### `
|
|
216
|
-
|
|
309
|
+
#### `render_uri_too_long` — 414
|
|
310
|
+
```ruby
|
|
311
|
+
render_uri_too_long(message: "That URL is too long to process")
|
|
312
|
+
```
|
|
217
313
|
|
|
314
|
+
#### `render_unsupported_media_type` — 415
|
|
218
315
|
```ruby
|
|
219
|
-
|
|
220
|
-
render_gone("This account has been permanently deleted")
|
|
316
|
+
render_unsupported_media_type(message: "Please send requests as application/json")
|
|
221
317
|
```
|
|
222
318
|
|
|
223
|
-
#### `
|
|
224
|
-
|
|
319
|
+
#### `render_range_not_satisfiable` — 416
|
|
320
|
+
```ruby
|
|
321
|
+
render_range_not_satisfiable(message: "Requested byte range is out of bounds")
|
|
322
|
+
```
|
|
225
323
|
|
|
324
|
+
#### `render_expectation_failed` — 417
|
|
226
325
|
```ruby
|
|
227
|
-
|
|
228
|
-
render_precondition_failed("Resource has been modified since your last request")
|
|
326
|
+
render_expectation_failed(message: "Expect header value cannot be met")
|
|
229
327
|
```
|
|
230
328
|
|
|
231
|
-
#### `
|
|
232
|
-
|
|
329
|
+
#### `render_im_a_teapot` — 418
|
|
330
|
+
```ruby
|
|
331
|
+
render_im_a_teapot(message: "I'm a teapot — I cannot brew coffee")
|
|
332
|
+
```
|
|
233
333
|
|
|
334
|
+
#### `render_misdirected_request` — 421
|
|
234
335
|
```ruby
|
|
235
|
-
|
|
236
|
-
render_unsupported_media_type("Please send requests as application/json")
|
|
336
|
+
render_misdirected_request(message: "Request sent to the wrong server")
|
|
237
337
|
```
|
|
238
338
|
|
|
239
339
|
#### `render_unprocessable` — 422 Unprocessable Entity
|
|
240
340
|
Validation errors. The most commonly used error helper in Rails APIs.
|
|
241
341
|
|
|
242
342
|
```ruby
|
|
243
|
-
render_unprocessable("Validation failed", errors: user.errors)
|
|
244
|
-
render_unprocessable("Invalid data", errors: { name: ["can't be blank"]
|
|
343
|
+
render_unprocessable(message: "Validation failed", errors: user.errors)
|
|
344
|
+
render_unprocessable(message: "Invalid data", errors: { name: ["can't be blank"] })
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
#### `render_locked` — 423
|
|
348
|
+
```ruby
|
|
349
|
+
render_locked(message: "This record is locked by another user")
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
#### `render_failed_dependency` — 424
|
|
353
|
+
```ruby
|
|
354
|
+
render_failed_dependency(message: "Prerequisite resource creation failed")
|
|
245
355
|
```
|
|
246
356
|
|
|
247
|
-
#### `
|
|
248
|
-
|
|
357
|
+
#### `render_too_early` — 425
|
|
358
|
+
```ruby
|
|
359
|
+
render_too_early(message: "Request may be a replay — rejected for safety")
|
|
360
|
+
```
|
|
249
361
|
|
|
362
|
+
#### `render_upgrade_required` — 426
|
|
250
363
|
```ruby
|
|
251
|
-
|
|
252
|
-
render_locked("This record is locked by another user")
|
|
253
|
-
render_locked("Invoice is locked after approval", code: "INVOICE_LOCKED")
|
|
364
|
+
render_upgrade_required(message: "Please upgrade to TLS 1.3")
|
|
254
365
|
```
|
|
255
366
|
|
|
256
|
-
#### `
|
|
257
|
-
|
|
367
|
+
#### `render_precondition_required` — 428
|
|
368
|
+
```ruby
|
|
369
|
+
render_precondition_required(message: "Include an If-Match header with your request")
|
|
370
|
+
```
|
|
258
371
|
|
|
372
|
+
#### `render_too_many_requests` — 429
|
|
259
373
|
```ruby
|
|
260
|
-
render_too_many_requests
|
|
261
|
-
render_too_many_requests("
|
|
262
|
-
|
|
374
|
+
render_too_many_requests(message: "You have exceeded 100 requests per minute.")
|
|
375
|
+
render_too_many_requests(message: "Rate limit hit", meta: { retry_after: 60 })
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
#### `render_request_header_fields_too_large` — 431
|
|
379
|
+
```ruby
|
|
380
|
+
render_request_header_fields_too_large(message: "Cookie header is too large")
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
#### `render_unavailable_for_legal_reasons` — 451
|
|
384
|
+
```ruby
|
|
385
|
+
render_unavailable_for_legal_reasons(message: "This content is blocked in your region")
|
|
263
386
|
```
|
|
264
387
|
|
|
265
388
|
---
|
|
@@ -267,11 +390,8 @@ render_too_many_requests("API limit reached", code: "API_LIMIT_EXCEEDED")
|
|
|
267
390
|
### 5xx — Server Error Helpers
|
|
268
391
|
|
|
269
392
|
#### `render_server_error` — 500 Internal Server Error
|
|
270
|
-
An unexpected error occurred on the server.
|
|
271
|
-
|
|
272
393
|
```ruby
|
|
273
|
-
render_server_error
|
|
274
|
-
render_server_error("Something went wrong. Our team has been notified.")
|
|
394
|
+
render_server_error(message: "Something went wrong. Our team has been notified.")
|
|
275
395
|
|
|
276
396
|
# Common pattern — rescue unexpected exceptions
|
|
277
397
|
rescue StandardError => e
|
|
@@ -279,37 +399,55 @@ rescue StandardError => e
|
|
|
279
399
|
render_server_error("An unexpected error occurred")
|
|
280
400
|
```
|
|
281
401
|
|
|
282
|
-
#### `render_not_implemented` — 501
|
|
283
|
-
|
|
402
|
+
#### `render_not_implemented` — 501
|
|
403
|
+
```ruby
|
|
404
|
+
render_not_implemented(message: "CSV export is coming soon")
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### `render_bad_gateway` — 502
|
|
408
|
+
```ruby
|
|
409
|
+
render_bad_gateway(message: "Payment gateway is currently unavailable")
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
#### `render_service_unavailable` — 503
|
|
413
|
+
```ruby
|
|
414
|
+
render_service_unavailable(message: "Down for maintenance. Back in 30 minutes.")
|
|
415
|
+
render_service_unavailable(message: "Maintenance window", meta: { retry_after: 1800 })
|
|
416
|
+
```
|
|
284
417
|
|
|
418
|
+
#### `render_gateway_timeout` — 504
|
|
285
419
|
```ruby
|
|
286
|
-
|
|
287
|
-
render_not_implemented("CSV export is coming soon")
|
|
420
|
+
render_gateway_timeout(message: "The payment processor did not respond in time.")
|
|
288
421
|
```
|
|
289
422
|
|
|
290
|
-
#### `
|
|
291
|
-
|
|
423
|
+
#### `render_http_version_not_supported` — 505
|
|
424
|
+
```ruby
|
|
425
|
+
render_http_version_not_supported(message: "Only HTTP/1.1 and HTTP/2 are supported")
|
|
426
|
+
```
|
|
292
427
|
|
|
428
|
+
#### `render_variant_also_negotiates` — 506
|
|
293
429
|
```ruby
|
|
294
|
-
|
|
295
|
-
render_bad_gateway("Payment gateway is currently unavailable")
|
|
296
|
-
render_bad_gateway("Could not reach the SMS service", code: "SMS_GATEWAY_ERROR")
|
|
430
|
+
render_variant_also_negotiates(message: "Server content-negotiation loop detected")
|
|
297
431
|
```
|
|
298
432
|
|
|
299
|
-
#### `
|
|
300
|
-
|
|
433
|
+
#### `render_insufficient_storage` — 507
|
|
434
|
+
```ruby
|
|
435
|
+
render_insufficient_storage(message: "Disk quota exceeded on this node")
|
|
436
|
+
```
|
|
301
437
|
|
|
438
|
+
#### `render_loop_detected` — 508
|
|
302
439
|
```ruby
|
|
303
|
-
|
|
304
|
-
render_service_unavailable("We are currently under maintenance. Back in 30 minutes.")
|
|
440
|
+
render_loop_detected(message: "Infinite redirect loop detected")
|
|
305
441
|
```
|
|
306
442
|
|
|
307
|
-
#### `
|
|
308
|
-
|
|
443
|
+
#### `render_not_extended` — 510
|
|
444
|
+
```ruby
|
|
445
|
+
render_not_extended(message: "Further extensions required to fulfil this request")
|
|
446
|
+
```
|
|
309
447
|
|
|
448
|
+
#### `render_network_authentication_required` — 511
|
|
310
449
|
```ruby
|
|
311
|
-
|
|
312
|
-
render_gateway_timeout("The payment processor did not respond in time. Please try again.")
|
|
450
|
+
render_network_authentication_required(message: "Sign in to the network portal first")
|
|
313
451
|
```
|
|
314
452
|
|
|
315
453
|
---
|
|
@@ -320,27 +458,38 @@ render_gateway_timeout("The payment processor did not respond in time. Please tr
|
|
|
320
458
|
class UsersController < ApplicationController
|
|
321
459
|
|
|
322
460
|
def index
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
461
|
+
page = (params[:page] || 1).to_i
|
|
462
|
+
per_page = (params[:per_page] || 5).to_i
|
|
463
|
+
|
|
464
|
+
@users = Kaminari.paginate_array(User.active).page(page).per(per_page)
|
|
465
|
+
|
|
466
|
+
render_ok(
|
|
467
|
+
data: @users,
|
|
468
|
+
message: "Users fetched",
|
|
469
|
+
pagination: {
|
|
470
|
+
per_page: per_page.to_i,
|
|
471
|
+
current_page: @users.current_page,
|
|
472
|
+
next_page: @users.next_page,
|
|
473
|
+
prev_page: @users.prev_page,
|
|
474
|
+
total_pages: @users.total_pages,
|
|
475
|
+
total_count: @users.total_count
|
|
476
|
+
}
|
|
477
|
+
)
|
|
326
478
|
end
|
|
327
479
|
|
|
328
480
|
def show
|
|
329
481
|
user = User.find(params[:id])
|
|
330
482
|
render_ok(data: user, message: "User found")
|
|
331
483
|
rescue ActiveRecord::RecordNotFound
|
|
332
|
-
render_not_found("User ##{params[:id]} not found")
|
|
333
|
-
# → 404, { success: false, errors_code: "NOT_FOUND" }
|
|
484
|
+
render_not_found(message: "User ##{params[:id]} not found", error: { id: "User #{params[:id]} not exist"})
|
|
334
485
|
end
|
|
335
486
|
|
|
336
487
|
def create
|
|
337
488
|
user = User.new(user_params)
|
|
338
489
|
if user.save
|
|
339
490
|
render_created(data: user, message: "Account created successfully")
|
|
340
|
-
# → 201
|
|
341
491
|
else
|
|
342
|
-
render_unprocessable("Validation failed", errors: user.errors)
|
|
343
|
-
# → 422, { errors: { email: ["is invalid"] } }
|
|
492
|
+
render_unprocessable(message: "Validation failed", errors: user.errors)
|
|
344
493
|
end
|
|
345
494
|
end
|
|
346
495
|
|
|
@@ -348,26 +497,22 @@ class UsersController < ApplicationController
|
|
|
348
497
|
user = User.find(params[:id])
|
|
349
498
|
|
|
350
499
|
unless user == current_user || current_user.admin?
|
|
351
|
-
render_forbidden("You can only update your own profile")
|
|
352
|
-
# → 403
|
|
500
|
+
render_forbidden(message: "You can only update your own profile", error: { profile: "update your own profile" })
|
|
353
501
|
return
|
|
354
502
|
end
|
|
355
503
|
|
|
356
504
|
if user.update(user_params)
|
|
357
505
|
render_ok(data: user, message: "Profile updated")
|
|
358
506
|
else
|
|
359
|
-
render_conflict("Could not update profile", errors: user.errors)
|
|
360
|
-
# → 409
|
|
507
|
+
render_conflict(message: "Could not update profile", errors: user.errors)
|
|
361
508
|
end
|
|
362
509
|
end
|
|
363
510
|
|
|
364
511
|
def destroy
|
|
365
512
|
User.find(params[:id]).destroy!
|
|
366
513
|
render_no_content(message: "Account deleted")
|
|
367
|
-
# → 200
|
|
368
514
|
rescue ActiveRecord::RecordNotFound
|
|
369
|
-
render_gone("This account no longer exists")
|
|
370
|
-
# → 410
|
|
515
|
+
render_gone(message: "This account no longer exists")
|
|
371
516
|
end
|
|
372
517
|
|
|
373
518
|
end
|
|
@@ -378,11 +523,11 @@ class PaymentsController < ApplicationController
|
|
|
378
523
|
result = PaymentGateway.charge(amount: params[:amount], token: params[:token])
|
|
379
524
|
render_created(data: result, message: "Payment successful")
|
|
380
525
|
rescue PaymentGateway::CardDeclined => e
|
|
381
|
-
render_unprocessable(e.message)
|
|
526
|
+
render_unprocessable(message: e.message)
|
|
382
527
|
rescue PaymentGateway::Timeout
|
|
383
|
-
render_gateway_timeout("Payment processor timed out. You have not been charged.")
|
|
528
|
+
render_gateway_timeout(message: "Payment processor timed out. You have not been charged.")
|
|
384
529
|
rescue PaymentGateway::Error => e
|
|
385
|
-
render_bad_gateway("Payment gateway error: #{e.message}")
|
|
530
|
+
render_bad_gateway(message: "Payment gateway error: #{e.message}")
|
|
386
531
|
end
|
|
387
532
|
|
|
388
533
|
end
|
|
@@ -392,10 +537,9 @@ class ReportsController < ApplicationController
|
|
|
392
537
|
def generate
|
|
393
538
|
ReportJob.perform_later(current_user.id, params[:type])
|
|
394
539
|
render_accepted(
|
|
395
|
-
data:
|
|
540
|
+
data: { estimated_time: "2 minutes" },
|
|
396
541
|
message: "Report is being generated. We will email you when it is ready."
|
|
397
542
|
)
|
|
398
|
-
# → 202
|
|
399
543
|
end
|
|
400
544
|
|
|
401
545
|
end
|
|
@@ -403,103 +547,188 @@ end
|
|
|
403
547
|
|
|
404
548
|
---
|
|
405
549
|
|
|
406
|
-
##
|
|
550
|
+
## Pagination
|
|
407
551
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
render_success(data:, message:, meta:, pagy:, pagination:,code: , status:)
|
|
411
|
-
render_ok(data:, message:, meta:, pagination:)
|
|
412
|
-
render_created(data:, message:, pagination:)
|
|
413
|
-
render_accepted(data:, message:)
|
|
414
|
-
render_no_content(message:)
|
|
415
|
-
render_partial_content(data:, message:, meta:)
|
|
416
|
-
render_multi_status(data:, message:, meta:)
|
|
552
|
+
Respondo does **not** paginate data for you — your pagination library does that.
|
|
553
|
+
You build the pagination hash yourself and pass it via `pagination:`. This keeps the gem simple, dependency-free, and works with any library.
|
|
417
554
|
|
|
418
|
-
|
|
419
|
-
render_bad_request(message, errors:, code:)
|
|
420
|
-
render_unauthorized(message, code:)
|
|
421
|
-
render_payment_required(message, code:)
|
|
422
|
-
render_forbidden(message, code:)
|
|
423
|
-
render_not_found(message, code:)
|
|
424
|
-
render_method_not_allowed(message, code:)
|
|
425
|
-
render_not_acceptable(message, code:)
|
|
426
|
-
render_request_timeout(message, code:)
|
|
427
|
-
render_conflict(message, errors:, code:)
|
|
428
|
-
render_gone(message, code:)
|
|
429
|
-
render_precondition_failed(message, code:)
|
|
430
|
-
render_unsupported_media_type(message, code:)
|
|
431
|
-
render_unprocessable(message, errors:)
|
|
432
|
-
render_locked(message, code:)
|
|
433
|
-
render_too_many_requests(message, code:)
|
|
555
|
+
### Kaminari
|
|
434
556
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
render_bad_gateway(message, code:)
|
|
439
|
-
render_service_unavailable(message, code:)
|
|
440
|
-
render_gateway_timeout(message, code:)
|
|
441
|
-
```
|
|
557
|
+
```ruby
|
|
558
|
+
def index
|
|
559
|
+
@users = User.page(params[:page]).per(25)
|
|
442
560
|
|
|
443
|
-
|
|
561
|
+
render_ok(
|
|
562
|
+
data: @users,
|
|
563
|
+
message: "Users fetched",
|
|
564
|
+
pagination: {
|
|
565
|
+
current_page: @users.current_page,
|
|
566
|
+
per_page: @users.limit_value,
|
|
567
|
+
total_pages: @users.total_pages,
|
|
568
|
+
total_count: @users.total_count,
|
|
569
|
+
next_page: @users.next_page,
|
|
570
|
+
prev_page: @users.prev_page
|
|
571
|
+
}
|
|
572
|
+
)
|
|
573
|
+
end
|
|
574
|
+
```
|
|
444
575
|
|
|
445
|
-
|
|
576
|
+
### Pagy
|
|
446
577
|
|
|
447
|
-
|
|
578
|
+
```ruby
|
|
579
|
+
def index
|
|
580
|
+
@pagy, @users = pagy(User.all, items: 25)
|
|
448
581
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
582
|
+
render_ok(
|
|
583
|
+
data: @users,
|
|
584
|
+
message: "Users fetched",
|
|
585
|
+
pagination: {
|
|
586
|
+
current_page: @pagy.page,
|
|
587
|
+
per_page: @pagy.items,
|
|
588
|
+
total_pages: @pagy.pages,
|
|
589
|
+
total_count: @pagy.count,
|
|
590
|
+
next_page: @pagy.next,
|
|
591
|
+
prev_page: @pagy.prev
|
|
592
|
+
}
|
|
593
|
+
)
|
|
594
|
+
end
|
|
595
|
+
```
|
|
460
596
|
|
|
461
|
-
###
|
|
597
|
+
### WillPaginate
|
|
462
598
|
|
|
463
599
|
```ruby
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
600
|
+
def index
|
|
601
|
+
@users = User.paginate(page: params[:page], per_page: 25)
|
|
602
|
+
|
|
603
|
+
render_ok(
|
|
604
|
+
data: @users,
|
|
605
|
+
message: "Users fetched",
|
|
606
|
+
pagination: {
|
|
607
|
+
current_page: @users.current_page,
|
|
608
|
+
per_page: @users.per_page,
|
|
609
|
+
total_pages: @users.total_pages,
|
|
610
|
+
total_count: @users.total_entries,
|
|
611
|
+
next_page: @users.next_page,
|
|
612
|
+
prev_page: @users.previous_page
|
|
613
|
+
}
|
|
614
|
+
)
|
|
467
615
|
end
|
|
468
616
|
```
|
|
469
617
|
|
|
470
|
-
|
|
618
|
+
### No pagination
|
|
471
619
|
|
|
472
|
-
|
|
620
|
+
```ruby
|
|
621
|
+
render_ok(data: @user, message: "User found")
|
|
622
|
+
# → meta will have no pagination key at all
|
|
623
|
+
```
|
|
473
624
|
|
|
474
|
-
|
|
625
|
+
---
|
|
475
626
|
|
|
476
|
-
|
|
627
|
+
## Quick Reference Card
|
|
477
628
|
|
|
478
629
|
```ruby
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
```
|
|
630
|
+
# Core
|
|
631
|
+
render_success(data:, message:, meta:, code:, pagination:, code:, status:)
|
|
632
|
+
render_error(message:, errors:, code:, meta:, status:)
|
|
483
633
|
|
|
484
|
-
|
|
634
|
+
# 1xx — Informational
|
|
635
|
+
render_continue(message:, meta:)
|
|
636
|
+
render_switching_protocols(message:, meta:)
|
|
637
|
+
render_processing(message:, meta:)
|
|
638
|
+
render_early_hints(message:, meta:)
|
|
485
639
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
render_ok(data
|
|
489
|
-
|
|
640
|
+
# 2xx — Success
|
|
641
|
+
render_success(data:, message:, meta:, pagination:, code: , status:)
|
|
642
|
+
render_ok(data:, message:, meta:, pagination:)
|
|
643
|
+
render_created(data:, message:, meta:, pagination:)
|
|
644
|
+
render_accepted(data:, message:, meta:, pagination:)
|
|
645
|
+
render_non_authoritative(data:, message:, meta:, pagination:)
|
|
646
|
+
render_no_content(message:, meta:, pagination:)
|
|
647
|
+
render_reset_content(message:, meta:, pagination:)
|
|
648
|
+
render_partial_content(data:, message:, meta:, pagination:)
|
|
649
|
+
render_multi_status(data:, message:, meta:, pagination:)
|
|
650
|
+
render_already_reported(data:, message:, meta:, pagination:)
|
|
651
|
+
render_im_used(data:, message:, meta:, pagination:)
|
|
652
|
+
|
|
653
|
+
# 3xx — Redirects
|
|
654
|
+
render_multiple_choices(data:, message:, meta:, pagination:)
|
|
655
|
+
render_moved_permanently(message:, meta:, pagination:)
|
|
656
|
+
render_found(message:, meta:, pagination:)
|
|
657
|
+
render_see_other(message:, meta:, pagination:)
|
|
658
|
+
render_not_modified(message:, meta:, pagination:)
|
|
659
|
+
render_temporary_redirect(message:, meta:, pagination:)
|
|
660
|
+
render_permanent_redirect(message:, meta:, pagination:)
|
|
490
661
|
|
|
491
|
-
|
|
662
|
+
# 4xx — Client Errors
|
|
663
|
+
render_bad_request(message:, errors:, meta:)
|
|
664
|
+
render_unauthorized(message:, errors:, meta:)
|
|
665
|
+
render_payment_required(message:, errors:, meta:)
|
|
666
|
+
render_forbidden(message:, errors:, meta:)
|
|
667
|
+
render_not_found(message:, errors:, meta:)
|
|
668
|
+
render_method_not_allowed(message:, errors:, meta:)
|
|
669
|
+
render_not_acceptable(message:, errors:, meta:)
|
|
670
|
+
render_proxy_auth_required(message:, errors:, meta:)
|
|
671
|
+
render_request_timeout(message:, errors:, meta:)
|
|
672
|
+
render_conflict(message:, errors:, meta:)
|
|
673
|
+
render_gone(message:, errors:, meta:)
|
|
674
|
+
render_length_required(message:, errors:, meta:)
|
|
675
|
+
render_precondition_failed(message:, errors:, meta:)
|
|
676
|
+
render_payload_too_large(message:, errors:, meta:)
|
|
677
|
+
render_uri_too_long(message:, errors:, meta:)
|
|
678
|
+
render_unsupported_media_type(message:, errors:, meta:)
|
|
679
|
+
render_range_not_satisfiable(message:, errors:, meta:)
|
|
680
|
+
render_expectation_failed(message:, errors:, meta:)
|
|
681
|
+
render_im_a_teapot(message:, errors:, meta:)
|
|
682
|
+
render_misdirected_request(message:, errors:, meta:)
|
|
683
|
+
render_unprocessable(message:, errors:, meta:)
|
|
684
|
+
render_locked(message:, errors:, meta:)
|
|
685
|
+
render_failed_dependency(message:, errors:, meta:)
|
|
686
|
+
render_too_early(message:, errors:, meta:)
|
|
687
|
+
render_upgrade_required(message:, errors:, meta:)
|
|
688
|
+
render_precondition_required(message:, errors:, meta:)
|
|
689
|
+
render_too_many_requests(message:, errors:, meta:)
|
|
690
|
+
render_request_header_fields_too_large(message:, errors:, meta:)
|
|
691
|
+
render_unavailable_for_legal_reasons(message:, errors:, meta:)
|
|
492
692
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
693
|
+
# 5xx — Server Errors
|
|
694
|
+
render_server_error(message:, errors:, meta:)
|
|
695
|
+
render_not_implemented(message:, errors:, meta:)
|
|
696
|
+
render_bad_gateway(message:, errors:, meta:)
|
|
697
|
+
render_service_unavailable(message:, errors:, meta:)
|
|
698
|
+
render_gateway_timeout(message:, errors:, meta:)
|
|
699
|
+
render_http_version_not_supported(message:, errors:, meta:)
|
|
700
|
+
render_variant_also_negotiates(message:, errors:, meta:)
|
|
701
|
+
render_insufficient_storage(message:, errors:, meta:)
|
|
702
|
+
render_loop_detected(message:, errors:, meta:)
|
|
703
|
+
render_not_extended(message:, errors:, meta:)
|
|
704
|
+
render_network_authentication_required(message:, errors:, meta:)
|
|
496
705
|
```
|
|
497
706
|
|
|
498
|
-
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
## Auto-Serialization
|
|
710
|
+
|
|
711
|
+
Respondo automatically handles:
|
|
712
|
+
|
|
713
|
+
| Input type | Output |
|
|
714
|
+
|----------------------------------|-------------------------------------|
|
|
715
|
+
| `ActiveRecord::Base` instance | `record.as_json` |
|
|
716
|
+
| `ActiveRecord::Relation` | Array of `as_json` records |
|
|
717
|
+
| `ActiveModel::Errors` | `{ field: ["message", ...] }` |
|
|
718
|
+
| `Hash` | Passed through (values serialized) |
|
|
719
|
+
| `Array` | Each element serialized recursively |
|
|
720
|
+
| `Exception` | `{ message: e.message }` |
|
|
721
|
+
| Anything with `#as_json` | `.as_json` |
|
|
722
|
+
| Anything with `#to_h` | `.to_h` |
|
|
723
|
+
| Primitives (String, Integer...) | As-is |
|
|
724
|
+
|
|
725
|
+
### Custom serializer
|
|
499
726
|
|
|
500
727
|
```ruby
|
|
501
|
-
|
|
502
|
-
|
|
728
|
+
Respondo.configure do |config|
|
|
729
|
+
# Use ActiveModelSerializers, Blueprinter, Panko, etc.
|
|
730
|
+
config.serializer = ->(obj) { UserSerializer.new(obj).as_json }
|
|
731
|
+
end
|
|
503
732
|
```
|
|
504
733
|
|
|
505
734
|
---
|
|
@@ -511,7 +740,7 @@ Respondo.configure { |c| c.camelize_keys = true }
|
|
|
511
740
|
```
|
|
512
741
|
|
|
513
742
|
All keys in the response — including nested `meta.pagination` — are camelized:
|
|
514
|
-
`current_page` → `currentPage`, `total_count` → `totalCount`, `error_code` → `errorCode`.
|
|
743
|
+
`current_page` → `currentPage`, `total_count` → `totalCount`, `next_page` → `nextPage`, `error_code` → `errorCode`.
|
|
515
744
|
|
|
516
745
|
### Flutter Integration
|
|
517
746
|
|
|
@@ -545,9 +774,8 @@ lib/
|
|
|
545
774
|
├── version.rb # VERSION
|
|
546
775
|
├── configuration.rb # Config with defaults
|
|
547
776
|
├── serializer.rb # Auto-detects and serializes any object
|
|
548
|
-
├── pagination.rb # Kaminari / Pagy / WillPaginate extractor
|
|
549
777
|
├── response_builder.rb # Assembles the final Hash
|
|
550
|
-
├── controller_helpers.rb # All render_* helpers (
|
|
778
|
+
├── controller_helpers.rb # All render_* helpers (1xx–5xx)
|
|
551
779
|
└── railtie.rb # Auto-includes into Rails controllers
|
|
552
780
|
```
|
|
553
781
|
|