jsonapi-resources 0.1.1 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e3cc147ec7ed0dc456d04b86c715a7e9a13f47dd
4
- data.tar.gz: 77d151a254fa986e93ba80fd06775ed0bd3753c1
3
+ metadata.gz: 7639392d6acfd2092e36563bebd32e791b7df37a
4
+ data.tar.gz: 6c7e794b2646d60214746515810d9648566699e8
5
5
  SHA512:
6
- metadata.gz: e3349452f04356593d52125551ed57049a91f2b89a66010587d562c12d1fd47ffab5896b8078434bd733a7a931c772478d8615afa30240e66b60b9a9f10ae14f
7
- data.tar.gz: 4c75390e027e8af857d049f883836af04e6258ccd57209de458e0d63b809039684febc28720d3a3973e2a65c2068b70775f439b575e33a74b108a7da738bcc9c
6
+ metadata.gz: 0d53447be085f1ce717782ad9238116e30968e0380fe6e5612e3b5b1a680978285e363904c9c4a96b5ac115bcec8addad959851cd4dfeb58244ab704215eba1c
7
+ data.tar.gz: 725dd95e1409afd23a1f22a6f7399a8fcc402734adc5da297cc019f65186c2777b3bb8c2aacf7955e1dcd1cc89af1fced6702898222ca2b3746be2e4800bbebc
data/README.md CHANGED
@@ -330,6 +330,70 @@ class AuthorResource < JSONAPI::Resource
330
330
  end
