jpie 1.0.0 → 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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/release.mdc +62 -0
  3. data/.gitignore +5 -0
  4. data/.rubocop.yml +82 -38
  5. data/Gemfile +12 -10
  6. data/Gemfile.lock +10 -1
  7. data/README.md +675 -1235
  8. data/jpie.gemspec +15 -15
  9. data/kiln/app/resources/user_message_resource.rb +2 -0
  10. data/lib/jpie.rb +0 -1
  11. data/lib/json_api/active_storage/deserialization.rb +32 -22
  12. data/lib/json_api/active_storage/detection.rb +36 -41
  13. data/lib/json_api/active_storage/serialization.rb +13 -11
  14. data/lib/json_api/configuration.rb +4 -5
  15. data/lib/json_api/controllers/base_controller.rb +3 -3
  16. data/lib/json_api/controllers/concerns/controller_helpers/authorization.rb +30 -0
  17. data/lib/json_api/controllers/concerns/controller_helpers/document_meta.rb +20 -0
  18. data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +64 -0
  19. data/lib/json_api/controllers/concerns/controller_helpers/parsing.rb +127 -0
  20. data/lib/json_api/controllers/concerns/controller_helpers/resource_setup.rb +38 -0
  21. data/lib/json_api/controllers/concerns/controller_helpers.rb +11 -215
  22. data/lib/json_api/controllers/concerns/relationships/active_storage_removal.rb +65 -0
  23. data/lib/json_api/controllers/concerns/relationships/events.rb +44 -0
  24. data/lib/json_api/controllers/concerns/relationships/removal.rb +92 -0
  25. data/lib/json_api/controllers/concerns/relationships/response_helpers.rb +55 -0
  26. data/lib/json_api/controllers/concerns/relationships/serialization.rb +72 -0
  27. data/lib/json_api/controllers/concerns/relationships/sorting.rb +114 -0
  28. data/lib/json_api/controllers/concerns/relationships/updating.rb +73 -0
  29. data/lib/json_api/controllers/concerns/relationships_controller/active_storage_removal.rb +67 -0
  30. data/lib/json_api/controllers/concerns/relationships_controller/events.rb +44 -0
  31. data/lib/json_api/controllers/concerns/relationships_controller/removal.rb +92 -0
  32. data/lib/json_api/controllers/concerns/relationships_controller/response_helpers.rb +55 -0
  33. data/lib/json_api/controllers/concerns/relationships_controller/serialization.rb +72 -0
  34. data/lib/json_api/controllers/concerns/relationships_controller/sorting.rb +114 -0
  35. data/lib/json_api/controllers/concerns/relationships_controller/updating.rb +73 -0
  36. data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +93 -0
  37. data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +114 -0
  38. data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +91 -0
  39. data/lib/json_api/controllers/concerns/resource_actions/pagination.rb +51 -0
  40. data/lib/json_api/controllers/concerns/resource_actions/preloading.rb +64 -0
  41. data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +71 -0
  42. data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +63 -0
  43. data/lib/json_api/controllers/concerns/resource_actions/type_validation.rb +75 -0
  44. data/lib/json_api/controllers/concerns/resource_actions.rb +51 -602
  45. data/lib/json_api/controllers/relationships_controller.rb +26 -422
  46. data/lib/json_api/errors/parameter_not_allowed.rb +1 -1
  47. data/lib/json_api/railtie.rb +46 -9
  48. data/lib/json_api/resources/active_storage_blob_resource.rb +9 -1
  49. data/lib/json_api/resources/concerns/attributes_dsl.rb +69 -0
  50. data/lib/json_api/resources/concerns/filters_dsl.rb +32 -0
  51. data/lib/json_api/resources/concerns/meta_dsl.rb +23 -0
  52. data/lib/json_api/resources/concerns/model_class_helpers.rb +37 -0
  53. data/lib/json_api/resources/concerns/relationships_dsl.rb +71 -0
  54. data/lib/json_api/resources/concerns/sortable_fields_dsl.rb +36 -0
  55. data/lib/json_api/resources/resource.rb +13 -219
  56. data/lib/json_api/routing.rb +56 -47
  57. data/lib/json_api/serialization/concerns/attributes_deserialization.rb +27 -0
  58. data/lib/json_api/serialization/concerns/attributes_serialization.rb +50 -0
  59. data/lib/json_api/serialization/concerns/deserialization_helpers.rb +115 -0
  60. data/lib/json_api/serialization/concerns/includes_serialization.rb +82 -0
  61. data/lib/json_api/serialization/concerns/links_serialization.rb +33 -0
  62. data/lib/json_api/serialization/concerns/meta_serialization.rb +60 -0
  63. data/lib/json_api/serialization/concerns/model_attributes_transformation.rb +69 -0
  64. data/lib/json_api/serialization/concerns/relationship_processing.rb +119 -0
  65. data/lib/json_api/serialization/concerns/relationships_deserialization.rb +47 -0
  66. data/lib/json_api/serialization/concerns/relationships_serialization.rb +81 -0
  67. data/lib/json_api/serialization/deserializer.rb +10 -346
  68. data/lib/json_api/serialization/serializer.rb +17 -260
  69. data/lib/json_api/support/active_storage_support.rb +10 -13
  70. data/lib/json_api/support/collection_query.rb +14 -370
  71. data/lib/json_api/support/concerns/condition_building.rb +57 -0
  72. data/lib/json_api/support/concerns/nested_filters.rb +130 -0
  73. data/lib/json_api/support/concerns/pagination.rb +30 -0
  74. data/lib/json_api/support/concerns/polymorphic_filters.rb +75 -0
  75. data/lib/json_api/support/concerns/regular_filters.rb +81 -0
  76. data/lib/json_api/support/concerns/sorting.rb +88 -0
  77. data/lib/json_api/support/instrumentation.rb +13 -12
  78. data/lib/json_api/support/param_helpers.rb +9 -6
  79. data/lib/json_api/support/relationship_helpers.rb +4 -2
  80. data/lib/json_api/support/resource_identifier.rb +29 -29
  81. data/lib/json_api/support/responders.rb +5 -5
  82. data/lib/json_api/version.rb +1 -1
  83. metadata +51 -1
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Serialization
5
+ module DeserializationHelpers
6
+ private
7
+
8
+ def association_param_name(association_name)
9
+ return association_name.singularize unless @model_class
10
+
11
+ association = @model_class.reflect_on_association(association_name.to_sym)
12
+ return association_name.singularize unless association
13
+
14
+ association.name.to_s
15
+ end
16
+
17
+ def polymorphic_association?(association_name)
18
+ RelationshipHelpers.polymorphic_association?(@definition, association_name)
19
+ end
20
+
21
+ def validate_relationship_type(association_name, type)
22
+ relationship_def = find_relationship_definition(association_name)
23
+ return unless relationship_def
24
+
25
+ association = @model_class.reflect_on_association(association_name.to_sym)
26
+ return unless association
27
+
28
+ if relationship_def[:options][:polymorphic]
29
+ validate_polymorphic_type(association_name, type)
30
+ else
31
+ validate_non_polymorphic_type(association_name, type, association)
32
+ end
33
+ end
34
+
35
+ def find_relationship_definition(association_name)
36
+ RelationshipHelpers.find_relationship_definition(@definition, association_name)
37
+ end
38
+
39
+ def validate_polymorphic_type(association_name, type)
40
+ ResourceLoader.find(type)
41
+ rescue ResourceLoader::MissingResourceClass
42
+ raise ArgumentError,
43
+ "Invalid relationship type for #{association_name}: '#{type}' does not have a resource class defined"
44
+ end
45
+
46
+ def validate_non_polymorphic_type(association_name, type, association)
47
+ expected_type = RelationshipHelpers.model_type_name(association.klass)
48
+ return if type == expected_type
49
+
50
+ raise ArgumentError, "Invalid relationship type for #{association_name}: expected #{expected_type}, got #{type}"
51
+ end
52
+
53
+ def extract_id(resource_identifier)
54
+ id = RelationshipHelpers.extract_id_from_identifier(resource_identifier)
55
+ raise ArgumentError, "Missing id in relationship data" unless id
56
+
57
+ id
58
+ end
59
+
60
+ def extract_type(resource_identifier)
61
+ type = RelationshipHelpers.extract_type_from_identifier(resource_identifier)
62
+ raise ArgumentError, "Missing type in relationship data" unless type
63
+
64
+ type
65
+ end
66
+
67
+ def valid_relationship_data?(data)
68
+ if data.is_a?(Array)
69
+ data.all? { |r| valid_resource_identifier?(r) }
70
+ else
71
+ valid_resource_identifier?(data)
72
+ end
73
+ end
74
+
75
+ def valid_resource_identifier?(identifier)
76
+ return false unless identifier.is_a?(Hash)
77
+
78
+ has_id?(identifier) && has_type?(identifier)
79
+ end
80
+
81
+ def has_id?(identifier)
82
+ identifier[:id]
83
+ end
84
+
85
+ def has_type?(identifier)
86
+ identifier[:type]
87
+ end
88
+
89
+ def resource_model_class
90
+ @model_class
91
+ end
92
+
93
+ def active_storage_attachment?(association_name)
94
+ return false unless @model_class
95
+
96
+ self.class.active_storage_attachment?(association_name, @model_class)
97
+ end
98
+
99
+ def ensure_relationship_writable!(association_name)
100
+ return if active_storage_attachment?(association_name)
101
+
102
+ association = @model_class.reflect_on_association(association_name.to_sym)
103
+ readonly = relationship_options_for(association_name)[:readonly] == true
104
+ JSONAPI::RelationshipGuard.ensure_writable!(association, association_name, readonly:) if association
105
+ end
106
+
107
+ def relationship_options_for(association_name)
108
+ relationship_def = RelationshipHelpers.find_relationship_definition(@definition, association_name)
109
+ return {} unless relationship_def
110
+
111
+ relationship_def[:options] || {}
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Serialization
5
+ module IncludesSerialization
6
+ def serialize_included(includes, fields = {})
7
+ return [] if includes.empty?
8
+
9
+ included_records = []
10
+ processed = Set.new
11
+
12
+ includes.each do |include_path|
13
+ serialize_include_path(record, include_path, fields, included_records, processed)
14
+ end
15
+
16
+ included_records
17
+ end
18
+
19
+ private
20
+
21
+ def serialize_include_path(current_record, include_path, fields, included_records, processed)
22
+ path_parts = include_path.split(".")
23
+ association_name = path_parts.first.to_sym
24
+
25
+ return unless valid_include_association?(current_record, association_name)
26
+
27
+ related_array = get_related_records(current_record, association_name)
28
+ related_array.each do |related_record|
29
+ serialize_and_process_record(related_record, fields, included_records, processed)
30
+ serialize_nested_path(related_record, path_parts, fields, included_records, processed)
31
+ end
32
+ end
33
+
34
+ def valid_include_association?(current_record, association_name)
35
+ current_definition = ResourceLoader.find_for_model(current_record.class)
36
+ relationship_def = RelationshipHelpers.find_relationship_definition(current_definition, association_name)
37
+ return false unless relationship_def
38
+ return true if self.class.active_storage_attachment?(association_name, current_record.class)
39
+
40
+ association = current_record.class.reflect_on_association(association_name)
41
+ association.present?
42
+ end
43
+
44
+ def get_related_records(current_record, association_name)
45
+ if self.class.active_storage_attachment?(association_name, current_record.class)
46
+ return get_active_storage_records(current_record, association_name)
47
+ end
48
+
49
+ related = current_record.public_send(association_name)
50
+ Array(related)
51
+ end
52
+
53
+ def get_active_storage_records(current_record, association_name)
54
+ attachment = current_record.public_send(association_name)
55
+ return [] unless attachment.respond_to?(:attached?) && attachment.attached?
56
+ return attachment.blobs.to_a if attachment.is_a?(::ActiveStorage::Attached::Many)
57
+
58
+ [attachment.blob].compact
59
+ end
60
+
61
+ def serialize_and_process_record(related_record, fields, included_records, processed)
62
+ record_key = build_record_key(related_record)
63
+ return if processed.include?(record_key)
64
+
65
+ serializer = self.class.new(related_record)
66
+ included_records << serializer.serialize_record(fields)
67
+ processed.add(record_key)
68
+ end
69
+
70
+ def build_record_key(related_record)
71
+ "#{related_record.class.name}-#{related_record.id}"
72
+ end
73
+
74
+ def serialize_nested_path(related_record, path_parts, fields, included_records, processed)
75
+ return unless path_parts.length > 1
76
+
77
+ nested_path = path_parts[1..].join(".")
78
+ serialize_include_path(related_record, nested_path, fields, included_records, processed)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Serialization
5
+ module LinksSerialization
6
+ def serialize_links
7
+ links = { self: "/#{record_type}/#{record_id}" }
8
+ add_active_storage_download_link(links) if active_storage_blob?
9
+ links
10
+ end
11
+
12
+ private
13
+
14
+ def active_storage_blob?
15
+ defined?(::ActiveStorage) && record.is_a?(::ActiveStorage::Blob)
16
+ end
17
+
18
+ def add_active_storage_download_link(links)
19
+ links[:download] = rails_blob_path || fallback_blob_path
20
+ rescue StandardError
21
+ links[:download] = fallback_blob_path
22
+ end
23
+
24
+ def rails_blob_path
25
+ Rails.application.routes.url_helpers.rails_blob_path(record, only_path: true)
26
+ end
27
+
28
+ def fallback_blob_path
29
+ "/rails/active_storage/blobs/#{record.signed_id}/#{record.filename}"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Serialization
5
+ module MetaSerialization
6
+ def record_meta
7
+ custom_meta = custom_meta_from_definition
8
+ default_meta = build_default_meta
9
+
10
+ return unless default_meta.any? || custom_meta.any?
11
+
12
+ default_meta.merge(custom_meta)
13
+ end
14
+
15
+ private
16
+
17
+ def custom_meta_from_definition
18
+ meta = fetch_instance_meta || fetch_class_meta
19
+ normalize_meta(meta)
20
+ end
21
+
22
+ def fetch_instance_meta
23
+ return unless definition.method_defined?(:meta)
24
+
25
+ instance = definition.new(@record, {})
26
+ instance.meta
27
+ end
28
+
29
+ def fetch_class_meta
30
+ class_meta = definition.resource_meta
31
+ return unless class_meta
32
+
33
+ class_meta.is_a?(Proc) ? class_meta.call(record) : class_meta
34
+ end
35
+
36
+ def normalize_meta(meta)
37
+ meta = meta.to_h if meta.respond_to?(:to_h) && !meta.is_a?(Hash)
38
+ meta || {}
39
+ end
40
+
41
+ def build_default_meta
42
+ default_timestamp_meta
43
+ end
44
+
45
+ def default_timestamp_meta
46
+ {}.tap do |meta|
47
+ meta[:created_at] = format_timestamp(:created_at)
48
+ meta[:updated_at] = format_timestamp(:updated_at)
49
+ end.compact
50
+ end
51
+
52
+ def format_timestamp(attr)
53
+ return unless record.respond_to?(attr)
54
+
55
+ value = record.public_send(attr)
56
+ value&.iso8601
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Serialization
5
+ module ModelAttributesTransformation
6
+ def to_model_attributes
7
+ attrs = attributes.dup
8
+ attrs = apply_virtual_attribute_transformers(attrs)
9
+ attrs = process_relationships(attrs)
10
+ attrs.transform_keys(&:to_s)
11
+ end
12
+
13
+ def apply_virtual_attribute_transformers(attrs)
14
+ transformed_params, attributes_with_setters = invoke_setter_methods(attrs)
15
+ attributes_with_setters.each { |attr_sym| attrs.delete(attr_sym) }
16
+ merge_transformed_params(attrs, transformed_params)
17
+ attrs
18
+ end
19
+
20
+ def merge_transformed_params(attrs, transformed_params)
21
+ return attrs unless transformed_params.is_a?(Hash) && transformed_params.any?
22
+
23
+ transformed_params_symbolized = transformed_params.transform_keys(&:to_sym)
24
+ attrs.merge!(transformed_params_symbolized)
25
+ attrs
26
+ end
27
+
28
+ def process_relationships(attrs)
29
+ permitted_relationships = @definition.relationship_names.map(&:to_s)
30
+
31
+ relationships.each do |key, value|
32
+ association_name = key.to_s
33
+ next unless permitted_relationships.include?(association_name)
34
+
35
+ process_relationship(attrs, association_name, value)
36
+ end
37
+
38
+ attrs
39
+ end
40
+
41
+ def invoke_setter_methods(attrs)
42
+ definition_instance = create_definition_instance_for_setters
43
+ return [{}, []] unless definition_instance.respond_to?(:transformed_params, true)
44
+
45
+ attributes_with_setters = call_setters(attrs, definition_instance)
46
+ [definition_instance.transformed_params, attributes_with_setters]
47
+ end
48
+
49
+ def create_definition_instance_for_setters
50
+ @definition.new(nil, {})
51
+ end
52
+
53
+ def call_setters(attrs, definition_instance)
54
+ attributes_with_setters = []
55
+ attrs.each do |attr_sym, attr_value|
56
+ next unless has_setter?(definition_instance, attr_sym)
57
+
58
+ definition_instance.public_send(:"#{attr_sym}=", attr_value)
59
+ attributes_with_setters << attr_sym
60
+ end
61
+ attributes_with_setters
62
+ end
63
+
64
+ def has_setter?(definition_instance, attr_sym)
65
+ definition_instance.respond_to?(:"#{attr_sym}=", false)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Serialization
5
+ module RelationshipProcessing
6
+ def process_relationship(attrs, association_name, value)
7
+ value_hash = normalize_relationship_value(value)
8
+ data = extract_data_from_value(value_hash)
9
+ param_name = association_param_name(association_name)
10
+
11
+ ensure_relationship_writable!(association_name)
12
+
13
+ return handle_null_relationship(attrs, param_name, association_name) if data.nil?
14
+ return handle_empty_array_relationship(attrs, param_name, association_name) if empty_array?(data)
15
+
16
+ validate_relationship_data_format!(data, association_name)
17
+ process_relationship_data(attrs, association_name, param_name, data)
18
+ end
19
+
20
+ private
21
+
22
+ def normalize_relationship_value(value)
23
+ value.is_a?(Hash) ? value : value.to_h
24
+ end
25
+
26
+ def extract_data_from_value(value_hash)
27
+ value_hash[:data]
28
+ end
29
+
30
+ def empty_array?(data)
31
+ data.is_a?(Array) && data.empty?
32
+ end
33
+
34
+ def handle_null_relationship(attrs, param_name, association_name)
35
+ if active_storage_attachment?(association_name)
36
+ attrs[association_name.to_s] = nil
37
+ else
38
+ attrs["#{param_name}_id"] = nil
39
+ attrs["#{param_name}_type"] = nil if polymorphic_association?(association_name)
40
+ end
41
+ end
42
+
43
+ def handle_empty_array_relationship(attrs, param_name, association_name)
44
+ if active_storage_attachment?(association_name)
45
+ attrs[association_name.to_s] = []
46
+ else
47
+ attrs["#{param_name.singularize}_ids"] = []
48
+ end
49
+ end
50
+
51
+ def validate_relationship_data_format!(data, association_name)
52
+ return if valid_relationship_data?(data)
53
+
54
+ raise ArgumentError, "Invalid relationship data for #{association_name}: missing type or id"
55
+ end
56
+
57
+ def process_relationship_data(attrs, association_name, param_name, data)
58
+ if data.is_a?(Array)
59
+ process_to_many_relationship(attrs, association_name, param_name, data)
60
+ else
61
+ process_to_one_relationship(attrs, association_name, param_name, data)
62
+ end
63
+ end
64
+
65
+ def process_to_many_relationship(attrs, association_name, param_name, data)
66
+ ids = data.map { |r| extract_id(r) }
67
+ types = data.map { |r| extract_type(r) }
68
+
69
+ if types.any? && self.class.active_storage_blob_type?(types.first)
70
+ process_active_storage_attachment(attrs, association_name, ids, singular: false)
71
+ return
72
+ end
73
+
74
+ validate_relationship_type(association_name, types.first) unless polymorphic_association?(association_name)
75
+ attrs["#{param_name.singularize}_ids"] = ids
76
+ end
77
+
78
+ def process_to_one_relationship(attrs, association_name, param_name, data)
79
+ id = extract_id(data)
80
+ type = extract_type(data)
81
+
82
+ if self.class.active_storage_blob_type?(type)
83
+ return process_active_storage_attachment(attrs, association_name, id, singular: true)
84
+ end
85
+
86
+ process_regular_to_one_relationship(attrs, association_name, param_name, id, type)
87
+ end
88
+
89
+ def process_regular_to_one_relationship(attrs, association_name, param_name, id, type)
90
+ if polymorphic_association?(association_name)
91
+ process_polymorphic_relationship(attrs, association_name, param_name, id, type)
92
+ else
93
+ process_non_polymorphic_relationship(attrs, association_name, param_name, id, type)
94
+ end
95
+ end
96
+
97
+ def process_polymorphic_relationship(attrs, association_name, param_name, id, type)
98
+ class_name = validate_and_get_class_name(type, association_name)
99
+ attrs["#{param_name}_id"] = id
100
+ attrs["#{param_name}_type"] = class_name
101
+ end
102
+
103
+ def validate_and_get_class_name(type, association_name)
104
+ class_name = RelationshipHelpers.type_to_class_name(type)
105
+ class_name.constantize
106
+ class_name
107
+ rescue NameError
108
+ raise ArgumentError,
109
+ "Invalid relationship type for #{association_name}: " \
110
+ "'#{type}' does not correspond to a valid model class"
111
+ end
112
+
113
+ def process_non_polymorphic_relationship(attrs, association_name, param_name, id, type)
114
+ validate_relationship_type(association_name, type)
115
+ attrs["#{param_name}_id"] = id
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Serialization
5
+ module RelationshipsDeserialization
6
+ def relationships
7
+ rels = @params.dig(:data, :relationships) || @params[:relationships] || {}
8
+ rels = rels.to_h if rels.respond_to?(:to_h)
9
+ rels.is_a?(Hash) ? rels : {}
10
+ end
11
+
12
+ def relationship_ids(relationship_name)
13
+ relationship = find_relationship(relationship_name)
14
+ return [] unless relationship
15
+
16
+ data = extract_relationship_data(relationship)
17
+ return [] unless data
18
+
19
+ extract_ids_from_data(data)
20
+ end
21
+
22
+ def find_relationship(relationship_name)
23
+ relationships[relationship_name.to_sym]
24
+ end
25
+
26
+ def extract_relationship_data(relationship)
27
+ relationship[:data]
28
+ end
29
+
30
+ def extract_ids_from_data(data)
31
+ if data.is_a?(Array)
32
+ data.map { |r| extract_id_from_identifier(r) }
33
+ else
34
+ [extract_id_from_identifier(data)]
35
+ end
36
+ end
37
+
38
+ def extract_id_from_identifier(identifier)
39
+ RelationshipHelpers.extract_id_from_identifier(identifier)
40
+ end
41
+
42
+ def relationship_id(relationship_name)
43
+ relationship_ids(relationship_name).first
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Serialization
5
+ module RelationshipsSerialization
6
+ def serialize_relationships
7
+ relationships = {}
8
+ relationship_definitions = definition.relationship_definitions
9
+
10
+ relationship_definitions.each do |rel_def|
11
+ serialize_relationship(rel_def, relationships)
12
+ end
13
+
14
+ relationships
15
+ end
16
+
17
+ private
18
+
19
+ def serialize_relationship(rel_def, relationships)
20
+ association_name = rel_def[:name]
21
+
22
+ if active_storage_attachment?(association_name, record.class)
23
+ return serialize_active_storage_relationship_wrapper(rel_def, relationships, association_name)
24
+ end
25
+
26
+ serialize_regular_relationship(rel_def, relationships, association_name)
27
+ end
28
+
29
+ def serialize_active_storage_relationship_wrapper(rel_def, relationships, association_name)
30
+ result = { data: serialize_active_storage_relationship(association_name, record) }
31
+ result[:meta] = rel_def[:meta] if rel_def[:meta].present?
32
+ relationships[association_name] = result
33
+ end
34
+
35
+ def serialize_regular_relationship(rel_def, relationships, association_name)
36
+ association = record.class.reflect_on_association(association_name)
37
+ return unless association
38
+
39
+ result = { data: serialize_relationship_data(association) }
40
+ result[:meta] = rel_def[:meta] if rel_def[:meta].present?
41
+ relationships[association_name] = result
42
+ end
43
+
44
+ def serialize_relationship_data(association)
45
+ related = record.public_send(association.name)
46
+
47
+ if association.collection?
48
+ serialize_collection_relationship(related, association)
49
+ elsif related
50
+ serialize_single_relationship(related, association)
51
+ end
52
+ end
53
+
54
+ def serialize_collection_relationship(related, association)
55
+ return [] if related.nil?
56
+
57
+ related.map { |r| serialize_identifier_for_related(r, association) }
58
+ end
59
+
60
+ def serialize_single_relationship(related, association)
61
+ serialize_identifier_for_related(related, association)
62
+ end
63
+
64
+ def serialize_identifier_for_related(related_record, association)
65
+ base_def_for_related, use_instance_class = determine_sti_definition(related_record)
66
+
67
+ RelationshipHelpers.serialize_resource_identifier(
68
+ related_record,
69
+ association:,
70
+ resource_class: definition,
71
+ use_instance_class:,
72
+ base_resource_class: base_def_for_related,
73
+ )
74
+ end
75
+
76
+ def determine_sti_definition(_related_record)
77
+ [nil, true]
78
+ end
79
+ end
80
+ end
81
+ end