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,245 @@
|
|
1
|
+
module JsonApiServer # :nodoc:
|
2
|
+
# ==== Description
|
3
|
+
#
|
4
|
+
# Serializer class for a resource. Subclasses JsonApiServer::BaseSerializer.
|
5
|
+
#
|
6
|
+
# Example class:
|
7
|
+
#
|
8
|
+
# class CommentSerializer < JsonApiServer::ResourceSerializer
|
9
|
+
# resource_type 'comments'
|
10
|
+
#
|
11
|
+
# def links
|
12
|
+
# { self: File.join(base_url, "/comments/#{@object.id}") }
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def data
|
16
|
+
# {}.tap do |h|
|
17
|
+
# h['type'] = self.class.type
|
18
|
+
# h['id'] = @object.id
|
19
|
+
# h['attributes'] = attributes
|
20
|
+
# h['relationships'] = inclusions.relationships if inclusions?
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# def included
|
25
|
+
# inclusions.included if inclusions?
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# protected
|
29
|
+
#
|
30
|
+
# def attributes
|
31
|
+
# attributes_builder
|
32
|
+
# .add_multi(@object, 'title', 'comment')
|
33
|
+
# .add('created_at', @object.created_at.try(:iso8601, 9))
|
34
|
+
# .add('updated_at', @object.updated_at.try(:iso8601, 9))
|
35
|
+
# .attributes
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# def inclusions
|
39
|
+
# @inclusions ||= begin
|
40
|
+
# if relationship?('comment.author')
|
41
|
+
# relationships_builder.relate('author', user_serializer(@object.author))
|
42
|
+
# end
|
43
|
+
# if relationship?('comment.author.links')
|
44
|
+
# relationships_builder.include('author', user_serializer(@object.author),
|
45
|
+
# relate: { include: [:links] })
|
46
|
+
# end
|
47
|
+
# relationships_builder
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# def user_serializer(user, as_json_options = { include: [:data] })
|
52
|
+
# ::UserSerializer.new(
|
53
|
+
# user,
|
54
|
+
# includes: includes,
|
55
|
+
# fields: fields,
|
56
|
+
# as_json_options: as_json_options
|
57
|
+
# )
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# Create an instance from builder:
|
62
|
+
#
|
63
|
+
# builder = JsonApiServer::Builder.new(request, Comment.find(params[:id]))
|
64
|
+
# .add_include(['comment.author', 'comment.author.links'])
|
65
|
+
# .add_fields
|
66
|
+
#
|
67
|
+
# serializer = CommentSerializer.from_builder(builder)
|
68
|
+
#
|
69
|
+
class ResourceSerializer < JsonApiServer::BaseSerializer
|
70
|
+
# Array. Relationships to include. Array of strings. From the
|
71
|
+
# 'include' param. Extracted via JsonApiServer::Include#includes which
|
72
|
+
# is also available through JsonApiServer::Builder#includes. Defaults to
|
73
|
+
# nil. Set in initializer options.
|
74
|
+
#
|
75
|
+
# i.e.,
|
76
|
+
#
|
77
|
+
# GET /articles?include=comments.author,publisher becomes:
|
78
|
+
# includes = ['comments.author', 'publisher']
|
79
|
+
attr_reader :includes
|
80
|
+
|
81
|
+
# Hash. Fields requested by user. From the 'fields' param. Extracted
|
82
|
+
# via JsonApiServer::Fields#sparse_fields which is also available through
|
83
|
+
# JsonApiServer::Builder#sparse_fields. Defaults to nil. Set in initializer
|
84
|
+
# options.
|
85
|
+
#
|
86
|
+
# i.e.,
|
87
|
+
#
|
88
|
+
# GET /articles?include=author&fields[articles]=title,body&fields[people]=name becomes:
|
89
|
+
# fields = {'articles' => ['title', 'body'], 'people' => ['name']}
|
90
|
+
attr_reader :fields
|
91
|
+
|
92
|
+
# * <tt>object</tt> - instance of model or presenter or whatever stores data.
|
93
|
+
# * <tt>options</tt> - Hash (optional):
|
94
|
+
# * <tt>:includes</tt> - Instance of JsonApiServer::Include or nil. Sets #includes.
|
95
|
+
# * <tt>:fields</tt> - Instance of JsonApiServer::Fields or nil. Sets #fields.
|
96
|
+
def initialize(object, **options)
|
97
|
+
super(options)
|
98
|
+
@object = object
|
99
|
+
@includes = options[:includes]
|
100
|
+
@fields = options[:fields]
|
101
|
+
end
|
102
|
+
|
103
|
+
class << self
|
104
|
+
# 'type' used in #relationship_data and #data. i.e.:
|
105
|
+
#
|
106
|
+
# "data": {"type": "articles", "id": 2}
|
107
|
+
def type
|
108
|
+
@type || type_from_class_name
|
109
|
+
end
|
110
|
+
|
111
|
+
# 'type' used in #relationship_data.
|
112
|
+
def resource_type(type)
|
113
|
+
@type = type
|
114
|
+
end
|
115
|
+
|
116
|
+
# * <tt>:builder</tt> - Instance of JsonApiServer::Builder.
|
117
|
+
# #object, #includes and #fields will be extracted from it.
|
118
|
+
# * <tt>options</tt> - Hash, override values from Builder or set additional options.
|
119
|
+
# * <tt>:includes</tt> - Instance of JsonApiServer::Include or nil. Sets #includes.
|
120
|
+
# * <tt>:fields</tt> - Instance of JsonApiServer::Fields or nil. Sets #fields.
|
121
|
+
# * <tt>filter</tt> - Instance of JsonApiServer::Filter or nil. Sets #filter.
|
122
|
+
# * <tt>:paginator</tt> - Instance of JsonApiServer::Fields or nil. Sets #paginator.
|
123
|
+
# * <tt>:as_json_options</tt> - See options at JsonApiServer::BaseSerializer#as_json_options.
|
124
|
+
def from_builder(builder, **options)
|
125
|
+
opts = options.merge(fields: options[:fields] || builder.sparse_fields,
|
126
|
+
includes: options[:includes] || builder.includes,
|
127
|
+
paginator: options[:paginator] || builder.paginator,
|
128
|
+
filter: options[:filter] || builder.filter)
|
129
|
+
new(builder.query, opts)
|
130
|
+
end
|
131
|
+
|
132
|
+
protected
|
133
|
+
|
134
|
+
# Get type from class name.
|
135
|
+
def type_from_class_name
|
136
|
+
@type_from_class_name ||= begin
|
137
|
+
class_name = name.split('::').last
|
138
|
+
class_name.downcase!
|
139
|
+
class_name.gsub!(/serializer/, '')
|
140
|
+
class_name.pluralize
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Content when #as_json_options {include: [:relationship_data]} is specified. Defaults
|
146
|
+
# to { 'type' => <resource_type>, 'id' => <@object.id> }. resource_type can be
|
147
|
+
# set with class method #resource_type, otherwise it will be guessed from
|
148
|
+
# serializer class name.
|
149
|
+
def relationship_data
|
150
|
+
id = @object.try(:id) || @object.try(:[], :id)
|
151
|
+
{ 'type' => self.class.type, 'id' => id }
|
152
|
+
end
|
153
|
+
|
154
|
+
protected
|
155
|
+
|
156
|
+
# Returns a new instance of JsonApiServer::AttributesBuilder for
|
157
|
+
# the specified #fields <tt>type</tt>.
|
158
|
+
#
|
159
|
+
# * <tt>type</tt> - the resource type.
|
160
|
+
#
|
161
|
+
# i.e.,
|
162
|
+
#
|
163
|
+
# # GET /articles?include=author&fields[articles]=title,body&fields[people]=name becomes:
|
164
|
+
# # fields = {'articles' => ['title', 'body'], 'people' => ['name']}
|
165
|
+
#
|
166
|
+
# self.attributes_builder_for('articles')
|
167
|
+
# .add('title', @object.title)
|
168
|
+
# .add('body', @object.body)
|
169
|
+
# .add('created_at', @object.created_at)
|
170
|
+
#
|
171
|
+
def attributes_builder_for(type)
|
172
|
+
JsonApiServer::AttributesBuilder.new(fields_for(type))
|
173
|
+
end
|
174
|
+
|
175
|
+
# Instance of JsonApiServer::AttributesBuilder for the class's resource 'type'.
|
176
|
+
#
|
177
|
+
# i.e.,
|
178
|
+
#
|
179
|
+
# self.attributes_builder # => attributes_builder_for(self.class.type)
|
180
|
+
# .add('title', @object.title)
|
181
|
+
# .add('body', @object.body)
|
182
|
+
# .add('created_at', @object.created_at)
|
183
|
+
def attributes_builder
|
184
|
+
@attributes_builder ||= attributes_builder_for(self.class.type)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Instance of JsonApiServer::MetaBuilder.
|
188
|
+
#
|
189
|
+
# i.e.,
|
190
|
+
#
|
191
|
+
# self.meta_builder
|
192
|
+
# .add('total_records', 35)
|
193
|
+
# ...
|
194
|
+
# self.meta_builder
|
195
|
+
# .add('paging', 'showing 11 - 20')
|
196
|
+
# .meta # => { 'total_records': 35, 'paging': 'showing 11 - 20' }
|
197
|
+
#
|
198
|
+
def meta_builder
|
199
|
+
@meta_builder ||= JsonApiServer::MetaBuilder.new
|
200
|
+
end
|
201
|
+
|
202
|
+
# Instance of JsonApiServer::RelationshipsBuilder.
|
203
|
+
#
|
204
|
+
# i.e.,
|
205
|
+
#
|
206
|
+
# self.relationships_builder
|
207
|
+
# .relate(...)
|
208
|
+
# .relate_if(...)
|
209
|
+
# .relate_each(...)
|
210
|
+
#
|
211
|
+
# self.relationships_builder.relationships # get relationships section
|
212
|
+
# self.relationships_builder.included # get included section
|
213
|
+
def relationships_builder
|
214
|
+
@relationships_builder ||= JsonApiServer::RelationshipsBuilder.new
|
215
|
+
end
|
216
|
+
|
217
|
+
alias rb relationships_builder
|
218
|
+
|
219
|
+
# Returns true if relationship is in #includes array.
|
220
|
+
#
|
221
|
+
# * <tt>relationship</tt> - Name of relationship. String or symbol.
|
222
|
+
#
|
223
|
+
# i.e.,
|
224
|
+
#
|
225
|
+
# # GET /articles?include=comment.author,publisher becomes:
|
226
|
+
# # includes = ['comment.author', 'publisher']
|
227
|
+
#
|
228
|
+
# self.relationship?('comment.author') # => true
|
229
|
+
# self.relationship?('addresses') # => false
|
230
|
+
def relationship?(relationship)
|
231
|
+
@includes.respond_to?(:include?) && @includes.include?(relationship.to_s)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Returns true if there are requested inclusions. False otherwise.
|
235
|
+
def inclusions?
|
236
|
+
@includes.respond_to?(:any?) && @includes.any?
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns the fields for a specific type. i.e., fields_for('articles') or nil
|
240
|
+
# if type doesn't exist or fields is nil.
|
241
|
+
def fields_for(type)
|
242
|
+
@fields.respond_to?(:key) ? @fields[type.to_s] : nil
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# TODO: inheritance is dubious with introduction of :type and relationship_data.
|
2
|
+
module JsonApiServer # :nodoc:
|
3
|
+
# ==== Description
|
4
|
+
#
|
5
|
+
# Serializer for a collection/array of resources. Inherits from
|
6
|
+
# JsonApiServer::ResourceSerializer.
|
7
|
+
#
|
8
|
+
# ==== Example
|
9
|
+
#
|
10
|
+
# Given a resource serializer for Topic:
|
11
|
+
#
|
12
|
+
# class TopicSerializer < JsonApiServer::ResourceSerializer
|
13
|
+
# resource_type 'topics'
|
14
|
+
#
|
15
|
+
# def links
|
16
|
+
# { self: File.join(base_url, "/topics/#{@object.id}") }
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def data
|
20
|
+
# {
|
21
|
+
# type: self.class.type,
|
22
|
+
# id: @object.id,
|
23
|
+
# attributes: attributes
|
24
|
+
# }
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# protected
|
28
|
+
#
|
29
|
+
# def attributes
|
30
|
+
# attributes_builder
|
31
|
+
# .add('book', @object.book)
|
32
|
+
# .add('author', @object.author)
|
33
|
+
# .add('quote', @object.quote)
|
34
|
+
# .add('character', @object.character)
|
35
|
+
# .add('location', @object.location)
|
36
|
+
# .add('published', @object.published)
|
37
|
+
# .add('created_at', @object.created_at.try(:iso8601, 0))
|
38
|
+
# .add('updated_at', @object.updated_at.try(:iso8601, 0))
|
39
|
+
# .attributes
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# Create a Topics serializer like so:
|
44
|
+
#
|
45
|
+
# class TopicsSerializer < JsonApiServer::ResourcesSerializer
|
46
|
+
# serializer TopicSerializer
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# Create an instance from builder:
|
50
|
+
#
|
51
|
+
# builder = JsonApiServer::Builder.new(request, Topic.all)
|
52
|
+
# .add_pagination(pagination_options)
|
53
|
+
# .add_filter(filter_options)
|
54
|
+
# .add_sort(sort_options)
|
55
|
+
# .add_include(include_options)
|
56
|
+
# .add_fields
|
57
|
+
#
|
58
|
+
# # populates links with pagination info, merges data from each
|
59
|
+
# # Topic serializer instance.
|
60
|
+
# serializer = TopicsSerializer.from_builder(builder)
|
61
|
+
#
|
62
|
+
class ResourcesSerializer < JsonApiServer::ResourceSerializer
|
63
|
+
# Instance of JsonApiServer::Paginator or nil (default). Based on pagination
|
64
|
+
# params. Extracted via JsonApiServer::Pagination and available
|
65
|
+
# through JsonApiServer::Builder#paginator. Set in initializer options.
|
66
|
+
attr_reader :paginator
|
67
|
+
|
68
|
+
# Instance of JsonApiServer::Filter or nil (default). Based on filter
|
69
|
+
# params. Extracted via JsonApiServer::Filter and available
|
70
|
+
# through JsonApiServer::Builder#filter. Set in initializer options.
|
71
|
+
attr_reader :filter
|
72
|
+
|
73
|
+
class << self
|
74
|
+
attr_reader :objects_serializer
|
75
|
+
|
76
|
+
# A serializer class. If set,'objects' will be converted to instances of
|
77
|
+
# this serializer.
|
78
|
+
def serializer(klass)
|
79
|
+
@objects_serializer = klass
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# * <tt>objects</tt> - An array of objects. If #serializer is specified, the
|
84
|
+
# objects will be converted to this class.
|
85
|
+
# * <tt>options</tt> - Hash:
|
86
|
+
# * <tt>filter</tt> - Instance of JsonApiServer::Filter or nil. Sets #filter.
|
87
|
+
# * <tt>:paginator</tt> - Instance of JsonApiServer::Fields or nil. Sets #paginator.
|
88
|
+
# * <tt>:as_json_options</tt> - See options at JsonApiServer::BaseSerializer#as_json_options.
|
89
|
+
def initialize(objects, **options)
|
90
|
+
super(nil, options)
|
91
|
+
remove_instance_variable(:@object)
|
92
|
+
@paginator = options[:paginator]
|
93
|
+
@filter = options[:filter]
|
94
|
+
@objects = initalize_objects(objects)
|
95
|
+
end
|
96
|
+
|
97
|
+
def links
|
98
|
+
@paginator.try(:as_json) || {}
|
99
|
+
end
|
100
|
+
|
101
|
+
# Subclasses override for customized behaviour.
|
102
|
+
def data
|
103
|
+
data = @objects.try(:map) { |o| o.try(:data) }
|
104
|
+
data.try(:compact!) || data
|
105
|
+
end
|
106
|
+
|
107
|
+
# Subclasses override for customized behaviour.
|
108
|
+
def relationship_data
|
109
|
+
data = @objects.try(:map) { |o| o.try(:relationship_data) }
|
110
|
+
data.try(:compact!) || data
|
111
|
+
end
|
112
|
+
|
113
|
+
# Subclasses override for customized behaviour.
|
114
|
+
def included
|
115
|
+
included = @objects.try(:map) { |o| o.try(:included) }
|
116
|
+
included.try(:flatten!)
|
117
|
+
included.try(:compact!) || included
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
|
122
|
+
def initalize_objects(objects)
|
123
|
+
klass = self.class.objects_serializer
|
124
|
+
if klass && objects.respond_to?(:map)
|
125
|
+
objects.map { |object| klass.new(object, includes: includes, fields: fields) }
|
126
|
+
else
|
127
|
+
objects
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
#--
|
2
|
+
# TODO: https://github.com/ohler55/oj/issues/199
|
3
|
+
#++
|
4
|
+
module JsonApiServer # :nodoc:
|
5
|
+
# ==== Description
|
6
|
+
#
|
7
|
+
# to_json serializer method. Used by the various serializers.
|
8
|
+
module Serializer
|
9
|
+
# Serializer options from JsonApiServer::Configuration#serializer_options.
|
10
|
+
def serializer_options
|
11
|
+
JsonApiServer.configuration.serializer_options
|
12
|
+
end
|
13
|
+
|
14
|
+
# Classes override.
|
15
|
+
def as_json
|
16
|
+
{}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Serializes to JSON. Serializer options default to
|
20
|
+
# JsonApiServer.configuration.serializer_options unless
|
21
|
+
# alternate are specified with the <tt>options</tt> parameter.
|
22
|
+
# Default options are:
|
23
|
+
# escape_mode: :xss_safe,
|
24
|
+
# time: :xmlschema,
|
25
|
+
# mode: :compat
|
26
|
+
#
|
27
|
+
# Parameters:
|
28
|
+
# - options (Hash) - OJ serialization options: https://github.com/ohler55/oj#options. If none specified, it uses defaults.
|
29
|
+
def to_json(**options)
|
30
|
+
opts = options.empty? ? serializer_options : options
|
31
|
+
Oj.dump(as_json, opts)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
#--
|
2
|
+
# TODO:
|
3
|
+
# - Does not return 400 Bad Request if sort attribute is not supported.
|
4
|
+
# - Sort nested relationships specified with dot notation - topic.comments
|
5
|
+
#++
|
6
|
+
module JsonApiServer # :nodoc:
|
7
|
+
# === Description:
|
8
|
+
# Implements sort parameters per JSON API Spec:
|
9
|
+
# http://jsonapi.org/format/#fetching-sorting.
|
10
|
+
#
|
11
|
+
# From the spec: "The sort order for each sort field MUST be ascending unless it is
|
12
|
+
# prefixed with a minus (U+002D HYPHEN-MINUS, "-", in which case it MUST be
|
13
|
+
# descending."
|
14
|
+
#
|
15
|
+
# This class (1) whitelists sort params, (2) optionally specifies a default order,
|
16
|
+
# and (3) generates a sub-query based on these params.
|
17
|
+
#
|
18
|
+
# === Usage:
|
19
|
+
#
|
20
|
+
# A sort request will look like:
|
21
|
+
# /topics?sort=-created,title
|
22
|
+
#
|
23
|
+
# This gets converted to an array. Sort order is ASC by default. Minus = DESC.
|
24
|
+
# ['-created', 'title']
|
25
|
+
#
|
26
|
+
# Sort attributes are configured like so. <tt>:permitted</tt> are whitelisted attributes.
|
27
|
+
# <tt>:default</tt> specifies the default sort order. If a user specifies sort params other
|
28
|
+
# than those in <tt>:permitted</tt>, a JsonApiServer::BadRequest exception is
|
29
|
+
# raised which renders a 400 error.
|
30
|
+
# {
|
31
|
+
# permitted: [:id, :title, { created: { col_name: :created_at}],
|
32
|
+
# default: { id: :desc }
|
33
|
+
# }
|
34
|
+
# In this example, id, title and created (alias for created_at column)
|
35
|
+
# are permitted sort params.
|
36
|
+
#
|
37
|
+
# ==== Example:
|
38
|
+
# # create sort options
|
39
|
+
# sort_options = {
|
40
|
+
# permitted: [:id, :title, { created: { col_name: :created_at}],
|
41
|
+
# default: { id: :desc }
|
42
|
+
# }
|
43
|
+
#
|
44
|
+
# # create instance
|
45
|
+
# sort = JsonApiServer::Sort.new(request, Topic, sort_options)
|
46
|
+
#
|
47
|
+
# # merge into master query
|
48
|
+
# recent_topics = Topic.recent.merge(sort.query)
|
49
|
+
#
|
50
|
+
# # see sort params
|
51
|
+
# puts sort.sort
|
52
|
+
#
|
53
|
+
# ==== Note:
|
54
|
+
# JsonApiServer::Builder class provides an easier way to use this class.
|
55
|
+
#
|
56
|
+
class Sort
|
57
|
+
#--
|
58
|
+
# ActiveRecord::QueryMethods order is defined order(*args)
|
59
|
+
# i.e., User.order(:name, email: :desc)
|
60
|
+
#++
|
61
|
+
|
62
|
+
# Controller request object.
|
63
|
+
attr_reader :request
|
64
|
+
|
65
|
+
# Model passed in constructor.
|
66
|
+
attr_reader :model
|
67
|
+
|
68
|
+
# (Hash) Sort options. Specify the attributes that can be used in ActiveRecord
|
69
|
+
# query method 'sort'. No sort attributes can be used except for those
|
70
|
+
# specified here.
|
71
|
+
#
|
72
|
+
# - <tt>permitted</tt> - array of model attribute names
|
73
|
+
# - <tt>default</tt> - used if no sort params are specified (or rejected)
|
74
|
+
#
|
75
|
+
# i.e.,
|
76
|
+
#
|
77
|
+
# Allows sorting on model attributes :id, :title, :created.
|
78
|
+
# If no sort params are specified in the request, it sorts by 'id' desc.
|
79
|
+
#
|
80
|
+
# {
|
81
|
+
# permitted: [:id, :title, { created: { col_name: :created_at}],
|
82
|
+
# default: { id: :desc }
|
83
|
+
# }
|
84
|
+
attr_reader :options
|
85
|
+
|
86
|
+
# Request query params (request.query_parameters).
|
87
|
+
attr_reader :params
|
88
|
+
|
89
|
+
# Params:
|
90
|
+
# - request - instance of request object
|
91
|
+
# - options - sort options. See #options documentation.
|
92
|
+
def initialize(request, model, options = {})
|
93
|
+
@request = request
|
94
|
+
@model = model
|
95
|
+
@options = options
|
96
|
+
@params = request.query_parameters
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns an ActiveRecord::Relation if sort_params are present. Otherwise
|
100
|
+
# returns nil. Instance is a query fragment intended to be merged into another
|
101
|
+
# query.
|
102
|
+
#
|
103
|
+
# ==== Example:
|
104
|
+
#
|
105
|
+
# sort = JsonApiServer::Sort.new(request, Comment, options)
|
106
|
+
# Comment.recent.merge!(sort.query)
|
107
|
+
#
|
108
|
+
def relation
|
109
|
+
@relation ||= model.order(sort_params) if sort_params.present?
|
110
|
+
end
|
111
|
+
|
112
|
+
alias query relation
|
113
|
+
|
114
|
+
# Sort query parameter params[:sort].
|
115
|
+
def sort
|
116
|
+
@sort ||= params[:sort].to_s
|
117
|
+
end
|
118
|
+
|
119
|
+
# Instance of JsonApiServer::SortConfigs based on #options.
|
120
|
+
def configs
|
121
|
+
@configs ||= JsonApiServer::SortConfigs.new(options)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Calculated ActiveRecord 'order' parameters. Use in queries.
|
125
|
+
def sort_params
|
126
|
+
@sort_params ||= begin
|
127
|
+
attrs = sort.split(',')
|
128
|
+
sort_params = convert(attrs)
|
129
|
+
sort_params.empty? ? configs.default_order : sort_params
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
protected
|
134
|
+
|
135
|
+
# Converts to ActiveRecord query order parameters; whitelists based on configs.
|
136
|
+
# Raises JsonApiServer::BadRequest with descriptive message if attribute
|
137
|
+
# is not whitelisted.
|
138
|
+
def convert(attrs)
|
139
|
+
whitelisted = []
|
140
|
+
|
141
|
+
attrs.each do |attr|
|
142
|
+
attr.strip!
|
143
|
+
order = attr.start_with?('-') ? :desc : :asc
|
144
|
+
attr_name = order == :desc ? attr.slice(1..attr.length) : attr
|
145
|
+
config = configs.config_for(attr_name)
|
146
|
+
if config.nil?
|
147
|
+
msg = I18n.t('json_api_server.render_400.sort', param: attr_name)
|
148
|
+
raise JsonApiServer::BadRequest, msg
|
149
|
+
end
|
150
|
+
whitelisted << { (config[:col_name] || config[:attr]) => order }
|
151
|
+
end
|
152
|
+
|
153
|
+
whitelisted
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|