jpie 0.4.0 → 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
@@ -13,9 +13,11 @@ JPie is a modern, lightweight Rails library for developing JSON:API compliant se
13
13
  ⚡ **Powerful Generators** - Scaffold resources with relationships, meta attributes, and automatic inference
14
14
  📊 **Polymorphic Support** - Full support for complex polymorphic associations
15
15
  🔄 **STI Ready** - Single Table Inheritance works out of the box
16
+ 🔗 **Through Associations** - Full support for Rails `:through` associations
16
17
  ⚡ **Performance Optimized** - Efficient serialization with intelligent deduplication
17
18
  🛡️ **Authorization Ready** - Built-in scoping support for security
18
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
19
21
 
20
22
  ## Installation
21
23
 
@@ -25,9 +27,9 @@ Add JPie to your Rails application:
25
27
  bundle add jpie
26
28
  ```
27
29
 
28
- ## Quick Start - Default Implementation
30
+ ## Quick Start
29
31
 
30
- 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:
31
33
 
32
34
  ### 1. Create Your Model
33
35
 
@@ -35,6 +37,9 @@ JPie works out of the box with minimal configuration. Here's a complete example
35
37
  class User < ActiveRecord::Base
36
38
  validates :name, presence: true
37
39
  validates :email, presence: true, uniqueness: true
40
+
41
+ has_many :posts, dependent: :destroy
42
+ has_one :profile, dependent: :destroy
38
43
  end
39
44
  ```
40
45
 
@@ -43,6 +48,10 @@ end
43
48
  ```ruby
44
49
  class UserResource < JPie::Resource
45
50
  attributes :name, :email
51
+ meta_attributes :created_at, :updated_at
52
+
53
+ has_many :posts
54
+ has_one :profile
46
55
  end
47
56
  ```
48
57
 
@@ -62,16 +71,28 @@ Rails.application.routes.draw do
62
71
  end
63
72
  ```
64
73
 
65
- That's it! You now have a fully functional JSON:API compliant server.
74
+ That's it! You now have a fully functional JSON:API compliant server with automatic CRUD operations, sorting, includes, and validation.
75
+
76
+ ## 📚 Comprehensive Examples
77
+
78
+ JPie includes a complete set of examples demonstrating all features:
79
+
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
87
+
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/)**
66
89
 
67
90
  ## Generators
68
91
 
69
- JPie includes a resource generator for quickly creating new resource classes with proper JSON:API structure.
92
+ JPie includes a resource generator for quickly creating new resource classes:
70
93
 
71
94
  ### Basic Usage
72
95
 
73
- The generator uses semantic field definitions that explicitly categorize each field by its JSON:API purpose:
74
-
75
96
  ```bash
76
97
  # Generate a basic resource with semantic syntax
77
98
  rails generate jpie:resource User attribute:name attribute:email meta:created_at
@@ -85,13 +106,10 @@ rails generate jpie:resource User attribute:name email created_at updated_at
85
106
 
86
107
  **Generated file:**
87
108
  ```ruby
88
- # frozen_string_literal: true
89
-
90
109
  class UserResource < JPie::Resource
91
110
  attributes :name, :email
92
-
93
111
  meta_attributes :created_at, :updated_at
94
-
112
+
95
113
  has_many :comments
96
114
  has_one :author
97
115
  end
@@ -99,122 +117,52 @@ end
99
117
 
100
118
  ### Semantic Field Syntax
101
119
 
102
- The generator uses a semantic approach focused on JSON:API concepts rather than database types:
103
-
104
120
  | Syntax | Purpose | Example |
105
121
  |--------|---------|---------|
106
122
  | `attribute:field` | Regular JSON:API attribute | `attribute:name` |
107
123
  | `meta:field` | JSON:API meta attribute | `meta:created_at` |
108
124
  | `has_many:resource` | JSON:API relationship | `has_many:posts` |
109
125
  | `has_one:resource` | JSON:API relationship | `has_one:profile` |
110
- | `relationship:type:resource` | Explicit relationship | `relationship:has_many:posts` |
111
-
112
- ### Advanced Examples
113
-
114
- ##### Comprehensive Resource
115
-
116
- ```bash
117
- rails generate jpie:resource Article \
118
- attribute:title \
119
- attribute:content \
120
- meta:published_at \
121
- meta:created_at \
122
- meta:updated_at \
123
- has_one:author \
124
- has_many:comments \
125
- has_many:tags \
126
- --model=Post
127
- ```
128
-
129
- **Generated file:**
130
- ```ruby
131
- # frozen_string_literal: true
132
-
133
- class ArticleResource < JPie::Resource
134
- model Post
135
126
 
