Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/jsonapi/acts_as_resource_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ def process_request
base_url: base_url,
key_formatter: key_formatter,
route_formatter: route_formatter,
serialization_options: serialization_options
serialization_options: serialization_options,
controller: self
)
op.options[:cache_serializer_output] = !JSONAPI.configuration.resource_cache.nil?

Expand Down
5 changes: 4 additions & 1 deletion lib/jsonapi/basic_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,9 @@ def inherited(subclass)
end

check_reserved_resource_name(subclass._type, subclass.name)

subclass._routed = false
subclass._warned_missing_route = false
end

def rebuild_relationships(relationships)
Expand Down Expand Up @@ -494,7 +497,7 @@ def resource_type_for(model)
end
end

attr_accessor :_attributes, :_relationships, :_type, :_model_hints
attr_accessor :_attributes, :_relationships, :_type, :_model_hints, :_routed, :_warned_missing_route
attr_writer :_allowed_filters, :_paginator, :_allowed_sort

def create(context)
Expand Down
195 changes: 78 additions & 117 deletions lib/jsonapi/link_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,74 +2,85 @@ module JSONAPI
class LinkBuilder
attr_reader :base_url,
:primary_resource_klass,
:route_formatter,
:engine,
:routes
:engine_mount_point,
:url_helpers

@@url_helper_methods = {}

def initialize(config = {})
@base_url = config[:base_url]
@base_url = config[:base_url]
@primary_resource_klass = config[:primary_resource_klass]
@engine = build_engine

if engine?
@routes = @engine.routes
else
@routes = Rails.application.routes
end
@route_formatter = config[:route_formatter]
@engine = build_engine
@engine_mount_point = @engine ? @engine.routes.find_script_name({}) : ""

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

def engine?
!!@engine
end

def primary_resources_url
@primary_resources_url_cached ||= "#{ base_url }#{ primary_resources_path }"
rescue NoMethodError
warn "primary_resources_url for #{@primary_resource_klass} could not be generated" if JSONAPI.configuration.warn_on_missing_routes
if @primary_resource_klass._routed
primary_resources_path = resources_path(primary_resource_klass)
@primary_resources_url_cached ||= "#{ base_url }#{ engine_mount_point }#{ primary_resources_path }"
else
if JSONAPI.configuration.warn_on_missing_routes && !@primary_resource_klass._warned_missing_route
warn "primary_resources_url for #{@primary_resource_klass} could not be generated"
@primary_resource_klass._warned_missing_route = true
end
nil
end
end

def query_link(query_params)
"#{ primary_resources_url }?#{ query_params.to_query }"
url = primary_resources_url
return url if url.nil?
"#{ url }?#{ query_params.to_query }"
end

def relationships_related_link(source, relationship, query_params = {})
if relationship.parent_resource.singleton?
url_helper_name = singleton_related_url_helper_name(relationship)
url = call_url_helper(url_helper_name)
if relationship._routed
url = "#{ self_link(source) }/#{ route_for_relationship(relationship) }"
url = "#{ url }?#{ query_params.to_query }" if query_params.present?
url
else
url_helper_name = related_url_helper_name(relationship)
url = call_url_helper(url_helper_name, source.id)
if JSONAPI.configuration.warn_on_missing_routes && !relationship._warned_missing_route
warn "related_link for #{relationship} could not be generated"
relationship._warned_missing_route = true
end
nil
end

url = "#{ base_url }#{ url }"
url = "#{ url }?#{ query_params.to_query }" if query_params.present?
url
rescue NoMethodError
warn "related_link for #{relationship} could not be generated" if JSONAPI.configuration.warn_on_missing_routes
end

def relationships_self_link(source, relationship)
if relationship.parent_resource.singleton?
url_helper_name = singleton_relationship_self_url_helper_name(relationship)
url = call_url_helper(url_helper_name)
if relationship._routed
"#{ self_link(source) }/relationships/#{ route_for_relationship(relationship) }"
else
url_helper_name = relationship_self_url_helper_name(relationship)
url = call_url_helper(url_helper_name, source.id)
if JSONAPI.configuration.warn_on_missing_routes && !relationship._warned_missing_route
warn "self_link for #{relationship} could not be generated"
relationship._warned_missing_route = true
end
nil
end

