diff --git a/README.md b/README.md index 68e115d..e4a5995 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,18 @@ JSONAPI.render(data: resources, This returns a JSON API compliant hash representing the described document. +#### Rendering a relationship +```ruby +JSONAPI.render(data: resource, + relationship: :posts, + include: include_string, + fields: fields_hash, + meta: meta_hash, + links: links_hash) +``` + +This returns a JSON API compliant hash representing the described document. + ### Rendering errors ```ruby diff --git a/lib/jsonapi/renderer/document.rb b/lib/jsonapi/renderer/document.rb index aaea715..fd1de9c 100644 --- a/lib/jsonapi/renderer/document.rb +++ b/lib/jsonapi/renderer/document.rb @@ -4,6 +4,7 @@ module JSONAPI class Renderer + # @private class Document def initialize(params = {}) @data = params.fetch(:data, :no_data) @@ -13,7 +14,8 @@ def initialize(params = {}) @fields = _symbolize_fields(params[:fields] || {}) @jsonapi = params[:jsonapi] @include = JSONAPI::IncludeDirective.new(params[:include] || {}) - @cache = params[:cache] + @relationship = params[:relationship] + @cache = params[:cache] end def to_hash @@ -23,18 +25,24 @@ def to_hash private + # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity def document_hash {}.tap do |hash| - if @data != :no_data + if @relationship + hash.merge!(relationship_hash) + elsif @data != :no_data hash.merge!(data_hash) elsif @errors.any? hash.merge!(errors_hash) end - hash[:links] = @links if @links.any? - hash[:meta] = @meta unless @meta.nil? - hash[:jsonapi] = @jsonapi unless @jsonapi.nil? + hash[:links] = @links if @links.any? + hash[:meta] = @meta unless @meta.nil? + hash[:jsonapi] = @jsonapi unless @jsonapi.nil? end end + # rubocop:enable Metrics/PerceivedComplexity, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity def data_hash primary, included = @@ -45,13 +53,29 @@ def data_hash end end - def resources_processor - if @cache - CachedResourcesProcessor.new(@cache) - else - SimpleResourcesProcessor.new + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize + def relationship_hash + rel_name = @relationship.to_sym + data = @data.jsonapi_related([rel_name])[rel_name] + included = + if @include.key?(rel_name) + resources_processor.process(data, @include[rel_name], @fields) + .flatten! + else + [] + end + + res = @data.as_jsonapi(fields: [rel_name], include: [rel_name]) + rel = res[:relationships][rel_name] + @links = rel[:links].merge!(@links) + @meta ||= rel[:meta] + + {}.tap do |hash| + hash[:data] = rel[:data] + hash[:included] = included if included.any? end end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize def errors_hash {}.tap do |hash| @@ -59,6 +83,14 @@ def errors_hash end end + def resources_processor + if @cache + CachedResourcesProcessor.new(@cache) + else + SimpleResourcesProcessor.new + end + end + def _symbolize_fields(fields) fields.each_with_object({}) do |(k, v), h| h[k.to_sym] = v.map(&:to_sym) diff --git a/lib/jsonapi/renderer/resources_processor.rb b/lib/jsonapi/renderer/resources_processor.rb index 0217c49..dd37110 100644 --- a/lib/jsonapi/renderer/resources_processor.rb +++ b/lib/jsonapi/renderer/resources_processor.rb @@ -2,7 +2,7 @@ module JSONAPI class Renderer - # @api private + # @private class ResourcesProcessor def process(resources, include, fields) @resources = resources diff --git a/spec/renderer_spec.rb b/spec/renderer_spec.rb index 0edc8ae..71cf473 100644 --- a/spec/renderer_spec.rb +++ b/spec/renderer_spec.rb @@ -336,4 +336,104 @@ def as_jsonapi expect(actual).to eq(expected) end + + context 'when rendering a relationship' do + it 'renders the linkage data only' do + actual = subject.render(data: @users[0], relationship: :posts) + expected = { + data: [{ type: 'posts', id: '2' }], + links: { + self: 'http://api.example.com/users/1/relationships/posts', + related: { + href: 'http://api.example.com/users/1/posts', + meta: { + do_not_use: true + } + } + }, + meta: { + deleted_posts: 5 + } + } + + expect(actual).to eq(expected) + end + + it 'renders supports include parameter' do + actual = subject.render(data: @users[0], relationship: :posts, + include: 'posts.author') + actual_included = actual.delete(:included) + + expected = { + data: [{ type: 'posts', id: '2' }], + links: { + self: 'http://api.example.com/users/1/relationships/posts', + related: { + href: 'http://api.example.com/users/1/posts', + meta: { + do_not_use: true + } + } + }, + meta: { + deleted_posts: 5 + } + } + expected_included = [ + { + type: 'users', + id: '1', + attributes: { + name: 'User 1', + address: '123 Example st.' + }, + relationships: { + posts: { + links: { + self: 'http://api.example.com/users/1/relationships/posts', + related: { + href: 'http://api.example.com/users/1/posts', + meta: { + do_not_use: true + } + } + }, + meta: { + deleted_posts: 5 + } + } + }, + links: { + self: 'http://api.example.com/users/1' + }, + meta: { + user_meta: 'is_meta' + } + }, + { + type: 'posts', + id: '2', + attributes: { + title: 'Post 2', + date: 'today' + }, + relationships: { + author: { + data: { type: 'users', id: '1' }, + links: { + self: 'http://api.example.com/posts/2/relationships/author', + related: 'http://api.example.com/posts/2/author' + }, + meta: { + author_active: true + } + } + } + } + ] + + expect(actual).to eq(expected) + expect(actual_included).to match_array(expected_included) + end + end end