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.
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 # default message
140
- render_bad_request("The 'date' parameter is required")
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 # "Unauthorized"
150
- render_unauthorized("Please log in to continue")
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
- Requested resource does not exist.
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
- render_not_found
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
- #### `render_method_not_allowed` — 405 Method Not Allowed
182
- The HTTP verb used is not supported for this endpoint.
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
- render_method_not_allowed
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
- #### `render_not_acceptable` — 406 Not Acceptable
190
- The server cannot produce a response matching the client's Accept header.
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
- render_not_acceptable
194
- render_not_acceptable("Only application/json is supported")
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
- #### `render_request_timeout` — 408 Request Timeout
198
- The request took too long to process.
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
- render_request_timeout
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
- #### `render_conflict` — 409 Conflict
206
- Request conflicts with the current state of the resource. Use for duplicate records, state conflicts.
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
- render_conflict
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
- #### `render_gone` — 410 Gone
216
- Resource existed but has been permanently deleted.
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
- render_gone
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
- #### `render_precondition_failed` — 412 Precondition Failed
224
- Conditional request headers (If-Match, If-None-Match) did not match.
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
- render_precondition_failed
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
- #### `render_unsupported_media_type` — 415 Unsupported Media Type
232
- The Content-Type header is not supported.
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
- render_unsupported_media_type
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"], age: ["must be over 18"] })
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
- #### `render_locked` — 423 Locked
248
- Resource is locked and cannot be modified.
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
- render_locked
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
- #### `render_too_many_requests` — 429 Too Many Requests
257
- Rate limit exceeded.
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("You have exceeded 100 requests per minute. Retry after 60 seconds.")
262
- render_too_many_requests("API limit reached", code: "API_LIMIT_EXCEEDED")
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 Not Implemented
283
- The requested feature has not been built yet.
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
- render_not_implemented
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
- #### `render_bad_gateway` — 502 Bad Gateway
291
- An upstream service (third-party API, microservice) returned an invalid response.
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
- render_bad_gateway
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
- #### `render_service_unavailable` — 503 Service Unavailable
300
- Server is temporarily unable to handle the request — maintenance, overloaded.
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
- render_service_unavailable
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
- #### `render_gateway_timeout` — 504 Gateway Timeout
308
- An upstream service timed out before responding.
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
- render_gateway_timeout
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
- users = User.active.page(params[:page]).per(25)
324
- render_ok(data: users, message: "Users fetched")
325
- # → 200, with pagination meta auto-included
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: { estimated_time: "2 minutes" },
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
- ## Quick Reference Card
550
+ ## Pagination
407
551
 
408
- ```ruby
409
- # 2xx Success
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
- # 4xx — Client Errors
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
- # 5xx — Server Errors
436
- render_server_error(message, code:)
437
- render_not_implemented(message, code:)
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
- ## Auto-Serialization
576
+ ### Pagy
446
577
 
447
- Respondo automatically handles:
578
+ ```ruby
579
+ def index
580
+ @pagy, @users = pagy(User.all, items: 25)
448
581
 
449
- | Input type | Output |
450
- |---------------------------------|---------------------------------------------|
451
- | `ActiveRecord::Base` instance | `record.as_json` |
452
- | `ActiveRecord::Relation` | Array of `as_json` records |
453
- | `ActiveModel::Errors` | `{ field: ["message", ...] }` |
454
- | `Hash` | Passed through (values serialized) |
455
- | `Array` | Each element serialized recursively |
456
- | `Exception` | `{ message: e.message }` |
457
- | Anything with `#as_json` | `.as_json` |
458
- | Anything with `#to_h` | `.to_h` |
459
- | Primitives (String, Integer...) | As-is |
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
- ### Custom serializer
597
+ ### WillPaginate
462
598
 
463
599
  ```ruby
464
- Respondo.configure do |config|
465
- # Use ActiveModelSerializers, Blueprinter, Panko, etc.
466
- config.serializer = ->(obj) { UserSerializer.new(obj).as_json }
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
- ## Pagination Meta
620
+ ```ruby
621
+ render_ok(data: @user, message: "User found")
622
+ # → meta will have no pagination key at all
623
+ ```
473
624
 
474
- Automatically detected — no extra code needed.
625
+ ---
475
626
 
476
- ### Kaminari
627
+ ## Quick Reference Card
477
628
 
478
629
  ```ruby
479
- users = User.page(params[:page]).per(25)
480
- render_ok(data: users)
481
- # meta.pagination is populated automatically
482
- ```
630
+ # Core
631
+ render_success(data:, message:, meta:, code:, pagination:, code:, status:)
632
+ render_error(message:, errors:, code:, meta:, status:)
483
633
 
484
- ### Pagy
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
- ```ruby
487
- pagy, users = pagy(User.all)
488
- render_ok(data: users, pagy: pagy)
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
- ### WillPaginate
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
- ```ruby
494
- users = User.paginate(page: params[:page], per_page: 25)
495
- render_ok(data: users)
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
- ### Suppress pagination
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
- # Even if the collection is paginated, hide the meta
502
- render_ok(data: users, pagination: false)
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 (2xx, 4xx, 5xx)
778
+ ├── controller_helpers.rb # All render_* helpers (1xx–5xx)
551
779
  └── railtie.rb # Auto-includes into Rails controllers
552
780
  ```
553
781