jpie 0.3.1 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67c69b10983622f570ced09dfe95788999ebe45bbae24882d047db319204d237
4
- data.tar.gz: 23bf36de8c560f109efbb3fd77b7cf963c90e216efcc1e7df94fac6fdad2c042
3
+ metadata.gz: 815bcf1e307d5c5195cf35302381f44f073638c784e6628f893c1eb9e3cb1762
4
+ data.tar.gz: 0dd7be88db93f112400c8b1750814f875b79c383e19610622f983e6d7b911793
5
5
  SHA512:
6
- metadata.gz: fb36641fc91e5be23485bac73d558ccb1f22bebe3924dc718eddfd2ef52b47ccfd37ef72fd90ea782fd96bb5ce4b5dacf038151464db033f69e52c7c272cd858
7
- data.tar.gz: d70f6b5a1fdb6f17c470823045790ee1a9ec5964e68cfbacee7a8c52fc5a1a81c0de0f54214b4b86646abef6c06738870a7b00bde567481c37c86fd7e87d61d4
6
+ metadata.gz: 2fdbec7a1d41afda8bfce7eb92f7d27feb4a8b73e32ea28a034dc8e1d8a96a8ba5fa7b9ab4396295f669c9a65df46b8f2334c0d1f9cfc93fddffc0085af250cb
7
+ data.tar.gz: 701c7e2a064ceecb89820ffcba6241e9c6daf774b422ced235fe173340403f78229c9385a9560e4a52dae7c2226bf8cad31d54e0068acab1480a787e7dbe12e9
data/CHANGELOG.md CHANGED
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2025-01-25
11
+
12
+ ### Added
13
+ - **Semantic Generator Syntax**: Complete rewrite of resource generator with JSON:API-focused field categorization
14
+ - `attribute:field` - Explicit JSON:API attribute definition
15
+ - `meta:field` - Explicit JSON:API meta attribute definition
16
+ - `has_many:resource` - Shorthand relationship syntax
17
+ - `relationship:type:field` - Explicit relationship syntax
18
+ - **Improved Developer Experience**: More intuitive and semantic generator approach focused on JSON:API concepts rather than database types
19
+
20
+ ### Enhanced
21
+ - **Generator Logic**: Refactored generator into cleaner, more maintainable methods with proper separation of concerns
22
+ - **Backward Compatibility**: Legacy `field:type` syntax fully preserved - existing usage continues to work unchanged
23
+ - **Code Quality**: Fixed all RuboCop violations in generator code with improved method structure
24
+ - **Test Coverage**: Comprehensive test suite covering semantic syntax, legacy compatibility, and all feature combinations
25
+
26
+ ### Improved
27
+ - **Generator Syntax**: Replaced meaningless database types (`name:string`) with semantic JSON:API categorization (`attribute:name`)
28
+ - **Documentation**: README completely updated to showcase new semantic approach with comprehensive examples
29
+ - **Generator Help**: Updated help text and banners to reflect new semantic syntax
30
+
31
+ ### Technical Details
32
+ - Generator automatically categorizes fields based on semantic prefixes
33
+ - Auto-detection of common meta attributes (`created_at`, `updated_at`, etc.) preserved
34
+ - Relationship inference and resource class detection maintained
35
+ - All 373 tests pass with 95.97% coverage maintained
36
+
37
+ ### Migration Guide
38
+ - **New syntax (recommended)**: `rails generate jpie:resource User attribute:name meta:created_at has_many:posts`
39
+ - **Legacy syntax (still works)**: `rails generate jpie:resource User name:string created_at:datetime --relationships=has_many:posts`
40
+ - No breaking changes - existing generators continue to work as before
41
+
10
42
  ## [0.3.1] - 2025-01-24
11
43
 
12
44
  ### Fixed
data/README.md CHANGED
@@ -10,6 +10,7 @@ JPie is a modern, lightweight Rails library for developing JSON:API compliant se
10
10
  ✨ **Modern Rails DSL** - Clean, intuitive syntax following Rails conventions
11
11
  🔧 **Method Overrides** - Define custom attribute methods directly on resource classes
12
12
  🎯 **Smart Inference** - Automatic model and resource class detection
13
+ ⚡ **Powerful Generators** - Scaffold resources with relationships, meta attributes, and automatic inference
13
14
  📊 **Polymorphic Support** - Full support for complex polymorphic associations
