jpie 3.10.1 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/lib/json_api/active_storage/detection.rb +19 -1
- data/lib/json_api/railtie.rb +2 -2
- data/lib/json_api/resources/concerns/relationships_dsl.rb +15 -0
- data/lib/json_api/resources/resource_loader.rb +19 -0
- data/lib/json_api/serialization/concerns/attributes_serialization.rb +15 -10
- data/lib/json_api/serialization/concerns/include_filtering.rb +9 -1
- data/lib/json_api/serialization/concerns/includes_serialization.rb +50 -35
- data/lib/json_api/serialization/concerns/meta_serialization.rb +1 -2
- data/lib/json_api/serialization/serializer.rb +8 -0
- data/lib/json_api/support/relationship_helpers.rb +5 -1
- data/lib/json_api/support/resource_identifier.rb +1 -3
- data/lib/json_api/support/type_conversion.rb +22 -4
- data/lib/json_api/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cc59c786e140082618fccd88462e9ea9e3c98d97aa6184752d9d06b0c99d8eb
|
|
4
|
+
data.tar.gz: 4e212599caf45aad70ba6f286a2be35e5d61df0aee6c62e23f3177f827e7bb83
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2360e7d99ce16c2e9a9b485920811cd56f69e6281cd2dfd5b565be44a23c6cea9f9b85f8bd23caabe45b8e8976ff67e650d43bdc3a49ca9de1aab75151dc33db
|
|
7
|
+
data.tar.gz: 29f4244ee319f9a68403f4aeda3f8a497b843c5b67e0cd975704bc5e3f9ac0c4041da86fc008c618ca4552c08b3f167eb6794ae36adc7ad4b14095ec6e3bc7b9
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ at release time.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.11.0]
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Read a model-backed attribute with `has_attribute?` and `read_attribute`.
|
|
15
|
+
The serializer called `ActiveRecord#attributes` three times per attribute per
|
|
16
|
+
record, and each call builds a hash of every column. A record with a fat text
|
|
17
|
+
column paid for that column on all three.
|
|
18
|
+
- Drop the per-record objects in the include walk. Each visited record
|
|
19
|
+
allocated a struct, a hash copy of the path context, and an empty keyword
|
|
20
|
+
hash; the attachment verdict now lifts out of the loop. The relationship
|
|
21
|
+
names an include path asks for are computed once per path, not once per
|
|
22
|
+
record.
|
|
23
|
+
- Precompute each include path's walk once per request. The walk re-split and
|
|
24
|
+
re-joined the path strings at every level for every parent record.
|
|
25
|
+
- Look a relationship definition up through a name-keyed index. The linear
|
|
26
|
+
scan converted every entry's name on every lookup, once per record per hop.
|
|
27
|
+
- Memoize the ActiveStorage attachment verdict per class and name. The
|
|
28
|
+
reflection lookup allocated a string per call, per relationship per record.
|
|
29
|
+
- Resolve a model's resource class and type name through identity-keyed front
|
|
30
|
+
caches. The name-keyed caches allocated their key strings on every call.
|
|
31
|
+
- Serialize a record through one resource instance. The attribute reader and
|
|
32
|
+
the meta reader each built their own, so a resource's memoized reads ran
|
|
33
|
+
twice.
|
|
34
|
+
- Skip the respond_to? probe on a loaded singular association target. The
|
|
35
|
+
probe walked ActiveModel's attribute matcher and allocated a string per
|
|
36
|
+
record.
|
|
37
|
+
|
|
10
38
|
## [3.10.1]
|
|
11
39
|
|
|
12
40
|
### Fixed
|
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "concurrent/map"
|
|
4
|
+
|
|
3
5
|
module JSONAPI
|
|
4
6
|
module ActiveStorage
|
|
5
7
|
module Detection
|
|
8
|
+
# The reflection lookup allocates a string per call, and the serializer
|
|
9
|
+
# asks per relationship per record. The verdict is a function of the
|
|
10
|
+
# class object and the name, so memoize it. Keys are class objects: the
|
|
11
|
+
# railtie clears the cache on code reload so a reloaded class cannot be
|
|
12
|
+
# retained.
|
|
13
|
+
@attachment_cache = Concurrent::Map.new
|
|
14
|
+
|
|
6
15
|
module_function
|
|
7
16
|
|
|
8
17
|
def attachment?(association_name, model_class)
|
|
9
18
|
return false unless defined?(::ActiveStorage)
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
cache = @attachment_cache.compute_if_absent(model_class) { Concurrent::Map.new }
|
|
21
|
+
cached = cache[association_name.to_sym]
|
|
22
|
+
return cached unless cached.nil?
|
|
23
|
+
|
|
24
|
+
cache[association_name.to_sym] =
|
|
25
|
+
model_class.respond_to?(:reflect_on_attachment) &&
|
|
12
26
|
model_class.reflect_on_attachment(association_name.to_sym).present?
|
|
13
27
|
end
|
|
14
28
|
|
|
29
|
+
def reset_cache!
|
|
30
|
+
@attachment_cache.clear
|
|
31
|
+
end
|
|
32
|
+
|
|
15
33
|
def blob_type?(type)
|
|
16
34
|
return false unless defined?(::ActiveStorage)
|
|
17
35
|
|
data/lib/json_api/railtie.rb
CHANGED
|
@@ -105,8 +105,8 @@ module JSONAPI
|
|
|
105
105
|
# that pre-populate ResourceLoader caches in their own to_prepare blocks
|
|
106
106
|
# run after this and re-apply their entries.
|
|
107
107
|
app.reloader.to_prepare do
|
|
108
|
-
JSONAPI::ResourceLoader
|
|
109
|
-
|
|
108
|
+
[JSONAPI::ResourceLoader, JSONAPI::TypeConversion,
|
|
109
|
+
JSONAPI::ActiveStorage::Detection,].each(&:reset_cache!)
|
|
110
110
|
Railtie.setup_base_controllers
|
|
111
111
|
end
|
|
112
112
|
|
|
@@ -31,6 +31,7 @@ module JSONAPI
|
|
|
31
31
|
|
|
32
32
|
def reset_relationship_definitions!
|
|
33
33
|
remove_instance_variable(:@relationship_definitions) if defined?(@relationship_definitions)
|
|
34
|
+
remove_instance_variable(:@relationship_definition_index) if defined?(@relationship_definition_index)
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def relationship_names
|
|
@@ -39,6 +40,20 @@ module JSONAPI
|
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
module RelationshipHelperMethods
|
|
43
|
+
# Name-keyed lookup over relationship_definitions. The serializer asks
|
|
44
|
+
# for one definition by name once per record per hop; a linear scan
|
|
45
|
+
# with a to_s per entry allocated thousands of strings per request.
|
|
46
|
+
# Both spellings key the same entry, so a lookup converts nothing.
|
|
47
|
+
def relationship_definition_index
|
|
48
|
+
return @relationship_definition_index if defined?(@relationship_definition_index)
|
|
49
|
+
|
|
50
|
+
index = relationship_definitions.each_with_object({}) do |r, h|
|
|
51
|
+
h[r[:name]] = r
|
|
52
|
+
h[r[:name].to_s] = r
|
|
53
|
+
end
|
|
54
|
+
@relationship_definition_index = index.freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
42
57
|
def register_relationship(name:, type:, meta:, options:)
|
|
43
58
|
@relationships ||= []
|
|
44
59
|
@relationships << { name: name.to_sym, type:, meta:, options: }
|
|
@@ -25,10 +25,17 @@ module JSONAPI
|
|
|
25
25
|
# caches on code reload.
|
|
26
26
|
@find_cache = Concurrent::Map.new
|
|
27
27
|
@model_cache = Concurrent::Map.new
|
|
28
|
+
# Identity front cache for find_for_model: the string cache key allocated
|
|
29
|
+
# on every call, and the serializer calls per record and per identifier.
|
|
30
|
+
# Nested by fallback flag and namespace so a configuration flip cannot
|
|
31
|
+
# serve a stale class. Values come through the string-keyed path, so an
|
|
32
|
+
# application that pre-populates @model_cache keeps its pins.
|
|
33
|
+
@model_identity_cache = Concurrent::Map.new
|
|
28
34
|
|
|
29
35
|
def self.reset_cache!
|
|
30
36
|
@find_cache.clear
|
|
31
37
|
@model_cache.clear
|
|
38
|
+
@model_identity_cache.clear
|
|
32
39
|
end
|
|
33
40
|
|
|
34
41
|
def self.find(resource_type, namespace: nil)
|
|
@@ -59,6 +66,18 @@ module JSONAPI
|
|
|
59
66
|
def self.find_for_model(model_class, namespace: nil)
|
|
60
67
|
return ActiveStorageBlobResource if active_storage_blob?(model_class)
|
|
61
68
|
|
|
69
|
+
bucket = model_identity_bucket(namespace)
|
|
70
|
+
bucket[model_class] || (bucket[model_class] = find_for_model_uncached(model_class, namespace))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.model_identity_bucket(namespace)
|
|
74
|
+
fallback = @model_identity_cache.compute_if_absent(JSONAPI.configuration.namespace_fallback ? 1 : 0) do
|
|
75
|
+
Concurrent::Map.new
|
|
76
|
+
end
|
|
77
|
+
fallback.compute_if_absent(namespace) { Concurrent::Map.new }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.find_for_model_uncached(model_class, namespace)
|
|
62
81
|
key = model_cache_key(model_class, namespace)
|
|
63
82
|
cached = @model_cache[key]
|
|
64
83
|
return cached if cached
|
|
@@ -20,9 +20,8 @@ module JSONAPI
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def build_attributes_hash
|
|
23
|
-
permitted_attrs = definition.permitted_attributes
|
|
23
|
+
permitted_attrs = definition.permitted_attributes
|
|
24
24
|
attributes = {}
|
|
25
|
-
definition_instance = definition.new(record, {})
|
|
26
25
|
|
|
27
26
|
permitted_attrs.each do |attr_sym|
|
|
28
27
|
attributes[attr_sym] = get_attribute_value(definition_instance, attr_sym)
|
|
@@ -34,16 +33,22 @@ module JSONAPI
|
|
|
34
33
|
def get_attribute_value(definition_instance, attr_sym)
|
|
35
34
|
return definition_instance.public_send(attr_sym) if definition_instance.respond_to?(attr_sym, false)
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
return record.attributes[attr_name] if model_has_attribute?(attr_name)
|
|
39
|
-
|
|
40
|
-
nil
|
|
36
|
+
read_model_attribute(attr_sym.to_s)
|
|
41
37
|
end
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
# ActiveRecord#attributes builds a hash of every column on every call,
|
|
40
|
+
# and this reader called it three times per attribute per record: twice
|
|
41
|
+
# to ask whether the column exists, once to read it. A record with a fat
|
|
42
|
+
# text column paid for that column on all three. has_attribute? and
|
|
43
|
+
# read_attribute answer from the attribute set itself, one attribute at a
|
|
44
|
+
# time. A model that is not ActiveRecord keeps the hash route.
|
|
45
|
+
def read_model_attribute(attr_name)
|
|
46
|
+
if record.respond_to?(:has_attribute?) && record.respond_to?(:read_attribute)
|
|
47
|
+
return record.has_attribute?(attr_name) ? record.read_attribute(attr_name) : nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
attributes = record.attributes if record.respond_to?(:attributes)
|
|
51
|
+
attributes.is_a?(Hash) ? attributes[attr_name] : nil
|
|
47
52
|
end
|
|
48
53
|
end
|
|
49
54
|
end
|
|
@@ -10,7 +10,15 @@ module JSONAPI
|
|
|
10
10
|
# denies. The request-scoped cache memoizes the scopes and the per-id
|
|
11
11
|
# verdicts, so each record is vetted at most once per request.
|
|
12
12
|
def filter_loaded_records(association, related_klass)
|
|
13
|
-
|
|
13
|
+
target = association.target
|
|
14
|
+
# A collection target is an Array; a singular target is the record or
|
|
15
|
+
# nil. A respond_to?(:to_a) probe on a record walks ActiveModel's
|
|
16
|
+
# attribute matcher and allocates a string per probe.
|
|
17
|
+
loaded_array = if target.is_a?(Array)
|
|
18
|
+
target
|
|
19
|
+
else
|
|
20
|
+
target.nil? ? [] : [target]
|
|
21
|
+
end
|
|
14
22
|
return [] if loaded_array.empty?
|
|
15
23
|
|
|
16
24
|
include_filter_cache.filter(loaded_array, related_klass)
|
|
@@ -4,10 +4,19 @@ require_relative "include_filtering"
|
|
|
4
4
|
|
|
5
5
|
module JSONAPI
|
|
6
6
|
module Serialization
|
|
7
|
-
IncludeContext = Struct.new(:fields, :included_records, :processed, :all_includes,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
IncludeContext = Struct.new(:fields, :included_records, :processed, :all_includes, :requested, :walks,
|
|
8
|
+
keyword_init: true,) do
|
|
9
|
+
# Per-request memos: the relationship names each include path asks for,
|
|
10
|
+
# and the precomputed walk for each path. They default lazily, so a
|
|
11
|
+
# context built by hand (a caller outside this gem) works without
|
|
12
|
+
# naming them.
|
|
13
|
+
def requested = self[:requested] ||= {}
|
|
14
|
+
|
|
15
|
+
def walks = self[:walks] ||= {}
|
|
16
|
+
end
|
|
17
|
+
# Precomputed shape of one include path: association symbols per depth and
|
|
18
|
+
# the dotted prefix string that names each depth from the root.
|
|
19
|
+
IncludePathWalk = Struct.new(:names, :prefixes, keyword_init: true)
|
|
11
20
|
|
|
12
21
|
module IncludesSerialization
|
|
13
22
|
include IncludePathHelpers
|
|
@@ -19,7 +28,7 @@ module JSONAPI
|
|
|
19
28
|
|
|
20
29
|
ctx = context || build_include_context(fields, all_includes)
|
|
21
30
|
ctx.all_includes ||= all_includes
|
|
22
|
-
all_includes.each { |path| serialize_include_path(record, path, ctx,
|
|
31
|
+
all_includes.each { |path| serialize_include_path(record, path_walk_for(path, ctx), 0, ctx) }
|
|
23
32
|
ctx.included_records
|
|
24
33
|
end
|
|
25
34
|
|
|
@@ -29,34 +38,45 @@ module JSONAPI
|
|
|
29
38
|
|
|
30
39
|
private
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
# The split, the association symbols, and the prefix strings depend on
|
|
42
|
+
# the path alone, so compute them once per path instead of once per
|
|
43
|
+
# parent record. The old walk re-split and re-joined the path strings at
|
|
44
|
+
# every level for every record it passed.
|
|
45
|
+
def path_walk_for(path, ctx)
|
|
46
|
+
ctx.walks[path] ||= begin
|
|
47
|
+
names = path.split(".").map(&:to_sym)
|
|
48
|
+
prefixes = names.each_with_index.map { |_, i| names[0..i].join(".") }
|
|
49
|
+
IncludePathWalk.new(names: names, prefixes: prefixes)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def serialize_include_path(current_record, walk, depth, ctx)
|
|
54
|
+
association_name = walk.names[depth]
|
|
35
55
|
return unless valid_include_association?(current_record, association_name)
|
|
36
56
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
process_related_records(get_related_records(current_record, association_name), path_ctx, ctx)
|
|
57
|
+
serialize_walk_level(current_record, get_related_records(current_record, association_name),
|
|
58
|
+
walk, depth, ctx,)
|
|
40
59
|
end
|
|
41
60
|
|
|
42
|
-
|
|
61
|
+
# The attachment verdict holds for the whole related array: it reads the
|
|
62
|
+
# association name and the parent class, which do not change inside the
|
|
63
|
+
# loop.
|
|
64
|
+
def serialize_walk_level(current_record, related_array, walk, depth, ctx)
|
|
65
|
+
attachment = self.class.active_storage_attachment?(walk.names[depth], current_record.class)
|
|
66
|
+
nested = depth + 1 < walk.names.size
|
|
67
|
+
|
|
43
68
|
related_array.each do |related_record|
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
serialize_walk_record(related_record, current_record, walk, depth, ctx, attachment: attachment)
|
|
70
|
+
serialize_include_path(related_record, walk, depth + 1, ctx) if nested
|
|
46
71
|
end
|
|
47
72
|
end
|
|
48
73
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def parent_context_for(association_name, current_record)
|
|
56
|
-
if self.class.active_storage_attachment?(association_name, current_record.class)
|
|
57
|
-
{ parent_record: current_record, association_name: }
|
|
74
|
+
def serialize_walk_record(related_record, current_record, walk, depth, ctx, attachment:)
|
|
75
|
+
if attachment
|
|
76
|
+
serialize_and_process_record(related_record, walk.prefixes[depth], ctx,
|
|
77
|
+
parent_record: current_record, association_name: walk.names[depth],)
|
|
58
78
|
else
|
|
59
|
-
|
|
79
|
+
serialize_and_process_record(related_record, walk.prefixes[depth], ctx)
|
|
60
80
|
end
|
|
61
81
|
end
|
|
62
82
|
|
|
@@ -122,12 +142,12 @@ module JSONAPI
|
|
|
122
142
|
end
|
|
123
143
|
|
|
124
144
|
def serialize_and_process_record(related_record, path_to_record, ctx, parent_record: nil, association_name: nil)
|
|
125
|
-
return
|
|
145
|
+
return unless ctx.processed.add?(build_record_key(related_record))
|
|
126
146
|
|
|
127
|
-
requested =
|
|
147
|
+
requested = ctx.requested[path_to_record] ||=
|
|
148
|
+
include_paths_to_relationship_names(ctx.all_includes, path_to_record)
|
|
128
149
|
serializer = child_serializer(related_record, parent_record:, association_name:)
|
|
129
150
|
ctx.included_records << serializer.serialize_record(ctx.fields, requested_relationships: requested)
|
|
130
|
-
ctx.processed.add(build_record_key(related_record))
|
|
131
151
|
end
|
|
132
152
|
|
|
133
153
|
# An included record serializes with the same request context as its
|
|
@@ -138,15 +158,10 @@ module JSONAPI
|
|
|
138
158
|
authorization_context:, include_filter_cache:, namespace:,)
|
|
139
159
|
end
|
|
140
160
|
|
|
161
|
+
# A two-slot array beats the string key: no interpolation, no copy of
|
|
162
|
+
# the class name, and the same class-plus-id identity.
|
|
141
163
|
def build_record_key(related_record)
|
|
142
|
-
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def serialize_nested_path(related_record, path_parts, path_from_root, ctx)
|
|
146
|
-
return unless path_parts.length > 1
|
|
147
|
-
|
|
148
|
-
nested_path = path_parts[1..].join(".")
|
|
149
|
-
serialize_include_path(related_record, nested_path, ctx, path_from_root: path_from_root)
|
|
164
|
+
[related_record.class, related_record.id]
|
|
150
165
|
end
|
|
151
166
|
end
|
|
152
167
|
end
|
|
@@ -83,6 +83,14 @@ module JSONAPI
|
|
|
83
83
|
|
|
84
84
|
attr_reader :record, :definition, :parent_record, :association_name, :authorization_context, :namespace
|
|
85
85
|
|
|
86
|
+
# One resource instance per serialized record, shared by the attribute
|
|
87
|
+
# reader and the meta reader. The two built separate instances, which
|
|
88
|
+
# doubled the instantiation and kept the resource's own memoized reads
|
|
89
|
+
# from being shared between an attribute and a meta entry.
|
|
90
|
+
def definition_instance
|
|
91
|
+
@definition_instance ||= definition.new(record)
|
|
92
|
+
end
|
|
93
|
+
|
|
86
94
|
# Shared per request when the controller passes one in; a standalone
|
|
87
95
|
# serializer builds its own, which still dedupes within its own walk.
|
|
88
96
|
def include_filter_cache
|
|
@@ -63,7 +63,11 @@ module JSONAPI
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def find_relationship_definition(definition, relationship_name)
|
|
66
|
-
definition.
|
|
66
|
+
if definition.respond_to?(:relationship_definition_index)
|
|
67
|
+
definition.relationship_definition_index[relationship_name]
|
|
68
|
+
else
|
|
69
|
+
definition.relationship_definitions.find { |r| r[:name].to_s == relationship_name.to_s }
|
|
70
|
+
end
|
|
67
71
|
end
|
|
68
72
|
end
|
|
69
73
|
end
|
|
@@ -73,9 +73,7 @@ module JSONAPI
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def polymorphic_association?(definition, relationship_name)
|
|
76
|
-
relationship_def = definition
|
|
77
|
-
r[:name].to_s == relationship_name.to_s
|
|
78
|
-
end
|
|
76
|
+
relationship_def = RelationshipHelpers.find_relationship_definition(definition, relationship_name)
|
|
79
77
|
return false unless relationship_def
|
|
80
78
|
|
|
81
79
|
relationship_def[:options][:polymorphic] == true
|
|
@@ -9,15 +9,26 @@ module JSONAPI
|
|
|
9
9
|
# relationship identifier. Cache frozen results keyed by name and format.
|
|
10
10
|
# The railtie clears the cache on code reload.
|
|
11
11
|
@type_name_cache = Concurrent::Map.new
|
|
12
|
+
# Identity front cache: the name-keyed cache still allocated its key
|
|
13
|
+
# string on every call. Nested by kind and format so a lookup allocates
|
|
14
|
+
# nothing; values come through the name-keyed cache, and both clear
|
|
15
|
+
# together on code reload.
|
|
16
|
+
@class_name_cache = Concurrent::Map.new
|
|
12
17
|
|
|
13
18
|
def self.reset_cache!
|
|
14
19
|
@type_name_cache.clear
|
|
20
|
+
@class_name_cache.clear
|
|
15
21
|
end
|
|
16
22
|
|
|
17
23
|
def self.type_name_cache
|
|
18
24
|
@type_name_cache
|
|
19
25
|
end
|
|
20
26
|
|
|
27
|
+
def self.class_name_bucket(kind, format)
|
|
28
|
+
by_kind = @class_name_cache.compute_if_absent(kind) { Concurrent::Map.new }
|
|
29
|
+
by_kind.compute_if_absent(format) { Concurrent::Map.new }
|
|
30
|
+
end
|
|
31
|
+
|
|
21
32
|
module_function
|
|
22
33
|
|
|
23
34
|
def type_to_class_name(type, namespace: nil)
|
|
@@ -38,8 +49,14 @@ module JSONAPI
|
|
|
38
49
|
name = model_class.name
|
|
39
50
|
return format_type_name(name.underscore.pluralize, format) if name.nil?
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
bucket = TypeConversion.class_name_bucket(:model, format)
|
|
53
|
+
bucket[model_class] ||= named_type_name("m|#{name}|#{format}", name.underscore.pluralize, format)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The name-keyed cache stays the source of truth so a reloaded class
|
|
57
|
+
# resolves to the same frozen string.
|
|
58
|
+
def named_type_name(key, full_name, format)
|
|
59
|
+
TypeConversion.type_name_cache[key] ||= format_type_name(full_name, format).freeze
|
|
43
60
|
end
|
|
44
61
|
|
|
45
62
|
def resource_type_name(definition_class, format: nil)
|
|
@@ -47,8 +64,9 @@ module JSONAPI
|
|
|
47
64
|
name = definition_class.name
|
|
48
65
|
return format_type_name(name.sub(/Resource$/, "").underscore.pluralize, format) if name.nil?
|
|
49
66
|
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
bucket = TypeConversion.class_name_bucket(:resource, format)
|
|
68
|
+
bucket[definition_class] ||=
|
|
69
|
+
named_type_name("r|#{name}|#{format}", name.sub(/Resource$/, "").underscore.pluralize, format)
|
|
52
70
|
end
|
|
53
71
|
|
|
54
72
|
def format_type_name(full_name, format)
|
data/lib/json_api/version.rb
CHANGED