jpie 0.4.0 → 0.4.2

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,66 @@ Add JPie to your Rails application:
25
27
  bundle add jpie
26
28
  ```
27
29
 
28
- ## Quick Start - Default Implementation
30
+ ## Development Setup
29
31
 
30
- JPie works out of the box with minimal configuration. Here's a complete example of the default implementation:
32
+ This project uses [Overcommit](https://github.com/sds/overcommit) to enforce code quality through Git hooks. After cloning the repository:
33
+
34
+ ### Quick Setup (Recommended)
35
+
36
+ ```bash
37
+ # One command to set up everything
38
+ ./bin/setup-hooks
39
+ ```
40
+
41
+ ### Manual Setup
42
+
43
+ If you prefer to set up manually:
44
+
45
+ ```bash
46
+ # 1. Install dependencies
47
+ bundle install
48
+
49
+ # 2. Install overcommit globally (one-time setup)
50
+ gem install overcommit
51
+
52
+ # 3. Install the Git hooks for this project
53
+ overcommit --install
54
+
55
+ # 4. Sign the configuration (required for security)
56
+ overcommit --sign
57
+ ```
58
+
59
+ ### 3. Automated Quality Checks
60
+
61
+ The following checks run automatically:
62
+
63
+ **Pre-commit hooks:**
64
+ - ✅ **RuboCop** - Code style and quality analysis
65
+ - ✅ **Trailing whitespace** - Prevents whitespace issues
66
+ - ✅ **Merge conflicts** - Catches unresolved conflicts
67
+
68
+ **Pre-push hooks:**
69
+ - ✅ **RSpec** - Full test suite execution
70
+
71
+ ### 4. Manual Quality Checks
72
+
73
+ You can run these checks manually:
74
+
75
+ ```bash
76
+ # Run RuboCop with auto-fix
77
+ bundle exec rubocop -A
78
+
79
+ # Run tests
80
+ bundle exec rspec
81
+
82
+ # Test hooks without committing
83
+ overcommit --run pre-commit
84
+ overcommit --run pre-push
85
+ ```
86
+
87
+ ## Quick Start
88
+
89
+ JPie works out of the box with minimal configuration:
31
90
 
32
91
  ### 1. Create Your Model
33
92
 
@@ -35,6 +94,9 @@ JPie works out of the box with minimal configuration. Here's a complete example
35
94
  class User < ActiveRecord::Base
36
95
  validates :name, presence: true
37
96
  validates :email, presence: true, uniqueness: true
97
+
98
+ has_many :posts, dependent: :destroy
99
+ has_one :profile, dependent: :destroy
38
100
  end
39
101
  ```
40
102
 
@@ -43,6 +105,10 @@ end
43
105
  ```ruby
44
106
  class UserResource < JPie::Resource
45
107
  attributes :name, :email
108
+ meta_attributes :created_at, :updated_at
109
+
110
+ has_many :posts
111
+ has_one :profile
46
112
  end
47
113
  ```
48
114
 
@@ -62,16 +128,28 @@ Rails.application.routes.draw do
62
128
  end
