jsonapi_responses 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0699dc3667f281c640290126057003d615a2925420486b95bf4c6b6ca0498493'
4
- data.tar.gz: 2ad39e4993c900b4d675fcba5cf45065d7b467a34f06c6e99d7e6a246dc87010
3
+ metadata.gz: ab50f413d2756f7f1f6b1faae376ebaf041a60fef7df43b29c037cfdfff295c2
4
+ data.tar.gz: a3921894b3f3c0a3565e4be39667c4c9f643b9837c35c1a1e8c9c1ac491c477d
5
5
  SHA512:
6
- metadata.gz: c9272215b1f4238ab65d30e961a928d1d20307287e1d3b82a9dbbe79f0f08c63567973e3b38cde93777064f20c8d0d977541910cf2b3433604eef0891a662e3b
7
- data.tar.gz: 6b2f625abcbe757534d222f16a1edcbbdd24f1947f5071a6f26c7ad1b2745fe188c51f54d69aaf777124e8af3eacb33a8769a9bc701c3538cf95623fbdc013f7
6
+ metadata.gz: 0bfebe0f3f65b8125776d5161b7c26ac0d673ae979e8ca84ebca91422ef895812260e857ee43404c5a684eeec2e816258397229d25f172b3b4875d311cd6b979
7
+ data.tar.gz: 83a0979577818624cabe42b395c12df5c1ea594768a855469ecd9de0673911bc6b6843731020f21931b81da35f8eb83582d9d99f826f19d13cfc52381d3e9955
data/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.0] - 2025-11-21
4
+
5
+ ### Added
6
+
7
+ - **Automatic Pagination Support**: `respond_for_index` now auto-detects Kaminari/WillPaginate pagination and includes meta automatically
8
+ - **Pagination Helpers in Responder**: Added `paginated?`, `pagination_meta`, and `render_collection_with_meta` methods
9
+ - **Smart Meta Handling**: Automatically merges pagination meta with context meta if both are present
10
+
11
+ ### Features
12
+
13
+ - Auto-detection of paginated collections (checks for `current_page`, `total_pages`, `total_count` methods)
14
+ - Automatic inclusion of pagination metadata: `current_page`, `total_pages`, `total_count`, `per_page`
15
+ - Backward compatible - works with manual `meta` in context or auto-detects pagination
16
+ - Works with both Kaminari and WillPaginate gems
17
+
18
+ ### Example Usage
19
+
20
+ ```ruby
21
+ # Controller - Automatic pagination meta
22
+ def index
23
+ @academies = Academy.page(params[:page]).per(15)
24
+ render_with(@academies) # Auto-includes pagination meta!
25
+ end
26
+
27
+ # Manual meta still works
28
+ def index
29
+ @academies = Academy.page(params[:page]).per(15)
30
+ render_with(@academies, context: { meta: { custom: 'data' } })
31
+ # Merges pagination + custom meta
32
+ end
33
+
34
+ # Custom responder with pagination helper
35
+ class MyResponder < JsonapiResponses::Responder
36
+ def render
37
+ render_collection_with_meta(record, { custom: 'meta' })
38
+ end
39
+ end
40
+ ```
41
+
3
42
  ## [1.0.0] - 2025-10-07
4
43
 
5
44
  ### 🎉 Major Release - Breaking Changes
@@ -7,11 +46,13 @@
7
46
  This is a major release with significant architectural improvements and breaking changes.
8
47
 
9
48
  ### Breaking Changes
49
+
10
50
  - **Renamed `BaseSerializer`** → `ApplicationSerializer` (follows Rails convention)
11
51
  - **Renamed `BaseResponder`** → `ApplicationResponder` (follows Rails convention like ApplicationPolicy)
12
52
  - Responders now follow **Pundit-style pattern**: one responder class per controller with multiple action methods
13
53
 
14
54
  ### Added
55
+
15
56
  - **Pundit-Style Responders**: One responder class per controller (e.g., `AcademyResponder`, `CourseResponder`)
16
57
  - **Action-based method calls**: Use `render_with(@records, responder: AcademyResponder, action: :featured)`
17
58
  - **ApplicationResponder base class**: With common helpers like `render_collection_with_meta`, `base_meta`, `filters_applied`
@@ -43,7 +84,7 @@ class AcademyResponder < ApplicationResponder
43
84
  def featured # ← Method instead of render
44
85
  # ...
45
86
  end
46
-
87
+
47
88
  def popular
48
89
  # ...
49
90
  end
@@ -63,6 +104,7 @@ render_with(@academies, responder: AcademyResponder, action: :featured)
63
104
  #### 3. Consolidate Responders
64
105
 
65
106
  Instead of one file per action:
107
+
66
108
  ```
67
109
  # Before
68
110
  app/responders/academy_featured_responder.rb
@@ -74,7 +116,8 @@ app/responders/academy_responder.rb # All methods inside
74
116
  ```
