jpie 0.3.0

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