14
15
  🔄 **STI Ready** - Single Table Inheritance works out of the box
15
16
  ⚡ **Performance Optimized** - Efficient serialization with intelligent deduplication
@@ -63,6 +64,140 @@ end
63
64
 
64
65
  That's it! You now have a fully functional JSON:API compliant server.
65
66
 
67
+ ## Generators
68
+
69
+ JPie includes a resource generator for quickly creating new resource classes with proper JSON:API structure.
70
+
71
+ ### Basic Usage
72
+
73
+ The generator uses semantic field definitions that explicitly categorize each field by its JSON:API purpose:
74
+
75
+ ```bash
76
+ # Generate a basic resource with semantic syntax
77
+ rails generate jpie:resource User attribute:name attribute:email meta:created_at
78
+
79
+ # Shorthand for relationships
80
+ rails generate jpie:resource Post attribute:title attribute:content has_many:comments has_one:author
81
+
82
+ # Mix explicit categorization with auto-detection
83
+ rails generate jpie:resource User attribute:name email created_at updated_at
84
+ ```
85
+
86
+ **Generated file:**
87
+ ```ruby
88
+ # frozen_string_literal: true
89
+
90
+ class UserResource < JPie::Resource
91
+ attributes :name, :email
92
+
93
+ meta_attributes :created_at, :updated_at
94
+
95
+ has_many :comments
96
+ has_one :author
97
+ end
98
+ ```
99
+
100
+ ### Semantic Field Syntax
101
+
102
+ The generator uses a semantic approach focused on JSON:API concepts rather than database types:
103
+
104
+ | Syntax | Purpose | Example |
105
+ |--------|---------|---------|
106
+ | `attribute:field` | Regular JSON:API attribute | `attribute:name` |
107
+ | `meta:field` | JSON:API meta attribute | `meta:created_at` |
108
+ | `has_many:resource` | JSON:API relationship | `has_many:posts` |
109
+ | `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
+
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` |
186
+
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
+
66
201
  ## Modern DSL Examples
67
202
 
68
203
  JPie provides a clean, modern DSL that follows Rails conventions:
@@ -79,7 +214,7 @@ class UserResource < JPie::Resource
79
214
  meta :account_status, :last_login
80
215
  # or: meta_attributes :account_status, :last_login
81
216
 
82
- # Relationships
217
+ # Includes for related data (used with ?include= parameter)
83
218
  has_many :posts
84
219
  has_one :profile
85
220
 
@@ -119,7 +254,8 @@ class UsersController < ApplicationController
119
254
  end
120
255
 
121
256
  def create
122
- user = User.new(deserialize_params)
257
+ attributes = deserialize_params
258
+ user = model_class.new(attributes)
123
259
  user.created_by = current_user
124
260
  user.save!
125
261
 
@@ -203,6 +339,42 @@ Content-Type: application/vnd.api+json
203
339
  }
204
340
  ```
205
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
+
206
378
  ## Customization and Overrides
207
379
 
208
380
  Once you have the basic implementation working, you can customize JPie's behavior as needed:
@@ -261,7 +433,7 @@ class UsersController < ApplicationController
261
433
  # Override create to add custom logic
262
434
  def create
263
435
  attributes = deserialize_params
264
- user = User.new(attributes)
436
+ user = model_class.new(attributes)
265
437
  user.created_by = current_user
266
438
  user.save!
267
439
 
@@ -462,7 +634,7 @@ end
462
634
 
463
635
  ### Polymorphic Associations
464
636
 
465
- JPie supports polymorphic associations seamlessly. Here's a complete example with comments that can belong to multiple types of commentable resources:
637
+ JPie supports polymorphic associations for includes. Here's a complete example with comments that can belong to multiple types of commentable resources:
466
638
 
467
639
  #### Models with Polymorphic Associations
468
640
 
@@ -495,53 +667,60 @@ end
495
667
  #### Resources for Polymorphic Associations
496
668
 
497
669
  ```ruby
498
- # Comment resource with belongs_to polymorphic relationship
670
+ # Comment resource
499
671
  class CommentResource < JPie::Resource
500
672
  attributes :content, :created_at
501
673
 
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
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
513
682
  end
514
683
 
515
- relationship :author do
516
- UserResource.new(object.author, context) if object.author
684
+ def author
685
+ object.author
517
686
  end
518
687
  end