url = "#{ base_url }#{ url }"
url
rescue NoMethodError
warn "self_link for #{relationship} could not be generated" if JSONAPI.configuration.warn_on_missing_routes
end

def self_link(source)
"#{ base_url }#{ resource_path(source) }"
rescue NoMethodError
warn "self_link for #{source.class} could not be generated" if JSONAPI.configuration.warn_on_missing_routes
if source.class._routed
resource_url(source)
else
if JSONAPI.configuration.warn_on_missing_routes && !source.class._warned_missing_route
warn "self_link for #{source.class} could not be generated"
source.class._warned_missing_route = true
end
nil
end
end

private
Expand All @@ -81,105 +92,55 @@ def build_engine
unless scopes.empty?
"#{ scopes.first.to_s.camelize }::Engine".safe_constantize
end
# :nocov:

# :nocov:
rescue LoadError => _e
nil
# :nocov:
# :nocov:
end
end

def call_url_helper(method, *args)
routes.url_helpers.public_send(method, args)
rescue NoMethodError => e
raise e
def format_route(route)
route_formatter.format(route)
end

def path_from_resource_class(klass)
url_helper_name = resources_url_helper_name_from_class(klass)
call_url_helper(url_helper_name)
end
def formatted_module_path_from_class(klass)
scopes = if @engine
module_scopes_from_class(klass)[1..-1]
else
module_scopes_from_class(klass)
end

def resource_path(source)
url_helper_name = resource_url_helper_name_from_source(source)
if source.class.singleton?
call_url_helper(url_helper_name)
unless scopes.empty?
"/#{ scopes.map {|scope| format_route(scope.to_s.underscore)}.compact.join('/') }/"
else
call_url_helper(url_helper_name, source.id)
"/"
end
end

def primary_resources_path
path_from_resource_class(primary_resource_klass)
def module_scopes_from_class(klass)
klass.name.to_s.split("::")[0...-1]
end

def url_helper_name_from_parts(parts)
(parts << "path").reject(&:blank?).join("_")
def resources_path(source_klass)
formatted_module_path_from_class(source_klass) + format_route(source_klass._type.to_s)
end

def resources_path_parts_from_class(klass)
if engine?
scopes = module_scopes_from_class(klass)[1..-1]
else
scopes = module_scopes_from_class(klass)
end

base_path_name = scopes.map { |scope| scope.underscore }.join("_")
end_path_name = klass._type.to_s
[base_path_name, end_path_name]
end

def resources_url_helper_name_from_class(klass)
url_helper_name_from_parts(resources_path_parts_from_class(klass))
end
def resource_path(source)
url = "#{resources_path(source.class)}"

def resource_path_parts_from_class(klass)
if engine?
scopes = module_scopes_from_class(klass)[1..-1]
else
scopes = module_scopes_from_class(klass)
unless source.class.singleton?
url = "#{url}/#{source.id}"
end

base_path_name = scopes.map { |scope| scope.underscore }.join("_")
end_path_name = klass._type.to_s.singularize
[base_path_name, end_path_name]
end

def resource_url_helper_name_from_source(source)
url_helper_name_from_parts(resource_path_parts_from_class(source.class))
end

def related_url_helper_name(relationship)
relationship_parts = resource_path_parts_from_class(relationship.parent_resource)
relationship_parts << "related"
relationship_parts << relationship.name
url_helper_name_from_parts(relationship_parts)
end

def singleton_related_url_helper_name(relationship)
relationship_parts = []
relationship_parts << "related"
relationship_parts << relationship.name
relationship_parts += resource_path_parts_from_class(relationship.parent_resource)
url_helper_name_from_parts(relationship_parts)
end

def relationship_self_url_helper_name(relationship)
relationship_parts = resource_path_parts_from_class(relationship.parent_resource)
relationship_parts << "relationships"
relationship_parts << relationship.name
url_helper_name_from_parts(relationship_parts)
url
end

