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.
- checksums.yaml +4 -4
- data/.cursor/rules/release.mdc +62 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +82 -38
- data/Gemfile +12 -10
- data/Gemfile.lock +10 -1
- data/README.md +675 -1235
- data/jpie.gemspec +15 -15
- data/kiln/app/resources/user_message_resource.rb +2 -0
- data/lib/jpie.rb +0 -1
- data/lib/json_api/active_storage/deserialization.rb +32 -22
- data/lib/json_api/active_storage/detection.rb +36 -41
- data/lib/json_api/active_storage/serialization.rb +13 -11
- data/lib/json_api/configuration.rb +4 -5
- data/lib/json_api/controllers/base_controller.rb +3 -3
- data/lib/json_api/controllers/concerns/controller_helpers/authorization.rb +30 -0
- data/lib/json_api/controllers/concerns/controller_helpers/document_meta.rb +20 -0
- data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +64 -0
- data/lib/json_api/controllers/concerns/controller_helpers/parsing.rb +127 -0
- data/lib/json_api/controllers/concerns/controller_helpers/resource_setup.rb +38 -0
- data/lib/json_api/controllers/concerns/controller_helpers.rb +11 -215
- data/lib/json_api/controllers/concerns/relationships/active_storage_removal.rb +65 -0
- data/lib/json_api/controllers/concerns/relationships/events.rb +44 -0
- data/lib/json_api/controllers/concerns/relationships/removal.rb +92 -0
- data/lib/json_api/controllers/concerns/relationships/response_helpers.rb +55 -0
- data/lib/json_api/controllers/concerns/relationships/serialization.rb +72 -0
- data/lib/json_api/controllers/concerns/relationships/sorting.rb +114 -0
- data/lib/json_api/controllers/concerns/relationships/updating.rb +73 -0
- data/lib/json_api/controllers/concerns/relationships_controller/active_storage_removal.rb +67 -0
- data/lib/json_api/controllers/concerns/relationships_controller/events.rb +44 -0
- data/lib/json_api/controllers/concerns/relationships_controller/removal.rb +92 -0
- data/lib/json_api/controllers/concerns/relationships_controller/response_helpers.rb +55 -0
- data/lib/json_api/controllers/concerns/relationships_controller/serialization.rb +72 -0
- data/lib/json_api/controllers/concerns/relationships_controller/sorting.rb +114 -0
- data/lib/json_api/controllers/concerns/relationships_controller/updating.rb +73 -0
- data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +93 -0
- data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +114 -0
- data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +91 -0
- data/lib/json_api/controllers/concerns/resource_actions/pagination.rb +51 -0
- data/lib/json_api/controllers/concerns/resource_actions/preloading.rb +64 -0
- data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +71 -0
- data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +63 -0
- data/lib/json_api/controllers/concerns/resource_actions/type_validation.rb +75 -0
- data/lib/json_api/controllers/concerns/resource_actions.rb +51 -602
- data/lib/json_api/controllers/relationships_controller.rb +26 -422
- data/lib/json_api/errors/parameter_not_allowed.rb +1 -1
- data/lib/json_api/railtie.rb +46 -9
- data/lib/json_api/resources/active_storage_blob_resource.rb +9 -1
- data/lib/json_api/resources/concerns/attributes_dsl.rb +69 -0
- data/lib/json_api/resources/concerns/filters_dsl.rb +32 -0
- data/lib/json_api/resources/concerns/meta_dsl.rb +23 -0
- data/lib/json_api/resources/concerns/model_class_helpers.rb +37 -0
- data/lib/json_api/resources/concerns/relationships_dsl.rb +71 -0
- data/lib/json_api/resources/concerns/sortable_fields_dsl.rb +36 -0
- data/lib/json_api/resources/resource.rb +13 -219
- data/lib/json_api/routing.rb +56 -47
- data/lib/json_api/serialization/concerns/attributes_deserialization.rb +27 -0
- data/lib/json_api/serialization/concerns/attributes_serialization.rb +50 -0
- data/lib/json_api/serialization/concerns/deserialization_helpers.rb +115 -0
- data/lib/json_api/serialization/concerns/includes_serialization.rb +82 -0
- data/lib/json_api/serialization/concerns/links_serialization.rb +33 -0
- data/lib/json_api/serialization/concerns/meta_serialization.rb +60 -0
- data/lib/json_api/serialization/concerns/model_attributes_transformation.rb +69 -0
- data/lib/json_api/serialization/concerns/relationship_processing.rb +119 -0
- data/lib/json_api/serialization/concerns/relationships_deserialization.rb +47 -0
- data/lib/json_api/serialization/concerns/relationships_serialization.rb +81 -0
- data/lib/json_api/serialization/deserializer.rb +10 -346
- data/lib/json_api/serialization/serializer.rb +17 -260
- data/lib/json_api/support/active_storage_support.rb +10 -13
- data/lib/json_api/support/collection_query.rb +14 -370
- data/lib/json_api/support/concerns/condition_building.rb +57 -0
- data/lib/json_api/support/concerns/nested_filters.rb +130 -0
- data/lib/json_api/support/concerns/pagination.rb +30 -0
- data/lib/json_api/support/concerns/polymorphic_filters.rb +75 -0
- data/lib/json_api/support/concerns/regular_filters.rb +81 -0
- data/lib/json_api/support/concerns/sorting.rb +88 -0
- data/lib/json_api/support/instrumentation.rb +13 -12
- data/lib/json_api/support/param_helpers.rb +9 -6
- data/lib/json_api/support/relationship_helpers.rb +4 -2
- data/lib/json_api/support/resource_identifier.rb +29 -29
- data/lib/json_api/support/responders.rb +5 -5
- data/lib/json_api/version.rb +1 -1
- 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
|