jpie 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.aiconfig +65 -0
  3. data/.rubocop.yml +110 -35
  4. data/CHANGELOG.md +93 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +776 -1903
  7. data/Rakefile +14 -3
  8. data/jpie.gemspec +35 -18
  9. data/lib/jpie/configuration.rb +12 -0
  10. data/lib/jpie/controller/crud_actions.rb +110 -0
  11. data/lib/jpie/controller/error_handling.rb +41 -0
  12. data/lib/jpie/controller/parameter_parsing.rb +35 -0
  13. data/lib/jpie/controller/rendering.rb +60 -0
  14. data/lib/jpie/controller.rb +18 -0
  15. data/lib/jpie/deserializer.rb +110 -0
  16. data/lib/jpie/errors.rb +70 -0
  17. data/lib/jpie/generators/resource_generator.rb +39 -0
  18. data/lib/jpie/generators/templates/resource.rb.erb +12 -0
  19. data/lib/jpie/railtie.rb +36 -0
  20. data/lib/jpie/resource/attributable.rb +98 -0
  21. data/lib/jpie/resource/inferrable.rb +43 -0
  22. data/lib/jpie/resource/sortable.rb +93 -0
  23. data/lib/jpie/resource.rb +107 -0
  24. data/lib/jpie/serializer.rb +205 -0
  25. data/lib/{json_api → jpie}/version.rb +2 -2
  26. data/lib/jpie.rb +23 -3
  27. metadata +145 -50
  28. data/.gitignore +0 -21
  29. data/.rspec +0 -3
  30. data/.travis.yml +0 -7
  31. data/Gemfile +0 -21
  32. data/Gemfile.lock +0 -312
  33. data/bin/console +0 -15
  34. data/bin/setup +0 -8
  35. data/kiln/app/resources/user_message_resource.rb +0 -2
  36. data/lib/json_api/active_storage/deserialization.rb +0 -106
  37. data/lib/json_api/active_storage/detection.rb +0 -74
  38. data/lib/json_api/active_storage/serialization.rb +0 -32
  39. data/lib/json_api/configuration.rb +0 -58
  40. data/lib/json_api/controllers/base_controller.rb +0 -26
  41. data/lib/json_api/controllers/concerns/controller_helpers.rb +0 -223
  42. data/lib/json_api/controllers/concerns/resource_actions.rb +0 -657
  43. data/lib/json_api/controllers/relationships_controller.rb +0 -504
  44. data/lib/json_api/controllers/resources_controller.rb +0 -6
  45. data/lib/json_api/errors/parameter_not_allowed.rb +0 -19
  46. data/lib/json_api/railtie.rb +0 -75
  47. data/lib/json_api/resources/active_storage_blob_resource.rb +0 -11
  48. data/lib/json_api/resources/resource.rb +0 -238
  49. data/lib/json_api/resources/resource_loader.rb +0 -35
  50. data/lib/json_api/routing.rb +0 -72
  51. data/lib/json_api/serialization/deserializer.rb +0 -362
  52. data/lib/json_api/serialization/serializer.rb +0 -320
  53. data/lib/json_api/support/active_storage_support.rb +0 -85
  54. data/lib/json_api/support/collection_query.rb +0 -406
  55. data/lib/json_api/support/instrumentation.rb +0 -42
  56. data/lib/json_api/support/param_helpers.rb +0 -51
  57. data/lib/json_api/support/relationship_guard.rb +0 -16
  58. data/lib/json_api/support/relationship_helpers.rb +0 -74
  59. data/lib/json_api/support/resource_identifier.rb +0 -87
  60. data/lib/json_api/support/responders.rb +0 -100
  61. data/lib/json_api/support/response_helpers.rb +0 -10
  62. data/lib/json_api/support/sort_parsing.rb +0 -21
  63. data/lib/json_api/support/type_conversion.rb +0 -21
  64. data/lib/json_api/testing/test_helper.rb +0 -76
  65. data/lib/json_api/testing.rb +0 -3
  66. data/lib/json_api.rb +0 -50
  67. data/lib/rubocop/cop/custom/hash_value_omission.rb +0 -53
