jsonapi-resources 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3e063aecb3183d3a1862dc4fd88f0f06ddc921dd
4
- data.tar.gz: 40b0476410fe269a08f16791f8b927d2aee02156
3
+ metadata.gz: 6eb51f8c096de8ece9f78168a967750a958fe620
4
+ data.tar.gz: 1e7e845621c801642776c99bd559185af85a645c
5
5
  SHA512:
6
- metadata.gz: f4135619c0ad660679b93ea9dcde314991134824e0d01540705b4a3055e0acccfab76d74b0e49cf94d969d949063a51c50328165265b90b2057065ec34b13eb3
7
- data.tar.gz: 335f3f2b30251c07b2286dbdcd176d3f199fd844d99f6c9619a0b54e598b66e0fe07b1386cddb1c18106b3e1d968e6a6a1f5eaabb11e3cc72d75d08bf54581ae
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(context)
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, context = nil)
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
@@ -12,7 +12,8 @@ module JSONAPI
12
12
  KEY_NOT_INCLUDED_IN_URL = 110
13
13
  INVALID_INCLUDE = 112
14
14
  RELATION_EXISTS = 113
15
+ INVALID_SORT_PARAM = 114
15
16
 
16
17
  RECORD_NOT_FOUND = 404
17
18
  LOCKED = 423
18
- end
19
+ end
@@ -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)
@@ -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
 
@@ -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
 
@@ -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, context = nil)
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
- _model_class.where(where_filters).includes(includes).each do |model|
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, context = nil)
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), 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)}
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Resources
3
- VERSION = "0.0.8"
3
+ VERSION = "0.0.9"
4
4
  end
5
5
  end
@@ -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, context)
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, context = nil)
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, context = nil)
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.8
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-21 00:00:00.000000000 Z
12
+ date: 2014-10-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler