jpie 0.4.5 → 1.0.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.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/release.mdc +62 -0
  3. data/.gitignore +26 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +76 -107
  6. data/.travis.yml +7 -0
  7. data/Gemfile +23 -0
  8. data/Gemfile.lock +321 -0
  9. data/README.md +1508 -136
  10. data/Rakefile +3 -14
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/jpie.gemspec +21 -38
  14. data/kiln/app/resources/user_message_resource.rb +4 -0
  15. data/lib/jpie.rb +3 -25
  16. data/lib/json_api/active_storage/deserialization.rb +116 -0
  17. data/lib/json_api/active_storage/detection.rb +69 -0
  18. data/lib/json_api/active_storage/serialization.rb +34 -0
  19. data/lib/json_api/configuration.rb +57 -0
  20. data/lib/json_api/controllers/base_controller.rb +26 -0
  21. data/lib/json_api/controllers/concerns/controller_helpers/authorization.rb +30 -0
  22. data/lib/json_api/controllers/concerns/controller_helpers/document_meta.rb +20 -0
  23. data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +64 -0
  24. data/lib/json_api/controllers/concerns/controller_helpers/parsing.rb +127 -0
  25. data/lib/json_api/controllers/concerns/controller_helpers/resource_setup.rb +38 -0
  26. data/lib/json_api/controllers/concerns/controller_helpers.rb +19 -0
  27. data/lib/json_api/controllers/concerns/relationships/active_storage_removal.rb +65 -0
  28. data/lib/json_api/controllers/concerns/relationships/events.rb +44 -0
  29. data/lib/json_api/controllers/concerns/relationships/removal.rb +92 -0
  30. data/lib/json_api/controllers/concerns/relationships/response_helpers.rb +55 -0
  31. data/lib/json_api/controllers/concerns/relationships/serialization.rb +72 -0
  32. data/lib/json_api/controllers/concerns/relationships/sorting.rb +114 -0
  33. data/lib/json_api/controllers/concerns/relationships/updating.rb +73 -0
  34. data/lib/json_api/controllers/concerns/relationships_controller/active_storage_removal.rb +67 -0
  35. data/lib/json_api/controllers/concerns/relationships_controller/events.rb +44 -0
  36. data/lib/json_api/controllers/concerns/relationships_controller/removal.rb +92 -0
  37. data/lib/json_api/controllers/concerns/relationships_controller/response_helpers.rb +55 -0
  38. data/lib/json_api/controllers/concerns/relationships_controller/serialization.rb +72 -0
  39. data/lib/json_api/controllers/concerns/relationships_controller/sorting.rb +114 -0
  40. data/lib/json_api/controllers/concerns/relationships_controller/updating.rb +73 -0
  41. data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +93 -0
  42. data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +114 -0
  43. data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +91 -0
  44. data/lib/json_api/controllers/concerns/resource_actions/pagination.rb +51 -0
  45. data/lib/json_api/controllers/concerns/resource_actions/preloading.rb +64 -0
  46. data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +71 -0
  47. data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +63 -0
  48. data/lib/json_api/controllers/concerns/resource_actions/type_validation.rb +75 -0
  49. data/lib/json_api/controllers/concerns/resource_actions.rb +106 -0
  50. data/lib/json_api/controllers/relationships_controller.rb +108 -0
  51. data/lib/json_api/controllers/resources_controller.rb +6 -0
  52. data/lib/json_api/errors/parameter_not_allowed.rb +19 -0
  53. data/lib/json_api/railtie.rb +112 -0
  54. data/lib/json_api/resources/active_storage_blob_resource.rb +19 -0
  55. data/lib/json_api/resources/concerns/attributes_dsl.rb +69 -0
  56. data/lib/json_api/resources/concerns/filters_dsl.rb +32 -0
  57. data/lib/json_api/resources/concerns/meta_dsl.rb +23 -0
  58. data/lib/json_api/resources/concerns/model_class_helpers.rb +37 -0
  59. data/lib/json_api/resources/concerns/relationships_dsl.rb +71 -0
  60. data/lib/json_api/resources/concerns/sortable_fields_dsl.rb +36 -0
  61. data/lib/json_api/resources/resource.rb +32 -0
  62. data/lib/json_api/resources/resource_loader.rb +35 -0
  63. data/lib/json_api/routing.rb +81 -0
  64. data/lib/json_api/serialization/concerns/attributes_deserialization.rb +27 -0
  65. data/lib/json_api/serialization/concerns/attributes_serialization.rb +50 -0
  66. data/lib/json_api/serialization/concerns/deserialization_helpers.rb +115 -0
  67. data/lib/json_api/serialization/concerns/includes_serialization.rb +82 -0
  68. data/lib/json_api/serialization/concerns/links_serialization.rb +33 -0
  69. data/lib/json_api/serialization/concerns/meta_serialization.rb +60 -0
  70. data/lib/json_api/serialization/concerns/model_attributes_transformation.rb +69 -0
  71. data/lib/json_api/serialization/concerns/relationship_processing.rb +119 -0
  72. data/lib/json_api/serialization/concerns/relationships_deserialization.rb +47 -0
  73. data/lib/json_api/serialization/concerns/relationships_serialization.rb +81 -0
  74. data/lib/json_api/serialization/deserializer.rb +26 -0
  75. data/lib/json_api/serialization/serializer.rb +77 -0
  76. data/lib/json_api/support/active_storage_support.rb +82 -0
  77. data/lib/json_api/support/collection_query.rb +50 -0
  78. data/lib/json_api/support/concerns/condition_building.rb +57 -0
  79. data/lib/json_api/support/concerns/nested_filters.rb +130 -0
  80. data/lib/json_api/support/concerns/pagination.rb +30 -0
  81. data/lib/json_api/support/concerns/polymorphic_filters.rb +75 -0
  82. data/lib/json_api/support/concerns/regular_filters.rb +81 -0
  83. data/lib/json_api/support/concerns/sorting.rb +88 -0
  84. data/lib/json_api/support/instrumentation.rb +43 -0
  85. data/lib/json_api/support/param_helpers.rb +54 -0
  86. data/lib/json_api/support/relationship_guard.rb +16 -0
  87. data/lib/json_api/support/relationship_helpers.rb +76 -0
  88. data/lib/json_api/support/resource_identifier.rb +87 -0
  89. data/lib/json_api/support/responders.rb +100 -0
  90. data/lib/json_api/support/response_helpers.rb +10 -0
  91. data/lib/json_api/support/sort_parsing.rb +21 -0
  92. data/lib/json_api/support/type_conversion.rb +21 -0
  93. data/lib/json_api/testing/test_helper.rb +76 -0
  94. data/lib/json_api/testing.rb +3 -0
  95. data/lib/{jpie → json_api}/version.rb +2 -2
  96. data/lib/json_api.rb +50 -0
  97. data/lib/rubocop/cop/custom/hash_value_omission.rb +53 -0
  98. metadata +100 -169
  99. data/.cursor/rules/dependencies.mdc +0 -19
  100. data/.cursor/rules/examples.mdc +0 -16
  101. data/.cursor/rules/git.mdc +0 -14
  102. data/.cursor/rules/project_structure.mdc +0 -30
  103. data/.cursor/rules/publish_gem.mdc +0 -73
  104. data/.cursor/rules/security.mdc +0 -14
  105. data/.cursor/rules/style.mdc +0 -15
  106. data/.cursor/rules/testing.mdc +0 -16
  107. data/.overcommit.yml +0 -35
  108. data/CHANGELOG.md +0 -164
  109. data/LICENSE.txt +0 -21
  110. data/PUBLISHING.md +0 -111
  111. data/examples/basic_example.md +0 -146
  112. data/examples/including_related_resources.md +0 -491
  113. data/examples/pagination.md +0 -303
  114. data/examples/relationships.md +0 -114
  115. data/examples/resource_attribute_configuration.md +0 -147
  116. data/examples/resource_meta_configuration.md +0 -244
  117. data/examples/rspec_testing.md +0 -130
  118. data/examples/single_table_inheritance.md +0 -160
  119. data/lib/jpie/configuration.rb +0 -12
  120. data/lib/jpie/controller/crud_actions.rb +0 -141
  121. data/lib/jpie/controller/error_handling/handler_setup.rb +0 -124
  122. data/lib/jpie/controller/error_handling/handlers.rb +0 -109
  123. data/lib/jpie/controller/error_handling.rb +0 -23
  124. data/lib/jpie/controller/json_api_validation.rb +0 -193
  125. data/lib/jpie/controller/parameter_parsing.rb +0 -78
  126. data/lib/jpie/controller/related_actions.rb +0 -45
  127. data/lib/jpie/controller/relationship_actions.rb +0 -291
  128. data/lib/jpie/controller/relationship_validation.rb +0 -117
  129. data/lib/jpie/controller/rendering.rb +0 -154
  130. data/lib/jpie/controller.rb +0 -45
  131. data/lib/jpie/deserializer.rb +0 -110
  132. data/lib/jpie/errors.rb +0 -117
  133. data/lib/jpie/generators/resource_generator.rb +0 -116
  134. data/lib/jpie/generators/templates/resource.rb.erb +0 -31
  135. data/lib/jpie/railtie.rb +0 -42
  136. data/lib/jpie/resource/attributable.rb +0 -112
  137. data/lib/jpie/resource/inferrable.rb +0 -43
  138. data/lib/jpie/resource/sortable.rb +0 -93
  139. data/lib/jpie/resource.rb +0 -147
  140. data/lib/jpie/routing.rb +0 -59
  141. data/lib/jpie/serializer.rb +0 -205