75
117
 
76
118
  ### Improved
77
- - Better alignment with Rails naming conventions (Application* prefix)
119
+
120
+ - Better alignment with Rails naming conventions (Application\* prefix)
78
121
  - Reduced file proliferation (3-4 responder files instead of 20+)
79
122
  - Easier to maintain and understand (Pundit-style pattern)
80
123
  - Better code organization and discoverability
@@ -82,18 +125,18 @@ app/responders/academy_responder.rb # All methods inside
82
125
  ## [0.3.0] - 2025-10-07
83
126
 
84
127
  ### Added
128
+
85
129
  - **Custom Responders System**: New architecture for handling complex custom actions
86
130
  - Added `JsonapiResponses::Responder` base class for creating dedicated responder objects
87
131
  - Responders encapsulate response logic, keeping controllers clean and promoting reusability
88
132
  - Full access to controller context, serialization helpers, and request params
89
-
90
133
  - **Responder Base Class Features**:
91
134
  - `serialize_collection` and `serialize_item` helpers for data serialization
92
135
  - `render_json` helper for consistent JSON rendering
93
136
  - `collection?` and `single_item?` type checking utilities
94
137
  - Access to `params`, `current_user`, `controller`, `record`, `serializer_class`, and `context`
95
-
96
138
  - **Enhanced render_with**:
139
+
97
140
  - New `responder:` parameter to use custom responder classes
98
141
  - New `serializer:` parameter to override default serializer detection
99
142
  - Example: `render_with(@records, responder: FeaturedResponder, serializer: CustomSerializer)`
@@ -104,17 +147,20 @@ app/responders/academy_responder.rb # All methods inside
104
147
  - `CategorizedResponder` - For grouped/categorized responses
105
148
 
106
149
  ### Changed
150
+
107
151
  - Updated `lib/jsonapi_responses.rb` to require `responder.rb`
108
152
  - Enhanced `Respondable` module to support responder classes
109
153
  - Improved documentation with comprehensive Responder guide
110
154
 
111
155
  ### Documentation
156
+
112
157
  - Added "Custom Responders" section to README with complete examples
113
158
  - Added comparison table: when to use each approach (mapping vs methods vs responders)
114
159
  - Added `RESPONDERS_FEATURE.md` with complete implementation guide
115
160
  - Documented Responder API and best practices
116
161
 
117
162
  ### Benefits
163
+
118
164
  - **Separation of Concerns**: Response logic separated from controllers
119
165
  - **Reusability**: Same responder can be used across multiple controllers
120
166
  - **Testability**: Test response logic independently from controllers
@@ -123,31 +169,35 @@ app/responders/academy_responder.rb # All methods inside
123
169
  ## [0.2.0] - 2025-10-06
124
170
 
125
171
  ### Added
172
+
126
173
  - **Custom Actions Support**: Support for custom actions beyond standard CRUD (index, show, create, update, destroy)
127
174
  - **Explicit Action Mapping**: `map_response_action` and `map_response_actions` for mapping custom actions to existing responses
128
175
  - **Metaprogramming Support**: Dynamic method generation for response handlers
129
176
  - `define_response_for`: Define custom response methods using blocks
130
177
  - `define_responses_for`: Define same behavior for multiple actions in batch
131
178
  - `define_crud_responses`: Intelligent CRUD responses with dynamic context
132
- - `generate_rest_responses`: Auto-generate namespaced REST responses (public_, admin_, etc.)
179
+ - `generate_rest_responses`: Auto-generate namespaced REST responses (public*, admin*, etc.)
133
180
  - **Dynamic Context**: Support for lambdas/procs in context generation with `instance_eval`
134
181
  - **Method Introspection**: `response_definitions` class attribute for debugging generated methods
135
182
  - **Enhanced Error Messages**: Detailed error responses with suggestions when actions are not supported
136
183
 
137
184
  ### Changed
185
+
138
186
  - **Breaking**: Removed automatic fallback behavior for action mapping (by design - explicit is better than implicit)
139
187
  - **Improved**: `render_invalid_action` now provides detailed error information with actionable suggestions
140
188
  - **Enhanced**: Better error handling with specific guidance on how to implement missing methods
141
189
 
142
190
  ### Technical Details
191
+
143
192
  - Added `class_attribute :response_definitions` for storing method definitions
144
193
  - Enhanced `render_with` method resolution to support custom and mapped actions
145
194
  - All generated methods are properly marked as private
146
195
  - Full backward compatibility maintained for existing CRUD operations
147
196
 
148
197
  ### Documentation
198
+
149
199
  - Added comprehensive documentation in CUSTOM_ACTIONS.md
