jpie 1.3.1 → 1.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dea557b52cade83077a80c1371aef0a85dc756bbe871e24c345cf9c8caa67fdf
4
- data.tar.gz: 2981d0c427c7b3e7b4ab7647614823ed324185812dc92c5daf9124c06c5c55b8
3
+ metadata.gz: be60282bbba362a7ac36dca3370a4733981bce7a949352513244879bc76546a7
4
+ data.tar.gz: 6c391beea011b7fd348f8a2439130ba174dd31fa7b8f9f4ea6eaef5c9fd916ee
5
5
  SHA512:
6
- metadata.gz: c58a23284f8d71094e492a75ef5e7e814b796fe97955ca67f916fea1b27442f73f6d6879c899f6cf488738ecd4f701374b1de9a82a4727623ec80168c7cab511
7
- data.tar.gz: 1922cb663461ba5e2c3e146489d8fbffb9a5449642cc4a6245ce643829cd108cb58fa85d11bb21fd6f84a76b4c5f102103f8ea27c6c11ed1916427b4658ff1fb
6
+ metadata.gz: 13f21d51760a269d54d1bcd46a9be3916506bae8c25af82eaabea8dc7a214d2f3d39b29437ca0703141a794444d13b7ab20ba26ac4f35f4fe3a8ca15207d1630
7
+ data.tar.gz: 35e11173e5942bc01059217acf3a5028f5980108b2c76e88b33fe03c9c2ae00a4a6a9c0051328e491a257d83700f02f6a206c7a34406cde968af1a6698ac63fd
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jpie (1.3.1)
4
+ jpie (1.5.1)
5
5
  actionpack (~> 8.0, >= 8.0.0)
6
6
  rails (~> 8.0, >= 8.0.0)
7
7
 
data/README.md CHANGED
@@ -238,6 +238,8 @@ Nested includes use dot notation and support arbitrary depth. Related resources
238
238
  }
239
239
  ```
240
240
 
241
+ Only relationships that appear in the `include` path are serialized (and thus loaded). For example, `include=messages` adds messages to `included` but does not serialize each message's `user`, `message_action`, or other relationships, so those associations are not queried. Use `include=messages,messages.user,messages.message_action` (etc.) to include and load nested relationships.
242
+
241
243
  ## Polymorphic Relationships
242
244
 
243
245
  Polymorphic relationships are accessed through the parent resource's relationship endpoints and includes. Route only the parent resource; the gem automatically handles polymorphic types through the relationship endpoints.
@@ -1392,7 +1394,7 @@ api.define(
1392
1394
  },
1393
1395
  {
1394
1396
  collectionPath: "users",
1395
- }
1397
+ },
1396
1398
  );
1397
1399
 
1398
1400
  // Model with relationships
@@ -1412,7 +1414,7 @@ api.define(
1412
1414
  },
1413
1415
  {
1414
1416
  collectionPath: "posts",
1415
- }
1417
+ },
1416
1418
  );
1417
1419
  ```
1418
1420
 
@@ -1622,7 +1624,7 @@ api.define(
1622
1624
  },
1623
1625
  {
1624
1626
  collectionPath: "workstreams",
1625
- }
1627
+ },
1626
1628
  );
1627
1629
 
1628
1630
  // Create with polymorphic relationship
@@ -65,7 +65,14 @@ module JSONAPI
65
65
  end
66
66
 
67
67
  def define_explicit_sti_routes(sti_resources, defaults)
68
- sti_resources.each { |sub_resource_name| jsonapi_resources(sub_resource_name, defaults:) }
68
+ sti_resources.each do |entry|
69
+ case entry
70
+ in Hash
71
+ entry.each { |sub_resource_name, options| jsonapi_resources(sub_resource_name, defaults:, **options) }
72
+ else
73
+ jsonapi_resources(entry, defaults:)
74
+ end
75
+ end
69
76
  end
70
77
 
71
78
  def define_auto_sti_routes(resource, resource_name, defaults, namespace = nil)
