jpie 0.3.1 → 0.4.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/{.aiconfig → .cursorrules} +14 -2
- data/CHANGELOG.md +51 -0
- data/README.md +179 -844
- data/examples/basic_example.md +146 -0
- data/examples/including_related_resources.md +491 -0
- data/examples/resource_attribute_configuration.md +147 -0
- data/examples/resource_meta_configuration.md +244 -0
- data/examples/single_table_inheritance.md +160 -0
- data/lib/jpie/controller/crud_actions.rb +10 -0
- data/lib/jpie/controller/error_handling.rb +168 -17
- data/lib/jpie/controller/json_api_validation.rb +171 -0
- data/lib/jpie/controller.rb +2 -0
- data/lib/jpie/errors.rb +41 -0
- data/lib/jpie/generators/resource_generator.rb +86 -9
- data/lib/jpie/generators/templates/resource.rb.erb +20 -1
- data/lib/jpie/resource/attributable.rb +21 -2
- data/lib/jpie/resource.rb +26 -0
- data/lib/jpie/version.rb +1 -1
- metadata +9 -3
data/README.md
CHANGED
@@ -10,11 +10,14 @@ JPie is a modern, lightweight Rails library for developing JSON:API compliant se
|
|
10
10
|
✨ **Modern Rails DSL** - Clean, intuitive syntax following Rails conventions
|
11
11
|
🔧 **Method Overrides** - Define custom attribute methods directly on resource classes
|
12
12
|
🎯 **Smart Inference** - Automatic model and resource class detection
|
13
|
+
⚡ **Powerful Generators** - Scaffold resources with relationships, meta attributes, and automatic inference
|
13
14
|
📊 **Polymorphic Support** - Full support for complex polymorphic associations
|
14
15
|
🔄 **STI Ready** - Single Table Inheritance works out of the box
|
16
|
+
🔗 **Through Associations** - Full support for Rails `:through` associations
|
15
17
|
⚡ **Performance Optimized** - Efficient serialization with intelligent deduplication
|
16
18
|
🛡️ **Authorization Ready** - Built-in scoping support for security
|
17
19
|
📋 **JSON:API Compliant** - Full specification compliance with sorting, includes, and meta
|
20
|
+
🚨 **Robust Error Handling** - Smart inheritance-aware error handling with full customization options
|
18
21
|
|
19
22
|
## Installation
|
20
23
|
|
@@ -24,9 +27,9 @@ Add JPie to your Rails application:
|
|
24
27
|
bundle add jpie
|
25
28
|
```
|
26
29
|
|
27
|
-
## Quick Start
|
30
|
+
## Quick Start
|
28
31
|
|
29
|
-
JPie works out of the box with minimal configuration
|
32
|
+
JPie works out of the box with minimal configuration:
|
30
33
|
|
31
34
|
### 1. Create Your Model
|
32
35
|
|
@@ -34,6 +37,9 @@ JPie works out of the box with minimal configuration. Here's a complete example
|
|
34
37
|
class User < ActiveRecord::Base
|
35
38
|
validates :name, presence: true
|
36
39
|
validates :email, presence: true, uniqueness: true
|
40
|
+
|
41
|
+
has_many :posts, dependent: :destroy
|
42
|
+
has_one :profile, dependent: :destroy
|
37
43
|
end
|
38
44
|
```
|
39
45
|
|
@@ -42,6 +48,10 @@ end
|
|
42
48
|
```ruby
|
43
49
|
class UserResource < JPie::Resource
|
44
50
|
attributes :name, :email
|
51
|
+
meta_attributes :created_at, :updated_at
|
52
|
+
|
53
|
+
has_many :posts
|
54
|
+
has_one :profile
|
45
55
|
end
|
46
56
|
```
|
47
57
|
|
@@ -61,971 +71,296 @@ Rails.application.routes.draw do
|
|
61
71
|
end
|
62
72
|
```
|
63
73
|
|
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
|
74
|
+
That's it! You now have a fully functional JSON:API compliant server with automatic CRUD operations, sorting, includes, and validation.
|
132
75
|
|
133
|
-
|
134
|
-
All defined attributes are automatically sortable:
|
76
|
+
## 📚 Comprehensive Examples
|
135
77
|
|
136
|
-
|
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
|
-
```
|
78
|
+
JPie includes a complete set of examples demonstrating all features:
|
169
79
|
|
170
|
-
|
80
|
+
- **[🚀 Basic Usage](https://github.com/emilkampp/jpie/blob/main/examples/basic_usage.rb)** - Fundamental setup and configuration
|
81
|
+
- **[🔗 Through Associations](https://github.com/emilkampp/jpie/blob/main/examples/through_associations.rb)** - Many-to-many relationships with `:through`
|
82
|
+
- **[🎨 Custom Attributes & Meta](https://github.com/emilkampp/jpie/blob/main/examples/custom_attributes_and_meta.rb)** - Custom computed attributes and meta data
|
83
|
+
- **[🔄 Polymorphic Associations](https://github.com/emilkampp/jpie/blob/main/examples/polymorphic_associations.rb)** - Complex polymorphic relationships
|
84
|
+
- **[🏗️ Single Table Inheritance](https://github.com/emilkampp/jpie/blob/main/examples/single_table_inheritance.rb)** - STI models and resources
|
85
|
+
- **[📊 Custom Sorting](https://github.com/emilkampp/jpie/blob/main/examples/custom_sorting.rb)** - Advanced sorting with complex algorithms
|
86
|
+
- **[⚠️ Error Handling](https://github.com/emilkampp/jpie/blob/main/examples/error_handling.rb)** - Comprehensive error handling strategies
|
171
87
|
|
172
|
-
|
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
|
-
```
|
88
|
+
Each example is self-contained with models, resources, controllers, and sample API requests/responses. **[📋 View all examples →](https://github.com/emilkampp/jpie/blob/main/examples/)**
|
205
89
|
|
206
|
-
##
|
90
|
+
## Generators
|
207
91
|
|
208
|
-
|
92
|
+
JPie includes a resource generator for quickly creating new resource classes:
|
209
93
|
|
210
|
-
###
|
94
|
+
### Basic Usage
|
211
95
|
|
212
|
-
|
96
|
+
```bash
|
97
|
+
# Generate a basic resource with semantic syntax
|
98
|
+
rails generate jpie:resource User attribute:name attribute:email meta:created_at
|
213
99
|
|
214
|
-
|
215
|
-
|
216
|
-
class UsersController < ApplicationController
|
217
|
-
include JPie::Controller
|
218
|
-
# Automatically uses UserResource
|
219
|
-
end
|
100
|
+
# Shorthand for relationships
|
101
|
+
rails generate jpie:resource Post attribute:title attribute:content has_many:comments has_one:author
|
220
102
|
|
221
|
-
#
|
222
|
-
|
223
|
-
include JPie::Controller
|
224
|
-
resource UserResource # Use a different resource class (modern syntax)
|
225
|
-
# or: jsonapi_resource UserResource # (backward compatible syntax)
|
226
|
-
end
|
103
|
+
# Mix explicit categorization with auto-detection
|
104
|
+
rails generate jpie:resource User attribute:name email created_at updated_at
|
227
105
|
```
|
228
106
|
|
229
|
-
|
230
|
-
|
231
|
-
JPie automatically infers the model from your resource class name, but you can override this:
|
232
|
-
|
107
|
+
**Generated file:**
|
233
108
|
```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
109
|
class UserResource < JPie::Resource
|
242
|
-
model CustomUser # Use a different model class
|
243
110
|
attributes :name, :email
|
244
|
-
|
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
|
111
|
+
meta_attributes :created_at, :updated_at
|
270
112
|
|
271
|
-
|
113
|
+
has_many :comments
|
114
|
+
has_one :author
|
272
115
|
end
|
273
116
|
```
|
274
117
|
|
275
|
-
###
|
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
|
-
```
|
118
|
+
### Semantic Field Syntax
|
296
119
|
|
297
|
-
|
120
|
+
| Syntax | Purpose | Example |
|
121
|
+
|--------|---------|---------|
|
122
|
+
| `attribute:field` | Regular JSON:API attribute | `attribute:name` |
|
123
|
+
| `meta:field` | JSON:API meta attribute | `meta:created_at` |
|
124
|
+
| `has_many:resource` | JSON:API relationship | `has_many:posts` |
|
125
|
+
| `has_one:resource` | JSON:API relationship | `has_one:profile` |
|
298
126
|
|
299
|
-
|
127
|
+
### Generator Options
|
300
128
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
attribute :display_name
|
306
|
-
meta_attribute :user_stats
|
129
|
+
| Option | Description | Example |
|
130
|
+
|--------|-------------|---------|
|
131
|
+
| `--model=NAME` | Specify model class | `--model=Person` |
|
132
|
+
| `--skip-model` | Skip explicit model declaration | `--skip-model` |
|
307
133
|
|
308
|
-
|
134
|
+
## Core Features
|
309
135
|
|
310
|
-
|
311
|
-
"#{object.first_name} #{object.last_name}"
|
312
|
-
end
|
136
|
+
### JSON:API Compliance
|
313
137
|
|
314
|
-
|
315
|
-
if context[:admin]
|
316
|
-
"#{full_name} [ADMIN VIEW] - #{object.email}"
|
317
|
-
else
|
318
|
-
full_name
|
319
|
-
end
|
320
|
-
end
|
138
|
+
JPie automatically handles all JSON:API specification requirements:
|
321
139
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
email_domain: object.email.split('@').last,
|
326
|
-
account_status: object.active? ? 'active' : 'inactive'
|
327
|
-
}
|
328
|
-
end
|
329
|
-
end
|
140
|
+
#### Sorting
|
141
|
+
```http
|
142
|
+
GET /users?sort=name,-created_at
|
330
143
|
```
|
331
144
|
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
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.
|
145
|
+
#### Includes
|
146
|
+
```http
|
147
|
+
GET /posts?include=author,comments,comments.author
|
148
|
+
```
|
348
149
|
|
349
|
-
####
|
150
|
+
#### Validation
|
151
|
+
- Request structure validation for POST/PATCH operations
|
152
|
+
- Include parameter validation
|
153
|
+
- Sort parameter validation with clear error messages
|
350
154
|
|
351
|
-
|
155
|
+
### Modern DSL
|
352
156
|
|
353
157
|
```ruby
|
354
158
|
class UserResource < JPie::Resource
|
355
|
-
|
356
|
-
|
159
|
+
# Multiple attributes at once
|
160
|
+
attributes :name, :email, :created_at
|
161
|
+
|
162
|
+
# Meta attributes (for additional data)
|
163
|
+
meta :account_status, :last_login
|
164
|
+
|
165
|
+
# Relationships for includes
|
166
|
+
has_many :posts
|
167
|
+
has_one :profile
|
168
|
+
|
169
|
+
# Custom sorting
|
170
|
+
sortable :popularity do |query, direction|
|
171
|
+
query.order(likes_count: direction)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Custom attribute methods (modern approach)
|
175
|
+
private
|
176
|
+
|
177
|
+
def account_status
|
178
|
+
object.active? ? 'active' : 'inactive'
|
179
|
+
end
|
357
180
|
end
|
358
181
|
```
|
359
182
|
|
360
|
-
|
183
|
+
### Through Associations
|
361
184
|
|
362
|
-
|
185
|
+
JPie seamlessly supports Rails `:through` associations:
|
363
186
|
|
364
187
|
```ruby
|
365
|
-
class
|
366
|
-
attributes :
|
367
|
-
|
368
|
-
|
369
|
-
|
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
|
188
|
+
class CarResource < JPie::Resource
|
189
|
+
attributes :make, :model, :year
|
190
|
+
|
191
|
+
# JPie handles the through association automatically
|
192
|
+
has_many :drivers, through: :car_drivers
|
377
193
|
end
|
378
194
|
```
|
379
195
|
|
380
|
-
The
|
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
|
-
```
|
196
|
+
The join table is completely hidden from the API response, providing a clean interface.
|
407
197
|
|
408
|
-
|
198
|
+
### Custom Attributes
|
409
199
|
|
410
|
-
|
200
|
+
Define custom computed attributes using method overrides:
|
411
201
|
|
412
202
|
```ruby
|
413
|
-
class
|
414
|
-
|
415
|
-
|
416
|
-
|
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
|
-
|
203
|
+
class UserResource < JPie::Resource
|
204
|
+
attributes :first_name, :last_name
|
205
|
+
attribute :full_name
|
206
|
+
|
434
207
|
private
|
435
|
-
|
436
|
-
def
|
437
|
-
{
|
438
|
-
post_count: object.posts.count,
|
439
|
-
comment_count: object.comments.count
|
440
|
-
}
|
208
|
+
|
209
|
+
def full_name
|
210
|
+
"#{object.first_name} #{object.last_name}"
|
441
211
|
end
|
442
212
|
end
|
443
213
|
```
|
444
214
|
|
445
|
-
###
|
215
|
+
### Authorization & Context
|
446
216
|
|
447
|
-
|
217
|
+
Pass context for authorization-aware responses:
|
448
218
|
|
449
219
|
```ruby
|
450
|
-
class
|
451
|
-
|
220
|
+
class UsersController < ApplicationController
|
221
|
+
include JPie::Controller
|
452
222
|
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
else
|
457
|
-
query.order(likes_count: :desc, comments_count: :desc)
|
458
|
-
end
|
223
|
+
def show
|
224
|
+
user = User.find(params[:id])
|
225
|
+
render_jsonapi(user, context: { current_user: current_user })
|
459
226
|
end
|
460
227
|
end
|
461
228
|
```
|
462
229
|
|
463
|
-
|
230
|
+
## Error Handling
|
464
231
|
|
465
|
-
JPie
|
232
|
+
JPie provides robust error handling with full customization options:
|
466
233
|
|
467
|
-
|
234
|
+
### Default Error Handling
|
468
235
|
|
469
|
-
|
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
|
236
|
+
JPie automatically handles common errors:
|
477
237
|
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
validates :title, :content, presence: true
|
484
|
-
end
|
238
|
+
| Error Type | HTTP Status | Description |
|
239
|
+
|------------|-------------|-------------|
|
240
|
+
| `JPie::Errors::Error` | Varies | Base JPie errors with custom status |
|
241
|
+
| `ActiveRecord::RecordNotFound` | 404 | Missing records |
|
242
|
+
| `ActiveRecord::RecordInvalid` | 422 | Validation failures |
|
485
243
|
|
486
|
-
|
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
|
-
```
|
244
|
+
### Customization Options
|
494
245
|
|
495
|
-
####
|
246
|
+
#### Override Specific Handlers
|
496
247
|
|
497
248
|
```ruby
|
498
|
-
|
499
|
-
|
500
|
-
|
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
|
249
|
+
class ApplicationController < ActionController::Base
|
250
|
+
# Define your handlers first
|
251
|
+
rescue_from ActiveRecord::RecordNotFound, with: :my_not_found_handler
|
542
252
|
|
543
|
-
|
544
|
-
|
545
|
-
end
|
253
|
+
include JPie::Controller
|
254
|
+
# JPie will detect existing handler and won't override it
|
546
255
|
end
|
547
256
|
```
|
548
257
|
|
549
|
-
####
|
258
|
+
#### Extend JPie Handlers
|
550
259
|
|
551
260
|
```ruby
|
552
|
-
class
|
261
|
+
class ApplicationController < ActionController::Base
|
553
262
|
include JPie::Controller
|
554
263
|
|
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
264
|
private
|
568
265
|
|
569
|
-
def
|
570
|
-
#
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
else
|
576
|
-
raise ArgumentError, "Commentable not specified"
|
577
|
-
end
|
266
|
+
def render_jpie_validation_error(error)
|
267
|
+
# Add custom logging
|
268
|
+
Rails.logger.error "Validation failed: #{error.message}"
|
269
|
+
|
270
|
+
# Call the original method or implement your own
|
271
|
+
super
|
578
272
|
end
|
579
273
|
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
274
|
```
|
591
275
|
|
592
|
-
####
|
276
|
+
#### Disable All JPie Error Handlers
|
593
277
|
|
594
278
|
```ruby
|
595
|
-
|
596
|
-
|
597
|
-
resources :comments, only: [:index, :create]
|
598
|
-
end
|
279
|
+
class ApplicationController < ActionController::Base
|
280
|
+
include JPie::Controller
|
599
281
|
|
600
|
-
|
601
|
-
resources :comments, only: [:index, :create]
|
602
|
-
end
|
282
|
+
disable_jpie_error_handlers
|
603
283
|
|
604
|
-
|
605
|
-
|
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
|
284
|
+
# Define your own handlers
|
285
|
+
rescue_from StandardError, with: :handle_standard_error
|
706
286
|
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
287
|
```
|
765
288
|
|
766
|
-
|
767
|
-
|
768
|
-
Controllers work seamlessly with STI models:
|
289
|
+
### Custom JPie Errors
|
769
290
|
|
770
291
|
```ruby
|
771
|
-
class
|
772
|
-
|
773
|
-
|
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.)
|
292
|
+
class CustomBusinessError < JPie::Errors::Error
|
293
|
+
def initialize(detail: 'Business logic error')
|
294
|
+
super(status: 422, title: 'Business Error', detail: detail)
|
295
|
+
end
|
784
296
|
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
297
|
|
799
|
-
|
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.)
|
298
|
+
# Use in controllers
|
299
|
+
raise CustomBusinessError.new(detail: 'Custom validation failed')
|
805
300
|
```
|
806
301
|
|
807
|
-
|
302
|
+
## Advanced Features
|
808
303
|
|
809
|
-
|
304
|
+
### Polymorphic Associations
|
810
305
|
|
811
|
-
|
306
|
+
JPie supports polymorphic associations for complex data relationships:
|
812
307
|
|
813
308
|
```ruby
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
t.integer :cargo_capacity # Truck-specific
|
824
|
-
t.timestamps
|
825
|
-
end
|
826
|
-
|
827
|
-
add_index :vehicles, :type
|
309
|
+
class CommentResource < JPie::Resource
|
310
|
+
attributes :content
|
311
|
+
has_one :author
|
312
|
+
|
313
|
+
# Polymorphic commentable (posts, articles, videos, etc.)
|
314
|
+
private
|
315
|
+
|
316
|
+
def author
|
317
|
+
object.author
|
828
318
|
end
|
829
319
|
end
|
830
320
|
```
|
831
321
|
|
832
|
-
|
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
|
322
|
+
### Single Table Inheritance
|
842
323
|
|
843
|
-
|
844
|
-
validates :cargo_capacity, presence: true
|
845
|
-
end
|
846
|
-
```
|
847
|
-
|
848
|
-
**3. Resources**
|
324
|
+
JPie automatically handles STI models:
|
849
325
|
|
850
326
|
```ruby
|
327
|
+
# Base resource
|
851
328
|
class VehicleResource < JPie::Resource
|
852
329
|
attributes :name, :brand, :year
|
853
|
-
meta_attributes :created_at, :updated_at
|
854
330
|
end
|
855
331
|
|
332
|
+
# STI resources inherit automatically
|
856
333
|
class CarResource < VehicleResource
|
857
|
-
attributes :engine_size
|
858
|
-
end
|
859
|
-
|
860
|
-
class TruckResource < VehicleResource
|
861
|
-
attributes :cargo_capacity
|
334
|
+
attributes :engine_size, :doors # Car-specific attributes
|
862
335
|
end
|
863
336
|
```
|
864
337
|
|
865
|
-
|
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
|
338
|
+
STI types are automatically inferred in JSON:API responses.
|
877
339
|
|
878
|
-
|
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
|
340
|
+
### Custom Sorting
|
993
341
|
|
994
|
-
|
342
|
+
Implement complex sorting logic:
|
995
343
|
|
996
344
|
```ruby
|
997
|
-
class PostResource < JPie::Resource
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
return model.none unless current_user
|
1003
|
-
Pundit.policy_scope(current_user, model)
|
345
|
+
class PostResource < JPie::Resource
|
346
|
+
sortable :popularity do |query, direction|
|
347
|
+
query.joins(:likes, :comments)
|
348
|
+
.group('posts.id')
|
349
|
+
.order("COUNT(likes.id) + COUNT(comments.id) #{direction.to_s.upcase}")
|
1004
350
|
end
|
1005
351
|
end
|
1006
352
|
```
|
1007
353
|
|
1008
|
-
|
354
|
+
## Performance & Best Practices
|
1009
355
|
|
1010
|
-
|
356
|
+
- **Efficient serialization** with automatic deduplication
|
357
|
+
- **Smart includes** with optimized queries
|
358
|
+
- **Validation caching** for improved performance
|
359
|
+
- **Error handling** that doesn't impact performance
|
1011
360
|
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
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
|
-
```
|
361
|
+
## Contributing
|
362
|
+
|
363
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/emilkampp/jpie.
|
1029
364
|
|
1030
365
|
## License
|
1031
366
|
|