150
- - Added metaprogramming guide in METAPROGRAMMING.md
200
+ - Added metaprogramming guide in METAPROGRAMMING.md
151
201
  - Added practical implementation example in PRACTICAL_EXAMPLE.md
152
202
  - Updated README with new functionality overview
153
203
 
data/README.md CHANGED
@@ -147,11 +147,160 @@ GET /api/v1/digital_products?view=minimal # Returns minimal response
147
147
  ### Performance Benefits
148
148
 
149
149
  By allowing the frontend to request only the needed data, you can:
150
+
150
151
  - Reduce response payload size
151
152
  - Improve API performance
152
153
  - Avoid creating multiple endpoints for different data requirements
153
154
  - Optimize database queries based on the requested view
154
155
 
156
+ ## Automatic Pagination Support
157
+
158
+ **New in v1.1.0:** JsonapiResponses now automatically detects and handles paginated collections using Kaminari, making pagination effortless.
159
+
160
+ ### Basic Usage
161
+
162
+ When you paginate your records with Kaminari, pagination metadata is automatically included in the response:
163
+
164
+ ```ruby
165
+ class Api::V1::AcademiesController < ApplicationController
166
+ include JsonapiResponses::Respondable
167
+
168
+ def index
169
+ academies = Academy.page(params[:page]).per(15)
170
+
171
+ # That's it! Pagination is automatic
172
+ render_with(academies)
173
+ end
174
+ end
175
+ ```
176
+
177
+ **Response:**
178
+
179
+ ```json
180
+ {
181
+ "data": [
182
+ { "id": 1, "name": "Academy 1", ... },
183
+ { "id": 2, "name": "Academy 2", ... }
184
+ ],
185
+ "meta": {
186
+ "current_page": 1,
187
+ "total_pages": 5,
188
+ "total_count": 73,
189
+ "per_page": 15
190
+ }
191
+ }
192
+ ```
193
+
194
+ ### How It Works
195
+
196
+ JsonapiResponses automatically detects if your collection responds to Kaminari's pagination methods:
197
+ - `current_page`
198
+ - `total_pages`
199
+ - `total_count`
200
+
201
+ If these methods exist, pagination metadata is automatically included in the response.
202
+
203
+ ### With Custom Views
204
+
205
+ Pagination works seamlessly with different view formats:
206
+
207
+ ```ruby
208
+ def index
209
+ academies = Academy
210
+ .includes(:owner, :courses)
211
+ .page(params[:page])
212
+ .per(params[:per_page] || 15)
213
+
214
+ # Supports view parameter and automatic pagination
215
+ render_with(academies)
216
+ end
217
+ ```
218
+
219
+ **Request:**
220
+ ```
221
+ GET /api/v1/academies?page=2&per_page=20&view=summary
222
+ ```
223
+
224
+ **Response:**
225
+ ```json
226
+ {
227
+ "data": [
228
+ { "id": 21, "name": "Academy 21", ... }
229
+ ],
230
+ "meta": {
231
+ "current_page": 2,
232
+ "total_pages": 4,
233
+ "total_count": 73,
234
+ "per_page": 20
235
+ }
236
+ }
237
+ ```
238
+
239
+ ### Requirements
240
+
241
+ - **Kaminari gem** must be installed and configured
242
+ - Your collection must be paginated with `.page()` method
243
+
244
+ ### Custom Pagination Metadata
245
+
246
+ You can add additional metadata alongside automatic pagination:
247
+
248
+ ```ruby
249
+ def index
250
+ academies = Academy.page(params[:page]).per(15)
251
+
252
+ render_with(
253
+ academies,
254
+ context: { view: view },
255
+ meta: {
256
+ fetched_at: Time.current,
257
+ filters_applied: params[:search].present?
258
+ }
259
+ )
260
+ end
261
+ ```
262
+
263
+ **Response:**
264
+ ```json
265
+ {
266
+ "data": [...],
267
+ "meta": {
268
+ "current_page": 1,
269
+ "total_pages": 5,
270
+ "total_count": 73,
271
+ "per_page": 15,
272
+ "fetched_at": "2024-01-15T10:30:00Z",
273
+ "filters_applied": true
274
+ }
275
+ }
276
+ ```
277
+
278
+ ### Using Pagination Helpers
279
+
280
+ For custom responders, use the built-in pagination helpers:
281
+
282
+ ```ruby
283
+ class AcademyResponder < JsonapiResponses::Responder
284
+ def respond_for_index
285
+ if paginated?(record)
286
+ render json: {
287
+ data: serialize_collection(record, serializer_class, context),
288
+ meta: pagination_meta(record, context)
289
+ }
290
+ else
291
+ render json: {
292
+ data: serialize_collection(record, serializer_class, context)
293
+ }
294
+ end
295
+ end
296
+ end
297
+ ```
298
+
299
+ Available helpers:
300
+ - `paginated?(record)` - Check if record supports pagination
301
+ - `pagination_meta(record, context)` - Extract pagination metadata hash
302
+ - `render_collection_with_meta(record, serializer_class, context)` - Render with automatic pagination
303
+
155
304
  ## Development
