jsonapi-resources 0.0.8 → 0.0.9
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 +4 -4
- data/README.md +25 -3
- data/lib/jsonapi/error_codes.rb +2 -1
- data/lib/jsonapi/exceptions.rb +15 -0
- data/lib/jsonapi/operation.rb +8 -8
- data/lib/jsonapi/request.rb +22 -1
- data/lib/jsonapi/resource.rb +23 -4
- data/lib/jsonapi/resource_controller.rb +5 -4
- data/lib/jsonapi/resources/version.rb +1 -1
- data/test/controllers/controller_test.rb +35 -0
- data/test/fixtures/active_record.rb +11 -7
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6eb51f8c096de8ece9f78168a967750a958fe620
|
4
|
+
data.tar.gz: 1e7e845621c801642776c99bd559185af85a645c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 57b8f79c39ddfe67ab9fc939292b8f9c9ed014cf0b673eab9c7e9957a927dd743b51c719c40c55973c81856898c349715c1ae3560a8e72c5efc37fe7c71e7407
|
7
|
+
data.tar.gz: d3ac8f45d9d357fc9adc44a88cb46596c5dcb81bac8b6f1bb3bbb20079eb797490689f4014e118a52a3885f418d9955ebc52e21f8de0f1fe66ccfb4fe18852f6
|
data/README.md
CHANGED
@@ -92,7 +92,7 @@ class AuthorResource < JSONAPI::Resource
|
|
92
92
|
model_name 'Person'
|
93
93
|
has_many :posts
|
94
94
|
|
95
|
-
def fetchable_fields
|
95
|
+
def fetchable_fields
|
96
96
|
if (context.current_user.guest)
|
97
97
|
super(context) - [:email]
|
98
98
|
else
|
@@ -132,6 +132,24 @@ end
|
|
132
132
|
|
133
133
|
The `context` is not by default used by the `ResourceController`, but may be used if you override the controller methods. By using the context you have the option to determine the createable and updateable fields based on the user.
|
134
134
|
|
135
|
+
##### Sortable Attributes
|
136
|
+
|
137
|
+
JR supports [sorting primary resources by multiple sort criteria](http://jsonapi.org/format/#fetching-sorting).
|
138
|
+
|
139
|
+
By default all attributes are assumed to be sortable. To prevent some attributes from being sortable, override the `self.sortable_fields` method on a resource.
|
140
|
+
|
141
|
+
Here's an example that prevents sorting by post's `body`:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
class PostResource < JSONAPI::Resource
|
145
|
+
attribute :id, :title, :body
|
146
|
+
|
147
|
+
def self.sortable_fields(context)
|
148
|
+
super(context) - [:body]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
135
153
|
##### Attribute Formatting
|
136
154
|
|
137
155
|
Attributes can have a Format. By default all attributes use the default formatter. If an attribute has the `format` option set the system will attempt to find a formatter based on this name. In the following example the `last_login_time` will be returned formatted to a certain time zone:
|
@@ -244,7 +262,7 @@ end
|
|
244
262
|
|
245
263
|
Basic finding by filters is supported by resources. However if you have more complex requirements for finding you can override the `find` and `find_by_key` methods on the resource.
|
246
264
|
|
247
|
-
Here's an example that defers the `find` operation to a `current_user` set on the `context
|
265
|
+
Here's an example that defers the `find` operation to a `current_user` set on the `context` option:
|
248
266
|
|
249
267
|
```ruby
|
250
268
|
class AuthorResource < JSONAPI::Resource
|
@@ -254,7 +272,8 @@ class AuthorResource < JSONAPI::Resource
|
|
254
272
|
|
255
273
|
filter :name
|
256
274
|
|
257
|
-
def self.find(attrs,
|
275
|
+
def self.find(attrs, options = {})
|
276
|
+
context = options[:context]
|
258
277
|
authors = context.current_user.find_authors(attrs)
|
259
278
|
|
260
279
|
return authors.map do |author|
|
@@ -313,6 +332,9 @@ module JSONAPI
|
|
313
332
|
COUNT_MISMATCH = 108
|
314
333
|
KEY_ORDER_MISMATCH = 109
|
315
334
|
KEY_NOT_INCLUDED_IN_URL = 110
|
335
|
+
INVALID_INCLUDE = 112
|
336
|
+
RELATION_EXISTS = 113
|
337
|
+
INVALID_SORT_PARAM = 114
|
316
338
|
|
317
339
|
RECORD_NOT_FOUND = 404
|
318
340
|
LOCKED = 423
|
data/lib/jsonapi/error_codes.rb
CHANGED
data/lib/jsonapi/exceptions.rb
CHANGED
@@ -130,6 +130,21 @@ module JSONAPI
|
|
130
130
|
end
|
131
131
|
end
|
132
132
|
|
133
|
+
class InvalidSortParam < Error
|
134
|
+
attr_accessor :sort_param, :resource
|
135
|
+
def initialize(resource, sort_param)
|
136
|
+
@resource = resource
|
137
|
+
@sort_param = sort_param
|
138
|
+
end
|
139
|
+
|
140
|
+
def errors
|
141
|
+
[JSONAPI::Error.new(code: JSONAPI::INVALID_SORT_PARAM,
|
142
|
+
status: :bad_request,
|
143
|
+
title: 'Invalid sort param',
|
144
|
+
detail: "#{sort_param} is not a valid sort param for #{resource}")]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
133
148
|
class ParametersNotAllowed < Error
|
134
149
|
attr_accessor :params
|
135
150
|
def initialize(params)
|
data/lib/jsonapi/operation.rb
CHANGED
@@ -39,7 +39,7 @@ module JSONAPI
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def apply(context)
|
42
|
-
resource = @resource_klass.find_by_key(@resource_id, context)
|
42
|
+
resource = @resource_klass.find_by_key(@resource_id, context: context)
|
43
43
|
resource.remove
|
44
44
|
|
45
45
|
return JSONAPI::OperationResult.new(:no_content)
|
@@ -62,7 +62,7 @@ module JSONAPI
|
|
62
62
|
end
|
63
63
|
|
64
64
|
def apply(context)
|
65
|
-
resource = @resource_klass.find_by_key(@resource_id, context)
|
65
|
+
resource = @resource_klass.find_by_key(@resource_id, context: context)
|
66
66
|
resource.replace_fields(values)
|
67
67
|
resource.save
|
68
68
|
|
@@ -81,7 +81,7 @@ module JSONAPI
|
|
81
81
|
end
|
82
82
|
|
83
83
|
def apply(context)
|
84
|
-
resource = @resource_klass.find_by_key(@resource_id, context)
|
84
|
+
resource = @resource_klass.find_by_key(@resource_id, context: context)
|
85
85
|
resource.create_has_one_link(@association_type, @key_value)
|
86
86
|
resource.save
|
87
87
|
|
@@ -100,7 +100,7 @@ module JSONAPI
|
|
100
100
|
end
|
101
101
|
|
102
102
|
def apply(context)
|
103
|
-
resource = @resource_klass.find_by_key(@resource_id, context)
|
103
|
+
resource = @resource_klass.find_by_key(@resource_id, context: context)
|
104
104
|
resource.replace_has_one_link(@association_type, @key_value)
|
105
105
|
resource.save
|
106
106
|
|
@@ -119,7 +119,7 @@ module JSONAPI
|
|
119
119
|
end
|
120
120
|
|
121
121
|
def apply(context)
|
122
|
-
resource = @resource_klass.find_by_key(@resource_id, context)
|
122
|
+
resource = @resource_klass.find_by_key(@resource_id, context: context)
|
123
123
|
@key_values.each do |value|
|
124
124
|
resource.create_has_many_link(@association_type, value)
|
125
125
|
end
|
@@ -139,7 +139,7 @@ module JSONAPI
|
|
139
139
|
end
|
140
140
|
|
141
141
|
def apply(context)
|
142
|
-
resource = @resource_klass.find_by_key(@resource_id, context)
|
142
|
+
resource = @resource_klass.find_by_key(@resource_id, context: context)
|
143
143
|
resource.replace_has_many_links(@association_type, @key_values)
|
144
144
|
resource.save
|
145
145
|
|
@@ -158,7 +158,7 @@ module JSONAPI
|
|
158
158
|
end
|
159
159
|
|
160
160
|
def apply(context)
|
161
|
-
resource = @resource_klass.find_by_key(@resource_id, context)
|
161
|
+
resource = @resource_klass.find_by_key(@resource_id, context: context)
|
162
162
|
resource.remove_has_many_link(@association_type, @associated_key)
|
163
163
|
|
164
164
|
return JSONAPI::OperationResult.new(:no_content)
|
@@ -178,7 +178,7 @@ module JSONAPI
|
|
178
178
|
end
|
179
179
|
|
180
180
|
def apply(context)
|
181
|
-
resource = @resource_klass.find_by_key(@resource_id, context)
|
181
|
+
resource = @resource_klass.find_by_key(@resource_id, context: context)
|
182
182
|
resource.remove_has_one_link(@association_type)
|
183
183
|
resource.save
|
184
184
|
|
data/lib/jsonapi/request.rb
CHANGED
@@ -5,7 +5,7 @@ module JSONAPI
|
|
5
5
|
class Request
|
6
6
|
include ResourceFor
|
7
7
|
|
8
|
-
attr_accessor :fields, :include, :filters, :errors, :operations, :resource_klass, :context
|
8
|
+
attr_accessor :fields, :include, :filters, :sort_params, :errors, :operations, :resource_klass, :context
|
9
9
|
|
10
10
|
def initialize(params = nil, options = {})
|
11
11
|
@context = options.fetch(:context, nil)
|
@@ -28,6 +28,7 @@ module JSONAPI
|
|
28
28
|
parse_fields(params)
|
29
29
|
parse_include(params)
|
30
30
|
parse_filters(params)
|
31
|
+
parse_sort_params(params)
|
31
32
|
when 'show_associations'
|
32
33
|
when 'show'
|
33
34
|
parse_fields(params)
|
@@ -144,6 +145,26 @@ module JSONAPI
|
|
144
145
|
@filters = filters
|
145
146
|
end
|
146
147
|
|
148
|
+
def parse_sort_params(params)
|
149
|
+
@sort_params = if params[:sort].present?
|
150
|
+
CSV.parse_line(params[:sort]).each do |sort_param|
|
151
|
+
check_sort_param(@resource_klass, sort_param)
|
152
|
+
end
|
153
|
+
else
|
154
|
+
[]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def check_sort_param(resource_klass, sort_param)
|
159
|
+
sort_param = sort_param.sub(/\A-/, '')
|
160
|
+
sortable_fields = resource_klass.sortable_fields(context)
|
161
|
+
|
162
|
+
unless sortable_fields.include? sort_param.to_sym
|
163
|
+
@errors.concat(JSONAPI::Exceptions::InvalidSortParam
|
164
|
+
.new(format_key(resource_klass._type), sort_param).errors)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
147
168
|
def parse_add_operation(params)
|
148
169
|
object_params_raw = params.require(format_key(@resource_klass._type))
|
149
170
|
|
data/lib/jsonapi/resource.rb
CHANGED
@@ -26,7 +26,7 @@ module JSONAPI
|
|
26
26
|
|
27
27
|
def create_has_many_link(association_type, association_key_value)
|
28
28
|
association = self.class._associations[association_type]
|
29
|
-
related_resource = self.class.resource_for(association.type).find_by_key(association_key_value, @context)
|
29
|
+
related_resource = self.class.resource_for(association.type).find_by_key(association_key_value, context: @context)
|
30
30
|
|
31
31
|
# ToDo: Add option to skip relations that already exist instead of returning an error?
|
32
32
|
relation = @model.send(association.type).where(association.primary_key => association_key_value).first
|
@@ -214,12 +214,19 @@ module JSONAPI
|
|
214
214
|
_updateable_associations | _attributes.keys
|
215
215
|
end
|
216
216
|
|
217
|
+
# Override in your resource to filter the sortable keys
|
218
|
+
def sortable_fields(context = nil)
|
219
|
+
_attributes.keys
|
220
|
+
end
|
221
|
+
|
217
222
|
def fields
|
218
223
|
_associations.keys | _attributes.keys
|
219
224
|
end
|
220
225
|
|
221
226
|
# Override this method if you have more complex requirements than this basic find method provides
|
222
|
-
def find(filters,
|
227
|
+
def find(filters, options = {})
|
228
|
+
context = options[:context]
|
229
|
+
sort_params = options.fetch(:sort_params) { [] }
|
223
230
|
includes = []
|
224
231
|
where_filters = {}
|
225
232
|
|
@@ -237,14 +244,16 @@ module JSONAPI
|
|
237
244
|
end
|
238
245
|
|
239
246
|
resources = []
|
240
|
-
|
247
|
+
order_options = construct_order_options(sort_params)
|
248
|
+
_model_class.where(where_filters).order(order_options).includes(includes).each do |model|
|
241
249
|
resources.push self.new(model, context)
|
242
250
|
end
|
243
251
|
|
244
252
|
return resources
|
245
253
|
end
|
246
254
|
|
247
|
-
def find_by_key(key,
|
255
|
+
def find_by_key(key, options = {})
|
256
|
+
context = options[:context]
|
248
257
|
model = _model_class.where({_primary_key => key}).first
|
249
258
|
if model.nil?
|
250
259
|
raise JSONAPI::Exceptions::RecordNotFound.new(key)
|
@@ -408,6 +417,16 @@ module JSONAPI
|
|
408
417
|
end
|
409
418
|
end
|
410
419
|
end
|
420
|
+
|
421
|
+
def construct_order_options(sort_params)
|
422
|
+
sort_params.each_with_object({}) { |sort_key, order_hash|
|
423
|
+
if sort_key.starts_with?('-')
|
424
|
+
order_hash[sort_key.slice(1..-1)] = :desc
|
425
|
+
else
|
426
|
+
order_hash[sort_key] = :asc
|
427
|
+
end
|
428
|
+
}
|
429
|
+
end
|
411
430
|
end
|
412
431
|
end
|
413
432
|
end
|
@@ -17,7 +17,8 @@ module JSONAPI
|
|
17
17
|
|
18
18
|
def index
|
19
19
|
render json: JSONAPI::ResourceSerializer.new.serialize_to_hash(
|
20
|
-
resource_klass.find(resource_klass.verify_filters(@request.filters, context),
|
20
|
+
resource_klass.find(resource_klass.verify_filters(@request.filters, context),
|
21
|
+
context: context, sort_params: @request.sort_params),
|
21
22
|
include: @request.include,
|
22
23
|
fields: @request.fields,
|
23
24
|
attribute_formatters: attribute_formatters,
|
@@ -32,10 +33,10 @@ module JSONAPI
|
|
32
33
|
if keys.length > 1
|
33
34
|
resources = []
|
34
35
|
keys.each do |key|
|
35
|
-
resources.push(resource_klass.find_by_key(key, context))
|
36
|
+
resources.push(resource_klass.find_by_key(key, context: context))
|
36
37
|
end
|
37
38
|
else
|
38
|
-
resources = resource_klass.find_by_key(keys[0], context)
|
39
|
+
resources = resource_klass.find_by_key(keys[0], context: context)
|
39
40
|
end
|
40
41
|
|
41
42
|
render json: JSONAPI::ResourceSerializer.new.serialize_to_hash(
|
@@ -53,7 +54,7 @@ module JSONAPI
|
|
53
54
|
|
54
55
|
parent_key = params[resource_klass._as_parent_key]
|
55
56
|
|
56
|
-
parent_resource = resource_klass.find_by_key(parent_key, context)
|
57
|
+
parent_resource = resource_klass.find_by_key(parent_key, context: context)
|
57
58
|
|
58
59
|
association = resource_klass._association(association_type)
|
59
60
|
render json: { association_type => parent_resource.send(association.foreign_key)}
|
@@ -157,6 +157,41 @@ class PostsControllerTest < ActionController::TestCase
|
|
157
157
|
assert_equal 3, json_response['posts'].size
|
158
158
|
end
|
159
159
|
|
160
|
+
def test_sorting_asc
|
161
|
+
get :index, {sort: 'title'}
|
162
|
+
|
163
|
+
assert_response :success
|
164
|
+
assert_equal "Delete This Later - Multiple2-1", json_response['posts'][0]['title']
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_sorting_desc
|
168
|
+
get :index, {sort: '-title'}
|
169
|
+
|
170
|
+
assert_response :success
|
171
|
+
assert_equal "Update This Later - Multiple", json_response['posts'][0]['title']
|
172
|
+
end
|
173
|
+
|
174
|
+
def test_sorting_by_multiple_fields
|
175
|
+
get :index, {sort: 'title,body'}
|
176
|
+
|
177
|
+
assert_response :success
|
178
|
+
assert_equal 8, json_response['posts'][0]['id']
|
179
|
+
end
|
180
|
+
|
181
|
+
def test_invalid_sort_param
|
182
|
+
get :index, {sort: 'asdfg'}
|
183
|
+
|
184
|
+
assert_response :bad_request
|
185
|
+
assert_match /asdfg is not a valid sort param for post/, response.body
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_excluded_sort_param
|
189
|
+
get :index, {sort: 'id'}
|
190
|
+
|
191
|
+
assert_response :bad_request
|
192
|
+
assert_match /id is not a valid sort param for post/, response.body
|
193
|
+
end
|
194
|
+
|
160
195
|
# ToDo: test validating the parameter values
|
161
196
|
# def test_index_invalid_filter_value
|
162
197
|
# get :index, {ids: [1,'asdfg1']}
|
@@ -308,12 +308,12 @@ class AuthorResource < JSONAPI::Resource
|
|
308
308
|
|
309
309
|
filter :name
|
310
310
|
|
311
|
-
def self.find(filters,
|
311
|
+
def self.find(filters, options = {})
|
312
312
|
resources = []
|
313
313
|
|
314
314
|
filters.each do |attr, filter|
|
315
315
|
_model_class.where("\"#{attr}\" LIKE \"%#{filter[0]}%\"").each do |model|
|
316
|
-
resources.push self.new(model, context)
|
316
|
+
resources.push self.new(model, options[:context])
|
317
317
|
end
|
318
318
|
end
|
319
319
|
return resources
|
@@ -372,6 +372,10 @@ class PostResource < JSONAPI::Resource
|
|
372
372
|
super(context) - [:subject]
|
373
373
|
end
|
374
374
|
|
375
|
+
def self.sortable_fields(context)
|
376
|
+
super(context) - [:id]
|
377
|
+
end
|
378
|
+
|
375
379
|
def self.verify_custom_filter(filter, values, context = nil)
|
376
380
|
case filter
|
377
381
|
when :id
|
@@ -392,7 +396,7 @@ class PostResource < JSONAPI::Resource
|
|
392
396
|
|
393
397
|
def self.verify_key(key, context = nil)
|
394
398
|
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key) unless is_num?(key)
|
395
|
-
raise JSONAPI::Exceptions::RecordNotFound.new(key) unless find_by_key(key, context)
|
399
|
+
raise JSONAPI::Exceptions::RecordNotFound.new(key) unless find_by_key(key, context: context)
|
396
400
|
return key
|
397
401
|
end
|
398
402
|
end
|
@@ -417,16 +421,16 @@ class BreedResource < JSONAPI::Resource
|
|
417
421
|
# This is unneeded, just here for testing
|
418
422
|
routing_options :param => :id
|
419
423
|
|
420
|
-
def self.find(attrs,
|
424
|
+
def self.find(attrs, options = {})
|
421
425
|
breeds = []
|
422
426
|
$breed_data.breeds.values.each do |breed|
|
423
|
-
breeds.push(BreedResource.new(breed, context))
|
427
|
+
breeds.push(BreedResource.new(breed, options[:context]))
|
424
428
|
end
|
425
429
|
breeds
|
426
430
|
end
|
427
431
|
|
428
|
-
def self.find_by_key(id,
|
429
|
-
BreedResource.new($breed_data.breeds[id.to_i], context)
|
432
|
+
def self.find_by_key(id, options = {})
|
433
|
+
BreedResource.new($breed_data.breeds[id.to_i], options[:context])
|
430
434
|
end
|
431
435
|
end
|
432
436
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonapi-resources
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Gebhardt
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-10-
|
12
|
+
date: 2014-10-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|