63
129
  ```
64
130
 
65
- That's it! You now have a fully functional JSON:API compliant server.
131
+ That's it! You now have a fully functional JSON:API compliant server with automatic CRUD operations, sorting, includes, and validation.
132
+
133
+ ## 📚 Comprehensive Examples
134
+
135
+ JPie includes a complete set of examples demonstrating all features:
136
+
137
+ - **[🚀 Basic Usage](https://github.com/emilkampp/jpie/blob/main/examples/basic_usage.rb)** - Fundamental setup and configuration
138
+ - **[🔗 Through Associations](https://github.com/emilkampp/jpie/blob/main/examples/through_associations.rb)** - Many-to-many relationships with `:through`
139
+ - **[🎨 Custom Attributes & Meta](https://github.com/emilkampp/jpie/blob/main/examples/custom_attributes_and_meta.rb)** - Custom computed attributes and meta data
140
+ - **[🔄 Polymorphic Associations](https://github.com/emilkampp/jpie/blob/main/examples/polymorphic_associations.rb)** - Complex polymorphic relationships
141
+ - **[🏗️ Single Table Inheritance](https://github.com/emilkampp/jpie/blob/main/examples/single_table_inheritance.rb)** - STI models and resources
142
+ - **[📊 Custom Sorting](https://github.com/emilkampp/jpie/blob/main/examples/custom_sorting.rb)** - Advanced sorting with complex algorithms
143
+ - **[⚠️ Error Handling](https://github.com/emilkampp/jpie/blob/main/examples/error_handling.rb)** - Comprehensive error handling strategies
144
+
145
+ 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
146
 
67
147
  ## Generators
68
148
 
69
- JPie includes a resource generator for quickly creating new resource classes with proper JSON:API structure.
149
+ JPie includes a resource generator for quickly creating new resource classes:
70
150
 
71
151
  ### Basic Usage
72
152
 
73
- The generator uses semantic field definitions that explicitly categorize each field by its JSON:API purpose:
74
-
75
153
  ```bash
76
154
  # Generate a basic resource with semantic syntax
77
155
  rails generate jpie:resource User attribute:name attribute:email meta:created_at
@@ -85,13 +163,10 @@ rails generate jpie:resource User attribute:name email created_at updated_at
85
163
 
86
164
  **Generated file:**
87
165
  ```ruby
88
- # frozen_string_literal: true
89
-
90
166
  class UserResource < JPie::Resource
91
167
  attributes :name, :email
92
-
93
168
  meta_attributes :created_at, :updated_at
94
-
169
+
95
170
  has_many :comments
96
171
  has_one :author
97
172
  end
@@ -99,122 +174,31 @@ end
99
174
 
100
175
  ### Semantic Field Syntax
101
176
 
102
- The generator uses a semantic approach focused on JSON:API concepts rather than database types:
103
-
104
177
  | Syntax | Purpose | Example |
105
178
  |--------|---------|---------|
106
179
  | `attribute:field` | Regular JSON:API attribute | `attribute:name` |
107
180
  | `meta:field` | JSON:API meta attribute | `meta:created_at` |
108
181
  | `has_many:resource` | JSON:API relationship | `has_many:posts` |
109
182
  | `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
-
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
- ```
145
-
146
- ##### Empty Resource Template
147
-
148
- ```bash
149
- rails generate jpie:resource User
150
- ```
151
-
152
- **Generated file:**
153
- ```ruby
154
- # frozen_string_literal: true
155
-
156
- class UserResource < JPie::Resource
157
- # Define your attributes here:
158
- # attributes :name, :email, :title
159
-
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
167
- ```
168
-
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
176
- ```
177
-
178
- This generates the same output as the semantic syntax, with automatic detection of meta attributes based on common field names.
179
183
 
180
184
  ### Generator Options
181
185
 
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` |
186
-
187
- ### Automatic Features
186
+ | Option | Description | Example |
187
+ |--------|-------------|---------|
188
+ | `--model=NAME` | Specify model class | `--model=Person` |
189
+ | `--skip-model` | Skip explicit model declaration | `--skip-model` |
188
190
 
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
191
+ ### Modern DSL
206
192
 
207
193
  ```ruby
208
194
  class UserResource < JPie::Resource
209
- # Attributes (multiple syntaxes supported)
195
+ # Multiple attributes at once
210
196
  attributes :name, :email, :created_at
211
- attribute :full_name
212
197
 
213
- # Meta attributes
198
+ # Meta attributes (for additional data)
214
199
  meta :account_status, :last_login
215
- # or: meta_attributes :account_status, :last_login
216
200
 
217
- # Includes for related data (used with ?include= parameter)
201
+ # Relationships for includes
218
202
  has_many :posts
219
203
  has_one :profile
220
204
 
@@ -222,971 +206,21 @@ class UserResource < JPie::Resource
222
206
  sortable :popularity do |query, direction|
223
207
  query.order(likes_count: direction)
224
208
  end
225
- # or: sortable_by :popularity do |query, direction|
226
209
 
