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.
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 - Default Implementation
30
+ ## Quick Start
28
31
 
29
- JPie works out of the box with minimal configuration. Here's a complete example of the default implementation:
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
- ### Sorting
134
- All defined attributes are automatically sortable:
76
+ ## 📚 Comprehensive Examples
135
77
 
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
- ```
78
+ JPie includes a complete set of examples demonstrating all features:
169
79
 
170
- Or by name in reverse order by name:
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
- ```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
- ```
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
- ## Customization and Overrides
90
+ ## Generators
207
91
 
208
- Once you have the basic implementation working, you can customize JPie's behavior as needed:
92
+ JPie includes a resource generator for quickly creating new resource classes:
209
93
 
210
- ### Resource Class Inference Override
94
+ ### Basic Usage
211
95
 
212
- JPie automatically infers the resource class from your controller name, but you can override this:
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
- ```ruby
215
- # Automatic inference (default behavior)
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
- # 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
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
- ### Model Specification Override
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
- 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
111
+ meta_attributes :created_at, :updated_at
270
112
 
271
- # show, update, destroy still use the automatic implementations
113
+ has_many :comments
114
+ has_one :author
272
115
  end
273
116
  ```
274
117
 
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
- ```
118
+ ### Semantic Field Syntax
296
119
 
297
- #### Using Method Overrides (New Approach)
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
- You can now define custom methods directly on your resource class instead of using blocks:
127
+ ### Generator Options
300
128
 
301
- ```ruby
302
- class UserResource < JPie::Resource
303
- attributes :name, :email
304
- attribute :full_name
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
- private
134
+ ## Core Features
309
135
 
310
- def full_name
311
- "#{object.first_name} #{object.last_name}"
312
- end
136
+ ### JSON:API Compliance
313
137
 
314
- def display_name
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
- 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
140
+ #### Sorting
141
+ ```http
142
+ GET /users?sort=name,-created_at
330
143
  ```
331
144
 
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.
145
+ #### Includes
146
+ ```http
147
+ GET /posts?include=author,comments,comments.author
148
+ ```
348
149
 
349
- #### Using meta_attributes Macro
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
- It's easy to add meta attributes:
155
+ ### Modern DSL
352
156
 
353
157
  ```ruby
354
158
  class UserResource < JPie::Resource
355
- meta_attributes :created_at, :updated_at
356
- meta_attributes :last_login_at
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
- #### Using Custom meta Method
183
+ ### Through Associations
361
184
 
362
- For more complex meta data, you can define a `meta` method that returns a hash:
185
+ JPie seamlessly supports Rails `:through` associations:
363
186
 
364
187
  ```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
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 `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
- ```
196
+ The join table is completely hidden from the API response, providing a clean interface.
407
197
 
408
- #### Meta Method Inheritance
198
+ ### Custom Attributes
409
199
 
410
- Meta methods work seamlessly with inheritance:
200
+ Define custom computed attributes using method overrides:
411
201
 
412
202
  ```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
-
203
+ class UserResource < JPie::Resource
204
+ attributes :first_name, :last_name
205
+ attribute :full_name
206
+
434
207
  private
435
-
436
- def calculate_user_metrics
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
- ### Custom Sorting
215
+ ### Authorization & Context
446
216
 
447
- Override the default sorting behavior with custom logic:
217
+ Pass context for authorization-aware responses:
448
218
 
449
219
  ```ruby
450
- class PostResource < JPie::Resource
451
- attributes :title, :content
220
+ class UsersController < ApplicationController
221
+ include JPie::Controller
452
222
 
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
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
- ### Polymorphic Associations
230
+ ## Error Handling
464
231
 
465
- JPie supports polymorphic associations seamlessly. Here's a complete example with comments that can belong to multiple types of commentable resources:
232
+ JPie provides robust error handling with full customization options:
466
233
 
467
- #### Models with Polymorphic Associations
234
+ ### Default Error Handling
468
235
 
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
236
+ JPie automatically handles common errors:
477
237
 
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
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
- # 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
- ```
244
+ ### Customization Options
494
245
 
495
- #### Resources for Polymorphic Associations
246
+ #### Override Specific Handlers
496
247
 
497
248
  ```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
249
+ class ApplicationController < ActionController::Base
250
+ # Define your handlers first
251
+ rescue_from ActiveRecord::RecordNotFound, with: :my_not_found_handler
542
252
 
543
- relationship :author do
544
- UserResource.new(object.author, context) if object.author
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
- #### Controllers for Polymorphic Resources
258
+ #### Extend JPie Handlers
550
259
 
551
260
  ```ruby
552
- class CommentsController < ApplicationController
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 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
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
- #### Routes for Polymorphic Resources
276
+ #### Disable All JPie Error Handlers
593
277
 
594
278
  ```ruby
595
- Rails.application.routes.draw do
596
- resources :posts do
597
- resources :comments, only: [:index, :create]
598
- end
279
+ class ApplicationController < ActionController::Base
280
+ include JPie::Controller
599
281
 
600
- resources :articles do
601
- resources :comments, only: [:index, :create]
602
- end
282
+ disable_jpie_error_handlers
603
283
 
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
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
- #### STI Controllers
767
-
768
- Controllers work seamlessly with STI models:
289
+ ### Custom JPie Errors
769
290
 
770
291
  ```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.)
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
- 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.)
298
+ # Use in controllers
299
+ raise CustomBusinessError.new(detail: 'Custom validation failed')
805
300
  ```
806
301
 
807
- #### Complete STI Example
302
+ ## Advanced Features
808
303
 
809
- Here's a complete example showing STI in action with HTTP requests and responses:
304
+ ### Polymorphic Associations
810
305
 
811
- **1. Database Setup**
306
+ JPie supports polymorphic associations for complex data relationships:
812
307
 
813
308
  ```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
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
- **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
322
+ ### Single Table Inheritance
842
323
 
843
- class Truck < Vehicle
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
- **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
338
+ STI types are automatically inferred in JSON:API responses.
877
339
 
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
340
+ ### Custom Sorting
993
341
 
994
- Override the default scope method to add authorization:
342
+ Implement complex sorting logic:
995
343
 
996
344
  ```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)
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
- ### Custom Context
354
+ ## Performance & Best Practices
1009
355
 
1010
- Override the context building to pass additional data to resources:
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
- ```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
- ```
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