Skip to content

Commit 3820417

Browse files
authored
Merge pull request #72 from MPursche/add_render_related_json
New Feature: Added options to "jsonapi_render" to allow rendering of related resource pages
2 parents 54b7900 + 922fdca commit 3820417

File tree

8 files changed

+170
-5
lines changed

8 files changed

+170
-5
lines changed

Rakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ require 'bundler/gem_tasks'
22
require 'rspec/core/rake_task'
33

44
RSpec::Core::RakeTask.new(:spec) do |test|
5-
test.pattern = 'spec/controllers/{user,post}s_controller.rb'
5+
test.pattern = 'spec/**/*_spec.rb'
66
end
77

88
task default: :spec

bin/rspec

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
#
4+
# This file was generated by Bundler.
5+
#
6+
# The application 'rspec' is installed as part of a gem, and
7+
# this file is here to facilitate running it.
8+
#
9+
10+
require "pathname"
11+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12+
Pathname.new(__FILE__).realpath)
13+
14+
require "rubygems"
15+
require "bundler/setup"
16+
17+
load Gem.bin_path("rspec-core", "rspec")

jsonapi-utils.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,6 @@ Gem::Specification.new do |spec|
3030
spec.add_development_dependency 'smart_rspec', '~> 0.1.6'
3131
spec.add_development_dependency 'pry', '~> 0.10.3'
3232
spec.add_development_dependency 'pry-byebug'
33+
spec.add_development_dependency 'debase'
34+
spec.add_development_dependency 'ruby-debug-ide'
3335
end

lib/jsonapi/utils/response/formatters.rb

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ module Formatters
1515
# @option options [JSONAPI::Resource] resource: it tells the formatter which resource
1616
# class to be used rather than use an infered one (default behaviour)
1717
#
18+
# @option options [JSONAPI::Resource] source_resource: it tells the formatter that this response is from a related resource
19+
# and the result should be interpreted as a related resources response
20+
#
21+
# @option options [String, Symbol] relationship_type: it tells that the formatter which relationship the data is from
22+
#
1823
# @option options [ActiveRecord::Base] model: ActiveRecord model class to be instantiated
1924
# when a Hash or Array of Hashes is passed as the "object" argument
2025
#
@@ -51,7 +56,7 @@ def jsonapi_format(object, options = {})
5156
# @api public
5257
def jsonapi_format_errors(object)
5358
if active_record_obj?(object)
54-
object = JSONAPI::Utils::Exceptions::ActiveRecord.new(object, @request.resource_klass, context)
59+
object = JSONAPI::Utils::Exceptions::ActiveRecord.new(object, @request.resource_klass, context)
5560
end
5661
errors = object.respond_to?(:errors) ? object.errors : object
5762
JSONAPI::Utils::Support::Error.sanitize(errors).uniq
@@ -84,6 +89,11 @@ def active_record_obj?(object)
8489
# @option options [JSONAPI::Resource] resource: it tells the builder which resource
8590
# class to be used rather than use an infered one (default behaviour)
8691
#
92+
# @option options [ActiveRecord::Base, JSONAPI::Resource] source: it tells the builder that this response is from a related resource
93+
# and the result should be interpreted as a related resources response
94+
#
95+
# @option options [String, Symbol] relationship: it tells that the builder which relationship the data is from
96+
#
8797
# @option options [Integer] count: if it's rendering a collection of resources, the default
8898
# gem's counting method can be bypassed by the use of this options. It's shows then the total
8999
# records resulting from that request and also calculates the pagination.
@@ -96,7 +106,19 @@ def build_response_document(object, options)
96106

97107
if object.respond_to?(:to_ary)
98108
records = build_collection(object, options)
99-
results.add_result(JSONAPI::ResourcesOperationResult.new(:ok, records, result_options(object, options)))
109+
110+
if params[:source].present? && params[:relationship].present?
111+
source_resource = turn_source_into_resource(options[:source], options)
112+
relationship_type = get_source_relationship(options)
113+
114+
results.add_result(JSONAPI::RelatedResourcesOperationResult.new(:ok,
115+
source_resource,
116+
relationship_type,
117+
records,
118+
result_options(object, options)))
119+
else
120+
results.add_result(JSONAPI::ResourcesOperationResult.new(:ok, records, result_options(object, options)))
121+
end
100122
else
101123
record = turn_into_resource(object, options)
102124
results.add_result(JSONAPI::ResourceOperationResult.new(:ok, record))
@@ -171,6 +193,34 @@ def turn_into_resource(record, options)
171193
end
172194
end
173195

196+
# Get JSONAPI::Resource for source object
197+
# @option options [JSONAPI::Resource] resource: it tells which resource
198+
# class to be used rather than use an infered one (default behaviour)
199+
# @return [JSONAPI::Resource]
200+
#
201+
# @api private
202+
def turn_source_into_resource(source, options)
203+
if source.kind_of? JSONAPI::Resource
204+
source
205+
else
206+
@request.source_klass.new(source, context)
207+
end
208+
end
209+
210+
# Get relationship type of source object
211+
# @option options [Symbol] relationship: it tells which relationship
212+
# to be used rather than use an infered one (default behaviour)
213+
# @return [Symbol]
214+
#
215+
# @api private
216+
def get_source_relationship(options)
217+
if options[:relationship].present?
218+
options[:relationship].to_sym
219+
else
220+
params[:relationship].to_sym || @request.resource_klass._type
221+
end
222+
end
223+
174224
# Apply some result options like pagination params and record count to collection responses.
175225
#
176226
# @param records [ActiveRecord::Relation, Hash, Array<Hash>]

