jpie 0.4.4 → 1.0.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 +4 -4
- data/.gitignore +21 -0
- data/.rspec +3 -0
- data/.rubocop.yml +35 -110
- data/.travis.yml +7 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +312 -0
- data/README.md +2072 -140
- data/Rakefile +3 -14
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/jpie.gemspec +18 -35
- data/kiln/app/resources/user_message_resource.rb +2 -0
- data/lib/jpie.rb +3 -28
- data/lib/json_api/active_storage/deserialization.rb +106 -0
- data/lib/json_api/active_storage/detection.rb +74 -0
- data/lib/json_api/active_storage/serialization.rb +32 -0
- data/lib/json_api/configuration.rb +58 -0
- data/lib/json_api/controllers/base_controller.rb +26 -0
- data/lib/json_api/controllers/concerns/controller_helpers.rb +223 -0
- data/lib/json_api/controllers/concerns/resource_actions.rb +657 -0
- data/lib/json_api/controllers/relationships_controller.rb +504 -0
- data/lib/json_api/controllers/resources_controller.rb +6 -0
- data/lib/json_api/errors/parameter_not_allowed.rb +19 -0
- data/lib/json_api/railtie.rb +75 -0
- data/lib/json_api/resources/active_storage_blob_resource.rb +11 -0
- data/lib/json_api/resources/resource.rb +238 -0
- data/lib/json_api/resources/resource_loader.rb +35 -0
- data/lib/json_api/routing.rb +72 -0
- data/lib/json_api/serialization/deserializer.rb +362 -0
- data/lib/json_api/serialization/serializer.rb +320 -0
- data/lib/json_api/support/active_storage_support.rb +85 -0
- data/lib/json_api/support/collection_query.rb +406 -0
- data/lib/json_api/support/instrumentation.rb +42 -0
- data/lib/json_api/support/param_helpers.rb +51 -0
- data/lib/json_api/support/relationship_guard.rb +16 -0
- data/lib/json_api/support/relationship_helpers.rb +74 -0
- data/lib/json_api/support/resource_identifier.rb +87 -0
- data/lib/json_api/support/responders.rb +100 -0
- data/lib/json_api/support/response_helpers.rb +10 -0
- data/lib/json_api/support/sort_parsing.rb +21 -0
- data/lib/json_api/support/type_conversion.rb +21 -0
- data/lib/json_api/testing/test_helper.rb +76 -0
- data/lib/json_api/testing.rb +3 -0
- data/lib/{jpie → json_api}/version.rb +2 -2
- data/lib/json_api.rb +50 -0
- data/lib/rubocop/cop/custom/hash_value_omission.rb +53 -0
- metadata +50 -167
- data/.cursor/rules/dependencies.mdc +0 -19
- data/.cursor/rules/examples.mdc +0 -16
- data/.cursor/rules/git.mdc +0 -14
- data/.cursor/rules/project_structure.mdc +0 -30
- data/.cursor/rules/security.mdc +0 -14
- data/.cursor/rules/style.mdc +0 -15
- data/.cursor/rules/testing.mdc +0 -16
- data/.overcommit.yml +0 -35
- data/CHANGELOG.md +0 -164
- data/LICENSE.txt +0 -21
- data/examples/basic_example.md +0 -146
- data/examples/including_related_resources.md +0 -491
- data/examples/pagination.md +0 -303
- data/examples/relationships.md +0 -114
- data/examples/resource_attribute_configuration.md +0 -147
- data/examples/resource_meta_configuration.md +0 -244
- data/examples/single_table_inheritance.md +0 -160
- data/lib/jpie/configuration.rb +0 -12
- data/lib/jpie/controller/crud_actions.rb +0 -141
- data/lib/jpie/controller/error_handling/handler_setup.rb +0 -124
- data/lib/jpie/controller/error_handling/handlers.rb +0 -109
- data/lib/jpie/controller/error_handling.rb +0 -23
- data/lib/jpie/controller/json_api_validation.rb +0 -193
- data/lib/jpie/controller/parameter_parsing.rb +0 -78
- data/lib/jpie/controller/related_actions.rb +0 -45
- data/lib/jpie/controller/relationship_actions.rb +0 -291
- data/lib/jpie/controller/relationship_validation.rb +0 -117
- data/lib/jpie/controller/rendering.rb +0 -154
- data/lib/jpie/controller.rb +0 -45
- data/lib/jpie/deserializer.rb +0 -110
- data/lib/jpie/errors.rb +0 -117
- data/lib/jpie/generators/resource_generator.rb +0 -116
- data/lib/jpie/generators/templates/resource.rb.erb +0 -31
- data/lib/jpie/railtie.rb +0 -42
- data/lib/jpie/resource/attributable.rb +0 -112
- data/lib/jpie/resource/inferrable.rb +0 -43
- data/lib/jpie/resource/sortable.rb +0 -93
- data/lib/jpie/resource.rb +0 -147
- data/lib/jpie/routing.rb +0 -59
- data/lib/jpie/rspec.rb +0 -71
- 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
|
data/lib/jpie/controller.rb
DELETED
|
@@ -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
|