156
305
 
157
306
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -165,14 +314,15 @@ Beyond the standard CRUD actions (index, show, create, update, destroy), you can
165
314
  ### Basic Usage
166
315
 
167
316
  **Option 1: Map to existing actions**
317
+
168
318
  ```ruby
169
319
  class Api::V1::CoursesController < ApplicationController
170
320
  include JsonapiResponses::Respondable
171
-
321
+
172
322
  # Map custom actions to existing response methods
173
323
  map_response_action :public_index, to: :index
174
324
  map_response_action :public_show, to: :show
175
-
325
+
176
326
  def public_index
177
327
  # Your logic here
178
328
  render_with(@courses) # Will use respond_for_index
@@ -181,17 +331,18 @@ end
181
331
  ```
182
332
 
183
333
  **Option 2: Define custom response methods**
334
+
184
335
  ```ruby
185
336
  class Api::V1::CoursesController < ApplicationController
186
337
  include JsonapiResponses::Respondable
187
-
338
+
188
339
  def dashboard_stats
189
340
  # Your logic here
190
341
  render_with(@stats)
191
342
  end
192
-
343
+
193
344
  private
194
-
345
+
195
346
  def respond_for_dashboard_stats(record, serializer_class, context)
196
347
  render json: {
197
348
  data: record,
@@ -202,17 +353,18 @@ end
202
353
  ```
203
354
 
204
355
  **Option 3: Metaprogramming (Recommended for complex scenarios)**
356
+
205
357
  ```ruby
206
358
  class Api::V1::CoursesController < ApplicationController
207
359
  include JsonapiResponses::Respondable
208
-
360
+
209
361
  # Generate methods automatically
210
362
  generate_rest_responses(
211
363
  namespace: 'public',
212
364
  actions: [:index, :show],
213
365
  context: { access_level: 'public' }
214
366
  )
215
-
367
+
216
368
  # Define similar responses in batch
217
369
  define_responses_for [:export_csv, :export_pdf] do |record, serializer_class, context|
218
370
  format = action_name.to_s.split('_').last
@@ -253,26 +405,26 @@ app/responders/
253
405
  # app/responders/application_responder.rb
254
406
  class ApplicationResponder < JsonapiResponses::Responder
255
407
  protected
256
-
408
+
257
409
  def render_collection_with_meta(type: nil, additional_meta: {})
258
410
  render_json({
259
411
  data: serialize_collection(record),
260
412
  meta: base_meta.merge({ type: type }.compact).merge(additional_meta)
261
413
  })
262
414
  end
263
-
415
+
264
416
  def base_meta
265
417
  {
266
418
  timestamp: Time.current.iso8601,
267
419
  count: record_count
268
420
  }.compact
269
421
  end
270
-
422
+
271
423
  def record_count
272
424
  return nil unless collection?
273
425
  record.respond_to?(:count) ? record.count : record.size
274
426
  end
275
-
427
+
276
428
  def filters_applied
277
429
  filter_keys = [:category_id, :level, :status]
278
430
  filters = {}
@@ -287,7 +439,7 @@ end
287
439
  ```ruby
288
440
  # app/responders/academy_responder.rb
289
441
  class AcademyResponder < ApplicationResponder
290
-
442
+
291
443
  # GET /api/v1/academies/featured
292
444
  def featured
293
445
  if params[:category_id].present?
@@ -296,7 +448,7 @@ class AcademyResponder < ApplicationResponder
296
448
  render_all_featured
297
449
  end
298
450
  end
299
-
451
+
300
452
  # GET /api/v1/academies/popular
301
453
  def popular
302
454
  render_collection_with_meta(
@@ -307,7 +459,7 @@ class AcademyResponder < ApplicationResponder
307
459
  }
308
460
  )
309
461
  end
310
-
462
+
311
463
  # GET /api/v1/academies/recommended
312
464
  def recommended
313
465
  render_collection_with_meta(
@@ -318,9 +470,9 @@ class AcademyResponder < ApplicationResponder
318
470
  }
319
471
  )
320
472
  end
321
-
473
+
322
474
  private
323
-
475
+
324
476
  def render_filtered_featured
325
477
  render_json({
326
478
  data: serialize_collection(record),
@@ -330,7 +482,7 @@ class AcademyResponder < ApplicationResponder
330
482
  }
331
483
  })
332
484
  end
333
-
485
+
334
486
  def render_all_featured
335
487
  render_collection_with_meta(type: 'featured')
336
488
  end
@@ -347,12 +499,12 @@ class Api::V1::AcademiesController < ApplicationController
347
499
  @academies = load_featured_academies