136
- attributes :title, :content
137
-
138
- meta_attributes :published_at, :created_at, :updated_at
139
-
140
- has_one :author
141
- has_many :comments
142
- has_many :tags
143
- end
144
- ```
127
+ ### Generator Options
145
128
 
146
- ##### Empty Resource Template
129
+ | Option | Description | Example |
130
+ |--------|-------------|---------|
131
+ | `--model=NAME` | Specify model class | `--model=Person` |
132
+ | `--skip-model` | Skip explicit model declaration | `--skip-model` |
147
133
 
148
- ```bash
149
- rails generate jpie:resource User
150
- ```
134
+ ## Core Features
151
135
 
152
- **Generated file:**
153
- ```ruby
154
- # frozen_string_literal: true
136
+ ### JSON:API Compliance
155
137
 
156
- class UserResource < JPie::Resource
157
- # Define your attributes here:
158
- # attributes :name, :email, :title
138
+ JPie automatically handles all JSON:API specification requirements:
159
139
 
160
- # Define your meta attributes here:
161
- # meta_attributes :created_at, :updated_at
162
-
163
- # Define your relationships here:
164
- # has_many :posts
165
- # has_one :user
166
- end
140
+ #### Sorting
141
+ ```http
142
+ GET /users?sort=name,-created_at
167
143
  ```
168
144
 
169
- ### Legacy Syntax Support
170
-
171
- The generator maintains backward compatibility with the Rails-style `field:type` syntax, but ignores the type portion:
172
-
173
- ```bash
174
- # Legacy syntax (still works, types ignored)
175
- rails generate jpie:resource User name:string email:string created_at:datetime
145
+ #### Includes
146
+ ```http
147
+ GET /posts?include=author,comments,comments.author
176
148
  ```
177
149
 
178
- This generates the same output as the semantic syntax, with automatic detection of meta attributes based on common field names.
179
-
180
- ### Generator Options
181
-
182
- | Option | Type | Description | Example |
183
- |--------|------|-------------|---------|
184
- | `--model=NAME` | String | Specify model class (overrides inference) | `--model=Person` |
185
- | `--skip-model` | Boolean | Skip explicit model declaration | `--skip-model` |
150
+ #### Validation
151
+ - Request structure validation for POST/PATCH operations
152
+ - Include parameter validation
153
+ - Sort parameter validation with clear error messages
186
154
 
187
- ### Automatic Features
188
-
189
- - **Model Inference**: Automatically infers model class from resource name
190
- - **Resource Inference**: Automatically infers related resource classes for relationships
191
- - **Meta Detection**: Auto-detects common meta attributes (`created_at`, `updated_at`, etc.)
192
- - **Clean Output**: Generates well-structured, commented resource files
193
-
194
- ### Best Practices
195
-
196
- 1. **Use semantic syntax** for clarity and JSON:API-appropriate thinking
197
- 2. **Be explicit about categorization** when the intent might be unclear
198
- 3. **Let JPie handle inference** for standard naming conventions
199
- 4. **Use `--model` only when necessary** for non-standard model mappings
200
-
201
- ## Modern DSL Examples
202
-
203
- JPie provides a clean, modern DSL that follows Rails conventions:
204
-
205
- ### Resource Definition
155
+ ### Modern DSL
206
156
 
207
157
  ```ruby
208
158
  class UserResource < JPie::Resource
209
- # Attributes (multiple syntaxes supported)
159
+ # Multiple attributes at once
210
160
  attributes :name, :email, :created_at
211
- attribute :full_name
212
161
 
213
- # Meta attributes
162
+ # Meta attributes (for additional data)
214
163
  meta :account_status, :last_login
215
- # or: meta_attributes :account_status, :last_login
216
164
 
217
- # Includes for related data (used with ?include= parameter)
165
+ # Relationships for includes
218
166
  has_many :posts
219
167
  has_one :profile
220
168
 
@@ -222,971 +170,197 @@ class UserResource < JPie::Resource
222
170
  sortable :popularity do |query, direction|
223
171
  query.order(likes_count: direction)
224
172
  end
225
- # or: sortable_by :popularity do |query, direction|
226
173
 
227
- # Custom attribute methods
174
+ # Custom attribute methods (modern approach)
228
175
  private
229
176
 
230
- def full_name
231
- "#{object.first_name} #{object.last_name}"
232
- end
233
-
234
177
  def account_status
235
178
  object.active? ? 'active' : 'inactive'
236
179
  end
237
180
  end
