Skip to content

Commit 3a75833

Browse files
committed
Build Links with strings, track route setup on resource and relationships
Revert building links with 'url_helpers' for performance reasons
1 parent 68a553f commit 3a75833

File tree

12 files changed

+431
-179
lines changed

12 files changed

+431
-179
lines changed

lib/jsonapi/acts_as_resource_controller.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ def process_request
102102
base_url: base_url,
103103
key_formatter: key_formatter,
104104
route_formatter: route_formatter,
105-
serialization_options: serialization_options
105+
serialization_options: serialization_options,
106+
controller: self
106107
)
107108
op.options[:cache_serializer_output] = !JSONAPI.configuration.resource_cache.nil?
108109

lib/jsonapi/basic_resource.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,9 @@ def inherited(subclass)
448448
end
449449

450450
check_reserved_resource_name(subclass._type, subclass.name)
451+
452+
subclass._routed = false
453+
subclass._warned_missing_route = false
451454
end
452455

453456
def rebuild_relationships(relationships)
@@ -494,7 +497,7 @@ def resource_type_for(model)
494497
end
495498
end
496499

497-
attr_accessor :_attributes, :_relationships, :_type, :_model_hints
500+
attr_accessor :_attributes, :_relationships, :_type, :_model_hints, :_routed, :_warned_missing_route
498501
attr_writer :_allowed_filters, :_paginator, :_allowed_sort
499502

500503
def create(context)

lib/jsonapi/link_builder.rb

Lines changed: 78 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -2,74 +2,85 @@ module JSONAPI
22
class LinkBuilder
33
attr_reader :base_url,
44
:primary_resource_klass,
5+
:route_formatter,
56
:engine,
6-
:routes
7+
:engine_mount_point,
8+
:url_helpers
9+
10+
@@url_helper_methods = {}
711

812
def initialize(config = {})
9-
@base_url = config[:base_url]
13+
@base_url = config[:base_url]
1014
@primary_resource_klass = config[:primary_resource_klass]
11-
@engine = build_engine
12-
13-
if engine?
14-
@routes = @engine.routes
15-
else
16-
@routes = Rails.application.routes
17-
end
15+
@route_formatter = config[:route_formatter]
16+
@engine = build_engine
17+
@engine_mount_point = @engine ? @engine.routes.find_script_name({}) : ""
1818

19-
# ToDo: Use NaiveCache for values. For this we need to not return nils and create composite keys which work
20-
# as efficient cache lookups. This could be an array of the [source.identifier, relationship] since the
21-
# ResourceIdentity will compare equality correctly
19+
# url_helpers may be either a controller which has the route helper methods, or the application router's
20+
# url helpers module, `Rails.application.routes.url_helpers`. Because the method no longer behaves as a
21+
# singleton, and it's expensive to generate the module, the controller is preferred.
22+
@url_helpers = config[:url_helpers]
2223
end
2324

2425
def engine?
2526
!!@engine
2627
end
2728

2829
def primary_resources_url
29-
@primary_resources_url_cached ||= "#{ base_url }#{ primary_resources_path }"
30-
rescue NoMethodError
31-
warn "primary_resources_url for #{@primary_resource_klass} could not be generated" if JSONAPI.configuration.warn_on_missing_routes
30+
if @primary_resource_klass._routed
31+
primary_resources_path = resources_path(primary_resource_klass)
32+
@primary_resources_url_cached ||= "#{ base_url }#{ engine_mount_point }#{ primary_resources_path }"
33+
else
34+
if JSONAPI.configuration.warn_on_missing_routes && !@primary_resource_klass._warned_missing_route
35+
warn "primary_resources_url for #{@primary_resource_klass} could not be generated"
36+
@primary_resource_klass._warned_missing_route = true
37+
end
38+
nil
39+
end
3240
end
3341