348
500
  render_with(@academies, responder: AcademyResponder, action: :featured)
349
501
  end
350
-
502
+
351
503
  def popular
352
504
  @academies = Academy.popular.limit(20)
353
505
  render_with(@academies, responder: AcademyResponder, action: :popular)
354
506
  end
355
-
507
+
356
508
  def recommended
357
509
  @academies = Academy.recommended_for(current_user)
358
510
  render_with(@academies, responder: AcademyResponder, action: :recommended)
@@ -363,12 +515,14 @@ end
363
515
  ### Benefits of This Pattern
364
516
 
365
517
  **Like Pundit Policies:**
518
+
366
519
  - ✅ One file per controller (not per action)
367
520
  - ✅ All related logic in one place
368
521
  - ✅ Easy to find and maintain
369
522
  - ✅ Shared helpers in base class
370
523
 
371
524
  **Example Structure:**
525
+
372
526
  ```
373
527
  AcademiesController → AcademyResponder (featured, popular, recommended)
374
528
  CoursesController → CourseResponder (featured, search, progress)
@@ -384,18 +538,18 @@ class MyCustomResponder < JsonapiResponses::Responder
384
538
  def render
385
539
  # Access to controller instance
386
540
  controller.current_user
387
-
541
+
388
542
  # Access to params
389
543
  params[:filter]
390
-
544
+
391
545
  # Serialize data
392
546
  serialize_collection(record) # For collections
393
547
  serialize_item(record) # For single items
394
-
548
+
395
549
  # Check record type
396
550
  collection? # true if record is a collection
397
551
  single_item? # true if record is a single item
398
-
552
+
399
553
  # Render JSON
400
554
  render_json({ data: [], meta: {} })
401
555
  end
@@ -419,14 +573,14 @@ class CategorizedResponder < JsonapiResponses::Responder
419
573
  private
420
574
 
421
575
  def structured_data?
422
- record.is_a?(Array) &&
423
- record.first.is_a?(Hash) &&
576
+ record.is_a?(Array) &&
577
+ record.first.is_a?(Hash) &&
424
578
  record.first.key?(:category)
425
579
  end
426
580
 
427
581
  def group_by_category
428
582
  categories = {}
429
-
583
+
430
584
  serialize_collection(record).each do |item|
431
585
  category_id = item.dig(:category, :id) || 'uncategorized'
432
586
  categories[category_id] ||= {
@@ -435,7 +589,7 @@ class CategorizedResponder < JsonapiResponses::Responder
435
589
  }
436
590
  categories[category_id][:items] << item
437
591
  end
438
-
592
+
439
593
  categories.values.map do |group|
440
594
  group.merge(count: group[:items].size)
441
595
  end
@@ -445,12 +599,12 @@ end
445
599
 
446
600
  ### When to Use Each Approach
447
601
 
448
- | Approach | Best For | Complexity |
449
- |----------|----------|------------|
450
- | `map_response_action` | Simple actions similar to existing ones | Low |
451
- | `respond_for_*` methods | 1-2 custom actions with simple logic | Medium |
452
- | Custom Responders | 3+ custom actions or complex response logic | High |
453
- | Metaprogramming | Batch generation of similar actions | High |
602
+ | Approach | Best For | Complexity |
603
+ | ----------------------- | ------------------------------------------- | ---------- |
604
+ | `map_response_action` | Simple actions similar to existing ones | Low |
605
+ | `respond_for_*` methods | 1-2 custom actions with simple logic | Medium |
606
+ | Custom Responders | 3+ custom actions or complex response logic | High |
607
+ | Metaprogramming | Batch generation of similar actions | High |
454
608
 
455
609
  ### Mixing Approaches
456
610
 
@@ -459,23 +613,23 @@ You can combine different approaches in the same controller:
459
613
  ```ruby
460
614
  class Api::V1::ProductsController < ApplicationController
461
615
  include JsonapiResponses::Respondable
462
-
616
+
463
617
  # Map simple actions
464
618
  map_response_action :public_index, to: :index
465
-
619
+
466
620
  # Use responder for complex actions
467
621
  def featured
468
622
  @products = Product.featured
469
623
  render_with(@products, responder: FeaturedResponder)
470
624
  end
471
-
625
+
472
626
  # Use custom method for one-off logic
473
627
  def statistics
474
628
  render_with(@stats)
475
629
  end
476
-
630
+
477
631
  private
478
-
632
+
479
633
  def respond_for_statistics(record, serializer_class, context)
480
634
  render json: { stats: record, generated_at: Time.current }
481
635
  end
