Skip to content

Commit 651de05

Browse files
committed
Fragment caching.
1 parent 1f31926 commit 651de05

File tree

7 files changed

+304
-132
lines changed

7 files changed

+304
-132
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
require 'jsonapi/renderer/resources_processor'
2+
3+
module JSONAPI
4+
class Renderer
5+
# @private
6+
class CachedResourcesProcessor < ResourcesProcessor
7+
class JSONString < String
8+
def to_json(*)
9+
self
10+
end
11+
end
12+
13+
def initialize(cache)
14+
@cache = cache
15+
end
16+
17+
def process_resources
18+
[@primary, @included].each do |resources|
19+
cache_hash = cache_key_map(resources)
20+
processed_resources = @cache.fetch_multi(cache_hash.keys) do |key|
21+
res, include, fields = cache_hash[key]
22+
json = res.as_jsonapi(include: include, fields: fields).to_json
23+
24+
JSONString.new(json)
25+
end
26+
27+
resources.replace(processed_resources.values)
28+
end
29+
end
30+
31+
def cache_key_map(resources)
32+
resources.each_with_object({}) do |res, h|
33+
ri = [res.jsonapi_type, res.jsonapi_id]
34+
include_dir = @include_rels[ri]
35+
fields = @fields[ri.first.to_sym]
36+
h[res.jsonapi_cache_key(include: include_dir, fields: fields)] =
37+
[res, include_dir, fields]
38+
end
39+
end
40+
end
41+
end
42+
end

lib/jsonapi/renderer/document.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'jsonapi/include_directive'
2-
require 'jsonapi/renderer/resources_processor'
2+
require 'jsonapi/renderer/simple_resources_processor'
3+
require 'jsonapi/renderer/cached_resources_processor'
34

45
module JSONAPI
56
class Renderer
@@ -12,6 +13,7 @@ def initialize(params = {})
1213
@fields = _symbolize_fields(params[:fields] || {})
1314
@jsonapi = params[:jsonapi]
1415
@include = JSONAPI::IncludeDirective.new(params[:include] || {})
16+
@cache = params[:cache]
1517
end
1618

1719
def to_hash
@@ -36,13 +38,21 @@ def document_hash
3638

3739
def data_hash
3840
primary, included =
39-
ResourcesProcessor.new(Array(@data), @include, @fields).process
41+
resources_processor.process(Array(@data), @include, @fields)
4042
{}.tap do |hash|
4143
hash[:data] = @data.respond_to?(:to_ary) ? primary : primary[0]
4244
hash[:included] = included if included.any?
4345
end
4446
end
4547

48+
def resources_processor
49+
if @cache
50+
CachedResourcesProcessor.new(@cache)
51+
else
52+
SimpleResourcesProcessor.new
53+
end
54+
end
55+
4656
def errors_hash
4757
{}.tap do |hash|
4858
hash[:errors] = @errors.flat_map(&:as_jsonapi)

lib/jsonapi/renderer/resources_processor.rb

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
module JSONAPI
44
class Renderer
5+
# @api private
56
class ResourcesProcessor
6-
def initialize(resources, include, fields)
7+
def process(resources, include, fields)
78
@resources = resources
89
@include = include
910
@fields = fields
10-
end
1111

12-
def process
1312
traverse_resources
1413
process_resources
1514

@@ -73,14 +72,7 @@ def enqueue_resource(res, prefix, include_dir)
7372
end
7473

7574
def process_resources
76-
[@primary, @included].each do |resources|
77-
resources.map! do |res|
78-
ri = [res.jsonapi_type, res.jsonapi_id]
79-
include_dir = @include_rels[ri]
80-
fields = @fields[res.jsonapi_type.to_sym]
81-
res.as_jsonapi(include: include_dir, fields: fields)
82-
end
83-
end
75+
raise 'Not implemented'
8476
end
8577
end
8678
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
require 'jsonapi/renderer/resources_processor'
2+
3+
module JSONAPI
4+
class Renderer
5+
# @api private
6+
class SimpleResourcesProcessor < ResourcesProcessor
7+
def process_resources
8+
[@primary, @included].each do |resources|
9+
resources.map! do |res|
10+
ri = [res.jsonapi_type, res.jsonapi_id]
11+
include_dir = @include_rels[ri]
12+
fields = @fields[res.jsonapi_type.to_sym]
13+
res.as_jsonapi(include: include_dir, fields: fields)
14+
end
15+
end
16+
end
17+
end
18+
end
19+
end