227
- # Custom attribute methods
210
+ # Custom attribute methods (modern approach)
228
211
  private
229
212
 
230
- def full_name
231
- "#{object.first_name} #{object.last_name}"
232
- end
233
-
234
213
  def account_status
235
214
  object.active? ? 'active' : 'inactive'
236
215
  end
237
216
  end
238
217
  ```
239
218
 
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:
307
-
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:
385
-
386
- ```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
442
-
443
- # show, update, destroy still use the automatic implementations
444
- end
445
- ```
446
-
447
- ### Custom Attributes
448
-
449
- Add computed or transformed attributes to your resources using either blocks or method overrides:
450
-
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)
470
-
471
- You can now define custom methods directly on your resource class instead of using blocks:
472
-
473
- ```ruby
474
- class UserResource < JPie::Resource
475
- attributes :name, :email
476
- attribute :full_name
477
- attribute :display_name
478
- meta_attribute :user_stats
479
-
480
- private
481
-
482
- def full_name
483
- "#{object.first_name} #{object.last_name}"
484
- 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
- end
502
- ```
503
-
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
510
-
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:
524
-
525
- ```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
- )
548
- end
549
- end
550
- ```
551
-
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
587
-
588
- def meta
589
- super.merge(
590
- resource_version: '1.0',
591
- timestamp: Time.current.iso8601
592
- )
593
- end
594
- end
595
-
596
- class UserResource < BaseResource
597
- attributes :name, :email
598
- meta_attributes :last_login_at
599
-
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
219
+ See the examples folder for more examples of how to use the DSL to solve various serialization/deserialization scenarios.
618
220
 
619
- Override the default sorting behavior with custom logic:
221
+ ## Contributing
620
222
 
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
- ```
634
-
635
- ### Polymorphic Associations
636
-
637
- JPie supports polymorphic associations for includes. Here's a complete example with comments that can belong to multiple types of commentable resources:
638
-
639
- #### Models with Polymorphic Associations
640
-
641
- ```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'
662
-
663
- validates :title, :body, presence: true
664
- end
665
- ```
666
-
667
- #### Resources for Polymorphic Associations
668
-
669
- ```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
715
-
716
- private
717
-
718
- def comments
719
- object.comments
720
- end
721
-
722
- def author
723
- object.author
724
- end
725
- end
726
- ```
727
-
728
- #### Controllers for Polymorphic Resources
729
-
730
- ```ruby
731
- class CommentsController < ApplicationController
732
- include JPie::Controller
733
-
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
747
-
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
768
- end
769
- ```
770
-
771
- #### Routes for Polymorphic Resources
772
-
773
- ```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]
781
- end
782
-
783
- resources :comments, only: [:show, :update, :destroy]
784
- end
785
- ```
786
-
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
- }
837
- ```
838
-
839
- ### Single Table Inheritance (STI)
840
-
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.
842
-
843
- #### STI Models
844
-
845
- ```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
860
- end
861
- ```
862
-
863
- #### STI Resources
864
-
865
- JPie automatically handles STI type inference and resource inheritance:
866
-
867
- ```ruby
868
- # Base resource
869
- class VehicleResource < JPie::Resource
870
- attributes :name, :brand, :year
871
- meta_attributes :created_at, :updated_at
872
- end
873
-
874
- # STI resources inherit from base resource
875
- class CarResource < VehicleResource
876
- attributes :engine_size # Car-specific attribute
877
- end
878
-
879
- class TruckResource < VehicleResource
880
- attributes :cargo_capacity # Truck-specific attribute
881
- end
882
- ```
883
-
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
938
-
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:
971
-
972
- **1. Database Setup**
973
-
974
- ```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
989
- end
990
- end
991
- ```
992
-
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
1154
-
1155
- Override the default scope method to add authorization:
1156
-
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
- ```
1168
-
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
- ```
223
+ Bug reports and pull requests are welcome on GitHub at https://github.com/emilkampp/jpie.
1190
224
 
1191
225
  ## License
1192
226