@@ -0,0 +1,99 @@
1
+ # ApplicationResponder - Base class for all responders
2
+ #
3
+ # This follows Rails convention (like ApplicationRecord, ApplicationController)
4
+ # and Pundit pattern (like ApplicationPolicy).
5
+ #
6
+ # Create one responder per controller with multiple action methods inside.
7
+ #
8
+ # @example Creating a resource responder
9
+ # class ProductResponder < ApplicationResponder
10
+ # # GET /products/featured
11
+ # def featured
12
+ # render_collection_with_meta(
13
+ # type: 'featured',
14
+ # additional_meta: { category: params[:category_id] }
15
+ # )
16
+ # end
17
+ #
18
+ # # GET /products/popular
19
+ # def popular
20
+ # render_collection_with_meta(
21
+ # type: 'popular',
22
+ # additional_meta: { period: params[:period] || 'month' }
23
+ # )
24
+ # end
25
+ # end
26
+ #
27
+ # @example Using in controller
28
+ # class ProductsController < ApplicationController
29
+ # def featured
30
+ # @products = Product.featured
31
+ # render_with(@products, responder: ProductResponder, action: :featured)
32
+ # end
33
+ # end
34
+ class ApplicationResponder < JsonapiResponses::Responder
35
+ # Common helper methods available to all responders
36
+
37
+ protected
38
+
39
+ # Render a standard collection with metadata
40
+ # @param type [String, Symbol] Type of collection (e.g., 'featured', 'popular')
41
+ # @param additional_meta [Hash] Additional metadata to include
42
+ def render_collection_with_meta(type: nil, additional_meta: {})
43
+ render_json({
44
+ data: serialize_collection(record),
45
+ meta: base_meta.merge({ type: type }.compact).merge(additional_meta)
46
+ })
47
+ end
48
+
49
+ # Render a single item with metadata
50
+ # @param additional_meta [Hash] Additional metadata to include
51
+ def render_item_with_meta(additional_meta: {})
52
+ render_json({
53
+ data: serialize_item(record),
54
+ meta: base_meta.merge(additional_meta)
55
+ })
56
+ end
57
+
58
+ # Render grouped/categorized data
59
+ # @param groups [Array, Hash] Pre-structured grouped data
60
+ def render_grouped_data(groups)
61
+ render_json(groups)
62
+ end
63
+
64
+ # Base metadata common to all responses
65
+ # @return [Hash] Base metadata including timestamp and count
66
+ def base_meta
67
+ {
68
+ timestamp: Time.current.iso8601,
69
+ count: record_count
70
+ }.compact
71
+ end
72
+
73
+ # Get the count of records
74
+ # @return [Integer, nil] Count if collection, nil otherwise
75
+ def record_count
76
+ return nil unless collection?
77
+ record.respond_to?(:count) ? record.count : record.size
78
+ end
79
+
80
+ # Check if a parameter is present
81
+ # @param key [Symbol, String] Parameter key
82
+ # @return [Boolean]
83
+ def param_present?(key)
84
+ params[key].present?
85
+ end
86
+
87
+ # Get filters applied from params
88
+ # @param filter_keys [Array<Symbol>] Keys to check for filters
89
+ # @return [Hash, nil] Hash of applied filters or nil if none
90
+ def filters_applied(filter_keys = [:category_id, :level, :status, :sort_by, :limit])
91
+ filters = {}
92
+
93
+ filter_keys.each do |key|
94
+ filters[key] = params[key] if params[key].present?
95
+ end
96
+
97
+ filters.empty? ? nil : filters
98
+ end
99
+ end
@@ -0,0 +1,88 @@
1
+ # ApplicationSerializer - Base class for all serializers
2
+ #
3
+ # This follows Rails convention (like ApplicationRecord, ApplicationController).
4
+ # All your model serializers should inherit from this class.
5
+ #
6
+ # @example Creating a model serializer
7
+ # class ProductSerializer < ApplicationSerializer
8
+ # def serializable_hash
9
+ # case context[:view]
10
+ # when :summary
11
+ # summary_hash
12
+ # when :minimal
13
+ # minimal_hash
14
+ # else
15
+ # full_hash
16
+ # end
17
+ # end
18
+ #
19
+ # private
20
+ #
21
+ # def full_hash
22
+ # {
23
+ # id: resource.id,
24
+ # name: resource.name,
25
+ # description: resource.description,
26
+ # price: resource.price,
27
+ # created_at: resource.created_at
28
+ # }
29
+ # end
30
+ #
31
+ # def summary_hash
32
+ # {
33
+ # id: resource.id,
34
+ # name: resource.name,
35
+ # price: resource.price
36
+ # }
37
+ # end
38
+ #
39
+ # def minimal_hash
40
+ # {
41
+ # id: resource.id,
42
+ # name: resource.name
43
+ # }
44
+ # end
45
+ # end
46
+ class ApplicationSerializer
47
+ attr_reader :resource, :context
48
+
49
+ # Initialize serializer with resource and optional context
50
+ # @param resource [Object] The object to serialize
51
+ # @param context [Hash] Additional context (e.g., current_user, view type)
52
+ def initialize(resource, context = {})
53
+ @resource = resource
54
+ @context = context
55
+ end
56
+
57
+ # Override this method in your serializers
58
+ # @return [Hash] Serialized representation of the resource
59
+ def serializable_hash
60
+ raise NotImplementedError, "#{self.class.name} must implement #serializable_hash"
61
+ end
62
+
63
+ # Access to current_user from context
64
+ # @return [User, nil]
65
+ def current_user
66
+ @context[:current_user]
67
+ end
68
+
69
+ # Access to view type from context
70
+ # @return [Symbol, nil] e.g., :summary, :minimal, :full
71
+ def view
72
+ @context[:view]
73
+ end
74
+
75
+ # Helper to serialize associations
76
+ # @param association [Object, Array] Association to serialize
77
+ # @param serializer_class [Class] Serializer class to use
78
+ # @return [Hash, Array<Hash>]
79
+ def serialize_association(association, serializer_class)
80
+ return nil if association.nil?
81
+
82
+ if association.respond_to?(:map)
83
+ association.map { |item| serializer_class.new(item, context).serializable_hash }
84
+ else
85
+ serializer_class.new(association, context).serializable_hash
86
+ end
87
+ end
88
+ end
@@ -212,7 +212,17 @@ module JsonapiResponses
212
212
  end