3442
def query_link(query_params)
35-
"#{ primary_resources_url }?#{ query_params.to_query }"
43+
url = primary_resources_url
44+
return url if url.nil?
45+
"#{ url }?#{ query_params.to_query }"
3646
end
3747

3848
def relationships_related_link(source, relationship, query_params = {})
39-
if relationship.parent_resource.singleton?
40-
url_helper_name = singleton_related_url_helper_name(relationship)
41-
url = call_url_helper(url_helper_name)
49+
if relationship._routed
50+
url = "#{ self_link(source) }/#{ route_for_relationship(relationship) }"
51+
url = "#{ url }?#{ query_params.to_query }" if query_params.present?
52+
url
4253
else
43-
url_helper_name = related_url_helper_name(relationship)
44-
url = call_url_helper(url_helper_name, source.id)
54+
if JSONAPI.configuration.warn_on_missing_routes && !relationship._warned_missing_route
55+
warn "related_link for #{relationship} could not be generated"
56+
relationship._warned_missing_route = true
57+
end
58+
nil
4559
end
46-
47-
url = "#{ base_url }#{ url }"
48-
url = "#{ url }?#{ query_params.to_query }" if query_params.present?
49-
url
50-
rescue NoMethodError
51-
warn "related_link for #{relationship} could not be generated" if JSONAPI.configuration.warn_on_missing_routes
5260
end
5361

5462
def relationships_self_link(source, relationship)
55-
if relationship.parent_resource.singleton?
56-
url_helper_name = singleton_relationship_self_url_helper_name(relationship)
57-
url = call_url_helper(url_helper_name)
63+
if relationship._routed
64+
"#{ self_link(source) }/relationships/#{ route_for_relationship(relationship) }"
5865
else
59-
url_helper_name = relationship_self_url_helper_name(relationship)
60-
url = call_url_helper(url_helper_name, source.id)
66+
if JSONAPI.configuration.warn_on_missing_routes && !relationship._warned_missing_route
67+
warn "self_link for #{relationship} could not be generated"
68+
relationship._warned_missing_route = true
69+
end
70+
nil
6171
end
62-
63-
url = "#{ base_url }#{ url }"
64-
url
65-
rescue NoMethodError
66-
warn "self_link for #{relationship} could not be generated" if JSONAPI.configuration.warn_on_missing_routes
6772
end
6873

6974
def self_link(source)
70-
"#{ base_url }#{ resource_path(source) }"
71-
rescue NoMethodError
72-
warn "self_link for #{source.class} could not be generated" if JSONAPI.configuration.warn_on_missing_routes
75+
if source.class._routed
76+
resource_url(source)
77+
else
78+
if JSONAPI.configuration.warn_on_missing_routes && !source.class._warned_missing_route
79+
warn "self_link for #{source.class} could not be generated"
80+
source.class._warned_missing_route = true
81+
end
82+
nil
83+
end
7384
end
7485

7586
private
@@ -81,105 +92,55 @@ def build_engine
8192
unless scopes.empty?
8293
"#{ scopes.first.to_s.camelize }::Engine".safe_constantize
8394
end
84-
# :nocov:
95+
96+
# :nocov:
8597
rescue LoadError => _e
8698
nil
87-
# :nocov:
99+
# :nocov:
88100
end
89101
end
90102

91-
def call_url_helper(method, *args)
92-
routes.url_helpers.public_send(method, args)
93-
rescue NoMethodError => e
94-
raise e
103+
def format_route(route)
104+
route_formatter.format(route)
95105
end
96106

97-
def path_from_resource_class(klass)
98-
url_helper_name = resources_url_helper_name_from_class(klass)
99-
call_url_helper(url_helper_name)
100-
end
107+
def formatted_module_path_from_class(klass)
108+
scopes = if @engine
109+
module_scopes_from_class(klass)[1..-1]
110+
else
111+
module_scopes_from_class(klass)
112+
end
101113

