respondo 2.0.0 → 2.1.1
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 +127 -0
- data/README.md +272 -31
- data/lib/generators/respondo/install/install_generator.rb +350 -0
- data/lib/respondo/controller_helpers.rb +1 -1
- data/lib/respondo/response_builder.rb +0 -22
- data/lib/respondo/version.rb +1 -1
- data/respondo.gemspec +13 -4
- metadata +25 -6
- data/lib/respondo/pagination.rb +0 -152
data/README.md
CHANGED
|
@@ -42,6 +42,78 @@ gem "respondo"
|
|
|
42
42
|
|
|
43
43
|
## Setup
|
|
44
44
|
|
|
45
|
+
### ✅ Recommended — use the install generator
|
|
46
|
+
|
|
47
|
+
After adding the gem, run:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
rails generate respondo:install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The interactive wizard walks you through every option and writes a fully commented `config/initializers/respondo.rb` tailored to your project. No need to read the full README or copy-paste config by hand.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
┌─ Project Info ─────────────────────────────────────────────────────┐
|
|
57
|
+
|
|
58
|
+
Project / app name
|
|
59
|
+
(Used as a comment header in the initializer)
|
|
60
|
+
› [MyApp]:
|
|
61
|
+
|
|
62
|
+
API version (e.g. v1 — added to every response meta block)
|
|
63
|
+
› [v1]:
|
|
64
|
+
|
|
65
|
+
┌─ Response Messages ────────────────────────────────────────────────┐
|
|
66
|
+
|
|
67
|
+
Default success message
|
|
68
|
+
› [Success]:
|
|
69
|
+
|
|
70
|
+
Default error message
|
|
71
|
+
› [An error occurred]:
|
|
72
|
+
|
|
73
|
+
...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The generator produces a file like this:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# frozen_string_literal: true
|
|
80
|
+
|
|
81
|
+
# Respondo initializer — MyApp
|
|
82
|
+
# Generated by: rails generate respondo:install
|
|
83
|
+
# Respondo version: 2.1.0
|
|
84
|
+
|
|
85
|
+
Respondo.configure do |config|
|
|
86
|
+
|
|
87
|
+
# ── Messages ─────────────────────────────────────────────────────────
|
|
88
|
+
config.default_success_message = "Success"
|
|
89
|
+
config.default_error_message = "Something went wrong"
|
|
90
|
+
|
|
91
|
+
# ── Request ID ───────────────────────────────────────────────────────
|
|
92
|
+
config.include_request_id = true
|
|
93
|
+
|
|
94
|
+
# ── Key Format ───────────────────────────────────────────────────────
|
|
95
|
+
config.camelize_keys = true
|
|
96
|
+
|
|
97
|
+
# ── Global Meta ──────────────────────────────────────────────────────
|
|
98
|
+
config.default_meta = {
|
|
99
|
+
api_version: "v1",
|
|
100
|
+
platform: "mobile"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# ── Custom Serializer ────────────────────────────────────────────────
|
|
104
|
+
# config.serializer = ->(obj) { MySerializer.new(obj).as_json }
|
|
105
|
+
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Re-run the generator any time to regenerate with different answers — it overwrites the existing initializer.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### Manual setup (alternative)
|
|
114
|
+
|
|
115
|
+
If you prefer to write the initializer yourself, create `config/initializers/respondo.rb`:
|
|
116
|
+
|
|
45
117
|
```ruby
|
|
46
118
|
# config/initializers/respondo.rb
|
|
47
119
|
Respondo.configure do |config|
|
|
@@ -71,8 +143,9 @@ Error responses additionally include:
|
|
|
71
143
|
|
|
72
144
|
| Key | Type | Description |
|
|
73
145
|
|----------|------|--------------------------------------|
|
|
74
|
-
| `errors` | Hash | Field-level errors `{field: [
|
|
75
|
-
|
|
146
|
+
| `errors` | Hash | Field-level errors `{ field: ["message", ...] }` |
|
|
147
|
+
|
|
148
|
+
> **`errors` is optional.** Pass it when you want to surface field-level detail to the client (e.g. form validation, token issues). Omit it for simple human-readable-only responses — `message` alone is perfectly valid.
|
|
76
149
|
|
|
77
150
|
---
|
|
78
151
|
|
|
@@ -232,222 +305,386 @@ render_permanent_redirect(message: "Permanently moved — update your bookmarks"
|
|
|
232
305
|
|
|
233
306
|
### 4xx — Client Error Helpers
|
|
234
307
|
|
|
308
|
+
> **Two usage patterns for every error helper:**
|
|
309
|
+
> - **Message only** — a human-readable string shown to the end user.
|
|
310
|
+
> - **Message + errors** — add `errors:` when you also need field-level detail for the client to act on (e.g. highlight a form field, log a specific token issue). `errors` is a Hash of `{ field: ["message", ...] }`.
|
|
311
|
+
|
|
235
312
|
#### `render_bad_request` — 400 Bad Request
|
|
236
313
|
```ruby
|
|
314
|
+
# Message only
|
|
237
315
|
render_bad_request(message: "The 'date' parameter is required")
|
|
238
|
-
|
|
316
|
+
|
|
317
|
+
# Message + errors
|
|
318
|
+
render_bad_request(message: "Invalid input", errors: { date: "must be a valid date"})
|
|
239
319
|
```
|
|
240
320
|
|
|
241
321
|
#### `render_unauthorized` — 401 Unauthorized
|
|
242
322
|
```ruby
|
|
323
|
+
# Message only
|
|
243
324
|
render_unauthorized(message: "Please log in to continue")
|
|
244
|
-
|
|
325
|
+
|
|
326
|
+
# Message + errors
|
|
327
|
+
render_unauthorized(message: "Token has expired", errors: { token: "has expired, please log in again"})
|
|
245
328
|
```
|
|
246
329
|
|
|
247
330
|
#### `render_payment_required` — 402 Payment Required
|
|
248
331
|
```ruby
|
|
332
|
+
# Message only
|
|
249
333
|
render_payment_required(message: "Upgrade to Pro to access this feature")
|
|
334
|
+
|
|
335
|
+
# Message + errors
|
|
336
|
+
render_payment_required( message: "Upgrade to Pro to access this feature", errors: { plan: "must be Pro or higher to access this feature"})
|
|
250
337
|
```
|
|
251
338
|
|
|
252
339
|
#### `render_forbidden` — 403 Forbidden
|
|
253
340
|
```ruby
|
|
341
|
+
# Message only
|
|
254
342
|
render_forbidden(message: "You can only edit your own posts")
|
|
343
|
+
|
|
344
|
+
# Message + errors
|
|
345
|
+
render_forbidden(message: "You can only edit your own posts",errors: { post: "does not belong to you" })
|
|
255
346
|
```
|
|
256
347
|
|
|
257
348
|
#### `render_not_found` — 404 Not Found
|
|
258
349
|
```ruby
|
|
350
|
+
# Message only
|
|
259
351
|
render_not_found(message: "User not found")
|
|
260
|
-
|
|
352
|
+
|
|
353
|
+
# Message + errors
|
|
354
|
+
render_not_found(message: "User not found", errors: { id: "no user exists with this ID" })
|
|
261
355
|
```
|
|
262
356
|
|
|
263
357
|
#### `render_method_not_allowed` — 405
|
|
264
358
|
```ruby
|
|
359
|
+
# Message only
|
|
265
360
|
render_method_not_allowed(message: "This endpoint only accepts POST requests")
|
|
361
|
+
|
|
362
|
+
# Message + errors
|
|
363
|
+
render_method_not_allowed(message: "This endpoint only accepts POST requests",errors: { method: "GET is not allowed, use POST" })
|
|
266
364
|
```
|
|
267
365
|
|
|
268
366
|
#### `render_not_acceptable` — 406
|
|
269
367
|
```ruby
|
|
368
|
+
# Message only
|
|
270
369
|
render_not_acceptable(message: "Only application/json is supported")
|
|
370
|
+
|
|
371
|
+
# Message + errors
|
|
372
|
+
render_not_acceptable(message: "Only application/json is supported", errors: { content_type: "must be application/json" })
|
|
271
373
|
```
|
|
272
374
|
|
|
273
375
|
#### `render_proxy_auth_required` — 407
|
|
274
376
|
```ruby
|
|
377
|
+
# Message only
|
|
275
378
|
render_proxy_auth_required(message: "Authenticate with the proxy first")
|
|
379
|
+
|
|
380
|
+
# Message + errors
|
|
381
|
+
render_proxy_auth_required(message: "Authenticate with the proxy first", errors: { proxy_token: "is missing or invalid"})
|
|
276
382
|
```
|
|
277
383
|
|
|
278
384
|
#### `render_request_timeout` — 408
|
|
279
385
|
```ruby
|
|
386
|
+
# Message only
|
|
280
387
|
render_request_timeout(message: "The query took too long. Try a smaller date range.")
|
|
388
|
+
|
|
389
|
+
# Message + errors
|
|
390
|
+
render_request_timeout( message: "The query took too long. Try a smaller date range.", errors: { date_range: "must span 90 days or fewer" })
|
|
281
391
|
```
|
|
282
392
|
|
|
283
393
|
#### `render_conflict` — 409 Conflict
|
|
284
394
|
```ruby
|
|
395
|
+
# Message only
|
|
285
396
|
render_conflict(message: "Email address is already registered")
|
|
286
|
-
|
|
397
|
+
|
|
398
|
+
# Message + errors
|
|
399
|
+
render_conflict(message: "Email address is already registered",errors: { email: "has already been taken" })
|
|
287
400
|
```
|
|
288
401
|
|
|
289
402
|
#### `render_gone` — 410 Gone
|
|
290
403
|
```ruby
|
|
404
|
+
# Message only
|
|
291
405
|
render_gone(message: "This account has been permanently deleted")
|
|
406
|
+
|
|
407
|
+
# Message + errors
|
|
408
|
+
render_gone(message: "This account has been permanently deleted",errors: { account: ["no longer exists and cannot be recovered"] })
|
|
292
409
|
```
|
|
293
410
|
|
|
294
411
|
#### `render_length_required` — 411
|
|
295
412
|
```ruby
|
|
413
|
+
# Message only
|
|
296
414
|
render_length_required(message: "Content-Length header is required")
|
|
415
|
+
|
|
416
|
+
# Message + errors
|
|
417
|
+
render_length_required(message: "Content-Length header is required",errors: { content_length: ["header is missing from the request"] })
|
|
297
418
|
```
|
|
298
419
|
|
|
299
420
|
#### `render_precondition_failed` — 412
|
|
300
421
|
```ruby
|
|
422
|
+
# Message only
|
|
301
423
|
render_precondition_failed(message: "Resource has been modified since your last request")
|
|
424
|
+
|
|
425
|
+
# Message + errors
|
|
426
|
+
render_precondition_failed(message: "Resource has been modified since your last request",errors: { etag: ["does not match the current resource version"] })
|
|
302
427
|
```
|
|
303
428
|
|
|
304
429
|
#### `render_payload_too_large` — 413
|
|
305
430
|
```ruby
|
|
431
|
+
# Message only
|
|
306
432
|
render_payload_too_large(message: "File exceeds the 10 MB upload limit")
|
|
433
|
+
|
|
434
|
+
# Message + errors
|
|
435
|
+
render_payload_too_large(message: "File exceeds the 10 MB upload limit",errors: { file: ["must be smaller than 10 MB"] })
|
|
307
436
|
```
|
|
308
437
|
|
|
309
438
|
#### `render_uri_too_long` — 414
|
|
310
439
|
```ruby
|
|
440
|
+
# Message only
|
|
311
441
|
render_uri_too_long(message: "That URL is too long to process")
|
|
442
|
+
|
|
443
|
+
# Message + errors
|
|
444
|
+
render_uri_too_long(message: "That URL is too long to process",errors: { url: ["must not exceed 2048 characters"] })
|
|
312
445
|
```
|
|
313
446
|
|
|
314
447
|
#### `render_unsupported_media_type` — 415
|
|
315
448
|
```ruby
|
|
449
|
+
# Message only
|
|
316
450
|
render_unsupported_media_type(message: "Please send requests as application/json")
|
|
451
|
+
|
|
452
|
+
# Message + errors
|
|
453
|
+
render_unsupported_media_type(message: "Please send requests as application/json",errors: { content_type: ["must be application/json, got text/xml"] })
|
|
317
454
|
```
|
|
318
455
|
|
|
319
456
|
#### `render_range_not_satisfiable` — 416
|
|
320
457
|
```ruby
|
|
458
|
+
# Message only
|
|
321
459
|
render_range_not_satisfiable(message: "Requested byte range is out of bounds")
|
|
460
|
+
|
|
461
|
+
# Message + errors
|
|
462
|
+
render_range_not_satisfiable(message: "Requested byte range is out of bounds", errors: { range: ["exceeds the total file size"] })
|
|
322
463
|
```
|
|
323
464
|
|
|
324
465
|
#### `render_expectation_failed` — 417
|
|
325
466
|
```ruby
|
|
467
|
+
# Message only
|
|
326
468
|
render_expectation_failed(message: "Expect header value cannot be met")
|
|
469
|
+
|
|
470
|
+
# Message + errors
|
|
471
|
+
render_expectation_failed(message: "Expect header value cannot be met",errors: { expect: ["100-continue is not supported on this endpoint"] })
|
|
327
472
|
```
|
|
328
473
|
|
|
329
474
|
#### `render_im_a_teapot` — 418
|
|
330
475
|
```ruby
|
|
476
|
+
# Message only
|
|
331
477
|
render_im_a_teapot(message: "I'm a teapot — I cannot brew coffee")
|
|
478
|
+
|
|
479
|
+
# Message + errors
|
|
480
|
+
render_im_a_teapot(message: "I'm a teapot — I cannot brew coffee",errors: { beverage: ["coffee is not supported, try tea"] })
|
|
332
481
|
```
|
|
333
482
|
|
|
334
483
|
#### `render_misdirected_request` — 421
|
|
335
484
|
```ruby
|
|
485
|
+
# Message only
|
|
336
486
|
render_misdirected_request(message: "Request sent to the wrong server")
|
|
487
|
+
|
|
488
|
+
# Message + errors
|
|
489
|
+
render_misdirected_request(message: "Request sent to the wrong server",errors: { host: ["this server does not handle requests for this host"] })
|
|
337
490
|
```
|
|
338
491
|
|
|
339
492
|
#### `render_unprocessable` — 422 Unprocessable Entity
|
|
340
493
|
Validation errors. The most commonly used error helper in Rails APIs.
|
|
341
494
|
|
|
342
495
|
```ruby
|
|
343
|
-
|
|
344
|
-
render_unprocessable(message: "
|
|
496
|
+
# Message only
|
|
497
|
+
render_unprocessable(message: "Validation failed")
|
|
498
|
+
|
|
499
|
+
# Message + errors — pass an ActiveModel::Errors object directly
|
|
500
|
+
render_unprocessable(message: "Validation failed",errors: user.errors)
|
|
501
|
+
|
|
502
|
+
# Message + errors — pass a plain hash
|
|
503
|
+
render_unprocessable(message: "Invalid data",errors: { name: ["can't be blank"], email: ["is invalid"] })
|
|
345
504
|
```
|
|
346
505
|
|
|
347
506
|
#### `render_locked` — 423
|
|
348
507
|
```ruby
|
|
508
|
+
# Message only
|
|
349
509
|
render_locked(message: "This record is locked by another user")
|
|
510
|
+
|
|
511
|
+
# Message + errors
|
|
512
|
+
render_locked(message: "This record is locked by another user",errors: { record: ["is currently locked, try again later"] })
|
|
350
513
|
```
|
|
351
514
|
|
|
352
515
|
#### `render_failed_dependency` — 424
|
|
353
516
|
```ruby
|
|
517
|
+
# Message only
|
|
354
518
|
render_failed_dependency(message: "Prerequisite resource creation failed")
|
|
519
|
+
|
|
520
|
+
# Message + errors
|
|
521
|
+
render_failed_dependency(message: "Prerequisite resource creation failed",errors: { dependency: ["parent record must exist before creating this resource"] })
|
|
355
522
|
```
|
|
356
523
|
|
|
357
524
|
#### `render_too_early` — 425
|
|
358
525
|
```ruby
|
|
526
|
+
# Message only
|
|
359
527
|
render_too_early(message: "Request may be a replay — rejected for safety")
|
|
528
|
+
|
|
529
|
+
# Message + errors
|
|
530
|
+
render_too_early(message: "Request may be a replay — rejected for safety",errors: { request: ["early data replay detected, resend after handshake"] })
|
|
360
531
|
```
|
|
361
532
|
|
|
362
533
|
#### `render_upgrade_required` — 426
|
|
363
534
|
```ruby
|
|
535
|
+
# Message only
|
|
364
536
|
render_upgrade_required(message: "Please upgrade to TLS 1.3")
|
|
537
|
+
|
|
538
|
+
# Message + errors
|
|
539
|
+
render_upgrade_required(message: "Please upgrade to TLS 1.3",errors: { protocol: ["TLS 1.2 is no longer supported, upgrade to TLS 1.3"] })
|
|
365
540
|
```
|
|
366
541
|
|
|
367
542
|
#### `render_precondition_required` — 428
|
|
368
543
|
```ruby
|
|
544
|
+
# Message only
|
|
369
545
|
render_precondition_required(message: "Include an If-Match header with your request")
|
|
546
|
+
|
|
547
|
+
# Message + errors
|
|
548
|
+
render_precondition_required(message: "Include an If-Match header with your request",errors: { if_match: ["header is required to prevent lost updates"] })
|
|
370
549
|
```
|
|
371
550
|
|
|
372
551
|
#### `render_too_many_requests` — 429
|
|
373
552
|
```ruby
|
|
553
|
+
# Message only
|
|
374
554
|
render_too_many_requests(message: "You have exceeded 100 requests per minute.")
|
|
375
|
-
|
|
555
|
+
|
|
556
|
+
# Message + errors
|
|
557
|
+
render_too_many_requests(message: "Rate limit exceeded",errors: { rate_limit: ["100 requests per minute allowed, retry after 60 seconds"] },meta: { retry_after: 60 })
|
|
376
558
|
```
|
|
377
559
|
|
|
378
560
|
#### `render_request_header_fields_too_large` — 431
|
|
379
561
|
```ruby
|
|
562
|
+
# Message only
|
|
380
563
|
render_request_header_fields_too_large(message: "Cookie header is too large")
|
|
564
|
+
|
|
565
|
+
# Message + errors
|
|
566
|
+
render_request_header_fields_too_large(message: "Cookie header is too large",errors: { cookie: ["must not exceed 4096 bytes"] })
|
|
381
567
|
```
|
|
382
568
|
|
|
383
569
|
#### `render_unavailable_for_legal_reasons` — 451
|
|
384
570
|
```ruby
|
|
571
|
+
# Message only
|
|
385
572
|
render_unavailable_for_legal_reasons(message: "This content is blocked in your region")
|
|
573
|
+
|
|
574
|
+
# Message + errors
|
|
575
|
+
render_unavailable_for_legal_reasons(message: "This content is blocked in your region",errors: { region: ["content is not licensed for distribution in your country"] })
|
|
386
576
|
```
|
|
387
577
|
|
|
388
578
|
---
|
|
389
579
|
|
|
390
580
|
### 5xx — Server Error Helpers
|
|
391
581
|
|
|
582
|
+
> **Two usage patterns for every error helper:**
|
|
583
|
+
> - **Message only** — a human-readable string shown to the end user.
|
|
584
|
+
> - **Message + errors** — add `errors:` when you need to surface internal detail for debugging or logging (e.g. which downstream service failed). `errors` is a Hash of `{ field: ["message", ...] }`.
|
|
585
|
+
|
|
392
586
|
#### `render_server_error` — 500 Internal Server Error
|
|
393
587
|
```ruby
|
|
588
|
+
# Message only
|
|
394
589
|
render_server_error(message: "Something went wrong. Our team has been notified.")
|
|
395
590
|
|
|
591
|
+
# Message + errors
|
|
592
|
+
render_server_error(message: "Something went wrong. Our team has been notified.",errors: { server: ["unexpected exception in OrdersController#create"] })
|
|
593
|
+
|
|
396
594
|
# Common pattern — rescue unexpected exceptions
|
|
397
595
|
rescue StandardError => e
|
|
398
596
|
Rails.logger.error(e)
|
|
399
|
-
render_server_error("An unexpected error occurred")
|
|
597
|
+
render_server_error(message: "An unexpected error occurred",errors: { server: [e.message] })
|
|
400
598
|
```
|
|
401
599
|
|
|
402
600
|
#### `render_not_implemented` — 501
|
|
403
601
|
```ruby
|
|
602
|
+
# Message only
|
|
404
603
|
render_not_implemented(message: "CSV export is coming soon")
|
|
604
|
+
|
|
605
|
+
# Message + errors
|
|
606
|
+
render_not_implemented(message: "CSV export is coming soon",errors: { format: ["csv export is not yet implemented, use json"] })
|
|
405
607
|
```
|
|
406
608
|
|
|
407
609
|
#### `render_bad_gateway` — 502
|
|
408
610
|
```ruby
|
|
611
|
+
# Message only
|
|
409
612
|
render_bad_gateway(message: "Payment gateway is currently unavailable")
|
|
613
|
+
|
|
614
|
+
# Message + errors
|
|
615
|
+
render_bad_gateway(message: "Payment gateway is currently unavailable",errors: { gateway: ["Stripe returned a 502, please try again"] })
|
|
410
616
|
```
|
|
411
617
|
|
|
412
618
|
#### `render_service_unavailable` — 503
|
|
413
619
|
```ruby
|
|
620
|
+
# Message only
|
|
414
621
|
render_service_unavailable(message: "Down for maintenance. Back in 30 minutes.")
|
|
415
|
-
|
|
622
|
+
|
|
623
|
+
# Message + errors
|
|
624
|
+
render_service_unavailable(message: "Down for maintenance. Back in 30 minutes.",errors: { service: ["scheduled maintenance window until 03:00 UTC"] },meta: { retry_after: 1800 })
|
|
416
625
|
```
|
|
417
626
|
|
|
418
627
|
#### `render_gateway_timeout` — 504
|
|
419
628
|
```ruby
|
|
629
|
+
# Message only
|
|
420
630
|
render_gateway_timeout(message: "The payment processor did not respond in time.")
|
|
631
|
+
|
|
632
|
+
# Message + errors
|
|
633
|
+
render_gateway_timeout(message: "The payment processor did not respond in time.",errors: { gateway: ["upstream timeout after 30 seconds, you have not been charged"] })
|
|
421
634
|
```
|
|
422
635
|
|
|
423
636
|
#### `render_http_version_not_supported` — 505
|
|
424
637
|
```ruby
|
|
638
|
+
# Message only
|
|
425
639
|
render_http_version_not_supported(message: "Only HTTP/1.1 and HTTP/2 are supported")
|
|
640
|
+
|
|
641
|
+
# Message + errors
|
|
642
|
+
render_http_version_not_supported(message: "Only HTTP/1.1 and HTTP/2 are supported",errors: { http_version: ["HTTP/1.0 is not supported"] })
|
|
426
643
|
```
|
|
427
644
|
|
|
428
645
|
#### `render_variant_also_negotiates` — 506
|
|
429
646
|
```ruby
|
|
647
|
+
# Message only
|
|
430
648
|
render_variant_also_negotiates(message: "Server content-negotiation loop detected")
|
|
649
|
+
|
|
650
|
+
# Message + errors
|
|
651
|
+
render_variant_also_negotiates(message: "Server content-negotiation loop detected",errors: { variant: ["misconfigured content negotiation caused an infinite loop"] })
|
|
431
652
|
```
|
|
432
653
|
|
|
433
654
|
#### `render_insufficient_storage` — 507
|
|
434
655
|
```ruby
|
|
656
|
+
# Message only
|
|
435
657
|
render_insufficient_storage(message: "Disk quota exceeded on this node")
|
|
658
|
+
|
|
659
|
+
# Message + errors
|
|
660
|
+
render_insufficient_storage(message: "Disk quota exceeded on this node",errors: { storage: ["upload failed, node has 0 bytes remaining"] })
|
|
436
661
|
```
|
|
437
662
|
|
|
438
663
|
#### `render_loop_detected` — 508
|
|
439
664
|
```ruby
|
|
665
|
+
# Message only
|
|
440
666
|
render_loop_detected(message: "Infinite redirect loop detected")
|
|
667
|
+
|
|
668
|
+
# Message + errors
|
|
669
|
+
render_loop_detected(message: "Infinite redirect loop detected",errors: { redirect: ["request visited the same URL more than 10 times"] })
|
|
441
670
|
```
|
|
442
671
|
|
|
443
672
|
#### `render_not_extended` — 510
|
|
444
673
|
```ruby
|
|
674
|
+
# Message only
|
|
445
675
|
render_not_extended(message: "Further extensions required to fulfil this request")
|
|
676
|
+
|
|
677
|
+
# Message + errors
|
|
678
|
+
render_not_extended(message: "Further extensions required to fulfil this request",errors: { extension: ["mandatory extension 'auth' is missing from the request"] })
|
|
446
679
|
```
|
|
447
680
|
|
|
448
681
|
#### `render_network_authentication_required` — 511
|
|
449
682
|
```ruby
|
|
683
|
+
# Message only
|
|
450
684
|
render_network_authentication_required(message: "Sign in to the network portal first")
|
|
685
|
+
|
|
686
|
+
# Message + errors
|
|
687
|
+
render_network_authentication_required(message: "Sign in to the network portal first",errors: { network: ["captive portal authentication required before accessing the API"] })
|
|
451
688
|
```
|
|
452
689
|
|
|
453
690
|
---
|
|
@@ -467,12 +704,12 @@ class UsersController < ApplicationController
|
|
|
467
704
|
data: @users,
|
|
468
705
|
message: "Users fetched",
|
|
469
706
|
pagination: {
|
|
470
|
-
per_page:
|
|
471
|
-
current_page:
|
|
472
|
-
next_page:
|
|
473
|
-
prev_page:
|
|
474
|
-
total_pages:
|
|
475
|
-
total_count:
|
|
707
|
+
per_page: per_page.to_i,
|
|
708
|
+
current_page: @users.current_page,
|
|
709
|
+
next_page: @users.next_page,
|
|
710
|
+
prev_page: @users.prev_page,
|
|
711
|
+
total_pages: @users.total_pages,
|
|
712
|
+
total_count: @users.total_count
|
|
476
713
|
}
|
|
477
714
|
)
|
|
478
715
|
end
|
|
@@ -481,7 +718,7 @@ class UsersController < ApplicationController
|
|
|
481
718
|
user = User.find(params[:id])
|
|
482
719
|
render_ok(data: user, message: "User found")
|
|
483
720
|
rescue ActiveRecord::RecordNotFound
|
|
484
|
-
render_not_found(message: "User ##{params[:id]} not found",
|
|
721
|
+
render_not_found(message: "User ##{params[:id]} not found",errors: { id: ["no user exists with ID #{params[:id]}"] })
|
|
485
722
|
end
|
|
486
723
|
|
|
487
724
|
def create
|
|
@@ -497,7 +734,7 @@ class UsersController < ApplicationController
|
|
|
497
734
|
user = User.find(params[:id])
|
|
498
735
|
|
|
499
736
|
unless user == current_user || current_user.admin?
|
|
500
|
-
render_forbidden(message: "You can only update your own profile",
|
|
737
|
+
render_forbidden( message: "You can only update your own profile", errors: { profile: ["you do not have permission to update this profile"] } )
|
|
501
738
|
return
|
|
502
739
|
end
|
|
503
740
|
|
|
@@ -523,11 +760,11 @@ class PaymentsController < ApplicationController
|
|
|
523
760
|
result = PaymentGateway.charge(amount: params[:amount], token: params[:token])
|
|
524
761
|
render_created(data: result, message: "Payment successful")
|
|
525
762
|
rescue PaymentGateway::CardDeclined => e
|
|
526
|
-
render_unprocessable(message: e.message)
|
|
763
|
+
render_unprocessable(message: e.message, errors: { card: [e.message] })
|
|
527
764
|
rescue PaymentGateway::Timeout
|
|
528
|
-
render_gateway_timeout(message: "Payment processor timed out. You have not been charged.")
|
|
765
|
+
render_gateway_timeout( message: "Payment processor timed out. You have not been charged.", errors: { gateway: ["upstream timeout, transaction was not processed"] } )
|
|
529
766
|
rescue PaymentGateway::Error => e
|
|
530
|
-
render_bad_gateway(message: "Payment gateway error: #{e.message}")
|
|
767
|
+
render_bad_gateway( message: "Payment gateway error: #{e.message}", errors: { gateway: [e.message] })
|
|
531
768
|
end
|
|
532
769
|
|
|
533
770
|
end
|
|
@@ -628,7 +865,7 @@ render_ok(data: @user, message: "User found")
|
|
|
628
865
|
|
|
629
866
|
```ruby
|
|
630
867
|
# Core
|
|
631
|
-
render_success(data:, message:, meta:,
|
|
868
|
+
render_success(data:, message:, meta:, pagination:, code:, status:)
|
|
632
869
|
render_error(message:, errors:, code:, meta:, status:)
|
|
633
870
|
|
|
634
871
|
# 1xx — Informational
|
|
@@ -638,7 +875,7 @@ render_processing(message:, meta:)
|
|
|
638
875
|
render_early_hints(message:, meta:)
|
|
639
876
|
|
|
640
877
|
# 2xx — Success
|
|
641
|
-
render_success(data:, message:, meta:, pagination:, code
|
|
878
|
+
render_success(data:, message:, meta:, pagination:, code:, status:)
|
|
642
879
|
render_ok(data:, message:, meta:, pagination:)
|
|
643
880
|
render_created(data:, message:, meta:, pagination:)
|
|
644
881
|
render_accepted(data:, message:, meta:, pagination:)
|
|
@@ -770,13 +1007,17 @@ class ApiResponse<T> {
|
|
|
770
1007
|
```
|
|
771
1008
|
lib/
|
|
772
1009
|
├── respondo.rb # Entry point, configure, Railtie hook
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1010
|
+
├── respondo/
|
|
1011
|
+
│ ├── version.rb # VERSION
|
|
1012
|
+
│ ├── configuration.rb # Config with defaults
|
|
1013
|
+
│ ├── serializer.rb # Auto-detects and serializes any object
|
|
1014
|
+
│ ├── response_builder.rb # Assembles the final Hash
|
|
1015
|
+
│ ├── controller_helpers.rb # All render_* helpers (1xx–5xx)
|
|
1016
|
+
│ └── railtie.rb # Auto-includes into Rails controllers
|
|
1017
|
+
└── generators/
|
|
1018
|
+
└── respondo/
|
|
1019
|
+
└── install/
|
|
1020
|
+
└── install_generator.rb # rails generate respondo:install
|
|
780
1021
|
```
|
|
781
1022
|
|
|
782
1023
|
---
|