238
181
  ```
239
182
 
240
- ### Controller Definition
241
-
242
- ```ruby
243
- class UsersController < ApplicationController
244
- include JPie::Controller
245
-
246
- # Explicit resource (optional - auto-inferred by default)
247
- resource UserResource
248
- # or: jsonapi_resource UserResource
249
-
250
- # Override methods as needed
251
- def index
252
- users = current_user.admin? ? User.all : User.active
253
- render_jsonapi(users)
254
- end
255
-
256
- def create
257
- attributes = deserialize_params
258
- user = model_class.new(attributes)
259
- user.created_by = current_user
260
- user.save!
261
-
262
- render_jsonapi(user, status: :created)
263
- end
264
- end
265
- ```
266
-
267
- ## Suported JSON:API features
268
-
269
- ### Sorting
270
- All defined attributes are automatically sortable:
271
-
272
- ```http
273
- GET /users?sort=name
274
- HTTP/1.1 200 OK
275
- Content-Type: application/vnd.api+json
276
- {
277
- "data": [
278
- {
279
- "id": "1",
280
- "type": "users",
281
- "attributes": {
282
- "name": "Alice Anderson",
283
- "email": "alice@example.com"
284
- }
285
- },
286
- {
287
- "id": "2",
288
- "type": "users",
289
- "attributes": {
290
- "name": "Bob Brown",
291
- "email": "bob@example.com"
292
- }
293
- },
294
- {
295
- "id": "3",
296
- "type": "users",
297
- "attributes": {
298
- "name": "Carol Clark",
299
- "email": "carol@example.com"
300
- }
301
- }
302
- ]
303
- }
304
- ```
305
-
306
- Or by name in reverse order by name:
183
+ ### Through Associations
307
184
 
308
- ```http
309
- GET /users?sort=-name
310
- HTTP/1.1 200 OK
311
- Content-Type: application/vnd.api+json
312
- {
313
- "data": [
314
- {
315
- "id": "3",
316
- "type": "users",
317
- "attributes": {
318
- "name": "Carol Clark",
319
- "email": "carol@example.com"
320
- }
321
- },
322
- {
323
- "id": "2",
324
- "type": "users",
325
- "attributes": {
326
- "name": "Bob Brown",
327
- "email": "bob@example.com"
328
- }
329
- },
330
- {
331
- "id": "1",
332
- "type": "users",
333
- "attributes": {
334
- "name": "Alice Anderson",
335
- "email": "alice@example.com"
336
- }
337
- }
338
- ]
339
- }
340
- ```
341
-
342
- ### Includes
343
-
344
- JPie supports including related resources using the `?include=` parameter. Related resources are returned in the `included` section of the JSON:API response:
345
-
346
- ```http
347
- GET /posts/1?include=author
348
- HTTP/1.1 200 OK
349
- Content-Type: application/vnd.api+json
350
- {
351
- "data": {
352
- "id": "1",
353
- "type": "posts",
354
- "attributes": {
355
- "title": "My First Post",
356
- "content": "This is the content of my first post."
357
- }
358
- },
359
- "included": [
360
- {
361
- "id": "5",
362
- "type": "users",
363
- "attributes": {
364
- "name": "John Doe",
365
- "email": "john@example.com"
366
- }
367
- }
368
- ]
369
- }
370
- ```
371
-
372
- Multiple includes and nested includes are also supported:
373
-
374
- ```http
375
- GET /posts?include=author,comments,comments.author
376
- ```
377
-
378
- ## Customization and Overrides
379
-
380
- Once you have the basic implementation working, you can customize JPie's behavior as needed:
381
-
382
- ### Resource Class Inference Override
383
-
384
- JPie automatically infers the resource class from your controller name, but you can override this:
185
+ JPie seamlessly supports Rails `:through` associations:
385
186
 
386
187
  ```ruby
387
- # Automatic inference (default behavior)
388
- class UsersController < ApplicationController
389
- include JPie::Controller
390
- # Automatically uses UserResource
391
- end
392
-
393
- # Explicit resource specification (override)
394
- class UsersController < ApplicationController
395
- include JPie::Controller
396
- resource UserResource # Use a different resource class (modern syntax)
397
- # or: jsonapi_resource UserResource # (backward compatible syntax)
398
- end
399
- ```
400
-
401
- ### Model Specification Override
402
-
403
- JPie automatically infers the model from your resource class name, but you can override this:
404
-
405
- ```ruby
406
- # Automatic inference (default behavior)
407
- class UserResource < JPie::Resource
408
- attributes :name, :email
409
- # Automatically uses User model
410
- end
411
-
412
- # Explicit model specification (override)
413
- class UserResource < JPie::Resource
414
- model CustomUser # Use a different model class
415
- attributes :name, :email
416
- end
417
- ```
418
-
419
- ### Controller Method Overrides
420
-
421
- You can override any of the automatic CRUD methods:
422
-
423
- ```ruby
424
- class UsersController < ApplicationController
425
- include JPie::Controller
426
-
427
- # Override index to add filtering
428
- def index
429
- users = User.where(active: true)
430
- render_jsonapi(users)
431
- end
432
-
433
- # Override create to add custom logic
434
- def create
435
- attributes = deserialize_params
436
- user = model_class.new(attributes)
437
- user.created_by = current_user
438
- user.save!
439
-
440
- render_jsonapi(user, status: :created)
441
- end
188
+ class CarResource < JPie::Resource
189
+ attributes :make, :model, :year
442
190
 