102-
def resource_path(source)
103-
url_helper_name = resource_url_helper_name_from_source(source)
104-
if source.class.singleton?
105-
call_url_helper(url_helper_name)
114+
unless scopes.empty?
115+
"/#{ scopes.map {|scope| format_route(scope.to_s.underscore)}.compact.join('/') }/"
106116
else
107-
call_url_helper(url_helper_name, source.id)
117+
"/"
108118
end
109119
end
110120

111-
def primary_resources_path
112-
path_from_resource_class(primary_resource_klass)
121+
def module_scopes_from_class(klass)
122+
klass.name.to_s.split("::")[0...-1]
113123
end
114124

115-
def url_helper_name_from_parts(parts)
116-
(parts << "path").reject(&:blank?).join("_")
125+
def resources_path(source_klass)
126+
formatted_module_path_from_class(source_klass) + format_route(source_klass._type.to_s)
117127
end
118128

119-
def resources_path_parts_from_class(klass)
120-
if engine?
121-
scopes = module_scopes_from_class(klass)[1..-1]
122-
else
123-
scopes = module_scopes_from_class(klass)
124-
end
125-
126-
base_path_name = scopes.map { |scope| scope.underscore }.join("_")
127-
end_path_name = klass._type.to_s
128-
[base_path_name, end_path_name]
129-
end
130-
131-
def resources_url_helper_name_from_class(klass)
132-
url_helper_name_from_parts(resources_path_parts_from_class(klass))
133-
end
129+
def resource_path(source)
130+
url = "#{resources_path(source.class)}"
134131

135-
def resource_path_parts_from_class(klass)
136-
if engine?
137-
scopes = module_scopes_from_class(klass)[1..-1]
138-
else
139-
scopes = module_scopes_from_class(klass)
132+
unless source.class.singleton?
133+
url = "#{url}/#{source.id}"
140134
end
141-
142-
base_path_name = scopes.map { |scope| scope.underscore }.join("_")
143-
end_path_name = klass._type.to_s.singularize
144-
[base_path_name, end_path_name]
145-
end
146-
147-
def resource_url_helper_name_from_source(source)
148-
url_helper_name_from_parts(resource_path_parts_from_class(source.class))
149-
end
150-
151-
def related_url_helper_name(relationship)
152-
relationship_parts = resource_path_parts_from_class(relationship.parent_resource)
153-
relationship_parts << "related"
154-
relationship_parts << relationship.name
155-
url_helper_name_from_parts(relationship_parts)
156-
end
157-
158-
def singleton_related_url_helper_name(relationship)
159-
relationship_parts = []
160-
relationship_parts << "related"
161-
relationship_parts << relationship.name
162-
relationship_parts += resource_path_parts_from_class(relationship.parent_resource)
163-
url_helper_name_from_parts(relationship_parts)
164-
end
165-
166-
def relationship_self_url_helper_name(relationship)
167-
relationship_parts = resource_path_parts_from_class(relationship.parent_resource)
168-
relationship_parts << "relationships"
169-
relationship_parts << relationship.name
170-
url_helper_name_from_parts(relationship_parts)
135+
url
171136
end
172137

173-
def singleton_relationship_self_url_helper_name(relationship)
174-
relationship_parts = []
175-
relationship_parts << "relationships"
176-
relationship_parts << relationship.name
177-
relationship_parts += resource_path_parts_from_class(relationship.parent_resource)
178-
url_helper_name_from_parts(relationship_parts)
138+
def resource_url(source)
139+
"#{ base_url }#{ engine_mount_point }#{ resource_path(source) }"
179140
end
180141

181-
def module_scopes_from_class(klass)
182-
klass.name.to_s.split("::")[0...-1]
142+
def route_for_relationship(relationship)
143+
format_route(relationship.name)
183144
end
184145
end
185146
end

lib/jsonapi/relationship.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ class Relationship
77

88
attr_writer :allow_include
99