def singleton_relationship_self_url_helper_name(relationship)
relationship_parts = []
relationship_parts << "relationships"
relationship_parts << relationship.name
relationship_parts += resource_path_parts_from_class(relationship.parent_resource)
url_helper_name_from_parts(relationship_parts)
def resource_url(source)
"#{ base_url }#{ engine_mount_point }#{ resource_path(source) }"
end

def module_scopes_from_class(klass)
klass.name.to_s.split("::")[0...-1]
def route_for_relationship(relationship)
format_route(relationship.name)
end
end
end
5 changes: 5 additions & 0 deletions lib/jsonapi/relationship.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class Relationship

attr_writer :allow_include

attr_accessor :_routed, :_warned_missing_route

def initialize(name, options = {})
@name = name.to_s
@options = options
Expand All @@ -27,6 +29,9 @@ def initialize(name, options = {})
@class_name = nil
@inverse_relationship = nil

@_routed = false
@_warned_missing_route = false

exclude_links(options.fetch(:exclude_links, :none))

# Custom methods are reserved for future use
Expand Down
3 changes: 3 additions & 0 deletions lib/jsonapi/resource_controller_metal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class ResourceControllerMetal < ActionController::Metal
JSONAPI::ActsAsResourceController
].freeze

# Note, the url_helpers are not loaded. This will prevent links from being generated for resources, and warnings
# will be emitted. Link support can be added by including `Rails.application.routes.url_helpers`, and links
# can be disabled, and warning suppressed, for a resource with `exclude_links :default`
MODULES.each do |mod|
include mod
end
Expand Down
2 changes: 2 additions & 0 deletions lib/jsonapi/resource_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,8 @@ def generate_link_builder(primary_resource_klass, options)
LinkBuilder.new(
base_url: options.fetch(:base_url, ''),
primary_resource_klass: primary_resource_klass,
route_formatter: options.fetch(:route_formatter, JSONAPI.configuration.route_formatter),
url_helpers: options.fetch(:url_helpers, options[:controller]),
)
end
end
Expand Down
8 changes: 8 additions & 0 deletions lib/jsonapi/routing_ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ def jsonapi_resource(*resources, &_block)
@resource_type = resources.first
res = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix(@resource_type))

res._routed = true

unless res.singleton?
warn "Singleton routes created for non singleton resource #{res}. Links may not be generated correctly."
end
Expand Down Expand Up @@ -84,6 +86,8 @@ def jsonapi_resources(*resources, &_block)
@resource_type = resources.first
res = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix(@resource_type))

res._routed = true

if res.singleton?
warn "Singleton resource #{res} should use `jsonapi_resource` instead."
end
Expand Down Expand Up @@ -220,6 +224,8 @@ def jsonapi_related_resource(*relationship)
relationship_name = relationship.first
relationship = source._relationships[relationship_name]

relationship._routed = true

formatted_relationship_name = format_route(relationship.name)

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

relationship._routed = true

formatted_relationship_name = format_route(relationship.name)
related_resource = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix(relationship.class_name.underscore))
options[:controller] ||= related_resource._type.to_s
Expand Down
6 changes: 5 additions & 1 deletion test/fixtures/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,7 @@ def create_responses_relationships
end

class AuthorsController < JSONAPI::ResourceControllerMetal
include Rails.application.routes.url_helpers
end

class PeopleController < JSONAPI::ResourceController
Expand Down Expand Up @@ -1991,7 +1992,9 @@ module V4
class PostResource < PostResource; end
class PersonResource < PersonResource; end
class ExpenseEntryResource < ExpenseEntryResource; end
class IsoCurrencyResource < IsoCurrencyResource; end
class IsoCurrencyResource < IsoCurrencyResource
has_many :expense_entries, exclude_links: :default
end

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

Expand Down Expand Up @@ -2389,6 +2392,7 @@ class PersonResource < JSONAPI::Resource

module ApiV2Engine
class PostResource < PostResource
has_one :person
end

class PersonResource < JSONAPI::Resource
Expand Down
Loading