519
688
 
520
- # Post resource with has_many polymorphic relationship
689
+ # Post resource
521
690
  class PostResource < JPie::Resource
522
691
  attributes :title, :content, :published_at
523
692
 
524
- # Has_many polymorphic relationship
525
- relationship :comments do
526
- object.comments.map { |comment| CommentResource.new(comment, context) }
693
+ # Define methods for includes
694
+ has_many :comments
695
+ has_one :author
696
+
697
+ private
698
+
699
+ def comments
700
+ object.comments
527
701
  end
528
702
 
529
- relationship :author do
530
- UserResource.new(object.author, context) if object.author
703
+ def author
704
+ object.author
531
705
  end
532
706
  end
533
707
 
534
- # Article resource with has_many polymorphic relationship
708
+ # Article resource
535
709
  class ArticleResource < JPie::Resource
536
710
  attributes :title, :body, :published_at
537
711
 
538
- # Has_many polymorphic relationship
539
- relationship :comments do
540
- object.comments.map { |comment| CommentResource.new(comment, context) }
712
+ # Define methods for includes
713
+ has_many :comments
714
+ has_one :author
715
+
716
+ private
717
+
718
+ def comments
719
+ object.comments
541
720
  end
542
721
 
543
- relationship :author do
544
- UserResource.new(object.author, context) if object.author
722
+ def author
723
+ object.author
545
724
  end
546
725
  end
547
726
  ```
@@ -561,7 +740,7 @@ class CommentsController < ApplicationController
561
740
  comment.author = current_user
562
741
  comment.save!
563
742
 
564
- render_jsonapi_resource(comment, status: :created)
743
+ render_jsonapi(comment, status: :created)
565
744
  end
566
745
 
567
746
  private
@@ -618,14 +797,6 @@ end
618
797
  "title": "My First Post",
619
798
  "content": "This is the content of my first post.",
620
799
  "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
800
  }
630
801
  },
631
802
  "included": [
@@ -635,14 +806,6 @@ end
635
806
  "attributes": {
636
807
  "content": "Great post!",
637
808
  "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
809
  }
647
810
  },
648
811
  {
@@ -651,14 +814,22 @@ end
651
814
  "attributes": {
652
815
  "content": "Thanks for sharing!",
653
816
  "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
- }
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"
662
833
  }
663
834
  }
664
835
  ]
@@ -794,16 +965,6 @@ TruckResource.scope # Returns only Truck records
794
965
  VehicleResource.scope # Returns all Vehicle records (including STI subclasses)
795
966
  ```
796
967
 
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
968
  #### Complete STI Example
808
969
 
809
970
  Here's a complete example showing STI in action with HTTP requests and responses:
@@ -5,11 +5,17 @@ require 'rails/generators/base'
5
5
  module JPie
6
6
  module Generators
7
7
  class ResourceGenerator < Rails::Generators::NamedBase
8
- desc 'Generate a JPie resource class'
8
+ source_root File.expand_path('templates', __dir__)
9
9
 
10
- argument :attributes, type: :array, default: [], banner: 'field:type field:type'
10
+ desc 'Generate a JPie resource class with semantic field definitions'
11
11
 
12
- class_option :model, type: :string, desc: 'Model class to associate with this resource'
12
+ argument :field_definitions, type: :array, default: [],
13
+ banner: 'attribute:field meta:field relationship:type:field'
14
+
15
+ class_option :model, type: :string,
16
+ desc: 'Model class to associate with this resource (defaults to inferred model)'
17
+ class_option :skip_model, type: :boolean, default: false,
18
+ desc: 'Skip explicit model declaration (use automatic inference)'
13
19
 
14
20
  def create_resource_file
15
21
  template 'resource.rb.erb', File.join('app/resources', "#{file_name}_resource.rb")
@@ -21,18 +27,89 @@ module JPie
21
27
  options[:model] || class_name
22
28
  end
23
29
 
30
+ def needs_explicit_model?
31
+ # Only need explicit model declaration if:
32
+ # 1. User explicitly provided a different model name, OR
33
+ # 2. User didn't use --skip-model flag AND the model name differs from the inferred name
34
+ return false if options[:skip_model]
35
+ return true if options[:model] && options[:model] != class_name
36
+
37
+ # For standard naming (UserResource -> User), we can skip the explicit declaration
38
+ false
39
+ end
40
+
24
41
  def resource_attributes