10+
attr_accessor :_routed, :_warned_missing_route
11+
1012
def initialize(name, options = {})
1113
@name = name.to_s
1214
@options = options
@@ -27,6 +29,9 @@ def initialize(name, options = {})
2729
@class_name = nil
2830
@inverse_relationship = nil
2931

32+
@_routed = false
33+
@_warned_missing_route = false
34+
3035
exclude_links(options.fetch(:exclude_links, :none))
3136

3237
# Custom methods are reserved for future use

lib/jsonapi/resource_controller_metal.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class ResourceControllerMetal < ActionController::Metal
1010
JSONAPI::ActsAsResourceController
1111
].freeze
1212

13+
# Note, the url_helpers are not loaded. This will prevent links from being generated for resources, and warnings
14+
# will be emitted. Link support can be added by including `Rails.application.routes.url_helpers`, and links
15+
# can be disabled, and warning suppressed, for a resource with `exclude_links :default`
1316
MODULES.each do |mod|
1417
include mod
1518
end

lib/jsonapi/resource_serializer.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,8 @@ def generate_link_builder(primary_resource_klass, options)
381381
LinkBuilder.new(
382382
base_url: options.fetch(:base_url, ''),
383383
primary_resource_klass: primary_resource_klass,
384+
route_formatter: options.fetch(:route_formatter, JSONAPI.configuration.route_formatter),
385+
url_helpers: options.fetch(:url_helpers, options[:controller]),
384386
)
385387
end
386388
end

lib/jsonapi/routing_ext.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ def jsonapi_resource(*resources, &_block)
2020
@resource_type = resources.first
2121
res = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix(@resource_type))
2222

23+
res._routed = true
24+
2325
unless res.singleton?
2426
warn "Singleton routes created for non singleton resource #{res}. Links may not be generated correctly."
2527
end
@@ -84,6 +86,8 @@ def jsonapi_resources(*resources, &_block)
8486
@resource_type = resources.first
8587
res = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix(@resource_type))
8688

89+
res._routed = true
90+
8791
if res.singleton?
8892
warn "Singleton resource #{res} should use `jsonapi_resource` instead."
8993
end
@@ -220,6 +224,8 @@ def jsonapi_related_resource(*relationship)
220224
relationship_name = relationship.first
221225
relationship = source._relationships[relationship_name]
222226

227+
relationship._routed = true
228+
223229
formatted_relationship_name = format_route(relationship.name)
224230

225231
if relationship.polymorphic?
@@ -242,6 +248,8 @@ def jsonapi_related_resources(*relationship)
242248
relationship_name = relationship.first
243249
relationship = source._relationships[relationship_name]
244250

251+
relationship._routed = true
252+
245253
formatted_relationship_name = format_route(relationship.name)
246254
related_resource = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix(relationship.class_name.underscore))
247255
options[:controller] ||= related_resource._type.to_s

test/fixtures/active_record.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,7 @@ def create_responses_relationships
901901
end
902902

903903
class AuthorsController < JSONAPI::ResourceControllerMetal
904+
include Rails.application.routes.url_helpers
904905
end
905906

906907
class PeopleController < JSONAPI::ResourceController
@@ -1991,7 +1992,9 @@ module V4
19911992
class PostResource < PostResource; end
19921993
class PersonResource < PersonResource; end
19931994
class ExpenseEntryResource < ExpenseEntryResource; end
1994-
class IsoCurrencyResource < IsoCurrencyResource; end
1995+
class IsoCurrencyResource < IsoCurrencyResource
1996+
has_many :expense_entries, exclude_links: :default
1997+
end
19951998

19961999
class AuthorResource < Api::V2::AuthorResource; end
19972000

@@ -2389,6 +2392,7 @@ class PersonResource < JSONAPI::Resource
23892392

23902393
module ApiV2Engine
23912394
class PostResource < PostResource
2395+
has_one :person
23922396
end
23932397

23942398
class PersonResource < JSONAPI::Resource

0 commit comments

Comments
 (0)