json_api_server 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.dockerignore +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.rubocop.yml +35 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +9 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +432 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/locales/en.yml +32 -0
- data/docker-compose.yml +10 -0
- data/json_api_server.gemspec +50 -0
- data/lib/json_api_server.rb +76 -0
- data/lib/json_api_server/api_version.rb +8 -0
- data/lib/json_api_server/attributes_builder.rb +135 -0
- data/lib/json_api_server/base_serializer.rb +169 -0
- data/lib/json_api_server/builder.rb +201 -0
- data/lib/json_api_server/cast.rb +89 -0
- data/lib/json_api_server/configuration.rb +99 -0
- data/lib/json_api_server/controller/error_handling.rb +164 -0
- data/lib/json_api_server/engine.rb +5 -0
- data/lib/json_api_server/error.rb +64 -0
- data/lib/json_api_server/errors.rb +50 -0
- data/lib/json_api_server/exceptions.rb +6 -0
- data/lib/json_api_server/fields.rb +76 -0
- data/lib/json_api_server/filter.rb +255 -0
- data/lib/json_api_server/filter_builders.rb +135 -0
- data/lib/json_api_server/filter_config.rb +71 -0
- data/lib/json_api_server/filter_parser.rb +88 -0
- data/lib/json_api_server/include.rb +158 -0
- data/lib/json_api_server/meta_builder.rb +39 -0
- data/lib/json_api_server/mime_types.rb +21 -0
- data/lib/json_api_server/pagination.rb +189 -0
- data/lib/json_api_server/paginator.rb +134 -0
- data/lib/json_api_server/relationships_builder.rb +215 -0
- data/lib/json_api_server/resource_serializer.rb +245 -0
- data/lib/json_api_server/resources_serializer.rb +131 -0
- data/lib/json_api_server/serializer.rb +34 -0
- data/lib/json_api_server/sort.rb +156 -0
- data/lib/json_api_server/sort_configs.rb +63 -0
- data/lib/json_api_server/validation_errors.rb +51 -0
- data/lib/json_api_server/version.rb +3 -0
- metadata +259 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
module JsonApiServer # :nodoc:
|
2
|
+
# Class for building meta element.
|
3
|
+
# http://jsonapi.org/format/#document-meta
|
4
|
+
#
|
5
|
+
# ==== Example
|
6
|
+
#
|
7
|
+
# MetaBuilder.new
|
8
|
+
# .add('copyright', "Copyright 2015 Example Corp.")
|
9
|
+
# .add('authors', ["Yehuda Katz", "Steve Klabnik", "Dan Gebhardt", "Tyler Kellen"])
|
10
|
+
# .merge({a: 'something', b: 'something else'})
|
11
|
+
# .meta # => { "copyright": "Copyright 2015 Example Corp.",
|
12
|
+
# "authors": ["Yehuda Katz", "Steve Klabnik", "Dan Gebhardt", "Tyler Kellen"],
|
13
|
+
# a: 'something',
|
14
|
+
# b: 'something else'
|
15
|
+
# }
|
16
|
+
#
|
17
|
+
class MetaBuilder
|
18
|
+
def initialize
|
19
|
+
@hash = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Add key and value.
|
23
|
+
def add(key, value)
|
24
|
+
@hash[key] = value
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# Push in multiple key/values with merge.
|
29
|
+
def merge(hash)
|
30
|
+
@hash.merge!(hash) if hash.respond_to?(:keys) && hash.any?
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns a hash if it has values, nil otherwise.
|
35
|
+
def meta
|
36
|
+
@hash.any? ? @hash : nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#++
|
2
|
+
# NOTE: it can cause issues in browsers: https://github.com/json-api/json-api/issues/1048
|
3
|
+
# __
|
4
|
+
module JsonApiServer # :nodoc:
|
5
|
+
# http://jsonapi.org/format/#introduction -> JSON API requires use of the JSON API
|
6
|
+
# media type (application/vnd.api+json) for exchanging data.
|
7
|
+
#
|
8
|
+
# Include this module in your config/initializers/mime_types.rb
|
9
|
+
#
|
10
|
+
# i.e,:
|
11
|
+
# # in config/initializers/mime_types.rb
|
12
|
+
# include JsonApiServer::MimeTypes
|
13
|
+
module MimeTypes
|
14
|
+
api_mime_types = %w[
|
15
|
+
application/vnd.api+json
|
16
|
+
text/x-json
|
17
|
+
application/json
|
18
|
+
]
|
19
|
+
Mime::Type.register 'application/vnd.api+json', :json, api_mime_types
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
module JsonApiServer # :nodoc:
|
2
|
+
# === Description
|
3
|
+
#
|
4
|
+
# JSON API Spec: http://jsonapi.org/format/#fetching-pagination - "Pagination links MUST
|
5
|
+
# appear in the links object that corresponds to a collection. To paginate the primary data,
|
6
|
+
# supply pagination links in the top-level links object."
|
7
|
+
#
|
8
|
+
# This class handles pagination. It (1) ensures <tt>page[number]</tt> is a positive integer,
|
9
|
+
# (2) <tt>page[limit]</tt> doesn't exceed a maximum value and (3) generates a pagination sub-query
|
10
|
+
# (to be merged into a master query). It uses the WillPaginate gem
|
11
|
+
# https://rubygems.org/gems/will_paginate/versions/3.1.6.
|
12
|
+
#
|
13
|
+
# === Usage:
|
14
|
+
#
|
15
|
+
# A paginating request will look like:
|
16
|
+
# /comments?page[number]=1&page[limit]=10
|
17
|
+
#
|
18
|
+
# Where:
|
19
|
+
#
|
20
|
+
# - <b><tt>page[number]</tt></b> is the current page
|
21
|
+
#
|
22
|
+
# - <b><tt>page[limit]</tt></b> is number of records per page
|
23
|
+
#
|
24
|
+
# The class takes an ActiveDispatch::Request object, an ActiveRecord::Base model
|
25
|
+
# (to generate a pagination sub-query) and two options:
|
26
|
+
#
|
27
|
+
# <b><tt>:max_per_page</tt></b> - maximum number of records per page which defaults to
|
28
|
+
# JsonApiServer::Configuration#default_max_per_page.
|
29
|
+
#
|
30
|
+
# <b><tt>:default_per_page</tt></b> - number of records to show if not specified in <tt>page[limit]</tt>
|
31
|
+
# defaults to JsonApiServer::Configuration#default_per_page).
|
32
|
+
#
|
33
|
+
# ===== Example:
|
34
|
+
#
|
35
|
+
# Create an instance in your controller:
|
36
|
+
#
|
37
|
+
# pagination = JsonApiServer::Pagination.new(request, Comment, max_per_page: 50, default_per_page: 10)
|
38
|
+
#
|
39
|
+
# Merge pagination sub-query into your ActiveRecord query:
|
40
|
+
#
|
41
|
+
# recent_comments = Comment.recent.merge(pagination.query)
|
42
|
+
#
|
43
|
+
# Get JsonApiServer::Paginator instance to create links in your JSON serializer:
|
44
|
+
#
|
45
|
+
# paginator = pagination.paginator_for(recent_comments)
|
46
|
+
# paginator.as_json # creates JSON API links
|
47
|
+
#
|
48
|
+
# Pass paginator as param to a class inheriting from JsonApiServer::ResourcesSerializer and
|
49
|
+
# it creates the links section for you.
|
50
|
+
#
|
51
|
+
# class CommentsSerializer < JsonApiServer::ResourcesSerializer
|
52
|
+
# serializer CommentSerializer
|
53
|
+
# end
|
54
|
+
# serializer = CommentsSerializer.new(recent_comments, paginator: paginator)
|
55
|
+
#
|
56
|
+
# ==== Note:
|
57
|
+
# JsonApiServer::Builder class provides an easier way to use this class.
|
58
|
+
#
|
59
|
+
class Pagination
|
60
|
+
# ActionDispatch::Request passed in constructor.
|
61
|
+
attr_reader :request
|
62
|
+
|
63
|
+
# ActiveRecord::Base model passed in constructor.
|
64
|
+
attr_reader :model
|
65
|
+
|
66
|
+
# Query parameters from #request.
|
67
|
+
attr_reader :params
|
68
|
+
|
69
|
+
# Maximum records per page. Prevents users from requesting too many records. Passed
|
70
|
+
# into constructor.
|
71
|
+
attr_reader :max_per_page
|
72
|
+
|
73
|
+
# Default number of records to show per page. If <tt>page[limit]</tt> is not present, it
|
74
|
+
# will use this value. If this is not set, it will use
|
75
|
+
# JsonApiServer.configuration.default_per_page.
|
76
|
+
attr_reader :default_per_page
|
77
|
+
|
78
|
+
# Arguments:
|
79
|
+
# - <tt>request</tt> - ActionDispatch::Request
|
80
|
+
# - <tt>model</tt> - ActiveRecord::Base model. Used to generate sub-query.
|
81
|
+
# - <tt>options</tt> - (Hash)
|
82
|
+
# - :max_per_page (Integer) - Optional. Defaults to JsonApiServer.configuration.default_max_per_page.
|
83
|
+
# - :default_per_page (Integer) - Optional. Defaults to JsonApiServer.configuration.default_per_page.
|
84
|
+
#
|
85
|
+
def initialize(request, model, **options)
|
86
|
+
@request = request
|
87
|
+
@model = model
|
88
|
+
@max_per_page = (options[:max_per_page] || self.class.default_max_per_page).to_i
|
89
|
+
@default_per_page = (options[:default_per_page] || self.class.default_per_page).to_i
|
90
|
+
@params = request.query_parameters
|
91
|
+
end
|
92
|
+
|
93
|
+
# Calls WillPaginate 'paginate' method with #page and #per_page. Returns an
|
94
|
+
# ActiveRecord::Relation object (a query fragment) which can be
|
95
|
+
# merged into another query with merge.
|
96
|
+
#
|
97
|
+
# ==== Example:
|
98
|
+
#
|
99
|
+
# pagination = JsonApiServer::Pagination.new(request, Comment, options)
|
100
|
+
# recent_comments = Comment.recent.merge(pagination.relation)
|
101
|
+
#
|
102
|
+
def relation
|
103
|
+
@relation ||= model.paginate(page: page, per_page: per_page)
|
104
|
+
end
|
105
|
+
|
106
|
+
alias query relation
|
107
|
+
|
108
|
+
class << self
|
109
|
+
# Default max per page. Defaults to JsonApiServer.configuration.default_max_per_page
|
110
|
+
def default_max_per_page
|
111
|
+
JsonApiServer.configuration.default_max_per_page
|
112
|
+
end
|
113
|
+
|
114
|
+
# Default per page. Defaults to JsonApiServer.configuration.default_per_page.
|
115
|
+
def default_per_page
|
116
|
+
JsonApiServer.configuration.default_per_page
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Create an instance of JsonApiServer::Paginator for a WillPaginate collection. Returns
|
121
|
+
# nil if not a WillPaginate collection.
|
122
|
+
#
|
123
|
+
# params:
|
124
|
+
# - <tt>collection</tt> (WillPaginate collection) - i.e., Comment.recent.paginate(page: x, per_page: y)
|
125
|
+
# - <tt>options</tt> (Hash):
|
126
|
+
# - <tt>per_page</tt> (Integer) - defaults to self.per_page.
|
127
|
+
# - <tt>base_url</tt> (String) - defaults to self.base_url (joins JsonApiServer.configuration.base_url with request.path).
|
128
|
+
def paginator_for(collection, options = {})
|
129
|
+
if collection.respond_to?(:current_page) && collection.respond_to?(:total_pages)
|
130
|
+
# call to_i on WillPaginate::PageNumber which DelegateClass(Integer)
|
131
|
+
# paginator(collection.current_page.to_i, collection.total_pages.to_i, options)
|
132
|
+
# HACK: collection.current_page.to_i disappears when merged? works w/o merge.
|
133
|
+
paginator(page, collection.total_pages.to_i, options)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Number of records per page. From query parameter <tt>page[limit]</tt>.
|
138
|
+
def per_page
|
139
|
+
@per_page ||= begin
|
140
|
+
l = begin
|
141
|
+
params[:page][:limit].to_i
|
142
|
+
rescue
|
143
|
+
default_per_page
|
144
|
+
end
|
145
|
+
l = [max_per_page, l].min
|
146
|
+
l <= 0 ? default_per_page : l
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
alias limit per_page
|
151
|
+
|
152
|
+
# The current page number. From query parameter page[number]</tt>.
|
153
|
+
def page
|
154
|
+
@page ||= begin
|
155
|
+
n = begin
|
156
|
+
params[:page][:number].to_i
|
157
|
+
rescue
|
158
|
+
1
|
159
|
+
end
|
160
|
+
n <= 0 ? 1 : n
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
alias number page
|
165
|
+
|
166
|
+
# Joins JsonApiServer::Configuration#base_url with request.path.
|
167
|
+
def base_url
|
168
|
+
@base_url ||= File.join(JsonApiServer.configuration.base_url, request.path)
|
169
|
+
end
|
170
|
+
|
171
|
+
protected
|
172
|
+
|
173
|
+
# Creates an instance of JsonApiServer::Paginator.
|
174
|
+
#
|
175
|
+
# params:
|
176
|
+
# - <tt>current_page</tt> (Integer)
|
177
|
+
# - <tt>total_pages</tt> (Integer)
|
178
|
+
# - <tt>options</tt> (Hash):
|
179
|
+
# - <tt>per_page</tt> (Integer) - defaults to self.per_page.
|
180
|
+
# - <tt>base_url</tt> (String) - defaults to self.base_url (joins
|
181
|
+
# JsonApiServer::Configuration#base_url with request.path.).
|
182
|
+
def paginator(current_page, total_pages, options = {})
|
183
|
+
per_page = options[:per_page] || self.per_page
|
184
|
+
base_url = options[:base_url] || self.base_url
|
185
|
+
JsonApiServer.paginator(current_page, total_pages, per_page,
|
186
|
+
base_url, params)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module JsonApiServer # :nodoc:
|
2
|
+
# Creates JSON API pagination entries per http://jsonapi.org/examples/#pagination.
|
3
|
+
#
|
4
|
+
# JsonApiServer::Paginator#as_json generates a hash like the following which can be
|
5
|
+
# added to JsonApiServer::BaseSerializer#links section.
|
6
|
+
#
|
7
|
+
# - 'next' is nil when self is the last page.
|
8
|
+
# - 'prev' is nil when self is the first page.
|
9
|
+
#
|
10
|
+
# ===== Example:
|
11
|
+
# "links": {
|
12
|
+
# "self": "http://example.com/articles?page[number]=3&page[limit]=5",
|
13
|
+
# "first": "http://example.com/articles?page[number]=1&page[limit]=5",
|
14
|
+
# "prev": "http://example.com/articles?page[number]=2&page[limit]=5",
|
15
|
+
# "next": "http://example.com/articles?page[number]=4&page[limit]=5",
|
16
|
+
# "last": "http://example.com/articles?page[number]=13&page[limit]=5"
|
17
|
+
# }
|
18
|
+
class Paginator
|
19
|
+
@attrs = %w[first last self next prev]
|
20
|
+
|
21
|
+
# Params:
|
22
|
+
# - <tt>current_page</tt> (Integer)
|
23
|
+
# - <tt>total_pages</tt> (Integer)
|
24
|
+
# - <tt>per_page</tt> (Integer)
|
25
|
+
# - <tt>base_url</tt> (String) - Base url for resource, i.e., <tt>http://example.com/articles</tt>.
|
26
|
+
# - <tt>params</tt> (Hash) - Request parameters. Pagination params are merged into these.
|
27
|
+
def initialize(current_page, total_pages, per_page, base_url, params = {})
|
28
|
+
@current_page = current_page
|
29
|
+
@total_pages = total_pages
|
30
|
+
@per_page = per_page
|
31
|
+
@base_url = base_url
|
32
|
+
@params = params
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
attr_accessor :attrs
|
37
|
+
end
|
38
|
+
|
39
|
+
# First page url.
|
40
|
+
def first
|
41
|
+
@first ||= build_url(merge_params(1))
|
42
|
+
end
|
43
|
+
|
44
|
+
# Last page url.
|
45
|
+
def last
|
46
|
+
@last ||= build_url(merge_params(@total_pages))
|
47
|
+
end
|
48
|
+
|
49
|
+
# Current page url.
|
50
|
+
def self
|
51
|
+
@self ||= build_url(merge_params(@current_page))
|
52
|
+
end
|
53
|
+
|
54
|
+
# Next page url.
|
55
|
+
def next
|
56
|
+
@next ||= begin
|
57
|
+
n = calculate_next
|
58
|
+
n.nil? ? nil : build_url(merge_params(n))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Previous page url.
|
63
|
+
def prev
|
64
|
+
@prev ||= begin
|
65
|
+
p = calculate_prev
|
66
|
+
p.nil? ? nil : build_url(merge_params(p))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns hash:
|
71
|
+
# # i.e.,
|
72
|
+
# {
|
73
|
+
# self: "http://example.com/articles?page[number]=3&page[limit]=5",
|
74
|
+
# first: "http://example.com/articles?page[number]=1&page[limit]=5",
|
75
|
+
# prev: "http://example.com/articles?page[number]=2&page[limit]=5",
|
76
|
+
# next: "http://example.com/articles?page[number]=4&page[limit]=5",
|
77
|
+
# last: "http://example.com/articles?page[number]=13&page[limit]=5"
|
78
|
+
# }
|
79
|
+
def as_json
|
80
|
+
self.class.attrs.each_with_object({}) { |attr, acc| acc[attr] = send(attr); }
|
81
|
+
end
|
82
|
+
|
83
|
+
alias to_h as_json
|
84
|
+
|
85
|
+
# Hash with pagination meta information. Useful for user interfaces, i.e.,
|
86
|
+
# 'page #{current_page} of #{total_pages}'.
|
87
|
+
#
|
88
|
+
# #i.e.,
|
89
|
+
# {
|
90
|
+
# links: {
|
91
|
+
# current_page: 2,
|
92
|
+
# total_pages: 13,
|
93
|
+
# per_page: 5
|
94
|
+
# }
|
95
|
+
# }
|
96
|
+
def meta_info
|
97
|
+
{
|
98
|
+
'links' => {
|
99
|
+
'current_page' => @current_page,
|
100
|
+
'total_pages' => @total_pages,
|
101
|
+
'per_page' => @per_page
|
102
|
+
}
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
|
108
|
+
# Merges pagination params with request params. Pagination params look like
|
109
|
+
# this to page[number]=x&page[limit]=y.
|
110
|
+
def merge_params(number)
|
111
|
+
@params.merge(page: { number: number, limit: @per_page })
|
112
|
+
end
|
113
|
+
|
114
|
+
# Merges base_url with modified params. Params are Url encoded, i.e.,
|
115
|
+
# page%5Blimit%5D=5&page%5Bnumber%5D=1
|
116
|
+
def build_url(params)
|
117
|
+
"#{@base_url}?#{params.to_query}"
|
118
|
+
end
|
119
|
+
|
120
|
+
# Calculates next page. Returns nil when value is invalid, i.e.,
|
121
|
+
# exceeds total_pages.
|
122
|
+
def calculate_next
|
123
|
+
n = @current_page + 1
|
124
|
+
n > @total_pages || n <= 0 ? nil : n
|
125
|
+
end
|
126
|
+
|
127
|
+
# Calculates previous page. Returns nil if value is invalid, i.e.,
|
128
|
+
# less than or equal to 0.
|
129
|
+
def calculate_prev
|
130
|
+
p = @current_page - 1
|
131
|
+
p <= 0 || p > @total_pages ? nil : p
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
module JsonApiServer # :nodoc:
|
2
|
+
# ==== Description
|
3
|
+
#
|
4
|
+
# Part of http://jsonapi.org/format/#fetching-includes:
|
5
|
+
# "An endpoint may support an include request parameter to allow the client to
|
6
|
+
# customize which related resources should be returned.""
|
7
|
+
#
|
8
|
+
# ie., GET /articles/1?include=comments,comment.author,tags HTTP/1.1
|
9
|
+
#
|
10
|
+
# Use this class to build <tt>relationships</tt> and <tt>included</tt> sections
|
11
|
+
# in serializers.
|
12
|
+
#
|
13
|
+
# ==== Examples
|
14
|
+
#
|
15
|
+
# Relate, relate conditionally, or relate a collection. In this example,
|
16
|
+
# Publisher is added only if a condition is met (current user is an admin).
|
17
|
+
#
|
18
|
+
# JsonApiServer::RelationshipsBuilder.new
|
19
|
+
# .relate('author', AuthorSerializer.new(@object.author))
|
20
|
+
# .relate_each('comments', @object.comments) {|c| CommentSerializer.new(c)}
|
21
|
+
# .relationships
|
22
|
+
#
|
23
|
+
# # produces something like this if current user is an admin:
|
24
|
+
# # {
|
25
|
+
# # "author"=>{
|
26
|
+
# # {data: {type: "authors", id: 6, attributes: {first_name: "john", last_name: "Doe"}}}
|
27
|
+
# # },
|
28
|
+
# # "publisher"=>{
|
29
|
+
# # {data: {type: "publishers", id: 1, attributes: {name: "abc"}}}
|
30
|
+
# # },
|
31
|
+
# # "comments"=>[
|
32
|
+
# # {data: {type: "comments", id: 1, attributes: {title: "a", comment: "b"}}},
|
33
|
+
# # {data: {type: "comments", id: 2, attributes: {title: "c", comment: "d"}}}
|
34
|
+
# # ]
|
35
|
+
# # }
|
36
|
+
#
|
37
|
+
# <tt>relationships</tt> can include all relationship data or it can reference data in
|
38
|
+
# <tt>included</tt>. To include and relate in one go, #include with the <tt>:relate</tt> option
|
39
|
+
# which takes a BaseSerializer#as_json_options hash.
|
40
|
+
#
|
41
|
+
# builder = JsonApiServer::RelationshipsBuilder.new
|
42
|
+
# .include('author', AuthorSerializer.new(@object.author),
|
43
|
+
# relate: { include: [:relationship_data] })
|
44
|
+
# .include_each('comments', @object.comments) {|c| CommentSerializer.new(c) }
|
45
|
+
#
|
46
|
+
# builder.relationships
|
47
|
+
# # produces something like this if current user is an admin:
|
48
|
+
# # {
|
49
|
+
# # "author" => {
|
50
|
+
# # {data: {id: 6, type: "authors"}}
|
51
|
+
# # },
|
52
|
+
# # "publisher" => {
|
53
|
+
# # {links: {self: 'http://.../publishers/1'}}
|
54
|
+
# # }
|
55
|
+
# # }
|
56
|
+
#
|
57
|
+
# builder.included
|
58
|
+
# # produces something like this:
|
59
|
+
# # [
|
60
|
+
# # {type: "author", id: 6, attributes: {first_name: "john", last_name: "Doe"}},
|
61
|
+
# # {type: "publisher", id: 1, attributes: {name: "abc"}},
|
62
|
+
# # {type: "comments", id: 1, attributes: {title: "a", comment: "b"}},
|
63
|
+
# # {type: "comments", id: 2, attributes: {title: "c", comment: "d"}}
|
64
|
+
# # ]
|
65
|
+
#
|
66
|
+
class RelationshipsBuilder
|
67
|
+
def initialize
|
68
|
+
@relationships = {}
|
69
|
+
@included = []
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns <tt>relationships</tt> object. Relationships added with
|
73
|
+
# #relate, #relate_if, #relate_each and #include (with <tt>:relate</tt> option).
|
74
|
+
def relationships
|
75
|
+
@relationships.each do |k, v|
|
76
|
+
if v.respond_to?(:uniq!)
|
77
|
+
v.uniq!
|
78
|
+
@relationships[k] = v.first if v.length == 1
|
79
|
+
end
|
80
|
+
end
|
81
|
+
@relationships
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns <tt>included</tt> object. Includes added with
|
85
|
+
# #include, #include_if, #include_each.
|
86
|
+
def included
|
87
|
+
@included.uniq! if @included.respond_to?(:uniq!)
|
88
|
+
@included
|
89
|
+
end
|
90
|
+
|
91
|
+
# Add relationships with this method.
|
92
|
+
#
|
93
|
+
# Arguments:
|
94
|
+
#
|
95
|
+
# - <tt>type</tt> - (String) Relationship type/name.
|
96
|
+
# - <tt>serializer</tt> - (instance of serializer or something that responds to :as_json) Content.
|
97
|
+
#
|
98
|
+
# i.e.,
|
99
|
+
# JsonApiServer::RelationshipsBuilder.new(['comment.author'])
|
100
|
+
# .relate('author', author_serializer)
|
101
|
+
#
|
102
|
+
# # outputs something like...
|
103
|
+
# # { 'author' => {
|
104
|
+
# # :data => {
|
105
|
+
# # :type => "people",
|
106
|
+
# # :id => 6,
|
107
|
+
# # :attributes => {:first_name=>"John", :last_name=>"Steinbeck"}
|
108
|
+
# # }
|
109
|
+
# # }
|
110
|
+
# # }
|
111
|
+
#
|
112
|
+
# JsonApiServer::RelationshipsBuilder.new(['comment.author'])
|
113
|
+
# .relate('author', author_serializer)
|
114
|
+
#
|
115
|
+
# # outputs something like...
|
116
|
+
# # { 'author' => {
|
117
|
+
# # :data => {
|
118
|
+
# # :type => "people",
|
119
|
+
# # :id => 6,
|
120
|
+
# # :attributes => {:first_name=>"John", :last_name=>"Steinbeck"}
|
121
|
+
# # }
|
122
|
+
# # }
|
123
|
+
# # }
|
124
|
+
def relate(type, serializer)
|
125
|
+
merge_relationship(type, serializer)
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
129
|
+
# Add a collection to <tt>relationships</tt> with this method.
|
130
|
+
#
|
131
|
+
# Arguments:
|
132
|
+
#
|
133
|
+
# - <tt>type</tt> - (String) Relationship type/name.
|
134
|
+
# - <tt>collection</tt> - Collection of objects to pass to serializer.
|
135
|
+
# - <tt>block</tt> - Block that returns a serializer or something that repsonds_to as_json.
|
136
|
+
#
|
137
|
+
# i.e.,
|
138
|
+
# JsonApiServer::RelationshipsBuilder.new(['comments'])
|
139
|
+
# .relate_each('comments', @comments) { |c| CommentSerializer.new(c) }
|
140
|
+
def relate_each(type, collection)
|
141
|
+
collection.each { |item| relate(type, yield(item)) }
|
142
|
+
self
|
143
|
+
end
|
144
|
+
|
145
|
+
# Add to <tt>included</tt> with this method.
|
146
|
+
#
|
147
|
+
# Arguments:
|
148
|
+
#
|
149
|
+
# - <tt>type</tt> - (String) Relationship type/name.
|
150
|
+
# - <tt>serializer</tt> - (instance of serializer or something that responds to :as_json) - relationship content.
|
151
|
+
# - <tt>options</tt> - (Hash) -
|
152
|
+
# - :relate (Hash) - Optional. Add to relationships. BaseSerializer#as_json_options hash, i.e, <tt>{ include: [:relationship_data] }</tt>.
|
153
|
+
#
|
154
|
+
# i.e.,
|
155
|
+
# # include and relate
|
156
|
+
# JsonApiServer::RelationshipsBuilder.new(['comment.author'])
|
157
|
+
# .include('author', author_serializer,
|
158
|
+
# relate: {include: [:relationship_data]})
|
159
|
+
#
|
160
|
+
# # or just include
|
161
|
+
# JsonApiServer::RelationshipsBuilder.new(['author'])
|
162
|
+
# .include('author', author_serializer)
|
163
|
+
def include(type, serializer, **options)
|
164
|
+
merge_included(serializer)
|
165
|
+
if options[:relate]
|
166
|
+
serializer.as_json_options = options[:relate]
|
167
|
+
relate(type, serializer)
|
168
|
+
end
|
169
|
+
self
|
170
|
+
end
|
171
|
+
|
172
|
+
# Add a collection to <tt>included</tt> with this method.
|
173
|
+
#
|
174
|
+
# Arguments:
|
175
|
+
#
|
176
|
+
# - <tt>type</tt> - (String) Relationship type/name.
|
177
|
+
# - <tt>collection</tt> - Collection of objects to pass to block.
|
178
|
+
# - <tt>options</tt> - (Hash) -
|
179
|
+
# - :relate (Hash) - Optional. Add to relationships. BaseSerializer#as_json_options hash, i.e, <tt>{ include: [:relationship_data] }</tt>.
|
180
|
+
# - <tt>block</tt> - Block that returns a serializer or something that repsonds_to as_json.
|
181
|
+
#
|
182
|
+
# i.e.,
|
183
|
+
#
|
184
|
+
# JsonApiServer::RelationshipsBuilder.new(['comments'])
|
185
|
+
# .include_each('comments', @comments) {|c| CommentSerializer.new(c)}
|
186
|
+
#
|
187
|
+
def include_each(type, collection, **options)
|
188
|
+
collection.each { |item| include(type, yield(item), options) }
|
189
|
+
self
|
190
|
+
end
|
191
|
+
|
192
|
+
protected
|
193
|
+
|
194
|
+
def merge_relationship(type, value)
|
195
|
+
content = value.as_json if value.respond_to?(:as_json)
|
196
|
+
return if content.blank?
|
197
|
+
|
198
|
+
if @relationships.key?(type)
|
199
|
+
unless @relationships[type].is_a?(Array)
|
200
|
+
@relationships[type] = [@relationships[type]]
|
201
|
+
end
|
202
|
+
@relationships[type].push(content)
|
203
|
+
else
|
204
|
+
@relationships.merge!(type => content)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def merge_included(value)
|
209
|
+
if value.respond_to?(:as_json)
|
210
|
+
content = value.respond_to?(:as_json_options) ? value.as_json(include: [:data]) : value.as_json
|
211
|
+
@included.push(content[:data]) if content.present?
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|