443
- # show, update, destroy still use the automatic implementations
191
+ # JPie handles the through association automatically
192
+ has_many :drivers, through: :car_drivers
444
193
  end
445
194
  ```
446
195
 
447
- ### Custom Attributes
448
-
449
- Add computed or transformed attributes to your resources using either blocks or method overrides:
196
+ The join table is completely hidden from the API response, providing a clean interface.
450
197
 
451
- #### Using Blocks (Original Approach)
452
-
453
- ```ruby
454
- class UserResource < JPie::Resource
455
- attribute :display_name do
456
- "#{object.first_name} #{object.last_name}"
457
- end
458
-
459
- attribute :admin_notes do
460
- if context[:current_user]&.admin?
461
- object.admin_notes
462
- else
463
- nil
464
- end
465
- end
466
- end
467
- ```
468
-
469
- #### Using Method Overrides (New Approach)
198
+ ### Custom Attributes
470
199
 
471
- You can now define custom methods directly on your resource class instead of using blocks:
200
+ Define custom computed attributes using method overrides:
472
201
 
473
202
  ```ruby
474
203
  class UserResource < JPie::Resource
475
- attributes :name, :email
204
+ attributes :first_name, :last_name
476
205
  attribute :full_name
477
- attribute :display_name
478
- meta_attribute :user_stats
479
-
206
+
480
207
  private
481
-
208
+
482
209
  def full_name
483
210
  "#{object.first_name} #{object.last_name}"
484
211
  end
485
-
486
- def display_name
487
- if context[:admin]
488
- "#{full_name} [ADMIN VIEW] - #{object.email}"
489
- else
490
- full_name
491
- end
492
- end
493
-
494
- def user_stats
495
- {
496
- name_length: object.name.length,
497
- email_domain: object.email.split('@').last,
498
- account_status: object.active? ? 'active' : 'inactive'
499
- }
500
- end
501
212
  end
502
213
  ```
503
214
 
504
- **Key Benefits of Method Overrides:**
505
- - **Cleaner syntax** - No need for blocks
506
- - **Better IDE support** - Full method definitions with proper syntax highlighting
507
- - **Easier testing** - Methods can be tested individually
508
- - **Private methods supported** - Use private methods for internal logic
509
- - **Access to object and context** - Full access to `object` and `context` like blocks
215
+ ### Authorization & Context
510
216
 
511
- **Method Precedence:**
512
- 1. **Blocks** (highest priority) - `attribute :name do ... end`
513
- 2. **Options blocks** - `attribute :name, block: proc { ... }`
514
- 3. **Custom methods** - `def name; ...; end`
515
- 4. **Model attributes** (lowest priority) - Direct model attribute lookup
516
-
517
- ### Meta attributes
518
-
519
- 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.
520
-
521
- #### Using meta_attributes Macro
522
-
523
- It's easy to add meta attributes:
217
+ Pass context for authorization-aware responses:
524
218
 
525
219
  ```ruby
526
- class UserResource < JPie::Resource
527
- meta_attributes :created_at, :updated_at
528
- meta_attributes :last_login_at
529
- end
530
- ```
531
-
532
- #### Using Custom meta Method
533
-
534
- For more complex meta data, you can define a `meta` method that returns a hash:
535
-
536
- ```ruby
537
- class UserResource < JPie::Resource
538
- attributes :name, :email
539
- meta_attributes :created_at, :updated_at
540
-
541
- def meta
542
- super.merge(
543
- full_name: "#{object.first_name} #{object.last_name}",
544
- user_role: context[:current_user]&.role || 'guest',
545
- account_status: object.active? ? 'active' : 'inactive',
546
- last_seen: object.last_login_at&.iso8601
547
- )
220
+ class UsersController < ApplicationController
221
+ include JPie::Controller
222
+
223
+ def show
224
+ user = User.find(params[:id])
225
+ render_jsonapi(user, context: { current_user: current_user })
548
226
  end
549
227
  end