331
331
  ```
332
332
 
333
+ #### Pagination
334
+
335
+ Pagination is performed using a `paginator`, which is a class responsible for parsing the `page` request parameters and applying the pagination logic to the results.
336
+
337
+ ##### Paginators
338
+
339
+ `JSONAPI::Resource` supports several pagination methods by default, and allows you to implement a custom system if the defaults do not meet your needs.
340
+
341
+ ###### Paged Paginator
342
+
343
+ The `paged` `paginator` returns results based on pages of a fixed size. Valid `page` parameters are `number` and `size`. If `number` is omitted the first page is returned. If `size` is omitted the `default_page_size` from the configuration settings is used.
344
+
345
+ ###### Offset Paginator
346
+
347
+ The `offset` `paginator` returns results based on an offset from the beginning of the resultset. Valid `page` parameters are `offset` and `limit`. If `offset` is omitted a value of 0 will be used. If `limit` is omitted the `default_page_size` from the configuration settings is used.
348
+
349
+ ###### Custom Paginators
350
+
351
+ Custom `paginators` can be used. These should derive from `Paginator`. The `apply` method takes a `relation` and is expected to return a `relation`. The `initialize` method receives the parameters from the `page` request parameters. It is up to the paginator author to parse and validate these parameters.
352
+
353
+ For example, here is a very simple single record at a time paginator:
354
+
355
+ ```ruby
356
+ class SingleRecordPaginator < JSONAPI::Paginator
357
+ def initialize(params)
358
+ # param parsing and validation here
359
+ @page = params.to_i
360
+ end
361
+
362
+ def apply(relation)
363
+ relation.offset(@page).limit(1)
364
+ end
365
+ end
366
+ ```
367
+
368
+ ##### Paginator Configuration
369
+
370
+ The default paginator, which will be used for all resources, is set using `JSONAPI.configure`. For example:
371
+
372
+ ```ruby
373
+ JSONAPI.configure do |config|
374
+ # built in paginators are :none, :offset, :cursor, :paged
375
+ self.default_paginator = :offset
376
+
377
+ self.default_page_size = 10
378
+ self.maximum_page_size = 20
379
+ end
380
+ ```
381
+
382
+ If no `default_paginator` is configured, pagination will be disabled by default.
383
+
384
+ Paginators can also be set at the resource-level, which will override the default setting. This is done using the `paginator` method:
385
+
386
+ ```ruby
387
+ class BookResource < JSONAPI::Resource
388
+ attribute :title
389
+ attribute :isbn
390
+
391
+ paginator :offset
392
+ end
393
+ ```
394
+
395
+ To disable pagination in a resource, specify `:none` for `paginator`.
396
+
333
397
  #### Callbacks
334
398
 
335
399
  `ActiveSupport::Callbacks` is used to provide callback functionality, so the behavior is very similar to what you may be used to from `ActiveRecord`.
@@ -517,7 +581,10 @@ module JSONAPI
517
581
  INVALID_INCLUDE = 112
518
582
  RELATION_EXISTS = 113
519
583
  INVALID_SORT_PARAM = 114
520
-
584
+ INVALID_LINKS_OBJECT = 115
585
+ TYPE_MISMATCH = 116
586
+ INVALID_PAGE_OBJECT = 117
587
+ INVALID_PAGE_VALUE = 118
521
588
  RECORD_NOT_FOUND = 404
522
589
  LOCKED = 423
523
590
  end
data/Rakefile CHANGED
@@ -1,13 +1,9 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
3
  require "rake/testtask"
4
+ require './test/test_helper.rb'
4
5
 
5
- desc 'Run tests'
6
- test_task = Rake::TestTask.new(:test) do |t|
7
- t.libs << 'test'
8
- t.pattern = 'test/**/*_test.rb'
9
- t.verbose = true
10
- end
6
+ TestApp.load_tasks
11
7
 
12
8
  task default: :test
13
9
 
@@ -21,4 +17,4 @@ namespace :test do
21
17
  sh cmd.join(' ')
22
18
  end
23
19
  end
24
- end
20
+ end
@@ -1,6 +1,7 @@
1
1
  require 'jsonapi/resource'
2
2
  require 'jsonapi/resources/version'
3
3
  require 'jsonapi/configuration'
4
+ require 'jsonapi/paginator'
4
5
  require 'jsonapi/formatter'
5
6
  require 'jsonapi/routing_ext'
6
7
  require 'jsonapi/mime_types'
@@ -2,16 +2,29 @@ require 'jsonapi/formatter'
2
2
 
3
3
  module JSONAPI
4
4
  class Configuration
5
- attr_reader :json_key_format, :key_formatter, :allowed_request_params, :route_format, :route_formatter
5
+ attr_reader :json_key_format,
6
+ :key_formatter,
7
+ :route_format,
8
+ :route_formatter,
9
+ :allowed_request_params,
10
+ :default_paginator,
11
+ :default_page_size,
12
+ :maximum_page_size
6
13
 
7
14
  def initialize
8
15
  #:underscored_key, :camelized_key, :dasherized_key, or custom
9
- self.json_key_format = :underscored_key
16
+ self.json_key_format = :dasherized_key
10
17
 
11
18
  #:underscored_route, :camelized_route, :dasherized_route, or custom
12
- self.route_format = :underscored_route
19
+ self.route_format = :dasherized_route
13
20
 
14
- self.allowed_request_params = [:include, :fields, :format, :controller, :action, :sort]
21
+ self.allowed_request_params = [:include, :fields, :format, :controller, :action, :sort, :page]
22
+
23
+ # :none, :offset, :paged, or a custom paginator name
24
+ self.default_paginator = :none
25
+
26
+ self.default_page_size = 10
27
+ self.maximum_page_size = 20
15
28
  end
16
29
 
17
30
  def json_key_format=(format)
@@ -27,6 +40,18 @@ module JSONAPI
27
40
  def allowed_request_params=(allowed_request_params)
28
41
  @allowed_request_params = allowed_request_params
29
42
  end
43
+
44
+ def default_paginator=(default_paginator)
45
+ @default_paginator = default_paginator
46
+ end
47
+
48
+ def default_page_size=(default_page_size)
49
+ @default_page_size = default_page_size
50
+ end
51
+
52
+ def maximum_page_size=(maximum_page_size)
53
+ @maximum_page_size = maximum_page_size
54
+ end
30
55
  end
31
56
 
32
57
  class << self
@@ -12,8 +12,12 @@ 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
16
-
15
+ INVALID_SORT_CRITERIA = 114
16
+ INVALID_LINKS_OBJECT = 115
17
+ TYPE_MISMATCH = 116
18
+ INVALID_PAGE_OBJECT = 117
19
+ INVALID_PAGE_VALUE = 118
20
+ INVALID_SORT_FORMAT = 119
17
21
  RECORD_NOT_FOUND = 404
18
22
  UNSUPPORTED_MEDIA_TYPE = 415
19
23
  LOCKED = 423
@@ -58,18 +58,6 @@ module JSONAPI
58
58
  end
59
59
  end
60
60
 
61
- class HasOneRelationExists < Error
62
- def initialize
63
- end
64
-
65
- def errors
66
- [JSONAPI::Error.new(code: JSONAPI::RELATION_EXISTS,
67
- status: :bad_request,
68
- title: 'Relation exists',
69
- detail: 'The relation already exists.')]
70
- end
71
- end
72
-
73
61
  class FilterNotAllowed < Error
74
62
  attr_accessor :filter
75
63
  def initialize(filter)
@@ -114,6 +102,34 @@ module JSONAPI
114
102
  end
115
103
  end
116
104
 
105
+ class InvalidLinksObject < Error
106
+ attr_accessor :value
107
+ def initialize(value)
108
+ @value = value
109
+ end
110
+
111
+ def errors
112
+ [JSONAPI::Error.new(code: JSONAPI::INVALID_LINKS_OBJECT,
113
+ status: :bad_request,
114
+ title: 'Invalid Links Object',
115
+ detail: "#{value} is not a valid Links Object.")]
116
+ end
117
+ end
118
+
119
+ class TypeMismatch < Error
120
+ attr_accessor :type
121
+ def initialize(type)
122
+ @type = type
123
+ end
124
+
125
+ def errors
126
+ [JSONAPI::Error.new(code: JSONAPI::TYPE_MISMATCH,
127
+ status: :bad_request,
128
+ title: 'Type Mismatch',
129
+ detail: "#{type} is not a valid type for this operation.")]
130
+ end
131
+ end
132
+
117
133
  class InvalidField < Error
118
134
  attr_accessor :field, :type
119
135
  def initialize(type, field)
@@ -144,18 +160,33 @@ module JSONAPI
144
160
  end
145
161
  end
146
162
 
147
- class InvalidSortParam < Error
148
- attr_accessor :sort_param, :resource
149
- def initialize(resource, sort_param)
163
+ class InvalidSortCriteria < Error
164
+ attr_accessor :sort_criteria, :resource
165
+ def initialize(resource, sort_criteria)
150
166
  @resource = resource
151
- @sort_param = sort_param
167
+ @sort_criteria = sort_criteria
152
168
  end
153
169
 
154
170
  def errors
155
- [JSONAPI::Error.new(code: JSONAPI::INVALID_SORT_PARAM,
171
+ [JSONAPI::Error.new(code: JSONAPI::INVALID_SORT_CRITERIA,
156
172
  status: :bad_request,
157
- title: 'Invalid sort param',
158
- detail: "#{sort_param} is not a valid sort param for #{resource}")]
173
+ title: 'Invalid sort criteria',
174
+ detail: "#{sort_criteria} is not a valid sort criteria for #{resource}")]
175
+ end
176
+ end
177
+
178
+ class InvalidSortFormat < Error
179
+ attr_accessor :sort_criteria, :resource
180
+ def initialize(resource, sort_criteria)
181
+ @resource = resource
182
+ @sort_criteria = sort_criteria
183
+ end
184
+
185
+ def errors
186
+ [JSONAPI::Error.new(code: JSONAPI::INVALID_SORT_FORMAT,
187
+ status: :bad_request,
188
+ title: 'Invalid sort format',
189
+ detail: "#{sort_criteria} must start with a direction (+ or -)")]
159
190
  end
160
191
  end
161
192
 
@@ -257,5 +288,47 @@ module JSONAPI
257
288
  end
258
289
  end
259
290
 
291
+ class InvalidPageObject < Error
292
+ def errors
293
+ [JSONAPI::Error.new(code: JSONAPI::INVALID_PAGE_OBJECT,
294
+ status: :bad_request,
295
+ title: 'Invalid Page Object',
296
+ detail: 'Invalid Page Object.')]
297
+ end
298
+ end
299
+
300
+
301
+ class PageParametersNotAllowed < Error
302
+ attr_accessor :params
303
+ def initialize(params)
304
+ @params = params
305
+ end
306
+
307
+ def errors
308
+ params.collect { |param|
309
+ JSONAPI::Error.new(code: JSONAPI::PARAM_NOT_ALLOWED,
310
+ status: :bad_request,
311
+ title: 'Page parameter not allowed',
312
+ detail: "#{param} is not an allowed page parameter.")
313
+ }
314
+
315
+ end
316
+ end
317
+
318
+ class InvalidPageValue < Error
319
+ attr_accessor :page, :value
320
+ def initialize(page, value, msg = nil)
321
+ @page = page
322
+ @value = value
323
+ @msg = msg.nil? ? "#{value} is not a valid value for #{page} page parameter." : msg
324
+ end
325
+
326
+ def errors
327
+ [JSONAPI::Error.new(code: JSONAPI::INVALID_PAGE_VALUE,
328
+ status: :bad_request,
329
+ title: 'Invalid page value',
330
+ detail: @msg)]
331
+ end
332
+ end
260
333
  end
261
334
  end
@@ -68,24 +68,6 @@ module JSONAPI
68
68
  end
69
69
  end
70
70
 
71
- class CreateHasOneAssociationOperation < Operation
72
- attr_reader :resource_id, :association_type, :key_value
73
-
74
- def initialize(resource_klass, resource_id, association_type, key_value)
75
- @resource_id = resource_id
76
- @key_value = key_value
77
- @association_type = association_type.to_sym
78
- super(resource_klass)
79
- end
80
-
81
- def apply(context)
82
- resource = @resource_klass.find_by_key(@resource_id, context: context)
83
- resource.create_has_one_link(@association_type, @key_value)
84
-
85
- return JSONAPI::OperationResult.new(:no_content)
86
- end
87
- end
88
-
89
71
  class ReplaceHasOneAssociationOperation < Operation
90
72
  attr_reader :resource_id, :association_type, :key_value
91
73
 
@@ -0,0 +1,98 @@
1
+ module JSONAPI
2
+ class Paginator
3
+ def initialize(params)
4
+ end
5
+
6
+ def apply(relation)
7
+ # :nocov:
8
+ relation
9
+ # :nocov:
10
+ end
11
+
12
+ class << self
13
+ # :nocov:
14
+ if RUBY_VERSION >= '2.0'
15
+ def paginator_for(paginator)
16
+ paginator_class_name = "#{paginator.to_s.camelize}Paginator"
17
+ Object.const_get(paginator_class_name) if paginator_class_name
18
+ end
19
+ else
20
+ def paginator_for(paginator)
21
+ paginator_class_name = "#{paginator.to_s.camelize}Paginator"
22
+ paginator_class_name.safe_constantize if paginator_class_name
23
+ end
24
+ end
25
+ # :nocov:
26
+ end
27
+ end
28
+ end
29
+
30
+ class OffsetPaginator < JSONAPI::Paginator
31
+ def initialize(params)
32
+ parse_pagination_params(params)
33
+ end
34
+
35
+ def apply(relation)
36
+ relation.offset(@offset).limit(@limit)
37
+ end
38
+
39
+ private
40
+ def parse_pagination_params(params)
41
+ if params.nil?
42
+ @offset = 0
43
+ @limit = JSONAPI.configuration.default_page_size
44
+ elsif params.is_a?(ActionController::Parameters)
45
+ validparams = params.permit(:offset, :limit)
46
+
47
+ @offset = validparams[:offset] ? validparams[:offset].to_i : 0
48
+ @limit = validparams[:limit] ? validparams[:limit].to_i : JSONAPI.configuration.default_page_size
49
+
50
+ if @limit < 1
51
+ raise JSONAPI::Exceptions::InvalidPageValue.new(:limit, validparams[:limit])
52
+ elsif @limit > JSONAPI.configuration.maximum_page_size
53
+ raise JSONAPI::Exceptions::InvalidPageValue.new(:limit, validparams[:limit],
54
+ "Limit exceeds maximum page size of #{JSONAPI.configuration.maximum_page_size}.")
55
+ end
56
+ else
57
+ raise JSONAPI::Exceptions::InvalidPageObject.new
58
+ end
59
+ rescue ActionController::UnpermittedParameters => e
60
+ raise JSONAPI::Exceptions::PageParametersNotAllowed.new(e.params)
61
+ end
62
+ end
63
+
64
+ class PagedPaginator < JSONAPI::Paginator
65
+ def initialize(params)
66
+ parse_pagination_params(params)
67
+ end
68
+
69
+ def apply(relation)
70
+ offset = (@number - 1) * @size
71
+ relation.offset(offset).limit(@size)
72
+ end
73
+
74
+ private
75
+ def parse_pagination_params(params)
76
+ if params.nil?
77
+ @number = 1
78
+ @size = JSONAPI.configuration.default_page_size
79
+ elsif params.is_a?(ActionController::Parameters)
80
+ validparams = params.permit(:number, :size)
81
+
82
+ @size = validparams[:size] ? validparams[:size].to_i : JSONAPI.configuration.default_page_size
83
+ @number = validparams[:number] ? validparams[:number].to_i : 1
84
+
85
+ if @size < 1
86
+ raise JSONAPI::Exceptions::InvalidPageValue.new(:size, validparams[:size])
87
+ elsif @size > JSONAPI.configuration.maximum_page_size
88
+ raise JSONAPI::Exceptions::InvalidPageValue.new(:size, validparams[:size],
89
+ "size exceeds maximum page size of #{JSONAPI.configuration.maximum_page_size}.")
90
+ end
91
+ else
92
+ @size = JSONAPI.configuration.default_page_size
93
+ @number = params.to_i
94
+ end
95
+ rescue ActionController::UnpermittedParameters => e
96
+ raise JSONAPI::Exceptions::PageParametersNotAllowed.new(e.params)
97
+ end
98
+ end