@@ -1,238 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module JSONAPI
4
- class Resource
5
- class << self
6
- def resource_for_model(model_class)
7
- resource_const = "#{model_class.name}Resource"
8
- resource_const.safe_constantize if resource_const.respond_to?(:safe_constantize) || defined?(ActiveSupport)
9
- rescue NameError
10
- nil
11
- end
12
-
13
- def attributes(*attrs)
14
- @attributes ||= []
15
- @attributes.concat(attrs.map(&:to_sym))
16
- @attributes.uniq!
17
- end
18
-
19
- def creatable_fields(*fields)
20
- @creatable_fields ||= []
21
- @creatable_fields.concat(fields.map(&:to_sym))
22
- @creatable_fields.uniq!
23
- end
24
-
25
- def updatable_fields(*fields)
26
- @updatable_fields ||= []
27
- @updatable_fields.concat(fields.map(&:to_sym))
28
- @updatable_fields.uniq!
29
- end
30
-
31
- def filters(*filter_names)
32
- @filters ||= []
33
- @filters.concat(filter_names.map(&:to_sym))
34
- @filters.uniq!
35
- end
36
-
37
- def permitted_filters_through
38
- relationship_names
39
- end
40
-
41
- def sortable_fields(*field_names)
42
- @sortable_fields ||= []
43
- @sortable_fields.concat(field_names.map(&:to_sym))
44
- @sortable_fields.uniq!
45
- end
46
-
47
- def meta(hash = nil, &block)
48
- @meta = hash || block
49
- end
50
-
51
- def resource_meta
52
- if instance_variable_defined?(:@meta)
53
- @meta
54
- elsif superclass != JSONAPI::Resource && superclass.respond_to?(:resource_meta)
55
- superclass.resource_meta
56
- end
57
- end
58
-
59
- def has_one(name, meta: nil, **options)
60
- @relationships ||= []
61
-
62
- unless options.key?(:polymorphic)
63
- model_klass = reflection_model_class
64
- if model_klass.respond_to?(:reflect_on_association)
65
- reflection = model_klass.reflect_on_association(name)
66
- options[:polymorphic] = reflection&.polymorphic?
67
- end
68
- end
69
-
70
- @relationships << { name: name.to_sym, type: :has_one, meta:, options: }
71
- end
72
-
73
- def has_many(name, meta: nil, **options)
74
- @relationships ||= []
75
-
76
- if options[:append_only] && options[:purge_on_nil] == true
77
- raise ArgumentError, "Cannot use append_only: true with purge_on_nil: true"
78
- end
79
-
80
- options[:purge_on_nil] = false if options[:append_only] && !options.key?(:purge_on_nil)
81
-
82
- unless options.key?(:polymorphic)
83
- model_klass = reflection_model_class
84
- if model_klass.respond_to?(:reflect_on_association)
85
- reflection = model_klass.reflect_on_association(name)
86
- options[:polymorphic] = reflection&.polymorphic?
87
- end
88
- end
89
-
90
- @relationships << { name: name.to_sym, type: :has_many, meta:, options: }
91
- end
92
-
93
- def belongs_to(name, meta: nil, **options)
94
- @relationships ||= []
95
-
96
- unless options.key?(:polymorphic)
97
- model_klass = reflection_model_class
98
- if model_klass.respond_to?(:reflect_on_association)
99
- reflection = model_klass.reflect_on_association(name)
100
- options[:polymorphic] = reflection&.polymorphic?
101
- end
102
- end
103
-
104
- @relationships << { name: name.to_sym, type: :belongs_to, meta:, options: }
105
- end
106
-
107
- def permitted_attributes
108
- # For STI subclasses, merge with parent class attributes unless the subclass declares its own
109
- declared_attributes = instance_variable_defined?(:@attributes)
110
- attrs = @attributes || []
111
- if !declared_attributes &&
112
- superclass != JSONAPI::Resource &&
113
- superclass.respond_to?(:permitted_attributes)
114
- attrs = superclass.permitted_attributes + attrs
115
- end
116
- attrs.uniq
117
- end
118
-
119
- def permitted_creatable_fields
120
- if instance_variable_defined?(:@creatable_fields)
121
- fields = @creatable_fields || []
122
- elsif superclass != JSONAPI::Resource &&
123
- superclass.respond_to?(:permitted_creatable_fields) &&
124
- superclass.instance_variable_defined?(:@creatable_fields)
125
- parent_fields = superclass.permitted_creatable_fields
126
- fields = parent_fields
127
- else
128
- fields = permitted_attributes
129
- end
130
-
131
- fields.uniq
132
- end
133
-
134
- def permitted_updatable_fields
135
- if instance_variable_defined?(:@updatable_fields)
136
- fields = @updatable_fields || []
137
- elsif superclass != JSONAPI::Resource &&
138
- superclass.respond_to?(:permitted_updatable_fields) &&
139
- superclass.instance_variable_defined?(:@updatable_fields)
140
- parent_fields = superclass.permitted_updatable_fields
141
- fields = parent_fields
142
- else
143
- fields = permitted_attributes
144
- end
145
-
146
- fields.uniq
147
- end
148
-
149
- def permitted_filters
150
- # For STI subclasses, merge with parent class filters unless the subclass declares its own
151
- declared_filters = instance_variable_defined?(:@filters)
152
- filter_list = @filters || []
153
- if !declared_filters &&
154
- superclass != JSONAPI::Resource &&
155
- superclass.respond_to?(:permitted_filters)
156
- filter_list = superclass.permitted_filters + filter_list
157
- end
158
- filter_list.uniq
159
- end
160
-
161
- def permitted_sortable_fields
162
- # Include both attributes (which are sortable) and sort-only fields
163
- # For STI subclasses, merge with parent class sortable fields when no subclass DSL is declared
164
- declared_sortable = instance_variable_defined?(:@sortable_fields)
165
- declared_attributes = instance_variable_defined?(:@attributes)
166
- sort_fields = @sortable_fields || []
167
- if !declared_sortable &&
168
- !declared_attributes &&
169
- superclass != JSONAPI::Resource &&
170
- superclass.respond_to?(:permitted_sortable_fields)
171
- parent_sort_fields = superclass.permitted_sortable_fields
172
- # Only merge parent's sort-only fields, not attributes (attributes are already included via permitted_attributes)
173
- parent_attributes = superclass.permitted_attributes
174
- parent_sort_only = parent_sort_fields - parent_attributes
175
- sort_fields = parent_sort_only + sort_fields
176
- end
177
- # Combine with attributes (all attributes are sortable)
178
- (permitted_attributes + sort_fields).uniq
179
- end
180
-
181
- def relationship_definitions
182
- # For STI subclasses, merge with parent class relationships
183
- declared_relationships = instance_variable_defined?(:@relationships)
184
- rels = @relationships || []
185
- if !declared_relationships &&
186
- superclass != JSONAPI::Resource &&
187
- superclass.respond_to?(:relationship_definitions)
188
- rels = superclass.relationship_definitions + rels
189
- end
190
- rels.uniq { |r| r[:name] }
191
- end
192
-
193
- def relationship_names
194
- relationship_definitions.map { |r| r[:name] }
195
- end
196
-
197
- def model_class
198
- name.sub(/Resource$/, "").classify.constantize
199
- end
200
-
201
- def safe_model_class
202
- return nil unless respond_to?(:name) && name
203
- return nil unless defined?(ActiveSupport)
204
-
205
- name.sub(/Resource$/, "").classify.safe_constantize
206
- rescue NoMethodError
207
- nil
208
- end
209
-
210
- def reflection_model_class
211
- model_class
212
- rescue NameError
213
- safe_model_class
214
- rescue StandardError
215
- safe_model_class
216
- end
217
- end
218
-
219
- # Instance method to initialize resource with model instance
220
- # This allows the serializer to instantiate resources directly
221
- def initialize(record = nil, context = {})
222
- @record = record
223
- @context = context
224
- @transformed_params = {}
225
- end
226
-
227
- # Accessor for the underlying model instance
228
- # @record is the preferred internal name, but :resource is kept for backward compatibility
229
- attr_reader :record
230
- alias resource record
231
-
232
- # Returns transformed params accumulated by resource setters during deserialization
233
- # Resources can override this method or use the default implementation
234
- def transformed_params
235
- @transformed_params || {}
236
- end
237
- end
238
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module JSONAPI
4
- class ResourceLoader
5
- class MissingResourceClass < JSONAPI::Error
6
- def initialize(resource_type)
7
- super("Resource class for '#{resource_type}' not found. Define #{resource_type.singularize.classify}Resource < JSONAPI::Resource")
8
- end
9
- end
10
-
11
- def self.find(resource_type)
12
- resource_class_name = "#{resource_type.singularize.classify}Resource"
13
- resource_class_name.constantize
14
- rescue NameError
15
- raise MissingResourceClass, resource_type
16
- end
17
-
18
- def self.find_for_model(model_class)
19
- # Handle ActiveStorage::Blob specially
20
- return ActiveStorageBlobResource if defined?(::ActiveStorage) && model_class == ::ActiveStorage::Blob
21
-
22
- # For STI subclasses, try the specific subclass resource first
23
- resource_type = model_class.name.underscore.pluralize
24
- begin
25
- find(resource_type)
26
- rescue MissingResourceClass
27
- # For STI subclasses, fall back to base class resource
28
- raise unless model_class.respond_to?(:base_class) && model_class.base_class != model_class
29
-
30
- base_resource_type = model_class.base_class.name.underscore.pluralize
31
- find(base_resource_type)
32
- end
33
- end
34
- end
35
- end
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module JSONAPI
4
- module Routing
5
- def jsonapi_resources(resource, controller: nil, defaults: {}, sti: false, **options, &block)
6
- resource_name = resource.to_s
7
-
8
- # Smart Controller Detection:
9
- # If no controller is specified, try to find one in the current scope.
10
- # If we find "Trust::VendorsController", use it (by passing nil to 'resources').
11
- # If NOT found, default to the generic "json_api/resources".
12
- if controller.nil?
13
- scoped_module = @scope[:module]
14
- potential_controller_name = if scoped_module
15
- "#{scoped_module.to_s.camelize}::#{resource_name.pluralize.camelize}Controller"
16
- else
17
- "#{resource_name.pluralize.camelize}Controller"
18
- end
19
-
20
- begin
21
- # Check if the controller exists constant
22
- potential_controller_name.constantize
23
- # It exists! Leave controller as nil so Rails uses standard routing conventions
24
- rescue NameError
25
- # It doesn't exist, fallback to generic controller
26
- controller = "json_api/resources"
27
- end
28
- end
29
-
30
- # Validate that resource class exists at boot time
31
- JSONAPI::ResourceLoader.find(resource_name)
32
-
33
- defaults = defaults.merge(format: :jsonapi, resource_type: resource_name)
34
-
35
- options[:only] = :index if sti
36
-
37
- resources(resource, controller:, defaults:, **options) do
38
- member do
39
- get "relationships/:relationship_name", to: "json_api/relationships#show", as: :relationship
40
- patch "relationships/:relationship_name", to: "json_api/relationships#update"
41
- delete "relationships/:relationship_name", to: "json_api/relationships#destroy"
42
- end
43
-
44
- instance_eval(&block) if block
45
- end
46
-
47
- return unless sti
48
-
49
- if sti.is_a?(Array)
50
- sti.each do |sub_resource_name|
51
- jsonapi_resources(sub_resource_name, defaults:)
52
- end
53
- else
54
- begin
55
- resource_class = JSONAPI::ResourceLoader.find(resource_name)
56
- model_class = resource_class.model_class
57
-
58
- if model_class.respond_to?(:descendants)
59
- model_class.descendants.each do |subclass|
60
- sub_resource_name = subclass.name.underscore.pluralize.to_sym
61
- next if sub_resource_name == resource.to_sym
62
-
63
- jsonapi_resources(sub_resource_name, defaults:)
64
- end
65
- end
66
- rescue NameError, JSONAPI::ResourceLoader::MissingResourceClass
67
- # Silently skip if model/resource not found
68
- end
69
- end
70
- end
71
- end
72
- end
@@ -1,362 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json_api/support/relationship_guard"
4
-
5
- module JSONAPI
6
- class Deserializer
7
- include ActiveStorageSupport
8
-
9
- def initialize(params, model_class:, action: :create)
10
- @params = ParamHelpers.deep_symbolize_params(params)
11
- @model_class = model_class
12
- @definition = ResourceLoader.find_for_model(model_class)
13
- @action = action.to_sym
14
- end
15
-
16
- def attributes
17
- attrs = extract_attributes_from_params
18
- attrs = attrs.transform_keys(&:to_sym) if attrs.respond_to?(:transform_keys)
19
- permitted_attrs = permitted_attributes_for_action
20
- attrs.slice(*permitted_attrs)
21
- end
22
-
23
- def extract_attributes_from_params
24
- @params.dig(:data, :attributes) || @params[:attributes] || {}
25
- end
26
-
27
- def permitted_attributes_for_action
28
- fields = if @action == :create
29
- @definition.permitted_creatable_fields
30
- else
31
- @definition.permitted_updatable_fields
32
- end
33
- fields.map(&:to_sym)
34
- end
35
-
36
- def relationships
37
- # Handle both nested (:data => {:relationships => ...}) and flat (:relationships => ...) structures
38
- rels = @params.dig(:data, :relationships) || @params[:relationships] || {}
39
- rels = rels.to_h if rels.respond_to?(:to_h)
40
- rels.is_a?(Hash) ? rels : {}
41
- end
42
-
43
- def relationship_ids(relationship_name)
44
- relationship = find_relationship(relationship_name)
45
- return [] unless relationship
46
-
47
- data = extract_relationship_data(relationship)
48
- return [] unless data
49
-
50
- extract_ids_from_data(data)
51
- end
52
-
53
- def find_relationship(relationship_name)
54
- relationships[relationship_name.to_sym]
55
- end
56
-
57
- def extract_relationship_data(relationship)
58
- relationship[:data]
59
- end
60
-
61
- def extract_ids_from_data(data)
62
- if data.is_a?(Array)
63
- data.map { |r| extract_id_from_identifier(r) }
64
- else
65
- [extract_id_from_identifier(data)]
66
- end
67
- end
68
-
69
- def extract_id_from_identifier(identifier)
70
- RelationshipHelpers.extract_id_from_identifier(identifier)
71
- end
72
-
73
- def relationship_id(relationship_name)
74
- relationship_ids(relationship_name).first
75
- end
76
-
77
- def to_model_attributes
78
- attrs = attributes.dup
79
- attrs = apply_virtual_attribute_transformers(attrs)
80
- attrs = process_relationships(attrs)
81
- attrs.transform_keys(&:to_s)
82
- end
83
-
84
- def apply_virtual_attribute_transformers(attrs)
85
- transformed_params, attributes_with_setters = invoke_setter_methods(attrs)
86
- attributes_with_setters.each { |attr_sym| attrs.delete(attr_sym) }
87
- merge_transformed_params(attrs, transformed_params)
88
- attrs
89
- end
90
-
91
- def merge_transformed_params(attrs, transformed_params)
92
- return attrs unless transformed_params.is_a?(Hash) && transformed_params.any?
93
-
94
- transformed_params_symbolized = transformed_params.transform_keys(&:to_sym)
95
- attrs.merge!(transformed_params_symbolized)
96
- attrs
97
- end
98
-
99
- def process_relationships(attrs)
100
- permitted_relationships = @definition.relationship_names.map(&:to_s)
101
-
102
- relationships.each do |key, value|
103
- association_name = key.to_s
104
- next unless permitted_relationships.include?(association_name)
105
-
106
- process_relationship(attrs, association_name, value)
107
- end
108
-
109
- attrs
110
- end
111
-
112
- def invoke_setter_methods(attrs)
113
- definition_instance = create_definition_instance_for_setters
114
- return [{}, []] unless definition_instance.respond_to?(:transformed_params, true)
115
-
116
- attributes_with_setters = call_setters(attrs, definition_instance)
117
- [definition_instance.transformed_params, attributes_with_setters]
118
- end
119
-
120
- def create_definition_instance_for_setters
121
- @definition.new(nil, {})
122
- end
123
-
124
- def call_setters(attrs, definition_instance)
125
- attributes_with_setters = []
126
- attrs.each do |attr_sym, attr_value|
127
- next unless has_setter?(definition_instance, attr_sym)
128
-
129
- definition_instance.public_send(:"#{attr_sym}=", attr_value)
130
- attributes_with_setters << attr_sym
131
- end
132
- attributes_with_setters
133
- end
134
-
135
- def has_setter?(definition_instance, attr_sym)
136
- definition_instance.respond_to?(:"#{attr_sym}=", false)
137
- end
138
-
139
- def process_relationship(attrs, association_name, value)
140
- value_hash = normalize_relationship_value(value)
141
- data = extract_data_from_value(value_hash)
142
- param_name = association_param_name(association_name)
143
-
144
- ensure_relationship_writable!(association_name)
145
-
146
- return handle_null_relationship(attrs, param_name, association_name) if data.nil?
147
- return handle_empty_array_relationship(attrs, param_name, association_name) if empty_array?(data)
148
-
149
- validate_relationship_data_format!(data, association_name)
150
- process_relationship_data(attrs, association_name, param_name, data)
151
- end
152
-
153
- def normalize_relationship_value(value)
154
- value.is_a?(Hash) ? value : value.to_h
155
- end
156
-
157
- def extract_data_from_value(value_hash)
158
- value_hash[:data]
159
- end
160
-
161
- def empty_array?(data)
162
- data.is_a?(Array) && data.empty?
163
- end
164
-
165
- def handle_null_relationship(attrs, param_name, association_name)
166
- # Handle ActiveStorage attachments specially
167
- if active_storage_attachment?(association_name)
168
- attrs[association_name.to_s] = nil
169
- else
170
- attrs["#{param_name}_id"] = nil
171
- attrs["#{param_name}_type"] = nil if polymorphic_association?(association_name)
172
- end
173
- end
174
-
175
- def handle_empty_array_relationship(attrs, param_name, association_name)
176
- # Check if this is an ActiveStorage attachment
177
- if active_storage_attachment?(association_name)
178
- attrs[association_name.to_s] = []
179
- else
180
- attrs["#{param_name.singularize}_ids"] = []
181
- end
182
- end
183
-
184
- def validate_relationship_data_format!(data, association_name)
185
- return if valid_relationship_data?(data)
186
-
187
- raise ArgumentError, "Invalid relationship data for #{association_name}: missing type or id"
188
- end
189
-
190
- def process_relationship_data(attrs, association_name, param_name, data)
191
- if data.is_a?(Array)
192
- process_to_many_relationship(attrs, association_name, param_name, data)
193
- else
194
- process_to_one_relationship(attrs, association_name, param_name, data)
195
- end
196
- end
197
-
198
- def process_to_many_relationship(attrs, association_name, param_name, data)
199
- ids = data.map { |r| extract_id(r) }
200
- types = data.map { |r| extract_type(r) }
201
-
202
- # Check if this is an ActiveStorage attachment
203
- if types.any? && self.class.active_storage_blob_type?(types.first)
204
- process_active_storage_attachment(attrs, association_name, ids, singular: false)
205
- return
206
- end
207
-
208
- validate_relationship_type(association_name, types.first) unless polymorphic_association?(association_name)
209
- attrs["#{param_name.singularize}_ids"] = ids
210
- end
211
-
212
- def process_to_one_relationship(attrs, association_name, param_name, data)
213
- id = extract_id(data)
214
- type = extract_type(data)
215
-
216
- if self.class.active_storage_blob_type?(type)
217
- return process_active_storage_attachment(attrs, association_name, id, singular: true)
218
- end
219
-
220
- process_regular_to_one_relationship(attrs, association_name, param_name, id, type)
221
- end
222
-
223
- def process_regular_to_one_relationship(attrs, association_name, param_name, id, type)
224
- if polymorphic_association?(association_name)
225
- process_polymorphic_relationship(attrs, association_name, param_name, id, type)
226
- else
227
- process_non_polymorphic_relationship(attrs, association_name, param_name, id, type)
228
- end
229
- end
230
-
231
- def process_polymorphic_relationship(attrs, association_name, param_name, id, type)
232
- class_name = validate_and_get_class_name(type, association_name)
233
- attrs["#{param_name}_id"] = id
234
- attrs["#{param_name}_type"] = class_name
235
- end
236
-
237
- def validate_and_get_class_name(type, association_name)
238
- class_name = RelationshipHelpers.type_to_class_name(type)
239
- class_name.constantize
240
- class_name
241
- rescue NameError
242
- raise ArgumentError,
243
- "Invalid relationship type for #{association_name}: " \
244
- "'#{type}' does not correspond to a valid model class"
245
- end
246
-
247
- def process_non_polymorphic_relationship(attrs, association_name, param_name, id, type)
248
- validate_relationship_type(association_name, type)
249
- attrs["#{param_name}_id"] = id
250
- end
251
-
252
- private
253
-
254
- def association_param_name(association_name)
255
- return association_name.singularize unless @model_class
256
-
257
- association = @model_class.reflect_on_association(association_name.to_sym)
258
- # If association doesn't exist, return the singularized name as fallback
259
- return association_name.singularize unless association
260
-
261
- # Use the actual association name (which is already singular for belongs_to, singular for has_many)
262
- association.name.to_s
263
- end
264
-
265
- def polymorphic_association?(association_name)
266
- RelationshipHelpers.polymorphic_association?(@definition, association_name)
267
- end
268
-
269
- def validate_relationship_type(association_name, type)
270
- relationship_def = find_relationship_definition(association_name)
271
- return unless relationship_def
272
-
273
- association = @model_class.reflect_on_association(association_name.to_sym)
274
- return unless association
275
-
276
- if relationship_def[:options][:polymorphic]
277
- validate_polymorphic_type(association_name, type)
278
- else
279
- validate_non_polymorphic_type(association_name, type, association)
280
- end
281
- end
282
-
283
- def find_relationship_definition(association_name)
284
- RelationshipHelpers.find_relationship_definition(@definition, association_name)
285
- end
286
-
287
- def validate_polymorphic_type(association_name, type)
288
- ResourceLoader.find(type)
289
- rescue ResourceLoader::MissingResourceClass
290
- raise ArgumentError,
291
- "Invalid relationship type for #{association_name}: '#{type}' does not have a resource class defined"
292
- end
293
-
294
- def validate_non_polymorphic_type(association_name, type, association)
295
- expected_type = RelationshipHelpers.model_type_name(association.klass)
296
- return if type == expected_type
297
-
298
- raise ArgumentError, "Invalid relationship type for #{association_name}: expected #{expected_type}, got #{type}"
299
- end
300
-
301
- def extract_id(resource_identifier)
302
- id = RelationshipHelpers.extract_id_from_identifier(resource_identifier)
303
- raise ArgumentError, "Missing id in relationship data" unless id
304
-
305
- id
306
- end
307
-
308
- def extract_type(resource_identifier)
309
- type = RelationshipHelpers.extract_type_from_identifier(resource_identifier)
310
- raise ArgumentError, "Missing type in relationship data" unless type
311
-
312
- type
313
- end
314
-
315
- def valid_relationship_data?(data)
316
- if data.is_a?(Array)
317
- data.all? { |r| valid_resource_identifier?(r) }
318
- else
319
- valid_resource_identifier?(data)
320
- end
321
- end
322
-
323
- def valid_resource_identifier?(identifier)
324
- return false unless identifier.is_a?(Hash)
325
-
326
- has_id?(identifier) && has_type?(identifier)
327
- end
328
-
329
- def has_id?(identifier)
330
- identifier[:id]
331
- end
332
-
333
- def has_type?(identifier)
334
- identifier[:type]
335
- end
336
-
337
- def resource_model_class
338
- @model_class
339
- end
340
-
341
- def active_storage_attachment?(association_name)
342
- return false unless @model_class
343
-
344
- self.class.active_storage_attachment?(association_name, @model_class)
345
- end
346
-
347
- def ensure_relationship_writable!(association_name)
348
- return if active_storage_attachment?(association_name)
349
-
350
- association = @model_class.reflect_on_association(association_name.to_sym)
351
- readonly = relationship_options_for(association_name)[:readonly] == true
352
- JSONAPI::RelationshipGuard.ensure_writable!(association, association_name, readonly:) if association
353
- end
354
-
355
- def relationship_options_for(association_name)
356
- relationship_def = RelationshipHelpers.find_relationship_definition(@definition, association_name)
357
- return {} unless relationship_def
358
-
359
- relationship_def[:options] || {}
360
- end
361
- end
362
- end