550
228
  ```
551
229
 
552
- The `meta` method has access to:
553
- - `super` - returns the hash from `meta_attributes`
554
- - `object` - the underlying model instance
555
- - `context` - any context passed during resource initialization
556
-
557
- **Example JSON:API Response with Custom Meta:**
558
-
559
- ```json
560
- {
561
- "data": {
562
- "id": "1",
563
- "type": "users",
564
- "attributes": {
565
- "name": "John Doe",
566
- "email": "john@example.com"
567
- },
568
- "meta": {
569
- "created_at": "2024-01-01T12:00:00Z",
570
- "updated_at": "2024-01-15T14:30:00Z",
571
- "full_name": "John Doe",
572
- "user_role": "admin",
573
- "account_status": "active",
574
- "last_seen": "2024-01-15T14:00:00Z"
575
- }
576
- }
577
- }
578
- ```
579
-
580
- #### Meta Method Inheritance
581
-
582
- Meta methods work seamlessly with inheritance:
583
-
584
- ```ruby
585
- class BaseResource < JPie::Resource
586
- meta_attributes :created_at, :updated_at
230
+ ## Error Handling
587
231
 
588
- def meta
589
- super.merge(
590
- resource_version: '1.0',
591
- timestamp: Time.current.iso8601
592
- )
593
- end
594
- end
232
+ JPie provides robust error handling with full customization options:
595
233
 
596
- class UserResource < BaseResource
597
- attributes :name, :email
598
- meta_attributes :last_login_at
234
+ ### Default Error Handling
599
235
 
600
- def meta
601
- super.merge(
602
- user_specific_data: calculate_user_metrics
603
- )
604
- end
605
-
606
- private
607
-
608
- def calculate_user_metrics
609
- {
610
- post_count: object.posts.count,
611
- comment_count: object.comments.count
612
- }
613
- end
614
- end
615
- ```
616
-
617
- ### Custom Sorting
618
-
619
- Override the default sorting behavior with custom logic:
620
-
621
- ```ruby
622
- class PostResource < JPie::Resource
623
- attributes :title, :content
624
-
625
- sortable_by :popularity do |query, direction|
626
- if direction == :asc
627
- query.order(:likes_count, :comments_count)
628
- else
629
- query.order(likes_count: :desc, comments_count: :desc)
630
- end
631
- end
632
- end
633
- ```
236
+ JPie automatically handles common errors:
634
237
 
635
- ### Polymorphic Associations
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 |
636
243
 
637
- JPie supports polymorphic associations for includes. Here's a complete example with comments that can belong to multiple types of commentable resources:
244
+ ### Customization Options
638
245
 
639
- #### Models with Polymorphic Associations
246
+ #### Override Specific Handlers
640
247
 
641
248
  ```ruby
642
- # Comment model with belongs_to polymorphic association
643
- class Comment < ActiveRecord::Base
644
- belongs_to :commentable, polymorphic: true
645
- belongs_to :author, class_name: 'User'
646
-
647
- validates :content, presence: true
648
- end
649
-
650
- # Post model with has_many polymorphic association
651
- class Post < ActiveRecord::Base
652
- has_many :comments, as: :commentable, dependent: :destroy
653
- belongs_to :author, class_name: 'User'
654
-
655
- validates :title, :content, presence: true
656
- end
657
-
658
- # Article model with has_many polymorphic association
659
- class Article < ActiveRecord::Base
660
- has_many :comments, as: :commentable, dependent: :destroy
661
- belongs_to :author, class_name: 'User'
249
+ class ApplicationController < ActionController::Base
250
+ # Define your handlers first
251
+ rescue_from ActiveRecord::RecordNotFound, with: :my_not_found_handler
662
252
 
663
- validates :title, :body, presence: true
253
+ include JPie::Controller
254
+ # JPie will detect existing handler and won't override it
664
255
  end
665
256
  ```
666
257
 
667
- #### Resources for Polymorphic Associations
258
+ #### Extend JPie Handlers
668
259
 
669
260
  ```ruby
670
- # Comment resource
671
- class CommentResource < JPie::Resource
672
- attributes :content, :created_at
673
-
674
- # Define methods for includes
675
- has_many :comments # For including nested comments
676
- has_one :author # For including the comment author
677
-
678
- private
679
-
680
- def commentable
681
- object.commentable
682
- end
683
-
684
- def author
685
- object.author
686
- end
687
- end
688
-
689
- # Post resource
690
- class PostResource < JPie::Resource
691
- attributes :title, :content, :published_at
692
-
693
- # Define methods for includes
694
- has_many :comments
695
- has_one :author
696
-
697
- private
698
-
699
- def comments
700
- object.comments
701
- end
702
-
703
- def author
704
- object.author
705
- end
706
- end
707
-
708
- # Article resource
709
- class ArticleResource < JPie::Resource
710
- attributes :title, :body, :published_at
711
-
712
- # Define methods for includes
713
- has_many :comments
714
- has_one :author
261
+ class ApplicationController < ActionController::Base
262
+ include JPie::Controller
715
263
 
716
264
  private
717
265
 
718
- def comments
719
- object.comments
720
- end
721
-
722
- def author
723
- object.author
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
724
272
  end
