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
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class ResourceInterface
# @return [String]
def jsonapi_id; end

# Returns a hash containing, for each included relationship, an array of the
# Returns a hash containing, for each included relationship, an array of the
# resources to be included from that one.
# @param included_relationships [Array<Symbol>] The keys of the relationships
# to be included.
Expand All @@ -57,8 +57,8 @@ class ResourceInterface

# Returns a JSON API-compliant representation of the resource as a hash.
# @param options [Hash]
# @option fields [Array<Symbol>, Nil] The requested fields, or nil.
# @option include [Array<Symbol>] The requested relationships to
# @option fields [Set<Symbol>, Nil] The requested fields, or nil.
# @option include [Set<Symbol>] The requested relationships to
# include (defaults to []).
# @return [Hash]
def as_jsonapi(options = {}); end
Expand Down Expand Up @@ -100,6 +100,32 @@ returns a JSON API-compliant representation of the error.

This returns a JSON API compliant hash representing the described document.

### Caching

The generated JSON fragments can be cached in any cache implementation
supporting the `fetch_multi` method.

When using caching, the serializable resources must implement an
additional `jsonapi_cache_key` method:
```ruby
# Returns a cache key for the resource, parametered by the `include` and
# `fields` options.
# @param options [Hash]
# @option fields [Set<Symbol>, Nil] The requested fields, or nil.
# @option include [Set<Symbol>] The requested relationships to
# include (defaults to []).
# @return [String]
def jsonapi_cache_key(options = {}); end
```

The cache instance must be passed to the renderer as follows:
```ruby
JSONAPI.render(data: resources,
include: include_string,
fields: fields_hash,
cache: cache_instance)
```

## License

jsonapi-renderer is released under the [MIT License](http://www.opensource.org/licenses/MIT).
42 changes: 42 additions & 0 deletions lib/jsonapi/renderer/cached_resources_processor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
require 'jsonapi/renderer/resources_processor'

module JSONAPI
class Renderer
# @private
class CachedResourcesProcessor < ResourcesProcessor
class JSONString < String
def to_json(*)
self
end
end

def initialize(cache)
@cache = cache
end

def process_resources
[@primary, @included].each do |resources|
cache_hash = cache_key_map(resources)
processed_resources = @cache.fetch_multi(cache_hash.keys) do |key|
res, include, fields = cache_hash[key]
json = res.as_jsonapi(include: include, fields: fields).to_json

JSONString.new(json)
end

resources.replace(processed_resources.values)
end
end

def cache_key_map(resources)
resources.each_with_object({}) do |res, h|
ri = [res.jsonapi_type, res.jsonapi_id]
include_dir = @include_rels[ri]
fields = @fields[ri.first.to_sym]
h[res.jsonapi_cache_key(include: include_dir, fields: fields)] =
[res, include_dir, fields]
end
end
end
end
end
14 changes: 12 additions & 2 deletions lib/jsonapi/renderer/document.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'jsonapi/include_directive'
require 'jsonapi/renderer/resources_processor'
require 'jsonapi/renderer/simple_resources_processor'
require 'jsonapi/renderer/cached_resources_processor'

module JSONAPI
class Renderer
Expand All @@ -12,6 +13,7 @@ def initialize(params = {})
@fields = _symbolize_fields(params[:fields] || {})
@jsonapi = params[:jsonapi]
@include = JSONAPI::IncludeDirective.new(params[:include] || {})
@cache = params[:cache]
end

def to_hash
Expand All @@ -36,13 +38,21 @@ def document_hash

def data_hash
primary, included =
ResourcesProcessor.new(Array(@data), @include, @fields).process
resources_processor.process(Array(@data), @include, @fields)
{}.tap do |hash|
hash[:data] = @data.respond_to?(:to_ary) ? primary : primary[0]
hash[:included] = included if included.any?
end
end

def resources_processor
if @cache
CachedResourcesProcessor.new(@cache)
else
SimpleResourcesProcessor.new
end
end

def errors_hash
{}.tap do |hash|
hash[:errors] = @errors.flat_map(&:as_jsonapi)
Expand Down
14 changes: 3 additions & 11 deletions lib/jsonapi/renderer/resources_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

module JSONAPI
class Renderer
# @api private
class ResourcesProcessor
def initialize(resources, include, fields)
def process(resources, include, fields)
@resources = resources
@include = include
@fields = fields
end

def process
traverse_resources
process_resources

Expand Down Expand Up @@ -73,14 +72,7 @@ def enqueue_resource(res, prefix, include_dir)
end

def process_resources
[@primary, @included].each do |resources|
resources.map! do |res|
ri = [res.jsonapi_type, res.jsonapi_id]
include_dir = @include_rels[ri]
fields = @fields[res.jsonapi_type.to_sym]
res.as_jsonapi(include: include_dir, fields: fields)
end
end
raise 'Not implemented'
end
end
end
Expand Down
19 changes: 19 additions & 0 deletions lib/jsonapi/renderer/simple_resources_processor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require 'jsonapi/renderer/resources_processor'

module JSONAPI
class Renderer
# @api private
class SimpleResourcesProcessor < ResourcesProcessor
def process_resources
[@primary, @included].each do |resources|
resources.map! do |res|
ri = [res.jsonapi_type, res.jsonapi_id]
include_dir = @include_rels[ri]
fields = @fields[res.jsonapi_type.to_sym]
res.as_jsonapi(include: include_dir, fields: fields)
end
end
end
end
end
end
101 changes: 101 additions & 0 deletions spec/caching_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
require 'spec_helper'

class Cache
def initialize
@cache = {}
end

def fetch_multi(keys)
keys.each_with_object({}) do |k, h|
@cache[k] = yield(k) unless @cache.key?(k)
h[k] = @cache[k]
end
end
end

describe JSONAPI::Renderer, '#render' do
before(:all) do
@users = [
UserResource.new(1, 'User 1', '123 Example st.', []),
UserResource.new(2, 'User 2', '234 Example st.', []),
UserResource.new(3, 'User 3', '345 Example st.', []),
UserResource.new(4, 'User 4', '456 Example st.', [])
]
@posts = [
PostResource.new(1, 'Post 1', 'yesterday', @users[1]),
PostResource.new(2, 'Post 2', 'today', @users[0]),
PostResource.new(3, 'Post 3', 'tomorrow', @users[1])
]
@users[0].posts = [@posts[1]]
@users[1].posts = [@posts[0], @posts[2]]
end

it 'renders included relationships' do
cache = Cache.new
# Warm up the cache.
subject.render(data: @users[0],
include: 'posts',
cache: cache)
# Actual call on warm cache.
actual = subject.render(data: @users[0],
include: 'posts',
cache: cache)
expected = {
data: {
type: 'users',
id: '1',
attributes: {
name: 'User 1',
address: '123 Example st.'
},
relationships: {
posts: {
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
}
}
},
links: {
self: 'http://api.example.com/users/1'
},
meta: {
user_meta: 'is_meta'
}
},
included: [
{
type: 'posts',
id: '2',
attributes: {
title: 'Post 2',
date: 'today'
},
relationships: {
author: {
links: {
self: 'http://api.example.com/posts/2/relationships/author',
related: 'http://api.example.com/posts/2/author'
},
meta: {
author_active: true
}
}
}
}
]
}

expect(JSON.parse(actual.to_json)).to eq(JSON.parse(expected.to_json))
expect(actual[:data]).to be_a(JSONAPI::Renderer::CachedResourcesProcessor::JSONString)
end
end
Loading