json_api_server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|