@@ -2,32 +2,62 @@
2
2
 
3
3
  module JSONAPI
4
4
  module Serialization
5
+ IncludeContext = Struct.new(:fields, :included_records, :processed, :all_includes, keyword_init: true)
6
+ PathContext = Struct.new(:path_parts, :path_to_related, :association_name, :current_record, keyword_init: true)
7
+ IncludeIteration = Struct.new(:related_record, :path_parts, :path_to_related, :association_name, :current_record,
8
+ keyword_init: true,)
9
+
5
10
  module IncludesSerialization
6
- def serialize_included(includes, fields = {})
7
- return [] if includes.empty?
11
+ include IncludePathHelpers
8
12
 
9
- included_records = []
10
- processed = Set.new
13
+ def serialize_included(includes, fields = {})
14
+ all_includes = normalize_include_paths(includes)
15
+ return [] if all_includes.empty?
11
16
 
12
- includes.each do |include_path|
13
- serialize_include_path(record, include_path, fields, included_records, processed)
14
- end
17
+ ctx = build_include_context(fields, all_includes)
18
+ all_includes.each { |path| serialize_include_path(record, path, ctx, path_from_root: "") }
19
+ ctx.included_records
20
+ end
15
21
 
16
- included_records
22
+ def build_include_context(fields, all_includes)
23
+ IncludeContext.new(
24
+ fields: fields,
25
+ included_records: [],
26
+ processed: Set.new,
27
+ all_includes: all_includes,
28
+ )
17
29
  end
18
30
 
19
31
  private
20
32
 
21
- def serialize_include_path(current_record, include_path, fields, included_records, processed)
33
+ def serialize_include_path(current_record, include_path, ctx, path_from_root: "")
22
34
  path_parts = include_path.split(".")
23
35
  association_name = path_parts.first.to_sym
24
-
25
36
  return unless valid_include_association?(current_record, association_name)
26
37
 
27
- related_array = get_related_records(current_record, association_name)
38
+ path_to_related = path_from_root.blank? ? path_parts.first : "#{path_from_root}.#{path_parts.first}"
39
+ path_ctx = PathContext.new(path_parts:, path_to_related:, association_name:, current_record: current_record)
40
+ process_related_records(get_related_records(current_record, association_name), path_ctx, ctx)
41
+ end
42
+
43
+ def process_related_records(related_array, path_ctx, ctx)
28
44
  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)
45
+ iter = IncludeIteration.new(related_record: related_record, **path_ctx.to_h)
46
+ serialize_related_record(iter, ctx)
47
+ end
48
+ end
49
+
50
+ def serialize_related_record(iter, ctx)
51
+ parent_context = parent_context_for(iter.association_name, iter.current_record)
52
+ serialize_and_process_record(iter.related_record, iter.path_to_related, ctx, **parent_context)
53
+ serialize_nested_path(iter.related_record, iter.path_parts, iter.path_to_related, ctx)
54
+ end
55
+
56
+ def parent_context_for(association_name, current_record)
57
+ if self.class.active_storage_attachment?(association_name, current_record.class)
58
+ { parent_record: current_record, association_name: }
59
+ else
60
+ {}
31
61
  end
32
62
  end
33
63
 
@@ -54,7 +84,16 @@ module JSONAPI
54
84
  related_klass = association.klass
55
85
  return [] if related_klass.blank?
56
86
 
57
- build_scoped_relation(related_klass, association).to_a
87
+ scope = build_scoped_relation(related_klass, association)
88
+ association.loaded? ? scope_loaded_to_records_scope(association, scope) : scope.to_a
89
+ end
90
+
91
+ def scope_loaded_to_records_scope(association, scope)
92
+ loaded_array = association.target.respond_to?(:to_a) ? association.target.to_a : Array(association.target)
93
+ loaded_ids = loaded_array.filter_map(&:id)
94
+ return [] if loaded_ids.empty?
95
+
96
+ scope.where(id: loaded_ids).to_a
58
97
  end