725
273
  end
726
274
  ```
727
275
 
728
- #### Controllers for Polymorphic Resources
276
+ #### Disable All JPie Error Handlers
729
277
 
730
278
  ```ruby
731
- class CommentsController < ApplicationController
279
+ class ApplicationController < ActionController::Base
732
280
  include JPie::Controller
733
281
 
734
- # Override create to handle polymorphic assignment
735
- def create
736
- attributes = deserialize_params
737
- commentable = find_commentable
738
-
739
- comment = commentable.comments.build(attributes)
740
- comment.author = current_user
741
- comment.save!
742
-
743
- render_jsonapi(comment, status: :created)
744
- end
745
-
746
- private
282
+ disable_jpie_error_handlers
747
283
 
748
- def find_commentable
749
- # Extract commentable info from request path or parameters
750
- if params[:post_id]
751
- Post.find(params[:post_id])
752
- elsif params[:article_id]
753
- Article.find(params[:article_id])
754
- else
755
- raise ArgumentError, "Commentable not specified"
756
- end
757
- end
758
- end
759
-
760
- class PostsController < ApplicationController
761
- include JPie::Controller
762
- # Uses default CRUD operations with polymorphic comments included
763
- end
764
-
765
- class ArticlesController < ApplicationController
766
- include JPie::Controller
767
- # Uses default CRUD operations with polymorphic comments included
284
+ # Define your own handlers
285
+ rescue_from StandardError, with: :handle_standard_error
768
286
  end
769
287
  ```
770
288
 
771
- #### Routes for Polymorphic Resources
289
+ ### Custom JPie Errors
772
290
 
773
291
  ```ruby
774
- Rails.application.routes.draw do
775
- resources :posts do
776
- resources :comments, only: [:index, :create]
777
- end
778
-
779
- resources :articles do
780
- resources :comments, only: [:index, :create]
292
+ class CustomBusinessError < JPie::Errors::Error
293
+ def initialize(detail: 'Business logic error')
294
+ super(status: 422, title: 'Business Error', detail: detail)
781
295
  end
782
-
783
- resources :comments, only: [:show, :update, :destroy]
784
296
  end
785
- ```
786
297
 
787
- #### Example JSON:API Responses
788
-
789
- **GET /posts/1?include=comments,comments.author**
790
-
791
- ```json
792
- {
793
- "data": {
794
- "id": "1",
795
- "type": "posts",
796
- "attributes": {
797
- "title": "My First Post",
798
- "content": "This is the content of my first post.",
799
- "published_at": "2024-01-15T10:30:00Z"
800
- }
801
- },
802
- "included": [
803
- {
804
- "id": "1",
805
- "type": "comments",
806
- "attributes": {
807
- "content": "Great post!",
808
- "created_at": "2024-01-15T11:00:00Z"
809
- }
810
- },
811
- {
812
- "id": "2",
813
- "type": "comments",
814
- "attributes": {
815
- "content": "Thanks for sharing!",
816
- "created_at": "2024-01-15T12:00:00Z"
817
- }
818
- },
819
- {
820
- "id": "5",
821
- "type": "users",
822
- "attributes": {
823
- "name": "John Doe",
824
- "email": "john@example.com"
825
- }
826
- },
827
- {
828
- "id": "6",
829
- "type": "users",
830
- "attributes": {
831
- "name": "Jane Smith",
832
- "email": "jane@example.com"
833
- }
834
- }
835
- ]
836
- }
298
+ # Use in controllers
299
+ raise CustomBusinessError.new(detail: 'Custom validation failed')
837
300
  ```
838
301
 
839
- ### Single Table Inheritance (STI)
302
+ ## Advanced Features
840
303
 
841
- 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.
304
+ ### Polymorphic Associations
842
305
 
843
- #### STI Models
306
+ JPie supports polymorphic associations for complex data relationships:
844
307
 
845
308
  ```ruby
846
- # Base model
847
- class Vehicle < ActiveRecord::Base
848
- validates :name, presence: true
849
- validates :brand, presence: true
850
- validates :year, presence: true
851
- end
852
-
853
- # STI subclasses
854
- class Car < Vehicle
855
- validates :engine_size, presence: true
856
- end
857
-
858
- class Truck < Vehicle
859
- validates :cargo_capacity, presence: true
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
318
+ end
860
319
  end
861
320
  ```
862
321
 
863
- #### STI Resources
322
+ ### Single Table Inheritance
864
323
 
865
- JPie automatically handles STI type inference and resource inheritance:
324
+ JPie automatically handles STI models:
866
325
 
867
326
  ```ruby
868
327
  # Base resource
869
328
  class VehicleResource < JPie::Resource
870
329
  attributes :name, :brand, :year