lib/jsonapi/utils/response/renders.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ module Renders
1414
#
1515
# @option options [JSONAPI::Resource] resource: it tells the render which resource
1616
# class to be used rather than use an infered one (default behaviour)
17+
#
18+
# @option options [JSONAPI::Resource] source_resource: it tells the render that this response is from a related resource
19+
# and the result should be interpreted as a related resources response
20+
#
21+
# @option options [String, Symbol] relationship_type: it tells that the render which relationship the data is from
1722
#
1823
# @option options [ActiveRecord::Base] model: ActiveRecord model class to be instantiated
1924
# when a Hash or Array of Hashes is passed to the "json" key argument

spec/controllers/posts_controller_spec.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,80 @@
355355
end
356356
end
357357

358+
describe 'GET #related_resources' do
359+
shared_context 'related_resources request' do |use_resource:, explicit_relationship:|
360+
subject { get :get_related_resources, params: params }
361+
let (:params) { {
362+
source: "users",
363+
user_id: parent_id,
364+
relationship: "posts",
365+
use_resource: use_resource,
366+
explicit_relationship: explicit_relationship
367+
} }
368+
end
369+
370+
context 'using model as source' do
371+
include_context 'related_resources request', use_resource: false, explicit_relationship: false
372+
373+
it 'loads all posts of a user' do
374+
expect(subject).to have_http_status :ok
375+
expect(subject).to have_primary_data('posts')
376+
expect(subject).to have_data_attributes(fields)
377+
expect(subject).to have_relationships(relationships)
378+
379+
# it should use nested url
380+
expect(json.dig('links', 'first')).to include("/users/#{parent_id}/posts")
381+
expect(json.dig('links', 'last')).to include("/users/#{parent_id}/posts")
382+
end
383+
end
384+
385+
context 'using model as source and relationship from options' do
386+
include_context 'related_resources request', use_resource: false, explicit_relationship: true
387+
388+
it 'loads all posts of a user' do
389+
expect(subject).to have_http_status :ok
390+
expect(subject).to have_primary_data('posts')
391+
expect(subject).to have_data_attributes(fields)
392+
expect(subject).to have_relationships(relationships)
393+
394+
# it should use nested url
395+
expect(json.dig('links', 'first')).to include("/users/#{parent_id}/posts")
396+
expect(json.dig('links', 'last')).to include("/users/#{parent_id}/posts")
397+
end
398+
end
399+
400+
401+
context 'using resource as source' do
402+
include_context 'related_resources request', use_resource: true, explicit_relationship: false
403+
404+
it 'loads all posts of a user' do
405+
expect(subject).to have_http_status :ok
406+
expect(subject).to have_primary_data('posts')
407+
expect(subject).to have_data_attributes(fields)
408+
expect(subject).to have_relationships(relationships)
409+
410+
# it should use nested url
411+
expect(json.dig('links', 'first')).to include("/users/#{parent_id}/posts")
412+
expect(json.dig('links', 'last')).to include("/users/#{parent_id}/posts")
413+
end
414+
end
415+
416+
context 'using resource as source and relationship from options' do
417+
include_context 'related_resources request', use_resource: true, explicit_relationship: true
418+
419+
it 'loads all posts of a user' do
420+
expect(subject).to have_http_status :ok
421+
expect(subject).to have_primary_data('posts')
422+
expect(subject).to have_data_attributes(fields)
423+
expect(subject).to have_relationships(relationships)
424+
425+
# it should use nested url
426+
expect(json.dig('links', 'first')).to include("/users/#{parent_id}/posts")
427+
expect(json.dig('links', 'last')).to include("/users/#{parent_id}/posts")
428+
end
429+
end
430+
end
431+
358432
describe 'PATCH #update' do
359433
shared_context 'update request' do |action:|
360434
subject { patch action, params: params.merge(body) }

spec/support/controllers.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class BaseController < ActionController::Base
77
end
88

99
class PostsController < BaseController
10-
before_action :load_user, only: %i(index)
10+
before_action :load_user, only: %i(index get_related_resources)
1111

1212
# GET /users/:user_id/posts
1313
def index
@@ -49,6 +49,22 @@ def create
4949
end
5050
end
5151

52+
# GET /users/:user_id/posts
53+
def get_related_resources
54+
if params[:source] == "users" && params[:relationship] == "posts"
55+
# Example for custom method to fetch related resources
56+
@posts = @user.posts
57+
58+
jsonapi_render json: @posts, options: {
59+
source: (params[:use_resource] == "true") ? UserResource.new(@user, context) : @user,
60+
relationship: (params[:explicit_relationship] == "true") ? 'posts' : nil
61+
}
62+
else
63+
# handle other requests with default method
64+
process_request
65+
end
66+
end
67+
5268
# PATCH /posts/:id
5369
def update_with_error_on_base
5470
post = Post.find(params[:id])

spec/test_app.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,11 @@ def TestApp.draw_app_routes
6060
TestApp.routes.draw do
6161
jsonapi_resources :users do
6262
jsonapi_links :profile
63-
jsonapi_resources :posts, shallow: true
63+
jsonapi_related_resources :posts
6464
end
6565

6666
jsonapi_resource :profile
67+
jsonapi_resources :posts
6768

6869
patch :update_with_error_on_base, to: 'posts#update_with_error_on_base'
6970

0 commit comments

Comments
 (0)