59
98
 
60
99
  def build_scoped_relation(related_klass, association)
@@ -70,24 +109,24 @@ module JSONAPI
70
109
  [attachment.blob].compact
71
110
  end
72
111
 
73
- def serialize_and_process_record(related_record, fields, included_records, processed)
74
- record_key = build_record_key(related_record)
75
- return if processed.include?(record_key)
112
+ def serialize_and_process_record(related_record, path_to_record, ctx, parent_record: nil, association_name: nil)
113
+ return if ctx.processed.include?(build_record_key(related_record))
76
114
 
77
- serializer = self.class.new(related_record)
78
- included_records << serializer.serialize_record(fields)
79
- processed.add(record_key)
115
+ requested = include_paths_to_relationship_names(ctx.all_includes, path_to_record)
116
+ serializer = self.class.new(related_record, parent_record:, association_name:)
117
+ ctx.included_records << serializer.serialize_record(ctx.fields, requested_relationships: requested)
118
+ ctx.processed.add(build_record_key(related_record))
80
119
  end
81
120
 
82
121
  def build_record_key(related_record)
83
122
  "#{related_record.class.name}-#{related_record.id}"
84
123
  end
85
124
 
86
- def serialize_nested_path(related_record, path_parts, fields, included_records, processed)
125
+ def serialize_nested_path(related_record, path_parts, path_from_root, ctx)
87
126
  return unless path_parts.length > 1
88
127
 
89
128
  nested_path = path_parts[1..].join(".")
90
- serialize_include_path(related_record, nested_path, fields, included_records, processed)
129
+ serialize_include_path(related_record, nested_path, ctx, path_from_root: path_from_root)
91
130
  end
92
131
  end
93
132
  end
@@ -16,11 +16,23 @@ module JSONAPI
16
16
  end
17
17
 
18
18
  def add_active_storage_download_link(links)
19
- links[:download] = rails_blob_path || fallback_blob_path
19
+ links[:download] = variant_or_blob_path || fallback_blob_path
20
20
  rescue StandardError
21
21
  links[:download] = fallback_blob_path
22
22
  end
23
23
 
24
+ def variant_or_blob_path
25
+ return rails_blob_path unless parent_record && association_name && record.content_type&.start_with?("image/")
26
+
27
+ parent_definition = ResourceLoader.find_for_model(parent_record.class)
28
+ rel_def = RelationshipHelpers.find_relationship_definition(parent_definition, association_name)
29
+ variant_opts = rel_def&.dig(:options, :variant)
30
+ return rails_blob_path unless variant_opts.present?
31
+
32
+ representation = record.representation(**variant_opts.symbolize_keys)
33
+ Rails.application.routes.url_helpers.rails_representation_path(representation, only_path: true)
34
+ end
35
+
24
36
  def rails_blob_path
25
37
  Rails.application.routes.url_helpers.rails_blob_path(record, only_path: true)
26
38
  end
@@ -3,9 +3,13 @@
3
3
  module JSONAPI
4
4
  module Serialization
5
5
  module RelationshipsSerialization
6
- def serialize_relationships
6
+ def serialize_relationships(requested_relationship_names = nil)
7
7
  relationships = {}
8
8
  relationship_definitions = definition.relationship_definitions
9
+ if requested_relationship_names
10
+ requested_set = requested_relationship_names.map(&:to_sym)
11
+ relationship_definitions = relationship_definitions.select { |r| requested_set.include?(r[:name]) }
12
+ end
9
13
 
10
14
  relationship_definitions.each do |rel_def|
11
15
  serialize_relationship(rel_def, relationships)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Serialization