25
- return [] if attributes.empty?
42
+ parse_field_definitions.fetch(:attributes, [])
43
+ end
44
+
45
+ def meta_attributes_list
46
+ parse_field_definitions.fetch(:meta_attributes, [])
47
+ end
48
+
49
+ def relationships_list
50
+ parse_field_definitions.fetch(:relationships, [])
51
+ end
52
+
53
+ def parse_relationships
54
+ relationships_list.map do |rel|
55
+ # rel is already a hash with :type and :name from parse_field_definitions
56
+ rel
57
+ end
58
+ end
59
+
60
+ def parse_field_definitions
61
+ return @parsed_definitions if @parsed_definitions
62
+
63
+ @parsed_definitions = { attributes: [], meta_attributes: [], relationships: [] }
64
+
65
+ field_definitions.each do |definition|
66
+ process_field_definition(definition)
67
+ end
68
+
69
+ @parsed_definitions
70
+ end
71
+
72
+ def process_field_definition(definition)
73
+ case definition
74
+ when /^attribute:(.+)$/
75
+ @parsed_definitions[:attributes] << ::Regexp.last_match(1)
76
+ when /^meta:(.+)$/
77
+ @parsed_definitions[:meta_attributes] << ::Regexp.last_match(1)
78
+ when /^relationship:(.+):(.+)$/, /^(has_many|has_one|belongs_to):(.+)$/
79
+ add_relationship(::Regexp.last_match(1), ::Regexp.last_match(2))
80
+ when /^(.+):(.+)$/
81
+ process_legacy_field(::Regexp.last_match(1))
82
+ else
83
+ process_plain_field(definition)
84
+ end
85
+ end
86
+
87
+ def add_relationship(type, name)
88
+ @parsed_definitions[:relationships] << { type: type, name: name }
89
+ end
26
90
 
27
- attributes.map(&:name)
91
+ def process_legacy_field(field_name)
92
+ # Legacy support: field:type format - treat as attribute and ignore type
93
+ if meta_attribute_name?(field_name)
94
+ @parsed_definitions[:meta_attributes] << field_name
95
+ else
96
+ @parsed_definitions[:attributes] << field_name
97
+ end
28
98
  end
29
99
 
30
- def template_path
31
- File.expand_path('templates', __dir__)
100
+ def process_plain_field(field_name)
101
+ # Plain field name - treat as attribute
102
+ if meta_attribute_name?(field_name)
103
+ @parsed_definitions[:meta_attributes] << field_name
104
+ else
105
+ @parsed_definitions[:attributes] << field_name
106
+ end
32
107
  end
33
108
 
34
- def source_root
35
- template_path
109
+ def meta_attribute_name?(name)
110
+ # Check if field name suggests it's a meta attribute
111
+ meta_names = %w[created_at updated_at deleted_at published_at archived_at]
112
+ meta_names.include?(name)
36
113
  end
37
114
  end
38
115
  end
@@ -1,12 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class <%= class_name %>Resource < JPie::Resource
4
+ <% if needs_explicit_model? -%>
4
5
  model <%= model_class_name %>
6
+ <% end -%>
5
7
 
6
8
  <% if resource_attributes.any? -%>
7
9
  attributes <%= resource_attributes.map { |attr| ":#{attr}" }.join(', ') %>
8
10
  <% else -%>
9
11
  # Define your attributes here:
10
- # attributes :name, :email, :created_at
12
+ # attributes :name, :email, :title
13
+ <% end -%>
14
+
15
+ <% if meta_attributes_list.any? -%>
16
+ meta_attributes <%= meta_attributes_list.map { |attr| ":#{attr}" }.join(', ') %>
17
+ <% else -%>
18
+ # Define your meta attributes here:
19
+ # meta_attributes :created_at, :updated_at
20
+ <% end -%>
21
+
22
+ <% if parse_relationships.any? -%>
23
+ <% parse_relationships.each do |rel| -%>
24
+ <%= rel[:type] %> :<%= rel[:name] %>
25
+ <% end -%>
26
+ <% else -%>
27
+ # Define your relationships here:
28
+ # has_many :posts
29
+ # has_one :user
11
30
  <% end -%>
12
31
  end
data/lib/jpie/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JPie
4
- VERSION = '0.3.1'
4
+ VERSION = '0.4.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jpie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Kampp