213
213
 
214
214
  def respond_for_index(record, serializer_class, context)
215
- render json: serialize_collection(record, serializer_class, context)
215
+ response = { data: serialize_collection(record, serializer_class, context) }
216
+
217
+ # Auto-detect pagination and add meta if available
218
+ if paginated?(record)
219
+ response[:meta] = pagination_meta(record, context)
220
+ elsif context[:meta]
221
+ # Allow manual meta from context
222
+ response[:meta] = context[:meta]
223
+ end
224
+
225
+ render json: response
216
226
  end
217
227
 
218
228
  def respond_for_show(record, serializer_class, context)
@@ -259,5 +269,25 @@ module JsonapiResponses
259
269
  ]
260
270
  }, status: :bad_request
261
271
  end
272
+
273
+ # Check if record is paginated (Kaminari or WillPaginate support)
274
+ def paginated?(record)
275
+ record.respond_to?(:current_page) &&
276
+ record.respond_to?(:total_pages) &&
277
+ record.respond_to?(:total_count)
278
+ end
279
+
280
+ # Extract pagination metadata from paginated record
281
+ def pagination_meta(record, context = {})
282
+ base_meta = {
283
+ current_page: record.current_page,
284
+ total_pages: record.total_pages,
285
+ total_count: record.total_count,
286
+ per_page: record.try(:limit_value) || record.try(:per_page) || context[:per_page]
287
+ }.compact
288
+
289
+ # Merge with any additional meta from context
290
+ context[:meta] ? base_meta.merge(context[:meta]) : base_meta
291
+ end
262
292
  end
263
293
  end