5
+ module IncludePathHelpers
6
+ def normalize_include_paths(include_param)
7
+ case include_param
8
+ when Array
9
+ include_param.flat_map { |s| s.to_s.split(",").map(&:strip) }.uniq
10
+ when String
11
+ include_param.split(",").map(&:strip)
12
+ else
13
+ []
14
+ end
15
+ end
16
+
17
+ def include_paths_to_relationship_names(include_paths, path_prefix)
18
+ return [] if include_paths.blank?
19
+
20
+ prefix = path_prefix.present? ? "#{path_prefix}." : ""
21
+ include_paths.filter_map do |p|
22
+ path = p.to_s
23
+ next unless path == path_prefix || path.start_with?(prefix)
24
+
25
+ segments = path.split(".")
26
+ prefix_segments = path_prefix.split(".")
27
+ next if segments.length <= prefix_segments.length
28
+
29
+ segments[prefix_segments.length].to_sym
30
+ end.uniq
31
+ end
32
+ end
33
+ end
34
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative "concerns/attributes_serialization"
4
4
  require_relative "concerns/relationships_serialization"
5
5
  require_relative "concerns/links_serialization"
6
+ require_relative "include_path_helpers"
6
7
  require_relative "concerns/includes_serialization"
7
8
  require_relative "concerns/meta_serialization"
8
9
 
@@ -33,28 +34,32 @@ module JSONAPI
33
34
  @jsonapi_object = nil
34
35
  end
35
36
 
36
- def initialize(record, definition: nil, base_definition: nil)
37
+ def initialize(record, definition: nil, base_definition: nil, parent_record: nil, association_name: nil)
37
38
  @record = record
38
39
  @definition = definition || ResourceLoader.find_for_model(record.class)
39
40
  @base_definition = base_definition
41
+ @parent_record = parent_record
42
+ @association_name = association_name
40
43
  @sti_subclass = nil
41
44
  end
42
45
 
43
46
  def to_hash(include: [], fields: {}, document_meta: nil)
47
+ include_paths = normalize_include_paths(include)
48
+ top_level_relationships = include_paths_to_relationship_names(include_paths, "")
44
49
  {
45
50
  jsonapi: jsonapi_object,
46
- data: serialize_record(fields),
47
- included: serialize_included(include, fields),
51
+ data: serialize_record(fields, requested_relationships: top_level_relationships),
52
+ included: serialize_included(include_paths, fields),
48
53
  meta: document_meta,
49
54
  }.compact
50
55
  end
51
56
 
52
- def serialize_record(fields = {})
57
+ def serialize_record(fields = {}, requested_relationships: nil)
53
58
  {
54
59
  type: record_type,
55
60
  id: record_id,
56
61
  attributes: serialize_attributes(fields),
57
- relationships: serialize_relationships,
62
+ relationships: serialize_relationships(requested_relationships),
58
63
  links: serialize_links,
59
64
  meta: record_meta,
60
65
  }.compact
@@ -62,7 +67,7 @@ module JSONAPI
62
67
 
63
68
  private
64
69
 
65
- attr_reader :record, :definition
70
+ attr_reader :record, :definition, :parent_record, :association_name
66
71
 
67
72
  def base_definition
68
73
  @base_definition ||= definition
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONAPI
4
- VERSION = "1.3.1"
4
+ VERSION = "1.5.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jpie
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Kampp
@@ -70,7 +70,6 @@ files:
70
70
  - bin/console
71
71
  - bin/setup
72
72
  - jpie.gemspec
73
- - kiln/app/resources/user_message_resource.rb
74
73
  - lib/jpie.rb
75
74
  - lib/json_api.rb
76
75
  - lib/json_api/active_storage/deserialization.rb
@@ -127,6 +126,7 @@ files:
127
126
  - lib/json_api/serialization/concerns/relationships_deserialization.rb
128
127
  - lib/json_api/serialization/concerns/relationships_serialization.rb
129
128
  - lib/json_api/serialization/deserializer.rb
129
+ - lib/json_api/serialization/include_path_helpers.rb
130
130
  - lib/json_api/serialization/serializer.rb
131
131
  - lib/json_api/support/active_storage_support.rb
132
132
  - lib/json_api/support/collection_query.rb
@@ -1,4 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class UserMessageResource < MessageResource
4
- end