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,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
|