jpie 0.3.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 +7 -0
- data/.aiconfig +65 -0
- data/.rubocop.yml +140 -0
- data/CHANGELOG.md +93 -0
- data/LICENSE.txt +21 -0
- data/README.md +1032 -0
- data/Rakefile +19 -0
- data/jpie.gemspec +48 -0
- data/lib/jpie/configuration.rb +12 -0
- data/lib/jpie/controller/crud_actions.rb +110 -0
- data/lib/jpie/controller/error_handling.rb +41 -0
- data/lib/jpie/controller/parameter_parsing.rb +35 -0
- data/lib/jpie/controller/rendering.rb +60 -0
- data/lib/jpie/controller.rb +18 -0
- data/lib/jpie/deserializer.rb +110 -0
- data/lib/jpie/errors.rb +70 -0
- data/lib/jpie/generators/resource_generator.rb +39 -0
- data/lib/jpie/generators/templates/resource.rb.erb +12 -0
- data/lib/jpie/railtie.rb +36 -0
- data/lib/jpie/resource/attributable.rb +98 -0
- data/lib/jpie/resource/inferrable.rb +43 -0
- data/lib/jpie/resource/sortable.rb +93 -0
- data/lib/jpie/resource.rb +107 -0
- data/lib/jpie/serializer.rb +205 -0
- data/lib/jpie/version.rb +5 -0
- data/lib/jpie.rb +26 -0
- metadata +223 -0
data/README.md
ADDED
@@ -0,0 +1,1032 @@
|
|
1
|
+
# JPie
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/jpie)
|
4
|
+
[](https://github.com/emilkampp/jpie/actions)
|
5
|
+
|
6
|
+
JPie is a modern, lightweight Rails library for developing JSON:API compliant servers. It focuses on clean architecture with strong separation of concerns and extensibility.
|
7
|
+
|
8
|
+
## Key Features
|
9
|
+
|
10
|
+
✨ **Modern Rails DSL** - Clean, intuitive syntax following Rails conventions
|
11
|
+
🔧 **Method Overrides** - Define custom attribute methods directly on resource classes
|
12
|
+
🎯 **Smart Inference** - Automatic model and resource class detection
|
13
|
+
📊 **Polymorphic Support** - Full support for complex polymorphic associations
|
14
|
+
🔄 **STI Ready** - Single Table Inheritance works out of the box
|
15
|
+
⚡ **Performance Optimized** - Efficient serialization with intelligent deduplication
|
16
|
+
🛡️ **Authorization Ready** - Built-in scoping support for security
|
17
|
+
📋 **JSON:API Compliant** - Full specification compliance with sorting, includes, and meta
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Add JPie to your Rails application:
|
22
|
+
|
23
|
+
```bash
|
24
|
+
bundle add jpie
|
25
|
+
```
|
26
|
+
|
27
|
+
## Quick Start - Default Implementation
|
28
|
+
|
29
|
+
JPie works out of the box with minimal configuration. Here's a complete example of the default implementation:
|
30
|
+
|
31
|
+
### 1. Create Your Model
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class User < ActiveRecord::Base
|
35
|
+
validates :name, presence: true
|
36
|
+
validates :email, presence: true, uniqueness: true
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
### 2. Create Your Resource
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
class UserResource < JPie::Resource
|
44
|
+
attributes :name, :email
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
### 3. Create Your Controller
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
class UsersController < ApplicationController
|
52
|
+
include JPie::Controller
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
### 4. Set Up Routes
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
Rails.application.routes.draw do
|
60
|
+
resources :users
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
That's it! You now have a fully functional JSON:API compliant server.
|
65
|
+
|
66
|
+
## Modern DSL Examples
|
67
|
+
|
68
|
+
JPie provides a clean, modern DSL that follows Rails conventions:
|
69
|
+
|
70
|
+
### Resource Definition
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class UserResource < JPie::Resource
|
74
|
+
# Attributes (multiple syntaxes supported)
|
75
|
+
attributes :name, :email, :created_at
|
76
|
+
attribute :full_name
|
77
|
+
|
78
|
+
# Meta attributes
|
79
|
+
meta :account_status, :last_login
|
80
|
+
# or: meta_attributes :account_status, :last_login
|
81
|
+
|
82
|
+
# Relationships
|
83
|
+
has_many :posts
|
84
|
+
has_one :profile
|
85
|
+
|
86
|
+
# Custom sorting
|
87
|
+
sortable :popularity do |query, direction|
|
88
|
+
query.order(likes_count: direction)
|
89
|
+
end
|
90
|
+
# or: sortable_by :popularity do |query, direction|
|
91
|
+
|
92
|
+
# Custom attribute methods
|
93
|
+
private
|
94
|
+
|
95
|
+
def full_name
|
96
|
+
"#{object.first_name} #{object.last_name}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def account_status
|
100
|
+
object.active? ? 'active' : 'inactive'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
### Controller Definition
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
class UsersController < ApplicationController
|
109
|
+
include JPie::Controller
|
110
|
+
|
111
|
+
# Explicit resource (optional - auto-inferred by default)
|
112
|
+
resource UserResource
|
113
|
+
# or: jsonapi_resource UserResource
|
114
|
+
|
115
|
+
# Override methods as needed
|
116
|
+
def index
|
117
|
+
users = current_user.admin? ? User.all : User.active
|
118
|
+
render_jsonapi(users)
|
119
|
+
end
|
120
|
+
|
121
|
+
def create
|
122
|
+
user = User.new(deserialize_params)
|
123
|
+
user.created_by = current_user
|
124
|
+
user.save!
|
125
|
+
|
126
|
+
render_jsonapi(user, status: :created)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
## Suported JSON:API features
|
132
|
+
|
133
|
+
### Sorting
|
134
|
+
All defined attributes are automatically sortable:
|
135
|
+
|
136
|
+
```http
|
137
|
+
GET /users?sort=name
|
138
|
+
HTTP/1.1 200 OK
|
139
|
+
Content-Type: application/vnd.api+json
|
140
|
+
{
|
141
|
+
"data": [
|
142
|
+
{
|
143
|
+
"id": "1",
|
144
|
+
"type": "users",
|
145
|
+
"attributes": {
|
146
|
+
"name": "Alice Anderson",
|
147
|
+
"email": "alice@example.com"
|
148
|
+
}
|
149
|
+
},
|
150
|
+
{
|
151
|
+
"id": "2",
|
152
|
+
"type": "users",
|
153
|
+
"attributes": {
|
154
|
+
"name": "Bob Brown",
|
155
|
+
"email": "bob@example.com"
|
156
|
+
}
|
157
|
+
},
|
158
|
+
{
|
159
|
+
"id": "3",
|
160
|
+
"type": "users",
|
161
|
+
"attributes": {
|
162
|
+
"name": "Carol Clark",
|
163
|
+
"email": "carol@example.com"
|
164
|
+
}
|
165
|
+
}
|
166
|
+
]
|
167
|
+
}
|
168
|
+
```
|
169
|
+
|
170
|
+
Or by name in reverse order by name:
|
171
|
+
|
172
|
+
```http
|
173
|
+
GET /users?sort=-name
|
174
|
+
HTTP/1.1 200 OK
|
175
|
+
Content-Type: application/vnd.api+json
|
176
|
+
{
|
177
|
+
"data": [
|
178
|
+
{
|
179
|
+
"id": "3",
|
180
|
+
"type": "users",
|
181
|
+
"attributes": {
|
182
|
+
"name": "Carol Clark",
|
183
|
+
"email": "carol@example.com"
|
184
|
+
}
|
185
|
+
},
|
186
|
+
{
|
187
|
+
"id": "2",
|
188
|
+
"type": "users",
|
189
|
+
"attributes": {
|
190
|
+
"name": "Bob Brown",
|
191
|
+
"email": "bob@example.com"
|
192
|
+
}
|
193
|
+
},
|
194
|
+
{
|
195
|
+
"id": "1",
|
196
|
+
"type": "users",
|
197
|
+
"attributes": {
|
198
|
+
"name": "Alice Anderson",
|
199
|
+
"email": "alice@example.com"
|
200
|
+
}
|
201
|
+
}
|
202
|
+
]
|
203
|
+
}
|
204
|
+
```
|
205
|
+
|
206
|
+
## Customization and Overrides
|
207
|
+
|
208
|
+
Once you have the basic implementation working, you can customize JPie's behavior as needed:
|
209
|
+
|
210
|
+
### Resource Class Inference Override
|
211
|
+
|
212
|
+
JPie automatically infers the resource class from your controller name, but you can override this:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
# Automatic inference (default behavior)
|
216
|
+
class UsersController < ApplicationController
|
217
|
+
include JPie::Controller
|
218
|
+
# Automatically uses UserResource
|
219
|
+
end
|
220
|
+
|
221
|
+
# Explicit resource specification (override)
|
222
|
+
class UsersController < ApplicationController
|
223
|
+
include JPie::Controller
|
224
|
+
resource UserResource # Use a different resource class (modern syntax)
|
225
|
+
# or: jsonapi_resource UserResource # (backward compatible syntax)
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
### Model Specification Override
|
230
|
+
|
231
|
+
JPie automatically infers the model from your resource class name, but you can override this:
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
# Automatic inference (default behavior)
|
235
|
+
class UserResource < JPie::Resource
|
236
|
+
attributes :name, :email
|
237
|
+
# Automatically uses User model
|
238
|
+
end
|
239
|
+
|
240
|
+
# Explicit model specification (override)
|
241
|
+
class UserResource < JPie::Resource
|
242
|
+
model CustomUser # Use a different model class
|
243
|
+
attributes :name, :email
|
244
|
+
end
|
245
|
+
```
|
246
|
+
|
247
|
+
### Controller Method Overrides
|
248
|
+
|
249
|
+
You can override any of the automatic CRUD methods:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
class UsersController < ApplicationController
|
253
|
+
include JPie::Controller
|
254
|
+
|
255
|
+
# Override index to add filtering
|
256
|
+
def index
|
257
|
+
users = User.where(active: true)
|
258
|
+
render_jsonapi(users)
|
259
|
+
end
|
260
|
+
|
261
|
+
# Override create to add custom logic
|
262
|
+
def create
|
263
|
+
attributes = deserialize_params
|
264
|
+
user = User.new(attributes)
|
265
|
+
user.created_by = current_user
|
266
|
+
user.save!
|
267
|
+
|
268
|
+
render_jsonapi(user, status: :created)
|
269
|
+
end
|
270
|
+
|
271
|
+
# show, update, destroy still use the automatic implementations
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
275
|
+
### Custom Attributes
|
276
|
+
|
277
|
+
Add computed or transformed attributes to your resources using either blocks or method overrides:
|
278
|
+
|
279
|
+
#### Using Blocks (Original Approach)
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
class UserResource < JPie::Resource
|
283
|
+
attribute :display_name do
|
284
|
+
"#{object.first_name} #{object.last_name}"
|
285
|
+
end
|
286
|
+
|
287
|
+
attribute :admin_notes do
|
288
|
+
if context[:current_user]&.admin?
|
289
|
+
object.admin_notes
|
290
|
+
else
|
291
|
+
nil
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
297
|
+
#### Using Method Overrides (New Approach)
|
298
|
+
|
299
|
+
You can now define custom methods directly on your resource class instead of using blocks:
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
class UserResource < JPie::Resource
|
303
|
+
attributes :name, :email
|
304
|
+
attribute :full_name
|
305
|
+
attribute :display_name
|
306
|
+
meta_attribute :user_stats
|
307
|
+
|
308
|
+
private
|
309
|
+
|
310
|
+
def full_name
|
311
|
+
"#{object.first_name} #{object.last_name}"
|
312
|
+
end
|
313
|
+
|
314
|
+
def display_name
|
315
|
+
if context[:admin]
|
316
|
+
"#{full_name} [ADMIN VIEW] - #{object.email}"
|
317
|
+
else
|
318
|
+
full_name
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def user_stats
|
323
|
+
{
|
324
|
+
name_length: object.name.length,
|
325
|
+
email_domain: object.email.split('@').last,
|
326
|
+
account_status: object.active? ? 'active' : 'inactive'
|
327
|
+
}
|
328
|
+
end
|
329
|
+
end
|
330
|
+
```
|
331
|
+
|
332
|
+
**Key Benefits of Method Overrides:**
|
333
|
+
- **Cleaner syntax** - No need for blocks
|
334
|
+
- **Better IDE support** - Full method definitions with proper syntax highlighting
|
335
|
+
- **Easier testing** - Methods can be tested individually
|
336
|
+
- **Private methods supported** - Use private methods for internal logic
|
337
|
+
- **Access to object and context** - Full access to `object` and `context` like blocks
|
338
|
+
|
339
|
+
**Method Precedence:**
|
340
|
+
1. **Blocks** (highest priority) - `attribute :name do ... end`
|
341
|
+
2. **Options blocks** - `attribute :name, block: proc { ... }`
|
342
|
+
3. **Custom methods** - `def name; ...; end`
|
343
|
+
4. **Model attributes** (lowest priority) - Direct model attribute lookup
|
344
|
+
|
345
|
+
### Meta attributes
|
346
|
+
|
347
|
+
JPie supports adding meta data to your JSON:API resources in two ways: using the `meta_attributes` macro or by defining a custom `meta` method.
|
348
|
+
|
349
|
+
#### Using meta_attributes Macro
|
350
|
+
|
351
|
+
It's easy to add meta attributes:
|
352
|
+
|
353
|
+
```ruby
|
354
|
+
class UserResource < JPie::Resource
|
355
|
+
meta_attributes :created_at, :updated_at
|
356
|
+
meta_attributes :last_login_at
|
357
|
+
end
|
358
|
+
```
|
359
|
+
|
360
|
+
#### Using Custom meta Method
|
361
|
+
|
362
|
+
For more complex meta data, you can define a `meta` method that returns a hash:
|
363
|
+
|
364
|
+
```ruby
|
365
|
+
class UserResource < JPie::Resource
|
366
|
+
attributes :name, :email
|
367
|
+
meta_attributes :created_at, :updated_at
|
368
|
+
|
369
|
+
def meta
|
370
|
+
super.merge(
|
371
|
+
full_name: "#{object.first_name} #{object.last_name}",
|
372
|
+
user_role: context[:current_user]&.role || 'guest',
|
373
|
+
account_status: object.active? ? 'active' : 'inactive',
|
374
|
+
last_seen: object.last_login_at&.iso8601
|
375
|
+
)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
```
|
379
|
+
|
380
|
+
The `meta` method has access to:
|
381
|
+
- `super` - returns the hash from `meta_attributes`
|
382
|
+
- `object` - the underlying model instance
|
383
|
+
- `context` - any context passed during resource initialization
|
384
|
+
|
385
|
+
**Example JSON:API Response with Custom Meta:**
|
386
|
+
|
387
|
+
```json
|
388
|
+
{
|
389
|
+
"data": {
|
390
|
+
"id": "1",
|
391
|
+
"type": "users",
|
392
|
+
"attributes": {
|
393
|
+
"name": "John Doe",
|
394
|
+
"email": "john@example.com"
|
395
|
+
},
|
396
|
+
"meta": {
|
397
|
+
"created_at": "2024-01-01T12:00:00Z",
|
398
|
+
"updated_at": "2024-01-15T14:30:00Z",
|
399
|
+
"full_name": "John Doe",
|
400
|
+
"user_role": "admin",
|
401
|
+
"account_status": "active",
|
402
|
+
"last_seen": "2024-01-15T14:00:00Z"
|
403
|
+
}
|
404
|
+
}
|
405
|
+
}
|
406
|
+
```
|
407
|
+
|
408
|
+
#### Meta Method Inheritance
|
409
|
+
|
410
|
+
Meta methods work seamlessly with inheritance:
|
411
|
+
|
412
|
+
```ruby
|
413
|
+
class BaseResource < JPie::Resource
|
414
|
+
meta_attributes :created_at, :updated_at
|
415
|
+
|
416
|
+
def meta
|
417
|
+
super.merge(
|
418
|
+
resource_version: '1.0',
|
419
|
+
timestamp: Time.current.iso8601
|
420
|
+
)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
class UserResource < BaseResource
|
425
|
+
attributes :name, :email
|
426
|
+
meta_attributes :last_login_at
|
427
|
+
|
428
|
+
def meta
|
429
|
+
super.merge(
|
430
|
+
user_specific_data: calculate_user_metrics
|
431
|
+
)
|
432
|
+
end
|
433
|
+
|
434
|
+
private
|
435
|
+
|
436
|
+
def calculate_user_metrics
|
437
|
+
{
|
438
|
+
post_count: object.posts.count,
|
439
|
+
comment_count: object.comments.count
|
440
|
+
}
|
441
|
+
end
|
442
|
+
end
|
443
|
+
```
|
444
|
+
|
445
|
+
### Custom Sorting
|
446
|
+
|
447
|
+
Override the default sorting behavior with custom logic:
|
448
|
+
|
449
|
+
```ruby
|
450
|
+
class PostResource < JPie::Resource
|
451
|
+
attributes :title, :content
|
452
|
+
|
453
|
+
sortable_by :popularity do |query, direction|
|
454
|
+
if direction == :asc
|
455
|
+
query.order(:likes_count, :comments_count)
|
456
|
+
else
|
457
|
+
query.order(likes_count: :desc, comments_count: :desc)
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|
461
|
+
```
|
462
|
+
|
463
|
+
### Polymorphic Associations
|
464
|
+
|
465
|
+
JPie supports polymorphic associations seamlessly. Here's a complete example with comments that can belong to multiple types of commentable resources:
|
466
|
+
|
467
|
+
#### Models with Polymorphic Associations
|
468
|
+
|
469
|
+
```ruby
|
470
|
+
# Comment model with belongs_to polymorphic association
|
471
|
+
class Comment < ActiveRecord::Base
|
472
|
+
belongs_to :commentable, polymorphic: true
|
473
|
+
belongs_to :author, class_name: 'User'
|
474
|
+
|
475
|
+
validates :content, presence: true
|
476
|
+
end
|
477
|
+
|
478
|
+
# Post model with has_many polymorphic association
|
479
|
+
class Post < ActiveRecord::Base
|
480
|
+
has_many :comments, as: :commentable, dependent: :destroy
|
481
|
+
belongs_to :author, class_name: 'User'
|
482
|
+
|
483
|
+
validates :title, :content, presence: true
|
484
|
+
end
|
485
|
+
|
486
|
+
# Article model with has_many polymorphic association
|
487
|
+
class Article < ActiveRecord::Base
|
488
|
+
has_many :comments, as: :commentable, dependent: :destroy
|
489
|
+
belongs_to :author, class_name: 'User'
|
490
|
+
|
491
|
+
validates :title, :body, presence: true
|
492
|
+
end
|
493
|
+
```
|
494
|
+
|
495
|
+
#### Resources for Polymorphic Associations
|
496
|
+
|
497
|
+
```ruby
|
498
|
+
# Comment resource with belongs_to polymorphic relationship
|
499
|
+
class CommentResource < JPie::Resource
|
500
|
+
attributes :content, :created_at
|
501
|
+
|
502
|
+
# Polymorphic belongs_to relationship
|
503
|
+
relationship :commentable do
|
504
|
+
# Dynamically determine the resource class based on the commentable type
|
505
|
+
case object.commentable_type
|
506
|
+
when 'Post'
|
507
|
+
PostResource.new(object.commentable, context)
|
508
|
+
when 'Article'
|
509
|
+
ArticleResource.new(object.commentable, context)
|
510
|
+
else
|
511
|
+
nil
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
relationship :author do
|
516
|
+
UserResource.new(object.author, context) if object.author
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
# Post resource with has_many polymorphic relationship
|
521
|
+
class PostResource < JPie::Resource
|
522
|
+
attributes :title, :content, :published_at
|
523
|
+
|
524
|
+
# Has_many polymorphic relationship
|
525
|
+
relationship :comments do
|
526
|
+
object.comments.map { |comment| CommentResource.new(comment, context) }
|
527
|
+
end
|
528
|
+
|
529
|
+
relationship :author do
|
530
|
+
UserResource.new(object.author, context) if object.author
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
# Article resource with has_many polymorphic relationship
|
535
|
+
class ArticleResource < JPie::Resource
|
536
|
+
attributes :title, :body, :published_at
|
537
|
+
|
538
|
+
# Has_many polymorphic relationship
|
539
|
+
relationship :comments do
|
540
|
+
object.comments.map { |comment| CommentResource.new(comment, context) }
|
541
|
+
end
|
542
|
+
|
543
|
+
relationship :author do
|
544
|
+
UserResource.new(object.author, context) if object.author
|
545
|
+
end
|
546
|
+
end
|
547
|
+
```
|
548
|
+
|
549
|
+
#### Controllers for Polymorphic Resources
|
550
|
+
|
551
|
+
```ruby
|
552
|
+
class CommentsController < ApplicationController
|
553
|
+
include JPie::Controller
|
554
|
+
|
555
|
+
# Override create to handle polymorphic assignment
|
556
|
+
def create
|
557
|
+
attributes = deserialize_params
|
558
|
+
commentable = find_commentable
|
559
|
+
|
560
|
+
comment = commentable.comments.build(attributes)
|
561
|
+
comment.author = current_user
|
562
|
+
comment.save!
|
563
|
+
|
564
|
+
render_jsonapi_resource(comment, status: :created)
|
565
|
+
end
|
566
|
+
|
567
|
+
private
|
568
|
+
|
569
|
+
def find_commentable
|
570
|
+
# Extract commentable info from request path or parameters
|
571
|
+
if params[:post_id]
|
572
|
+
Post.find(params[:post_id])
|
573
|
+
elsif params[:article_id]
|
574
|
+
Article.find(params[:article_id])
|
575
|
+
else
|
576
|
+
raise ArgumentError, "Commentable not specified"
|
577
|
+
end
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
class PostsController < ApplicationController
|
582
|
+
include JPie::Controller
|
583
|
+
# Uses default CRUD operations with polymorphic comments included
|
584
|
+
end
|
585
|
+
|
586
|
+
class ArticlesController < ApplicationController
|
587
|
+
include JPie::Controller
|
588
|
+
# Uses default CRUD operations with polymorphic comments included
|
589
|
+
end
|
590
|
+
```
|
591
|
+
|
592
|
+
#### Routes for Polymorphic Resources
|
593
|
+
|
594
|
+
```ruby
|
595
|
+
Rails.application.routes.draw do
|
596
|
+
resources :posts do
|
597
|
+
resources :comments, only: [:index, :create]
|
598
|
+
end
|
599
|
+
|
600
|
+
resources :articles do
|
601
|
+
resources :comments, only: [:index, :create]
|
602
|
+
end
|
603
|
+
|
604
|
+
resources :comments, only: [:show, :update, :destroy]
|
605
|
+
end
|
606
|
+
```
|
607
|
+
|
608
|
+
#### Example JSON:API Responses
|
609
|
+
|
610
|
+
**GET /posts/1?include=comments,comments.author**
|
611
|
+
|
612
|
+
```json
|
613
|
+
{
|
614
|
+
"data": {
|
615
|
+
"id": "1",
|
616
|
+
"type": "posts",
|
617
|
+
"attributes": {
|
618
|
+
"title": "My First Post",
|
619
|
+
"content": "This is the content of my first post.",
|
620
|
+
"published_at": "2024-01-15T10:30:00Z"
|
621
|
+
},
|
622
|
+
"relationships": {
|
623
|
+
"comments": {
|
624
|
+
"data": [
|
625
|
+
{ "id": "1", "type": "comments" },
|
626
|
+
{ "id": "2", "type": "comments" }
|
627
|
+
]
|
628
|
+
}
|
629
|
+
}
|
630
|
+
},
|
631
|
+
"included": [
|
632
|
+
{
|
633
|
+
"id": "1",
|
634
|
+
"type": "comments",
|
635
|
+
"attributes": {
|
636
|
+
"content": "Great post!",
|
637
|
+
"created_at": "2024-01-15T11:00:00Z"
|
638
|
+
},
|
639
|
+
"relationships": {
|
640
|
+
"commentable": {
|
641
|
+
"data": { "id": "1", "type": "posts" }
|
642
|
+
},
|
643
|
+
"author": {
|
644
|
+
"data": { "id": "5", "type": "users" }
|
645
|
+
}
|
646
|
+
}
|
647
|
+
},
|
648
|
+
{
|
649
|
+
"id": "2",
|
650
|
+
"type": "comments",
|
651
|
+
"attributes": {
|
652
|
+
"content": "Thanks for sharing!",
|
653
|
+
"created_at": "2024-01-15T12:00:00Z"
|
654
|
+
},
|
655
|
+
"relationships": {
|
656
|
+
"commentable": {
|
657
|
+
"data": { "id": "1", "type": "posts" }
|
658
|
+
},
|
659
|
+
"author": {
|
660
|
+
"data": { "id": "6", "type": "users" }
|
661
|
+
}
|
662
|
+
}
|
663
|
+
}
|
664
|
+
]
|
665
|
+
}
|
666
|
+
```
|
667
|
+
|
668
|
+
### Single Table Inheritance (STI)
|
669
|
+
|
670
|
+
JPie provides comprehensive support for Rails Single Table Inheritance (STI) models. STI allows multiple models to share a single database table with a "type" column to differentiate between them.
|
671
|
+
|
672
|
+
#### STI Models
|
673
|
+
|
674
|
+
```ruby
|
675
|
+
# Base model
|
676
|
+
class Vehicle < ActiveRecord::Base
|
677
|
+
validates :name, presence: true
|
678
|
+
validates :brand, presence: true
|
679
|
+
validates :year, presence: true
|
680
|
+
end
|
681
|
+
|
682
|
+
# STI subclasses
|
683
|
+
class Car < Vehicle
|
684
|
+
validates :engine_size, presence: true
|
685
|
+
end
|
686
|
+
|
687
|
+
class Truck < Vehicle
|
688
|
+
validates :cargo_capacity, presence: true
|
689
|
+
end
|
690
|
+
```
|
691
|
+
|
692
|
+
#### STI Resources
|
693
|
+
|
694
|
+
JPie automatically handles STI type inference and resource inheritance:
|
695
|
+
|
696
|
+
```ruby
|
697
|
+
# Base resource
|
698
|
+
class VehicleResource < JPie::Resource
|
699
|
+
attributes :name, :brand, :year
|
700
|
+
meta_attributes :created_at, :updated_at
|
701
|
+
end
|
702
|
+
|
703
|
+
# STI resources inherit from base resource
|
704
|
+
class CarResource < VehicleResource
|
705
|
+
attributes :engine_size # Car-specific attribute
|
706
|
+
end
|
707
|
+
|
708
|
+
class TruckResource < VehicleResource
|
709
|
+
attributes :cargo_capacity # Truck-specific attribute
|
710
|
+
end
|
711
|
+
```
|
712
|
+
|
713
|
+
#### STI Type Inference
|
714
|
+
|
715
|
+
JPie automatically infers the correct JSON:API type from the STI model class:
|
716
|
+
|
717
|
+
```ruby
|
718
|
+
car = Car.create!(name: 'Civic', brand: 'Honda', year: 2020, engine_size: 1500)
|
719
|
+
car_resource = CarResource.new(car)
|
720
|
+
|
721
|
+
car_resource.type # => "cars" (automatically inferred from Car model)
|
722
|
+
```
|
723
|
+
|
724
|
+
#### STI Serialization
|
725
|
+
|
726
|
+
Each STI model serializes with its specific type and attributes:
|
727
|
+
|
728
|
+
```ruby
|
729
|
+
# Car serialization
|
730
|
+
car_serializer = JPie::Serializer.new(CarResource)
|
731
|
+
result = car_serializer.serialize(car)
|
732
|
+
|
733
|
+
# Result:
|
734
|
+
{
|
735
|
+
"data": {
|
736
|
+
"id": "1",
|
737
|
+
"type": "cars", # STI type
|
738
|
+
"attributes": {
|
739
|
+
"name": "Civic",
|
740
|
+
"brand": "Honda",
|
741
|
+
"year": 2020,
|
742
|
+
"engine_size": 1500 # Car-specific attribute
|
743
|
+
}
|
744
|
+
}
|
745
|
+
}
|
746
|
+
|
747
|
+
# Truck serialization
|
748
|
+
truck_serializer = JPie::Serializer.new(TruckResource)
|
749
|
+
result = truck_serializer.serialize(truck)
|
750
|
+
|
751
|
+
# Result:
|
752
|
+
{
|
753
|
+
"data": {
|
754
|
+
"id": "2",
|
755
|
+
"type": "trucks", # STI type
|
756
|
+
"attributes": {
|
757
|
+
"name": "F-150",
|
758
|
+
"brand": "Ford",
|
759
|
+
"year": 2021,
|
760
|
+
"cargo_capacity": 1000 # Truck-specific attribute
|
761
|
+
}
|
762
|
+
}
|
763
|
+
}
|
764
|
+
```
|
765
|
+
|
766
|
+
#### STI Controllers
|
767
|
+
|
768
|
+
Controllers work seamlessly with STI models:
|
769
|
+
|
770
|
+
```ruby
|
771
|
+
class CarsController < ApplicationController
|
772
|
+
include JPie::Controller
|
773
|
+
# Automatically uses CarResource and Car model
|
774
|
+
end
|
775
|
+
|
776
|
+
class TrucksController < ApplicationController
|
777
|
+
include JPie::Controller
|
778
|
+
# Automatically uses TruckResource and Truck model
|
779
|
+
end
|
780
|
+
|
781
|
+
class VehiclesController < ApplicationController
|
782
|
+
include JPie::Controller
|
783
|
+
# Uses VehicleResource and returns all vehicles (cars, trucks, etc.)
|
784
|
+
end
|
785
|
+
```
|
786
|
+
|
787
|
+
#### STI Scoping
|
788
|
+
|
789
|
+
Each STI resource automatically scopes to its specific type:
|
790
|
+
|
791
|
+
```ruby
|
792
|
+
CarResource.scope # Returns only Car records
|
793
|
+
TruckResource.scope # Returns only Truck records
|
794
|
+
VehicleResource.scope # Returns all Vehicle records (including STI subclasses)
|
795
|
+
```
|
796
|
+
|
797
|
+
#### STI in Polymorphic Relationships
|
798
|
+
|
799
|
+
JPie's serializer automatically determines the correct resource class for STI models in polymorphic relationships:
|
800
|
+
|
801
|
+
```ruby
|
802
|
+
# If a polymorphic relationship returns STI objects,
|
803
|
+
# JPie will automatically use the correct resource class
|
804
|
+
# (CarResource for Car objects, TruckResource for Truck objects, etc.)
|
805
|
+
```
|
806
|
+
|
807
|
+
#### Complete STI Example
|
808
|
+
|
809
|
+
Here's a complete example showing STI in action with HTTP requests and responses:
|
810
|
+
|
811
|
+
**1. Database Setup**
|
812
|
+
|
813
|
+
```ruby
|
814
|
+
# Migration
|
815
|
+
class CreateVehicles < ActiveRecord::Migration[7.0]
|
816
|
+
def change
|
817
|
+
create_table :vehicles do |t|
|
818
|
+
t.string :type, null: false # STI discriminator column
|
819
|
+
t.string :name, null: false
|
820
|
+
t.string :brand, null: false
|
821
|
+
t.integer :year, null: false
|
822
|
+
t.integer :engine_size # Car-specific
|
823
|
+
t.integer :cargo_capacity # Truck-specific
|
824
|
+
t.timestamps
|
825
|
+
end
|
826
|
+
|
827
|
+
add_index :vehicles, :type
|
828
|
+
end
|
829
|
+
end
|
830
|
+
```
|
831
|
+
|
832
|
+
**2. Models**
|
833
|
+
|
834
|
+
```ruby
|
835
|
+
class Vehicle < ApplicationRecord
|
836
|
+
validates :name, :brand, :year, presence: true
|
837
|
+
end
|
838
|
+
|
839
|
+
class Car < Vehicle
|
840
|
+
validates :engine_size, presence: true
|
841
|
+
end
|
842
|
+
|
843
|
+
class Truck < Vehicle
|
844
|
+
validates :cargo_capacity, presence: true
|
845
|
+
end
|
846
|
+
```
|
847
|
+
|
848
|
+
**3. Resources**
|
849
|
+
|
850
|
+
```ruby
|
851
|
+
class VehicleResource < JPie::Resource
|
852
|
+
attributes :name, :brand, :year
|
853
|
+
meta_attributes :created_at, :updated_at
|
854
|
+
end
|
855
|
+
|
856
|
+
class CarResource < VehicleResource
|
857
|
+
attributes :engine_size
|
858
|
+
end
|
859
|
+
|
860
|
+
class TruckResource < VehicleResource
|
861
|
+
attributes :cargo_capacity
|
862
|
+
end
|
863
|
+
```
|
864
|
+
|
865
|
+
**4. Controllers**
|
866
|
+
|
867
|
+
```ruby
|
868
|
+
class VehiclesController < ApplicationController
|
869
|
+
include JPie::Controller
|
870
|
+
# Returns all vehicles (cars, trucks, etc.)
|
871
|
+
end
|
872
|
+
|
873
|
+
class CarsController < ApplicationController
|
874
|
+
include JPie::Controller
|
875
|
+
# Returns only cars with car-specific attributes
|
876
|
+
end
|
877
|
+
|
878
|
+
class TrucksController < ApplicationController
|
879
|
+
include JPie::Controller
|
880
|
+
# Returns only trucks with truck-specific attributes
|
881
|
+
end
|
882
|
+
```
|
883
|
+
|
884
|
+
**5. Routes**
|
885
|
+
|
886
|
+
```ruby
|
887
|
+
Rails.application.routes.draw do
|
888
|
+
resources :vehicles, only: [:index, :show]
|
889
|
+
resources :cars
|
890
|
+
resources :trucks
|
891
|
+
end
|
892
|
+
```
|
893
|
+
|
894
|
+
**6. Example HTTP Requests and Responses**
|
895
|
+
|
896
|
+
**GET /cars/1**
|
897
|
+
```json
|
898
|
+
{
|
899
|
+
"data": {
|
900
|
+
"id": "1",
|
901
|
+
"type": "cars",
|
902
|
+
"attributes": {
|
903
|
+
"name": "Model 3",
|
904
|
+
"brand": "Tesla",
|
905
|
+
"year": 2023,
|
906
|
+
"engine_size": 0
|
907
|
+
},
|
908
|
+
"meta": {
|
909
|
+
"created_at": "2024-01-15T10:00:00Z",
|
910
|
+
"updated_at": "2024-01-15T10:00:00Z"
|
911
|
+
}
|
912
|
+
}
|
913
|
+
}
|
914
|
+
```
|
915
|
+
|
916
|
+
**GET /trucks/2**
|
917
|
+
```json
|
918
|
+
{
|
919
|
+
"data": {
|
920
|
+
"id": "2",
|
921
|
+
"type": "trucks",
|
922
|
+
"attributes": {
|
923
|
+
"name": "F-150",
|
924
|
+
"brand": "Ford",
|
925
|
+
"year": 2023,
|
926
|
+
"cargo_capacity": 1200
|
927
|
+
},
|
928
|
+
"meta": {
|
929
|
+
"created_at": "2024-01-15T11:00:00Z",
|
930
|
+
"updated_at": "2024-01-15T11:00:00Z"
|
931
|
+
}
|
932
|
+
}
|
933
|
+
}
|
934
|
+
```
|
935
|
+
|
936
|
+
**GET /vehicles (Mixed STI Collection)**
|
937
|
+
```json
|
938
|
+
{
|
939
|
+
"data": [
|
940
|
+
{
|
941
|
+
"id": "1",
|
942
|
+
"type": "cars",
|
943
|
+
"attributes": {
|
944
|
+
"name": "Model 3",
|
945
|
+
"brand": "Tesla",
|
946
|
+
"year": 2023,
|
947
|
+
"engine_size": 0
|
948
|
+
}
|
949
|
+
},
|
950
|
+
{
|
951
|
+
"id": "2",
|
952
|
+
"type": "trucks",
|
953
|
+
"attributes": {
|
954
|
+
"name": "F-150",
|
955
|
+
"brand": "Ford",
|
956
|
+
"year": 2023,
|
957
|
+
"cargo_capacity": 1200
|
958
|
+
}
|
959
|
+
}
|
960
|
+
]
|
961
|
+
}
|
962
|
+
```
|
963
|
+
|
964
|
+
**7. Creating STI Records**
|
965
|
+
|
966
|
+
**POST /cars**
|
967
|
+
```json
|
968
|
+
{
|
969
|
+
"data": {
|
970
|
+
"type": "cars",
|
971
|
+
"attributes": {
|
972
|
+
"name": "Model Y",
|
973
|
+
"brand": "Tesla",
|
974
|
+
"year": 2024,
|
975
|
+
"engine_size": 0
|
976
|
+
}
|
977
|
+
}
|
978
|
+
}
|
979
|
+
```
|
980
|
+
|
981
|
+
#### Custom STI Types
|
982
|
+
|
983
|
+
You can override the automatic type inference if needed:
|
984
|
+
|
985
|
+
```ruby
|
986
|
+
class CarResource < VehicleResource
|
987
|
+
type 'automobiles' # Custom type instead of 'cars'
|
988
|
+
attributes :engine_size
|
989
|
+
end
|
990
|
+
```
|
991
|
+
|
992
|
+
### Authorization and Scoping
|
993
|
+
|
994
|
+
Override the default scope method to add authorization:
|
995
|
+
|
996
|
+
```ruby
|
997
|
+
class PostResource < JPie::Resource
|
998
|
+
attributes :title, :content
|
999
|
+
|
1000
|
+
def self.scope(context = {})
|
1001
|
+
current_user = context[:current_user]
|
1002
|
+
return model.none unless current_user
|
1003
|
+
Pundit.policy_scope(current_user, model)
|
1004
|
+
end
|
1005
|
+
end
|
1006
|
+
```
|
1007
|
+
|
1008
|
+
### Custom Context
|
1009
|
+
|
1010
|
+
Override the context building to pass additional data to resources:
|
1011
|
+
|
1012
|
+
```ruby
|
1013
|
+
class UsersController < ApplicationController
|
1014
|
+
include JPie::Controller
|
1015
|
+
|
1016
|
+
private
|
1017
|
+
|
1018
|
+
def build_context
|
1019
|
+
{
|
1020
|
+
current_user: current_user,
|
1021
|
+
controller: self,
|
1022
|
+
action: action_name,
|
1023
|
+
request_ip: request.remote_ip,
|
1024
|
+
user_agent: request.user_agent
|
1025
|
+
}
|
1026
|
+
end
|
1027
|
+
end
|
1028
|
+
```
|
1029
|
+
|
1030
|
+
## License
|
1031
|
+
|
1032
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|