871
- meta_attributes :created_at, :updated_at
872
330
  end
873
331
 
874
- # STI resources inherit from base resource
332
+ # STI resources inherit automatically
875
333
  class CarResource < VehicleResource
876
- attributes :engine_size # Car-specific attribute
877
- end
878
-
879
- class TruckResource < VehicleResource
880
- attributes :cargo_capacity # Truck-specific attribute
334
+ attributes :engine_size, :doors # Car-specific attributes
881
335
  end
882
336
  ```
883
337
 
884
- #### STI Type Inference
885
-
886
- JPie automatically infers the correct JSON:API type from the STI model class:
887
-
888
- ```ruby
889
- car = Car.create!(name: 'Civic', brand: 'Honda', year: 2020, engine_size: 1500)
890
- car_resource = CarResource.new(car)
891
-
892
- car_resource.type # => "cars" (automatically inferred from Car model)
893
- ```
894
-
895
- #### STI Serialization
896
-
897
- Each STI model serializes with its specific type and attributes:
898
-
899
- ```ruby
900
- # Car serialization
901
- car_serializer = JPie::Serializer.new(CarResource)
902
- result = car_serializer.serialize(car)
903
-
904
- # Result:
905
- {
906
- "data": {
907
- "id": "1",
908
- "type": "cars", # STI type
909
- "attributes": {
910
- "name": "Civic",
911
- "brand": "Honda",
912
- "year": 2020,
913
- "engine_size": 1500 # Car-specific attribute
914
- }
915
- }
916
- }
917
-
918
- # Truck serialization
919
- truck_serializer = JPie::Serializer.new(TruckResource)
920
- result = truck_serializer.serialize(truck)
921
-
922
- # Result:
923
- {
924
- "data": {
925
- "id": "2",
926
- "type": "trucks", # STI type
927
- "attributes": {
928
- "name": "F-150",
929
- "brand": "Ford",
930
- "year": 2021,
931
- "cargo_capacity": 1000 # Truck-specific attribute
932
- }
933
- }
934
- }
935
- ```
936
-
937
- #### STI Controllers
338
+ STI types are automatically inferred in JSON:API responses.
938
339
 
939
- Controllers work seamlessly with STI models:
940
-
941
- ```ruby
942
- class CarsController < ApplicationController
943
- include JPie::Controller
944
- # Automatically uses CarResource and Car model
945
- end
946
-
947
- class TrucksController < ApplicationController
948
- include JPie::Controller
949
- # Automatically uses TruckResource and Truck model
950
- end
951
-
952
- class VehiclesController < ApplicationController
953
- include JPie::Controller
954
- # Uses VehicleResource and returns all vehicles (cars, trucks, etc.)
955
- end
956
- ```
957
-
958
- #### STI Scoping
959
-
960
- Each STI resource automatically scopes to its specific type:
961
-
962
- ```ruby
963
- CarResource.scope # Returns only Car records
964
- TruckResource.scope # Returns only Truck records
965
- VehicleResource.scope # Returns all Vehicle records (including STI subclasses)
966
- ```
967
-
968
- #### Complete STI Example
969
-
970
- Here's a complete example showing STI in action with HTTP requests and responses:
340
+ ### Custom Sorting
971
341
 
972
- **1. Database Setup**
342
+ Implement complex sorting logic:
973
343
 
974
344
  ```ruby
975
- # Migration
976
- class CreateVehicles < ActiveRecord::Migration[7.0]
977
- def change
978
- create_table :vehicles do |t|
979
- t.string :type, null: false # STI discriminator column
980
- t.string :name, null: false
981
- t.string :brand, null: false
982
- t.integer :year, null: false
983
- t.integer :engine_size # Car-specific
984
- t.integer :cargo_capacity # Truck-specific
985
- t.timestamps
986
- end
987
-
988
- add_index :vehicles, :type
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}")
989
350
  end
990
351
  end