@@ -0,0 +1,149 @@
1
+ module JsonapiResponses
2
+ # Base class for custom responders
3
+ # Responders encapsulate response logic for custom actions,
4
+ # keeping controllers clean and promoting reusability.
5
+ #
6
+ # @example Basic usage
7
+ # class FeaturedResponder < JsonapiResponses::Responder
8
+ # def render
9
+ # controller.render json: {
10
+ # data: serialize_collection(record),
11
+ # meta: {
12
+ # type: 'featured',
13
+ # count: record.count
14
+ # }
15
+ # }
16
+ # end
17
+ # end
18
+ #
19
+ # @example Using in controller
20
+ # def featured
21
+ # @academies = Academy.featured
22
+ # render_with(@academies, responder: FeaturedResponder)
23
+ # end
24
+ class Responder
25
+ attr_reader :controller, :record, :serializer_class, :context
26
+
27
+ # @param controller [ActionController::Base] The controller instance
28
+ # @param record [Object, Array, ActiveRecord::Relation] The record(s) to serialize
29
+ # @param serializer_class [Class] The serializer class to use
30
+ # @param context [Hash] Additional context for serialization
31
+ def initialize(controller, record, serializer_class, context = {})
32
+ @controller = controller
33
+ @record = record
34
+ @serializer_class = serializer_class
35
+ @context = context
36
+ end
37
+
38
+ # Render the response. Must be implemented by subclasses.
39
+ # @raise [NotImplementedError] if not implemented in subclass
40
+ def render
41
+ raise NotImplementedError, "#{self.class.name} must implement #render method"
42
+ end
43
+
44
+ protected
45
+
46
+ # Serialize a collection of records
47
+ # @param records [Array, ActiveRecord::Relation] Records to serialize
48
+ # @param custom_serializer [Class, nil] Optional custom serializer
49
+ # @param custom_context [Hash, nil] Optional custom context
50
+ # @return [Array<Hash>] Serialized collection
51
+ def serialize_collection(records = nil, custom_serializer = nil, custom_context = nil)
52
+ records ||= record
53
+ serializer = custom_serializer || serializer_class
54
+ ctx = custom_context || context
55
+
56
+ controller.send(:serialize_collection, records, serializer, ctx)
57
+ end
58
+
59
+ # Serialize a single record
60
+ # @param item [Object] Record to serialize
61
+ # @param custom_serializer [Class, nil] Optional custom serializer
62
+ # @param custom_context [Hash, nil] Optional custom context
63
+ # @return [Hash] Serialized record
64
+ def serialize_item(item = nil, custom_serializer = nil, custom_context = nil)
65
+ item ||= record
66
+ serializer = custom_serializer || serializer_class
67
+ ctx = custom_context || context
68
+
69
+ controller.send(:serialize_item, item, serializer, ctx)
70
+ end
71
+
72
+ # Access to params from the controller
73
+ # @return [ActionController::Parameters]
74
+ def params
75
+ controller.params
76
+ end
77
+
78
+ # Access to current_user from the controller (if available)
79
+ # @return [Object, nil]
80
+ def current_user
81
+ controller.respond_to?(:current_user, true) ? controller.send(:current_user) : nil
82
+ end
83
+
84
+ # Helper to render JSON directly through the controller
85
+ # @param data [Hash] Data to render
86
+ # @param options [Hash] Additional render options (status, etc.)
87
+ def render_json(data, options = {})
88
+ controller.render({ json: data }.merge(options))
89
+ end
90
+
91
+ # Helper to check if record is a collection
92
+ # @return [Boolean]
93
+ def collection?
94
+ record.is_a?(Array) ||
95
+ record.is_a?(ActiveRecord::Relation) ||
96
+ (record.respond_to?(:to_a) && !record.is_a?(Hash))
97
+ end
98
+
99
+ # Helper to check if record is a single item
100
+ # @return [Boolean]
101
+ def single_item?
102
+ !collection?
103
+ end
104
+
105
+ # Check if record is paginated (Kaminari or WillPaginate support)
106
+ # @return [Boolean]
107
+ def paginated?
108
+ record.respond_to?(:current_page) &&
109
+ record.respond_to?(:total_pages) &&
110
+ record.respond_to?(:total_count)
111
+ end
112
+
113
+ # Extract pagination metadata from paginated record
114
+ # @return [Hash, nil] Pagination metadata or nil if not paginated
115
+ def pagination_meta
116
+ return nil unless paginated?
117
+
118
+ {
119
+ current_page: record.current_page,
120
+ total_pages: record.total_pages,
121
+ total_count: record.total_count,
122
+ per_page: record.try(:limit_value) || record.try(:per_page)
123
+ }.compact
124
+ end
125
+
126
+ # Render collection with automatic pagination support
127
+ # @param records [Array, ActiveRecord::Relation] Records to render
128
+ # @param additional_meta [Hash] Additional metadata to include
129
+ def render_collection_with_meta(records = nil, additional_meta = {})
130
+ records ||= record
131
+ response = { data: serialize_collection(records) }
132
+
133
+ # Auto-detect pagination
134
+ if records.respond_to?(:current_page)
135
+ meta = {
136
+ current_page: records.current_page,
137
+ total_pages: records.total_pages,
138
+ total_count: records.total_count,
139
+ per_page: records.try(:limit_value) || records.try(:per_page)
140
+ }.compact
141
+ response[:meta] = meta.merge(additional_meta)
142
+ elsif additional_meta.any?
143
+ response[:meta] = additional_meta
144
+ end
145
+
146
+ render_json(response)
147
+ end
148
+ end
149
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonapiResponses
4
- VERSION = '1.0.0'.freeze
4
+ VERSION = '1.1.0'.freeze
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonapi_responses
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Ortega
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-08 00:00:00.000000000 Z
11
+ date: 2025-11-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: JsonapiResponses simplifies API response handling by allowing multiple
14
14
  response formats from a single endpoint, improving performance and reducing endpoint
@@ -28,11 +28,14 @@ files:
28
28
  - README.md
29
29
  - Rakefile
30
30
  - app/models/item.rb
31
+ - app/responders/application_responder.rb
32
+ - app/serializers/application_serializer.rb
31
33
  - app/serializers/item_serializer.rb
32
34
  - config/database.yml
33
35
  - lib/jsonapi_responses.rb
34
36
  - lib/jsonapi_responses/engine.rb
35
37
  - lib/jsonapi_responses/respondable.rb
38
+ - lib/jsonapi_responses/responder.rb
36
39
  - lib/jsonapi_responses/serializable.rb
37
40
  - lib/jsonapi_responses/user_context_provider.rb
38
41
  - lib/jsonapi_responses/version.rb