jsonapi_responses 1.0.0 → 1.0.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 +17 -6
- data/README.md +44 -38
- data/app/responders/application_responder.rb +99 -0
- data/app/serializers/application_serializer.rb +88 -0
- data/lib/jsonapi_responses/responder.rb +105 -0
- data/lib/jsonapi_responses/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a729df17b8e8b391b9813a8a5c33234f9ff41893b52e4e25709a0df6e424e67
|
4
|
+
data.tar.gz: 533245b43a98e243764f278a5a0290ca7e6d6d35307a068393e2db0492432f9f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4abb25e296611f19b14379bb81c463d83210028ed4115453f100afa493d817234ea754e3f19211f7008bb4ab35347e4ee02516e26c44644260864aa68fb6cdba
|
7
|
+
data.tar.gz: 3a8e7416b1017a718b505e041287a75512ec3aec5d73ed60e132c16d1c204a688cf135004c30ebb45c79df731c4be6f22d6fe76f2f3cf3e916c9ed10380129da
|
data/CHANGELOG.md
CHANGED
@@ -7,11 +7,13 @@
|
|
7
7
|
This is a major release with significant architectural improvements and breaking changes.
|
8
8
|
|
9
9
|
### Breaking Changes
|
10
|
+
|
10
11
|
- **Renamed `BaseSerializer`** → `ApplicationSerializer` (follows Rails convention)
|
11
12
|
- **Renamed `BaseResponder`** → `ApplicationResponder` (follows Rails convention like ApplicationPolicy)
|
12
13
|
- Responders now follow **Pundit-style pattern**: one responder class per controller with multiple action methods
|
13
14
|
|
14
15
|
### Added
|
16
|
+
|
15
17
|
- **Pundit-Style Responders**: One responder class per controller (e.g., `AcademyResponder`, `CourseResponder`)
|
16
18
|
- **Action-based method calls**: Use `render_with(@records, responder: AcademyResponder, action: :featured)`
|
17
19
|
- **ApplicationResponder base class**: With common helpers like `render_collection_with_meta`, `base_meta`, `filters_applied`
|
@@ -43,7 +45,7 @@ class AcademyResponder < ApplicationResponder
|
|
43
45
|
def featured # ← Method instead of render
|
44
46
|
# ...
|
45
47
|
end
|
46
|
-
|
48
|
+
|
47
49
|
def popular
|
48
50
|
# ...
|
49
51
|
end
|
@@ -63,6 +65,7 @@ render_with(@academies, responder: AcademyResponder, action: :featured)
|
|
63
65
|
#### 3. Consolidate Responders
|
64
66
|
|
65
67
|
Instead of one file per action:
|
68
|
+
|
66
69
|
```
|
67
70
|
# Before
|
68
71
|
app/responders/academy_featured_responder.rb
|
@@ -74,7 +77,8 @@ app/responders/academy_responder.rb # All methods inside
|
|
74
77
|
```
|
75
78
|
|
76
79
|
### Improved
|
77
|
-
|
80
|
+
|
81
|
+
- Better alignment with Rails naming conventions (Application\* prefix)
|
78
82
|
- Reduced file proliferation (3-4 responder files instead of 20+)
|
79
83
|
- Easier to maintain and understand (Pundit-style pattern)
|
80
84
|
- Better code organization and discoverability
|
@@ -82,18 +86,18 @@ app/responders/academy_responder.rb # All methods inside
|
|
82
86
|
## [0.3.0] - 2025-10-07
|
83
87
|
|
84
88
|
### Added
|
89
|
+
|
85
90
|
- **Custom Responders System**: New architecture for handling complex custom actions
|
86
91
|
- Added `JsonapiResponses::Responder` base class for creating dedicated responder objects
|
87
92
|
- Responders encapsulate response logic, keeping controllers clean and promoting reusability
|
88
93
|
- Full access to controller context, serialization helpers, and request params
|
89
|
-
|
90
94
|
- **Responder Base Class Features**:
|
91
95
|
- `serialize_collection` and `serialize_item` helpers for data serialization
|
92
96
|
- `render_json` helper for consistent JSON rendering
|
93
97
|
- `collection?` and `single_item?` type checking utilities
|
94
98
|
- Access to `params`, `current_user`, `controller`, `record`, `serializer_class`, and `context`
|
95
|
-
|
96
99
|
- **Enhanced render_with**:
|
100
|
+
|
97
101
|
- New `responder:` parameter to use custom responder classes
|
98
102
|
- New `serializer:` parameter to override default serializer detection
|
99
103
|
- Example: `render_with(@records, responder: FeaturedResponder, serializer: CustomSerializer)`
|
@@ -104,17 +108,20 @@ app/responders/academy_responder.rb # All methods inside
|
|
104
108
|
- `CategorizedResponder` - For grouped/categorized responses
|
105
109
|
|
106
110
|
### Changed
|
111
|
+
|
107
112
|
- Updated `lib/jsonapi_responses.rb` to require `responder.rb`
|
108
113
|
- Enhanced `Respondable` module to support responder classes
|
109
114
|
- Improved documentation with comprehensive Responder guide
|
110
115
|
|
111
116
|
### Documentation
|
117
|
+
|
112
118
|
- Added "Custom Responders" section to README with complete examples
|
113
119
|
- Added comparison table: when to use each approach (mapping vs methods vs responders)
|
114
120
|
- Added `RESPONDERS_FEATURE.md` with complete implementation guide
|
115
121
|
- Documented Responder API and best practices
|
116
122
|
|
117
123
|
### Benefits
|
124
|
+
|
118
125
|
- **Separation of Concerns**: Response logic separated from controllers
|
119
126
|
- **Reusability**: Same responder can be used across multiple controllers
|
120
127
|
- **Testability**: Test response logic independently from controllers
|
@@ -123,31 +130,35 @@ app/responders/academy_responder.rb # All methods inside
|
|
123
130
|
## [0.2.0] - 2025-10-06
|
124
131
|
|
125
132
|
### Added
|
133
|
+
|
126
134
|
- **Custom Actions Support**: Support for custom actions beyond standard CRUD (index, show, create, update, destroy)
|
127
135
|
- **Explicit Action Mapping**: `map_response_action` and `map_response_actions` for mapping custom actions to existing responses
|
128
136
|
- **Metaprogramming Support**: Dynamic method generation for response handlers
|
129
137
|
- `define_response_for`: Define custom response methods using blocks
|
130
138
|
- `define_responses_for`: Define same behavior for multiple actions in batch
|
131
139
|
- `define_crud_responses`: Intelligent CRUD responses with dynamic context
|
132
|
-
- `generate_rest_responses`: Auto-generate namespaced REST responses (
|
140
|
+
- `generate_rest_responses`: Auto-generate namespaced REST responses (public*, admin*, etc.)
|
133
141
|
- **Dynamic Context**: Support for lambdas/procs in context generation with `instance_eval`
|
134
142
|
- **Method Introspection**: `response_definitions` class attribute for debugging generated methods
|
135
143
|
- **Enhanced Error Messages**: Detailed error responses with suggestions when actions are not supported
|
136
144
|
|
137
145
|
### Changed
|
146
|
+
|
138
147
|
- **Breaking**: Removed automatic fallback behavior for action mapping (by design - explicit is better than implicit)
|
139
148
|
- **Improved**: `render_invalid_action` now provides detailed error information with actionable suggestions
|
140
149
|
- **Enhanced**: Better error handling with specific guidance on how to implement missing methods
|
141
150
|
|
142
151
|
### Technical Details
|
152
|
+
|
143
153
|
- Added `class_attribute :response_definitions` for storing method definitions
|
144
154
|
- Enhanced `render_with` method resolution to support custom and mapped actions
|
145
155
|
- All generated methods are properly marked as private
|
146
156
|
- Full backward compatibility maintained for existing CRUD operations
|
147
157
|
|
148
158
|
### Documentation
|
159
|
+
|
149
160
|
- Added comprehensive documentation in CUSTOM_ACTIONS.md
|
150
|
-
- Added metaprogramming guide in METAPROGRAMMING.md
|
161
|
+
- Added metaprogramming guide in METAPROGRAMMING.md
|
151
162
|
- Added practical implementation example in PRACTICAL_EXAMPLE.md
|
152
163
|
- Updated README with new functionality overview
|
153
164
|
|
data/README.md
CHANGED
@@ -147,6 +147,7 @@ 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
|
@@ -165,14 +166,15 @@ Beyond the standard CRUD actions (index, show, create, update, destroy), you can
|
|
165
166
|
### Basic Usage
|
166
167
|
|
167
168
|
**Option 1: Map to existing actions**
|
169
|
+
|
168
170
|
```ruby
|
169
171
|
class Api::V1::CoursesController < ApplicationController
|
170
172
|
include JsonapiResponses::Respondable
|
171
|
-
|
173
|
+
|
172
174
|
# Map custom actions to existing response methods
|
173
175
|
map_response_action :public_index, to: :index
|
174
176
|
map_response_action :public_show, to: :show
|
175
|
-
|
177
|
+
|
176
178
|
def public_index
|
177
179
|
# Your logic here
|
178
180
|
render_with(@courses) # Will use respond_for_index
|
@@ -181,17 +183,18 @@ end
|
|
181
183
|
```
|
182
184
|
|
183
185
|
**Option 2: Define custom response methods**
|
186
|
+
|
184
187
|
```ruby
|
185
188
|
class Api::V1::CoursesController < ApplicationController
|
186
189
|
include JsonapiResponses::Respondable
|
187
|
-
|
190
|
+
|
188
191
|
def dashboard_stats
|
189
192
|
# Your logic here
|
190
193
|
render_with(@stats)
|
191
194
|
end
|
192
|
-
|
195
|
+
|
193
196
|
private
|
194
|
-
|
197
|
+
|
195
198
|
def respond_for_dashboard_stats(record, serializer_class, context)
|
196
199
|
render json: {
|
197
200
|
data: record,
|
@@ -202,17 +205,18 @@ end
|
|
202
205
|
```
|
203
206
|
|
204
207
|
**Option 3: Metaprogramming (Recommended for complex scenarios)**
|
208
|
+
|
205
209
|
```ruby
|
206
210
|
class Api::V1::CoursesController < ApplicationController
|
207
211
|
include JsonapiResponses::Respondable
|
208
|
-
|
212
|
+
|
209
213
|
# Generate methods automatically
|
210
214
|
generate_rest_responses(
|
211
215
|
namespace: 'public',
|
212
216
|
actions: [:index, :show],
|
213
217
|
context: { access_level: 'public' }
|
214
218
|
)
|
215
|
-
|
219
|
+
|
216
220
|
# Define similar responses in batch
|
217
221
|
define_responses_for [:export_csv, :export_pdf] do |record, serializer_class, context|
|
218
222
|
format = action_name.to_s.split('_').last
|
@@ -253,26 +257,26 @@ app/responders/
|
|
253
257
|
# app/responders/application_responder.rb
|
254
258
|
class ApplicationResponder < JsonapiResponses::Responder
|
255
259
|
protected
|
256
|
-
|
260
|
+
|
257
261
|
def render_collection_with_meta(type: nil, additional_meta: {})
|
258
262
|
render_json({
|
259
263
|
data: serialize_collection(record),
|
260
264
|
meta: base_meta.merge({ type: type }.compact).merge(additional_meta)
|
261
265
|
})
|
262
266
|
end
|
263
|
-
|
267
|
+
|
264
268
|
def base_meta
|
265
269
|
{
|
266
270
|
timestamp: Time.current.iso8601,
|
267
271
|
count: record_count
|
268
272
|
}.compact
|
269
273
|
end
|
270
|
-
|
274
|
+
|
271
275
|
def record_count
|
272
276
|
return nil unless collection?
|
273
277
|
record.respond_to?(:count) ? record.count : record.size
|
274
278
|
end
|
275
|
-
|
279
|
+
|
276
280
|
def filters_applied
|
277
281
|
filter_keys = [:category_id, :level, :status]
|
278
282
|
filters = {}
|
@@ -287,7 +291,7 @@ end
|
|
287
291
|
```ruby
|
288
292
|
# app/responders/academy_responder.rb
|
289
293
|
class AcademyResponder < ApplicationResponder
|
290
|
-
|
294
|
+
|
291
295
|
# GET /api/v1/academies/featured
|
292
296
|
def featured
|
293
297
|
if params[:category_id].present?
|
@@ -296,7 +300,7 @@ class AcademyResponder < ApplicationResponder
|
|
296
300
|
render_all_featured
|
297
301
|
end
|
298
302
|
end
|
299
|
-
|
303
|
+
|
300
304
|
# GET /api/v1/academies/popular
|
301
305
|
def popular
|
302
306
|
render_collection_with_meta(
|
@@ -307,7 +311,7 @@ class AcademyResponder < ApplicationResponder
|
|
307
311
|
}
|
308
312
|
)
|
309
313
|
end
|
310
|
-
|
314
|
+
|
311
315
|
# GET /api/v1/academies/recommended
|
312
316
|
def recommended
|
313
317
|
render_collection_with_meta(
|
@@ -318,9 +322,9 @@ class AcademyResponder < ApplicationResponder
|
|
318
322
|
}
|
319
323
|
)
|
320
324
|
end
|
321
|
-
|
325
|
+
|
322
326
|
private
|
323
|
-
|
327
|
+
|
324
328
|
def render_filtered_featured
|
325
329
|
render_json({
|
326
330
|
data: serialize_collection(record),
|
@@ -330,7 +334,7 @@ class AcademyResponder < ApplicationResponder
|
|
330
334
|
}
|
331
335
|
})
|
332
336
|
end
|
333
|
-
|
337
|
+
|
334
338
|
def render_all_featured
|
335
339
|
render_collection_with_meta(type: 'featured')
|
336
340
|
end
|
@@ -347,12 +351,12 @@ class Api::V1::AcademiesController < ApplicationController
|
|
347
351
|
@academies = load_featured_academies
|
348
352
|
render_with(@academies, responder: AcademyResponder, action: :featured)
|
349
353
|
end
|
350
|
-
|
354
|
+
|
351
355
|
def popular
|
352
356
|
@academies = Academy.popular.limit(20)
|
353
357
|
render_with(@academies, responder: AcademyResponder, action: :popular)
|
354
358
|
end
|
355
|
-
|
359
|
+
|
356
360
|
def recommended
|
357
361
|
@academies = Academy.recommended_for(current_user)
|
358
362
|
render_with(@academies, responder: AcademyResponder, action: :recommended)
|
@@ -363,12 +367,14 @@ end
|
|
363
367
|
### Benefits of This Pattern
|
364
368
|
|
365
369
|
**Like Pundit Policies:**
|
370
|
+
|
366
371
|
- ✅ One file per controller (not per action)
|
367
372
|
- ✅ All related logic in one place
|
368
373
|
- ✅ Easy to find and maintain
|
369
374
|
- ✅ Shared helpers in base class
|
370
375
|
|
371
376
|
**Example Structure:**
|
377
|
+
|
372
378
|
```
|
373
379
|
AcademiesController → AcademyResponder (featured, popular, recommended)
|
374
380
|
CoursesController → CourseResponder (featured, search, progress)
|
@@ -384,18 +390,18 @@ class MyCustomResponder < JsonapiResponses::Responder
|
|
384
390
|
def render
|
385
391
|
# Access to controller instance
|
386
392
|
controller.current_user
|
387
|
-
|
393
|
+
|
388
394
|
# Access to params
|
389
395
|
params[:filter]
|
390
|
-
|
396
|
+
|
391
397
|
# Serialize data
|
392
398
|
serialize_collection(record) # For collections
|
393
399
|
serialize_item(record) # For single items
|
394
|
-
|
400
|
+
|
395
401
|
# Check record type
|
396
402
|
collection? # true if record is a collection
|
397
403
|
single_item? # true if record is a single item
|
398
|
-
|
404
|
+
|
399
405
|
# Render JSON
|
400
406
|
render_json({ data: [], meta: {} })
|
401
407
|
end
|
@@ -419,14 +425,14 @@ class CategorizedResponder < JsonapiResponses::Responder
|
|
419
425
|
private
|
420
426
|
|
421
427
|
def structured_data?
|
422
|
-
record.is_a?(Array) &&
|
423
|
-
record.first.is_a?(Hash) &&
|
428
|
+
record.is_a?(Array) &&
|
429
|
+
record.first.is_a?(Hash) &&
|
424
430
|
record.first.key?(:category)
|
425
431
|
end
|
426
432
|
|
427
433
|
def group_by_category
|
428
434
|
categories = {}
|
429
|
-
|
435
|
+
|
430
436
|
serialize_collection(record).each do |item|
|
431
437
|
category_id = item.dig(:category, :id) || 'uncategorized'
|
432
438
|
categories[category_id] ||= {
|
@@ -435,7 +441,7 @@ class CategorizedResponder < JsonapiResponses::Responder
|
|
435
441
|
}
|
436
442
|
categories[category_id][:items] << item
|
437
443
|
end
|
438
|
-
|
444
|
+
|
439
445
|
categories.values.map do |group|
|
440
446
|
group.merge(count: group[:items].size)
|
441
447
|
end
|
@@ -445,12 +451,12 @@ end
|
|
445
451
|
|
446
452
|
### When to Use Each Approach
|
447
453
|
|
448
|
-
| Approach
|
449
|
-
|
450
|
-
| `map_response_action`
|
451
|
-
| `respond_for_*` methods | 1-2 custom actions with simple logic
|
452
|
-
| Custom Responders
|
453
|
-
| Metaprogramming
|
454
|
+
| Approach | Best For | Complexity |
|
455
|
+
| ----------------------- | ------------------------------------------- | ---------- |
|
456
|
+
| `map_response_action` | Simple actions similar to existing ones | Low |
|
457
|
+
| `respond_for_*` methods | 1-2 custom actions with simple logic | Medium |
|
458
|
+
| Custom Responders | 3+ custom actions or complex response logic | High |
|
459
|
+
| Metaprogramming | Batch generation of similar actions | High |
|
454
460
|
|
455
461
|
### Mixing Approaches
|
456
462
|
|
@@ -459,23 +465,23 @@ You can combine different approaches in the same controller:
|
|
459
465
|
```ruby
|
460
466
|
class Api::V1::ProductsController < ApplicationController
|
461
467
|
include JsonapiResponses::Respondable
|
462
|
-
|
468
|
+
|
463
469
|
# Map simple actions
|
464
470
|
map_response_action :public_index, to: :index
|
465
|
-
|
471
|
+
|
466
472
|
# Use responder for complex actions
|
467
473
|
def featured
|
468
474
|
@products = Product.featured
|
469
475
|
render_with(@products, responder: FeaturedResponder)
|
470
476
|
end
|
471
|
-
|
477
|
+
|
472
478
|
# Use custom method for one-off logic
|
473
479
|
def statistics
|
474
480
|
render_with(@stats)
|
475
481
|
end
|
476
|
-
|
482
|
+
|
477
483
|
private
|
478
|
-
|
484
|
+
|
479
485
|
def respond_for_statistics(record, serializer_class, context)
|
480
486
|
render json: { stats: record, generated_at: Time.current }
|
481
487
|
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
|
@@ -0,0 +1,105 @@
|
|
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
|
+
end
|
105
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonapi_responses
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Oscar Ortega
|
@@ -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
|