991
352
  ```
992
353
 
993
- **2. Models**
994
-
995
- ```ruby
996
- class Vehicle < ApplicationRecord
997
- validates :name, :brand, :year, presence: true
998
- end
999
-
1000
- class Car < Vehicle
1001
- validates :engine_size, presence: true
1002
- end
1003
-
1004
- class Truck < Vehicle
1005
- validates :cargo_capacity, presence: true
1006
- end
1007
- ```
1008
-
1009
- **3. Resources**
1010
-
1011
- ```ruby
1012
- class VehicleResource < JPie::Resource
1013
- attributes :name, :brand, :year
1014
- meta_attributes :created_at, :updated_at
1015
- end
1016
-
1017
- class CarResource < VehicleResource
1018
- attributes :engine_size
1019
- end
1020
-
1021
- class TruckResource < VehicleResource
1022
- attributes :cargo_capacity
1023
- end
1024
- ```
1025
-
1026
- **4. Controllers**
1027
-
1028
- ```ruby
1029
- class VehiclesController < ApplicationController
1030
- include JPie::Controller
1031
- # Returns all vehicles (cars, trucks, etc.)
1032
- end
1033
-
1034
- class CarsController < ApplicationController
1035
- include JPie::Controller
1036
- # Returns only cars with car-specific attributes
1037
- end
1038
-
1039
- class TrucksController < ApplicationController
1040
- include JPie::Controller
1041
- # Returns only trucks with truck-specific attributes
1042
- end
1043
- ```
1044
-
1045
- **5. Routes**
1046
-
1047
- ```ruby
1048
- Rails.application.routes.draw do
1049
- resources :vehicles, only: [:index, :show]
1050
- resources :cars
1051
- resources :trucks
1052
- end
1053
- ```
1054
-
1055
- **6. Example HTTP Requests and Responses**
1056
-
1057
- **GET /cars/1**
1058
- ```json
1059
- {
1060
- "data": {
1061
- "id": "1",
1062
- "type": "cars",
1063
- "attributes": {
1064
- "name": "Model 3",
1065
- "brand": "Tesla",
1066
- "year": 2023,
1067
- "engine_size": 0
1068
- },
1069
- "meta": {
1070
- "created_at": "2024-01-15T10:00:00Z",
1071
- "updated_at": "2024-01-15T10:00:00Z"
1072
- }
1073
- }
1074
- }
1075
- ```
1076
-
1077
- **GET /trucks/2**
1078
- ```json
1079
- {
1080
- "data": {
1081
- "id": "2",
1082
- "type": "trucks",
1083
- "attributes": {
1084
- "name": "F-150",
1085
- "brand": "Ford",
1086
- "year": 2023,
1087
- "cargo_capacity": 1200
1088
- },
1089
- "meta": {
1090
- "created_at": "2024-01-15T11:00:00Z",
1091
- "updated_at": "2024-01-15T11:00:00Z"
1092
- }
1093
- }
1094
- }
1095
- ```
1096
-
1097
- **GET /vehicles (Mixed STI Collection)**
1098
- ```json
1099
- {
1100
- "data": [
1101
- {
1102
- "id": "1",
1103
- "type": "cars",
1104
- "attributes": {
1105
- "name": "Model 3",
1106
- "brand": "Tesla",
1107
- "year": 2023,
1108
- "engine_size": 0
1109
- }
1110
- },
1111
- {
1112
- "id": "2",
1113
- "type": "trucks",
1114
- "attributes": {
1115
- "name": "F-150",
1116
- "brand": "Ford",
1117
- "year": 2023,
1118
- "cargo_capacity": 1200
1119
- }
1120
- }
1121
- ]
1122
- }
1123
- ```
1124
-
1125
- **7. Creating STI Records**
1126
-
1127
- **POST /cars**
1128
- ```json
1129
- {
1130
- "data": {
1131
- "type": "cars",
1132
- "attributes": {
1133
- "name": "Model Y",
1134
- "brand": "Tesla",
1135
- "year": 2024,
1136
- "engine_size": 0
1137
- }
1138
- }
1139
- }
1140
- ```
1141
-
1142
- #### Custom STI Types
1143
-
1144
- You can override the automatic type inference if needed:
1145
-
1146
- ```ruby
1147
- class CarResource < VehicleResource
1148
- type 'automobiles' # Custom type instead of 'cars'
1149
- attributes :engine_size
1150
- end
1151
- ```
1152
-
1153
- ### Authorization and Scoping
354
+ ## Performance & Best Practices
1154
355
 
1155
- Override the default scope method to add authorization:
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
1156
360
 
1157
- ```ruby
1158
- class PostResource < JPie::Resource
1159
- attributes :title, :content
1160
-
1161
- def self.scope(context = {})
1162
- current_user = context[:current_user]
1163
- return model.none unless current_user
1164
- Pundit.policy_scope(current_user, model)
1165
- end
1166
- end
1167
- ```
361
+ ## Contributing
1168
362
 
1169
- ### Custom Context
1170
-
1171
- Override the context building to pass additional data to resources:
1172
-
1173
- ```ruby
1174
- class UsersController < ApplicationController
1175
- include JPie::Controller
1176
-
1177
- private
1178
-
1179
- def build_context
1180
- {
1181
- current_user: current_user,
1182
- controller: self,
1183
- action: action_name,
1184
- request_ip: request.remote_ip,
1185
- user_agent: request.user_agent
1186
- }
1187
- end
1188
- end
1189
- ```
363
+ Bug reports and pull requests are welcome on GitHub at https://github.com/emilkampp/jpie.
1190
364
 
1191
365
  ## License
1192
366