Skip to content

Commit 0a344a0

Browse files
authored
Merge pull request #1262 from cerebris/linkbuilder_track_routes_0_9
Build Links with strings, track route setup on resource and relationships
2 parents 60b043a + c31a32b commit 0a344a0

12 files changed

+690
-692
lines changed

lib/jsonapi/acts_as_resource_controller.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ def resource_serializer
125125
base_url: base_url,
126126
key_formatter: key_formatter,
127127
route_formatter: route_formatter,
128-
serialization_options: serialization_options
128+
serialization_options: serialization_options,
129+
controller: self
129130
)
130131
@resource_serializer
131132
end

lib/jsonapi/link_builder.rb

Lines changed: 76 additions & 115 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
95+
8496
# :nocov:
8597
rescue LoadError => _e
8698
nil
8799
# :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
@@ -4,6 +4,8 @@ class Relationship
44
:class_name, :polymorphic, :always_include_linkage_data,
55
:parent_resource, :eager_load_on_include
66

7+
attr_accessor :_routed, :_warned_missing_route
8+
79
def initialize(name, options = {})
810
@name = name.to_s
911
@options = options
@@ -15,6 +17,9 @@ def initialize(name, options = {})
1517
@always_include_linkage_data = options.fetch(:always_include_linkage_data, false) == true
1618
@eager_load_on_include = options.fetch(:eager_load_on_include, true) == true
1719

20+
@_routed = false
21+
@_warned_missing_route = false
22+
1823
exclude_links(options.fetch(:exclude_links, :none))
1924
end
2025

lib/jsonapi/resource.rb

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

459459
check_reserved_resource_name(subclass._type, subclass.name)
460+
461+
subclass._routed = false
462+
subclass._warned_missing_route = false
460463
end
461464

462465
def rebuild_relationships(relationships)
@@ -502,7 +505,7 @@ def resource_type_for(model)
502505
end
503506
end
504507

505-
attr_accessor :_attributes, :_relationships, :_type, :_model_hints
508+
attr_accessor :_attributes, :_relationships, :_type, :_model_hints, :_routed, :_warned_missing_route
506509
attr_writer :_allowed_filters, :_paginator
507510

508511
def create(context)

lib/jsonapi/resource_serializer.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,8 @@ def generate_link_builder(primary_resource_klass, options)
539539
LinkBuilder.new(
540540
base_url: options.fetch(:base_url, ''),
541541
primary_resource_klass: primary_resource_klass,
542+
route_formatter: options.fetch(:route_formatter, JSONAPI.configuration.route_formatter),
543+
url_helpers: options.fetch(:url_helpers, options[:controller]),
542544
)
543545
end
544546
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_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_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
@@ -223,6 +227,8 @@ def jsonapi_related_resource(*relationship)
223227
relationship_name = relationship.first
224228
relationship = source._relationships[relationship_name]
225229

230+
relationship._routed = true
231+
226232
formatted_relationship_name = format_route(relationship.name)
227233

228234
if relationship.polymorphic?
@@ -245,6 +251,8 @@ def jsonapi_related_resources(*relationship)
245251
relationship_name = relationship.first
246252
relationship = source._relationships[relationship_name]
247253

254+
relationship._routed = true
255+
248256
formatted_relationship_name = format_route(relationship.name)
249257
related_resource = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(relationship.class_name.underscore))
250258
options[:controller] ||= related_resource._type.to_s

test/fixtures/active_record.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1681,7 +1681,9 @@ module V4
16811681
class PostResource < PostResource; end
16821682
class PersonResource < PersonResource; end
16831683
class ExpenseEntryResource < ExpenseEntryResource; end
1684-
class IsoCurrencyResource < IsoCurrencyResource; end
1684+
class IsoCurrencyResource < IsoCurrencyResource
1685+
has_many :expense_entries, exclude_links: :default
1686+
end
16851687

16861688
class BookResource < Api::V2::BookResource
16871689
paginator :paged
@@ -1987,6 +1989,7 @@ class PersonResource < JSONAPI::Resource
19871989

19881990
module ApiV2Engine
19891991
class PostResource < PostResource
1992+
has_one :person
19901993
end
19911994

19921995
class PersonResource < JSONAPI::Resource

test/integration/requests/request_test.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def test_get_camelized_route_and_key_filtered
9292
def test_get_camelized_route_and_links
9393
original_config = JSONAPI.configuration.dup
9494
JSONAPI.configuration.json_key_format = :camelized_key
95+
JSONAPI.configuration.route_format = :camelized_route
9596
assert_cacheable_jsonapi_get '/api/v4/expenseEntries/1/relationships/isoCurrency'
9697
assert_hash_equals({'links' => {
9798
'self' => 'http://www.example.com/api/v4/expenseEntries/1/relationships/isoCurrency',

0 commit comments

Comments
 (0)