@@ -1,291 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'relationship_validation'
4
-
5
- module JPie
6
- module Controller
7
- module RelationshipActions
8
- extend ActiveSupport::Concern
9
- include RelationshipValidation
10
-
11
- # GET /resources/:id/relationships/:relationship_name
12
- # Returns relationship linkage data
13
- def relationship_show
14
- validate_relationship_exists
15
- resource = find_resource
16
- relationship_data = get_relationship_data(resource)
17
- render_relationship_data(relationship_data)
18
- end
19
-
20
- # PATCH /resources/:id/relationships/:relationship_name
21
- # Updates relationship linkage (replaces all relationships)
22
- def relationship_update
23
- validate_relationship_exists
24
- validate_relationship_update_request
25
- resource = find_resource
26
- relationship_data = parse_relationship_data
27
- update_relationship_data(resource, relationship_data)
28
- render_relationship_data(get_relationship_data(resource))
29
- end
30
-
31
- # POST /resources/:id/relationships/:relationship_name
32
- # Adds to relationship linkage (for to-many relationships)
33
- def relationship_create
34
- validate_relationship_exists
35
- validate_relationship_update_request
36
- resource = find_resource
37
-
38
- unless relationship_is_to_many?
39
- raise JPie::Errors::BadRequestError.new(
40
- detail: 'POST is only supported for to-many relationships'
41
- )
42
- end
43
-
44
- relationship_data = parse_relationship_data
45
-
46
- unless relationship_data.is_a?(Array)
47
- raise JPie::Errors::BadRequestError.new(
48
- detail: 'Adding to relationships requires an array of resource identifier objects'
49
- )
50
- end
51
-
52
- add_to_relationship(resource, relationship_data)
53
- render_relationship_data(get_relationship_data(resource))
54
- end
55
-
56
- # DELETE /resources/:id/relationships/:relationship_name
57
- # Removes from relationship linkage (for to-many relationships)
58
- def relationship_destroy
59
- validate_relationship_exists
60
- validate_relationship_update_request
61
- resource = find_resource
62
-
63
- unless relationship_is_to_many?
64
- raise JPie::Errors::BadRequestError.new(
65
- detail: 'DELETE is only supported for to-many relationships'
66
- )
67
- end
68
-
69
- relationship_data = parse_relationship_data
70
-
71
- unless relationship_data.is_a?(Array)
72
- raise JPie::Errors::BadRequestError.new(
73
- detail: 'Removing from relationships requires an array of resource identifier objects'
74
- )
75
- end
76
-
77
- remove_from_relationship(resource, relationship_data)
78
- render_relationship_data(get_relationship_data(resource))
79
- end
80
-
81
- private
82
-
83
- def find_resource
84
- resource_class.scope(context).find(params[:id])
85
- end
86
-
87
- def relationship_name
88
- @relationship_name ||= params[:relationship_name].to_sym
89
- end
90
-
91
- def relationship_config
92
- @relationship_config ||= resource_class._relationships[relationship_name]
93
- end
94
-
95
- def get_relationship_data(resource)
96
- relationship_method = relationship_name
97
- related_objects = resource.send(relationship_method)
98
-
99
- if related_objects.respond_to?(:each)
100
- # to-many relationship
101
- related_objects.map { |obj| { type: infer_type(obj), id: obj.id.to_s } }
102
- elsif related_objects
103
- # to-one relationship
104
- { type: infer_type(related_objects), id: related_objects.id.to_s }
105
- end
106
- end
107
-
108
- def parse_relationship_data
109
- body = request.body.read
110
- request.body.rewind
111
- parsed_body = JSON.parse(body)
112
-
113
- unless parsed_body.key?('data')
114
- raise JPie::Errors::BadRequestError.new(
115
- detail: 'Request must include a "data" member'
116
- )
117
- end
118
-
119
- parsed_body['data']
120
- end
121
-
122
- def update_relationship_data(resource, relationship_data)
123
- if relationship_data.nil?
124
- # Set relationship to null (only valid for to-one relationships)
125
- unless relationship_is_to_many?
126
- clear_relationship(resource)
127
- else
128
- raise JPie::Errors::BadRequestError.new(
129
- detail: 'Cannot set a to-many relationship to null'
130
- )
131
- end
132
- elsif relationship_data.is_a?(Array)
133
- # to-many relationship - replace all
134
- if relationship_is_to_many?
135
- replace_to_many_relationship(resource, relationship_data)
136
- else
137
- raise JPie::Errors::BadRequestError.new(
138
- detail: 'Invalid data type for to-one relationship'
139
- )
140
- end
141
- elsif relationship_data.is_a?(Hash)
142
- # to-one relationship - replace
143
- unless relationship_is_to_many?
144
- replace_to_one_relationship(resource, relationship_data)
145
- else
146
- raise JPie::Errors::BadRequestError.new(
147
- detail: 'Invalid data type for to-many relationship'
148
- )
149
- end
150
- else
151
- raise JPie::Errors::BadRequestError.new(
152
- detail: 'Relationship data must be null, an object, or an array of objects'
153
- )
154
- end
155
- end
156
-
157
- def add_to_relationship(resource, relationship_data)
158
- begin
159
- related_objects = find_related_objects(relationship_data)
160
- association = association_for_resource(resource)
161
-
162
- related_objects.each do |related_object|
163
- association << related_object unless association.include?(related_object)
164
- end
165
-
166
- resource.save!
167
- rescue ActiveRecord::AssociationTypeMismatch => e
168
- raise JPie::Errors::NotFoundError.new(
169
- detail: "Related resource not found: Invalid resource type for relationship"
170
- )
171
- rescue ActiveRecord::RecordInvalid => e
172
- raise JPie::Errors::ValidationError.new(
173
- detail: "Failed to add relationships: #{e.message}"
174
- )
175
- end
176
- end
177
-
178
- def remove_from_relationship(resource, relationship_data)
179
- begin
180
- related_objects = find_related_objects(relationship_data)
181
- association = association_for_resource(resource)
182
-
183
- related_objects.each do |related_object|
184
- association.delete(related_object)
185
- end
186
-
187
- resource.save!
188
- rescue ActiveRecord::AssociationTypeMismatch => e
189
- raise JPie::Errors::NotFoundError.new(
190
- detail: "Related resource not found: Invalid resource type for relationship"
191
- )
192
- rescue ActiveRecord::RecordInvalid => e
193
- raise JPie::Errors::ValidationError.new(
194
- detail: "Failed to remove relationships: #{e.message}"
195
- )
196
- end
197
- end
198
-
199
- def clear_relationship(resource)
200
- association_name = association_name_for_relationship
201
- resource.send("#{association_name}=", nil)
202
- resource.save!
203
- rescue ActiveRecord::RecordInvalid => e
204
- raise JPie::Errors::ValidationError.new(
205
- detail: "Failed to clear relationship: #{e.message}"
206
- )
207
- end
208
-
209
- def replace_to_many_relationship(resource, relationship_data)
210
- begin
211
- related_objects = find_related_objects(relationship_data)
212
- association_name = association_name_for_relationship
213
- resource.send("#{association_name}=", related_objects)
214
- resource.save!
215
- rescue ActiveRecord::AssociationTypeMismatch => e
216
- raise JPie::Errors::NotFoundError.new(
217
- detail: "Related resource not found: Invalid resource type for relationship"
218
- )
219
- rescue ActiveRecord::RecordInvalid => e
220
- raise JPie::Errors::ValidationError.new(
221
- detail: "Failed to replace relationships: #{e.message}"
222
- )
223
- end
224
- end
225
-
226
- def replace_to_one_relationship(resource, relationship_data)
227
- begin
228
- related_object = find_related_object(relationship_data)
229
- association_name = association_name_for_relationship
230
- resource.send("#{association_name}=", related_object)
231
- resource.save!
232
- rescue ActiveRecord::AssociationTypeMismatch => e
233
- raise JPie::Errors::NotFoundError.new(
234
- detail: "Related resource not found: Invalid resource type for relationship"
235
- )
236
- rescue ActiveRecord::RecordInvalid => e
237
- raise JPie::Errors::ValidationError.new(
238
- detail: "Failed to replace relationship: #{e.message}"
239
- )
240
- end
241
- end
242
-
243
- def find_related_objects(relationship_data)
244
- relationship_data.map { |data| find_related_object(data) }
245
- end
246
-
247
- def find_related_object(resource_identifier)
248
- validate_resource_identifier(resource_identifier)
249
-
250
- type = resource_identifier['type']
251
- id = resource_identifier['id']
252
-
253
- related_model_class = infer_model_class_from_type(type)
254
- related_model_class.find(id)
255
- rescue ActiveRecord::RecordNotFound
256
- raise JPie::Errors::NotFoundError.new(
257
- detail: "Related resource not found: #{type}##{id}"
258
- )
259
- end
260
-
261
- def association_for_resource(resource)
262
- association_name = association_name_for_relationship
263
- resource.send(association_name)
264
- end
265
-
266
- def association_name_for_relationship
267
- # For now, assume the relationship name matches the association name
268
- # This could be made more sophisticated to handle custom association names
269
- relationship_name
270
- end
271
-
272
- def infer_type(object)
273
- # Convert model class name to JSON:API type
274
- # e.g., "User" -> "users", "BlogPost" -> "blog-posts"
275
- object.class.name.underscore.dasherize.pluralize
276
- end
277
-
278
- def infer_model_class_from_type(type)
279
- # Convert JSON:API type back to model class
280
- # e.g., "users" -> User, "blog-posts" -> BlogPost
281
- class_name = type.singularize.underscore.camelize
282
- class_name.constantize
283
- end
284
-
285
- def render_relationship_data(relationship_data)
286
- response_data = { data: relationship_data }
287
- render json: response_data, content_type: 'application/vnd.api+json'
288
- end
289
- end
290
- end
291
- end
@@ -1,117 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module JPie
4
- module Controller
5
- module RelationshipValidation
6
- private
7
-
8
- def validate_relationship_exists
9
- relationship_name = params[:relationship_name]
10
- return unless relationship_name # Skip validation if no relationship_name param
11
-
12
- return if resource_class._relationships.key?(relationship_name.to_sym)
13
-
14
- raise JPie::Errors::NotFoundError.new(
15
- detail: "Relationship '#{relationship_name}' does not exist for #{resource_class.name}"
16
- )
17
- end
18
-
19
- def validate_relationship_update_request
20
- validate_content_type
21
- validate_request_body
22
- validate_relationship_type
23
- end
24
-
25
- def validate_content_type
26
- # Only validate content type for write operations
27
- return unless request.post? || request.patch? || request.put?
28
-
29
- content_type = request.content_type
30
- return if content_type&.include?('application/vnd.api+json')
31
-
32
- raise JPie::Errors::InvalidJsonApiRequestError.new(
33
- detail: 'Content-Type must be application/vnd.api+json for JSON:API requests'
34
- )
35
- end
36
-
37
- def validate_request_body
38
- body = request.body.read
39
- request.body.rewind
40
-
41
- raise JPie::Errors::BadRequestError.new(detail: 'Request body cannot be empty') if body.blank?
42
-
43
- JSON.parse(body)
44
- rescue JSON::ParserError => e
45
- raise JPie::Errors::BadRequestError.new(detail: "Invalid JSON: #{e.message}")
46
- end
47
-
48
- def validate_resource_identifier(resource_identifier)
49
- unless resource_identifier.is_a?(Hash) &&
50
- resource_identifier.key?('type') &&
51
- resource_identifier.key?('id')
52
- raise JPie::Errors::BadRequestError.new(
53
- detail: 'Resource identifier objects must have "type" and "id" members'
54
- )
55
- end
56
-
57
- type = resource_identifier['type']
58
- id = resource_identifier['id']
59
-
60
- unless type.is_a?(String) && id.is_a?(String)
61
- raise JPie::Errors::BadRequestError.new(
62
- detail: 'Resource identifier object members must be strings'
63
- )
64
- end
65
-
66
- if type.empty? || id.empty?
67
- raise JPie::Errors::BadRequestError.new(
68
- detail: 'Resource identifier object members cannot be empty strings'
69
- )
70
- end
71
- end
72
-
73
- def validate_relationship_type
74
- validate_relationship_exists
75
- data = parse_relationship_data
76
-
77
- if relationship_is_to_many?
78
- validate_to_many_relationship_data(data)
79
- else
80
- validate_to_one_relationship_data(data)
81
- end
82
- end
83
-
84
- def validate_to_many_relationship_data(data)
85
- if data.nil?
86
- raise JPie::Errors::BadRequestError.new(
87
- detail: 'Cannot set a to-many relationship to null'
88
- )
89
- end
90
-
91
- unless data.is_a?(Array)
92
- raise JPie::Errors::BadRequestError.new(
93
- detail: 'The value of data must be an array for to-many relationships'
94
- )
95
- end
96
-
97
- data.each { |identifier| validate_resource_identifier(identifier) }
98
- end
99
-
100
- def validate_to_one_relationship_data(data)
101
- unless data.nil? || data.is_a?(Hash)
102
- raise JPie::Errors::BadRequestError.new(
103
- detail: 'The value of data must be a single resource identifier object or null for to-one relationships'
104
- )
105
- end
106
-
107
- validate_resource_identifier(data) if data
108
- end
109
-
110
- def relationship_is_to_many?
111
- return false unless relationship_config
112
-
113
- relationship_config[:type] == :has_many
114
- end
115
- end
116
- end
117
- end
@@ -1,154 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module JPie
4
- module Controller
5
- module Rendering
6
- def resource_class
7
- # Default implementation that infers from controller name
8
- @resource_class ||= infer_resource_class
9
- end
10
-
11
- def serializer
12
- @serializer ||= JPie::Serializer.new(resource_class)
13
- end
14
-
15
- def deserializer
16
- @deserializer ||= JPie::Deserializer.new(resource_class)
17
- end
18
-
19
- protected
20
-
21
- def model_class
22
- resource_class.model
23
- end
24
-
25
- # More concise method names following Rails conventions
26
- def render_jsonapi(resource_or_resources, status: :ok, meta: nil, pagination: nil, original_scope: nil)
27
- includes = parse_include_params
28
- json_data = serializer.serialize(resource_or_resources, context, includes: includes)
29
-
30
- # Add pagination metadata and links if pagination is provided and valid
31
- if pagination && pagination[:per_page]
32
- add_pagination_metadata(json_data, resource_or_resources, pagination, original_scope)
33
- end
34
-
35
- json_data[:meta] = meta if meta
36
-
37
- render json: json_data, status:, content_type: 'application/vnd.api+json'
38
- end
39
-
40
- # Keep original methods for backward compatibility
41
- alias render_jsonapi_resource render_jsonapi
42
- alias render_jsonapi_resources render_jsonapi
43
-
44
- private
45
-
46
- def add_pagination_metadata(json_data, resources, pagination, original_scope)
47
- page = pagination[:page] || 1
48
- per_page = pagination[:per_page]
49
-
50
- # Get total count from the original scope before pagination
51
- total_count = get_total_count(resources, original_scope)
52
- total_pages = (total_count.to_f / per_page).ceil
53
-
54
- # Add pagination metadata
55
- json_data[:meta] ||= {}
56
- json_data[:meta][:pagination] = {
57
- page: page,
58
- per_page: per_page,
59
- total_pages: total_pages,
60
- total_count: total_count
61
- }
62
-
63
- # Add pagination links
64
- json_data[:links] = build_pagination_links(page, per_page, total_pages)
65
- end
66
-
67
- def get_total_count(resources, original_scope)
68
- # Use original scope if provided, otherwise fall back to resources
69
- scope_to_count = original_scope || resources
70
-
71
- # If scope is an ActiveRecord relation, get the count
72
- # Otherwise, if it's an array, get the length
73
- if scope_to_count.respond_to?(:count) && !scope_to_count.loaded?
74
- scope_to_count.count
75
- elsif scope_to_count.respond_to?(:size)
76
- scope_to_count.size
77
- else
78
- 0
79
- end
80
- end
81
-
82
- def build_pagination_links(page, per_page, total_pages)
83
- url_components = extract_url_components
84
- pagination_data = { page: page, per_page: per_page, total_pages: total_pages }
85
-
86
- links = build_base_pagination_links(url_components, pagination_data)
87
- add_conditional_pagination_links(links, url_components, pagination_data)
88
-
89
- links
90
- end
91
-
92
- def extract_url_components
93
- base_url = request.respond_to?(:base_url) ? request.base_url : 'http://example.com'
94
- path = request.respond_to?(:path) ? request.path : '/resources'
95
- query_params = request.respond_to?(:query_parameters) ? request.query_parameters.except('page') : {}
96
-
97
- { base_url: base_url, path: path, query_params: query_params }
98
- end
99
-
100
- def build_base_pagination_links(url_components, pagination_data)
101
- full_url = url_components[:base_url] + url_components[:path]
102
- query_params = url_components[:query_params]
103
- page = pagination_data[:page]
104
- per_page = pagination_data[:per_page]
105
- total_pages = pagination_data[:total_pages]
106
-
107
- {
108
- self: build_page_url(full_url, query_params, page, per_page),
109
- first: build_page_url(full_url, query_params, 1, per_page),
110
- last: build_page_url(full_url, query_params, total_pages, per_page)
111
- }
112
- end
113
-
114
- def add_conditional_pagination_links(links, url_components, pagination_data)
115
- full_url = url_components[:base_url] + url_components[:path]
116
- query_params = url_components[:query_params]
117
- page = pagination_data[:page]
118
- per_page = pagination_data[:per_page]
119
- total_pages = pagination_data[:total_pages]
120
-
121
- links[:prev] = build_page_url(full_url, query_params, page - 1, per_page) if page > 1
122
- links[:next] = build_page_url(full_url, query_params, page + 1, per_page) if page < total_pages
123
- end
124
-
125
- def build_page_url(base_url, query_params, page_num, per_page)
126
- params = query_params.merge(
127
- 'page' => page_num.to_s,
128
- 'per_page' => per_page.to_s
129
- )
130
- query_string = params.respond_to?(:to_query) ? params.to_query : params.map { |k, v| "#{k}=#{v}" }.join('&')
131
- "#{base_url}?#{query_string}"
132
- end
133
-
134
- def infer_resource_class
135
- # Convert controller name to resource class name
136
- # e.g., "UsersController" -> "UserResource"
137
- # e.g., "Api::V1::UsersController" -> "UserResource"
138
- controller_name = self.class.name
139
- return nil unless controller_name&.end_with?('Controller')
140
-
141
- # Remove "Controller" suffix and any namespace
142
- base_name = controller_name.split('::').last.chomp('Controller')
143
-
144
- # Convert plural controller name to singular resource name
145
- # e.g., "Users" -> "User"
146
- singular_name = base_name.singularize
147
- resource_class_name = "#{singular_name}Resource"
148
-
149
- # Try to constantize the resource class
150
- resource_class_name.constantize
151
- end
152
- end
153
- end
154
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'active_support/concern'
4
- require_relative 'controller/error_handling'
5
- require_relative 'controller/parameter_parsing'
6
- require_relative 'controller/rendering'
7
- require_relative 'controller/crud_actions'
8
- require_relative 'controller/json_api_validation'
9
- require_relative 'controller/relationship_actions'
10
- require_relative 'controller/related_actions'
11
-
12
- module JPie
13
- module Controller
14
- extend ActiveSupport::Concern
15
-
16
- include ErrorHandling
17
- include ParameterParsing
18
- include Rendering
19
- include CrudActions
20
- include JsonApiValidation
21
- include RelationshipActions
22
- include RelatedActions
23
-
24
- # Relationship route actions
25
- def show_relationship
26
- relationship_show
27
- end
28
-
29
- def update_relationship
30
- relationship_update
31
- end
32
-
33
- def create_relationship
34
- relationship_create
35
- end
36
-
37
- def destroy_relationship
38
- relationship_destroy
39
- end
40
-
41
- def show_related
42
- related_show
43
- end
44
- end
45
- end