spec/caching_spec.rb

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
require 'spec_helper'
2+
3+
class Cache
4+
def initialize
5+
@cache = {}
6+
end
7+
8+
def fetch_multi(keys)
9+
keys.each_with_object({}) do |k, h|
10+
@cache[k] = yield(k) unless @cache.key?(k)
11+
h[k] = @cache[k]
12+
end
13+
end
14+
end
15+
16+
describe JSONAPI::Renderer, '#render' do
17+
before(:all) do
18+
@users = [
19+
UserResource.new(1, 'User 1', '123 Example st.', []),
20+
UserResource.new(2, 'User 2', '234 Example st.', []),
21+
UserResource.new(3, 'User 3', '345 Example st.', []),
22+
UserResource.new(4, 'User 4', '456 Example st.', [])
23+
]
24+
@posts = [
25+
PostResource.new(1, 'Post 1', 'yesterday', @users[1]),
26+
PostResource.new(2, 'Post 2', 'today', @users[0]),
27+
PostResource.new(3, 'Post 3', 'tomorrow', @users[1])
28+
]
29+
@users[0].posts = [@posts[1]]
30+
@users[1].posts = [@posts[0], @posts[2]]
31+
end
32+
33+
it 'renders included relationships' do
34+
cache = Cache.new
35+
# Warm up the cache.
36+
subject.render(data: @users[0],
37+
include: 'posts',
38+
cache: cache)
39+
# Actual call on warm cache.
40+
actual = subject.render(data: @users[0],
41+
include: 'posts',
42+
cache: cache)
43+
expected = {
44+
data: {
45+
type: 'users',
46+
id: '1',
47+
attributes: {
48+
name: 'User 1',
49+
address: '123 Example st.'
50+
},
51+
relationships: {
52+
posts: {
53+
data: [{ type: 'posts', id: '2' }],
54+
links: {
55+
self: 'http://api.example.com/users/1/relationships/posts',
56+
related: {
57+
href: 'http://api.example.com/users/1/posts',
58+
meta: {
59+
do_not_use: true
60+
}
61+
}
62+
},
63+
meta: {
64+
deleted_posts: 5
65+
}
66+
}
67+
},
68+
links: {
69+
self: 'http://api.example.com/users/1'
70+
},
71+
meta: {
72+
user_meta: 'is_meta'
73+
}
74+
},
75+
included: [
76+
{
77+
type: 'posts',
78+
id: '2',
79+
attributes: {
80+
title: 'Post 2',
81+
date: 'today'
82+
},
83+
relationships: {
84+
author: {
85+
links: {
86+
self: 'http://api.example.com/posts/2/relationships/author',
87+
related: 'http://api.example.com/posts/2/author'
88+
},
89+
meta: {
90+
author_active: true
91+
}
92+
}
93+
}
94+
}
95+
]
96+
}
97+
98+
expect(JSON.parse(actual.to_json)).to eq(JSON.parse(expected.to_json))
99+
expect(actual[:data]).to be_a(JSONAPI::Renderer::CachedResourcesProcessor::JSONString)
100+
end
101+
end

spec/renderer_spec.rb

Lines changed: 0 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,5 @@
11
require 'spec_helper'
22

3-
class UserResource
4-
attr_accessor :id, :name, :address, :posts
5-
6-
def initialize(id, name, address, posts)
7-
@id = id
8-
@name = name
9-
@address = address
10-
@posts = posts
11-
end
12-
13-
def jsonapi_type
14-
'users'
15-
end
16-
17-
def jsonapi_id
18-
@id.to_s
19-
end
20-
21-
def jsonapi_related(included)
22-
if included.include?(:posts)
23-
{ posts: @posts.map { |p| p } }
24-
else
25-
{}
26-
end
27-
end
28-
29-
def as_jsonapi(options = {})
30-
fields = options[:fields] || [:name, :address, :posts]
31-
included = options[:include] || []
32-
33-
hash = { id: jsonapi_id, type: jsonapi_type }
34-
hash[:attributes] = { name: @name, address: @address }
35-
.select { |k, _| fields.include?(k) }
36-
if fields.include?(:posts)
37-
hash[:relationships] = { posts: {} }
38-
hash[:relationships][:posts] = {
39-
links: {
40-
self: "http://api.example.com/users/#{@id}/relationships/posts",
41-
related: {
42-
href: "http://api.example.com/users/#{@id}/posts",
43-
meta: {
44-
do_not_use: true
45-
}
46-
}
47-
},
48-
meta: {
49-
deleted_posts: 5
50-
}
51-
}
52-
if included.include?(:posts)
53-
hash[:relationships][:posts][:data] = @posts.map do |p|
54-
{ type: 'posts', id: p.id.to_s }
55-
end
56-
end
57-
end
58-
59-
hash[:links] = {
60-
self: "http://api.example.com/users/#{@id}"
61-
}
62-
hash[:meta] = { user_meta: 'is_meta' }
63-
64-
hash
65-
end
66-
end
67-
68-
class PostResource
69-
attr_accessor :id, :title, :date, :author
70-
71-
def initialize(id, title, date, author)
72-
@id = id
73-
@title = title
74-
@date = date
75-
@author = author
76-
end
77-
78-
def jsonapi_type
79-
'posts'
80-
end
81-
82-
def jsonapi_id
83-
@id.to_s
84-
end
85-
86-
def jsonapi_related(included)
87-
included.include?(:author) ? { author: [@author] } : {}
88-
end
89-
90-
def as_jsonapi(options = {})
91-
fields = options[:fields] || [:title, :date, :author]
92-
included = options[:include] || []
93-
hash = { id: jsonapi_id, type: jsonapi_type }
94-
95-
hash[:attributes] = { title: @title, date: @date }
96-
.select { |k, _| fields.include?(k) }
97-
if fields.include?(:author)
98-
hash[:relationships] = { author: {} }
99-
hash[:relationships][:author] = {
100-
links: {
101-
self: "http://api.example.com/posts/#{@id}/relationships/author",
102-
related: "http://api.example.com/posts/#{@id}/author"
103-
},
104-
meta: {
105-
author_active: true
106-
}
107-
}
108-
if included.include?(:author)
109-
hash[:relationships][:author][:data] =
110-
if @author.nil?
111-
nil
112-
else
113-
{ type: 'users', id: @author.id.to_s }
114-
end
115-
end
116-
end
117-
118-
hash
119-
end
120-
end
121-
1223
describe JSONAPI::Renderer, '#render' do
1234
before(:all) do
1245
@users = [

0 commit comments

Comments
 (0)