sanger-jsonapi-resources 0.1.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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +53 -0
  4. data/lib/generators/jsonapi/USAGE +13 -0
  5. data/lib/generators/jsonapi/controller_generator.rb +14 -0
  6. data/lib/generators/jsonapi/resource_generator.rb +14 -0
  7. data/lib/generators/jsonapi/templates/jsonapi_controller.rb +4 -0
  8. data/lib/generators/jsonapi/templates/jsonapi_resource.rb +4 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +320 -0
  10. data/lib/jsonapi/cached_resource_fragment.rb +127 -0
  11. data/lib/jsonapi/callbacks.rb +51 -0
  12. data/lib/jsonapi/compiled_json.rb +36 -0
  13. data/lib/jsonapi/configuration.rb +258 -0
  14. data/lib/jsonapi/error.rb +47 -0
  15. data/lib/jsonapi/error_codes.rb +60 -0
  16. data/lib/jsonapi/exceptions.rb +563 -0
  17. data/lib/jsonapi/formatter.rb +169 -0
  18. data/lib/jsonapi/include_directives.rb +100 -0
  19. data/lib/jsonapi/link_builder.rb +152 -0
  20. data/lib/jsonapi/mime_types.rb +41 -0
  21. data/lib/jsonapi/naive_cache.rb +30 -0
  22. data/lib/jsonapi/operation.rb +24 -0
  23. data/lib/jsonapi/operation_dispatcher.rb +88 -0
  24. data/lib/jsonapi/operation_result.rb +65 -0
  25. data/lib/jsonapi/operation_results.rb +35 -0
  26. data/lib/jsonapi/paginator.rb +209 -0
  27. data/lib/jsonapi/processor.rb +328 -0
  28. data/lib/jsonapi/relationship.rb +94 -0
  29. data/lib/jsonapi/relationship_builder.rb +167 -0
  30. data/lib/jsonapi/request_parser.rb +678 -0
  31. data/lib/jsonapi/resource.rb +1255 -0
  32. data/lib/jsonapi/resource_controller.rb +5 -0
  33. data/lib/jsonapi/resource_controller_metal.rb +16 -0
  34. data/lib/jsonapi/resource_serializer.rb +531 -0
  35. data/lib/jsonapi/resources/version.rb +5 -0
  36. data/lib/jsonapi/response_document.rb +135 -0
  37. data/lib/jsonapi/routing_ext.rb +262 -0
  38. data/lib/jsonapi-resources.rb +27 -0
  39. metadata +223 -0
@@ -0,0 +1,563 @@
1
+ module JSONAPI
2
+ module Exceptions
3
+ class Error < RuntimeError
4
+ attr :error_object_overrides
5
+
6
+ def initialize(error_object_overrides = {})
7
+ @error_object_overrides = error_object_overrides
8
+ end
9
+
10
+ def create_error_object(error_defaults)
11
+ JSONAPI::Error.new(error_defaults.merge(error_object_overrides))
12
+ end
13
+
14
+ def errors
15
+ # :nocov:
16
+ raise NotImplementedError, "Subclass of Error must implement errors method"
17
+ # :nocov:
18
+ end
19
+ end
20
+
21
+ class InternalServerError < Error
22
+ attr_accessor :exception
23
+
24
+ def initialize(exception, error_object_overrides = {})
25
+ @exception = exception
26
+ super(error_object_overrides)
27
+ end
28
+
29
+ def errors
30
+ if JSONAPI.configuration.include_backtraces_in_errors
31
+ meta = Hash.new
32
+ meta[:exception] = exception.message
33
+ meta[:backtrace] = exception.backtrace
34
+ end
35
+
36
+ [create_error_object(code: JSONAPI::INTERNAL_SERVER_ERROR,
37
+ status: :internal_server_error,
38
+ title: I18n.t('jsonapi-resources.exceptions.internal_server_error.title',
39
+ default: 'Internal Server Error'),
40
+ detail: I18n.t('jsonapi-resources.exceptions.internal_server_error.detail',
41
+ default: 'Internal Server Error'),
42
+ meta: meta)]
43
+ end
44
+ end
45
+
46
+ class InvalidResource < Error
47
+ attr_accessor :resource
48
+
49
+ def initialize(resource, error_object_overrides = {})
50
+ @resource = resource
51
+ super(error_object_overrides)
52
+ end
53
+
54
+ def errors
55
+ [create_error_object(code: JSONAPI::INVALID_RESOURCE,
56
+ status: :bad_request,
57
+ title: I18n.t('jsonapi-resources.exceptions.invalid_resource.title',
58
+ default: 'Invalid resource'),
59
+ detail: I18n.t('jsonapi-resources.exceptions.invalid_resource.detail',
60
+ default: "#{resource} is not a valid resource.", resource: resource))]
61
+ end
62
+ end
63
+
64
+ class RecordNotFound < Error
65
+ attr_accessor :id
66
+
67
+ def initialize(id, error_object_overrides = {})
68
+ @id = id
69
+ super(error_object_overrides)
70
+ end
71
+
72
+ def errors
73
+ [create_error_object(code: JSONAPI::RECORD_NOT_FOUND,
74
+ status: :not_found,
75
+ title: I18n.translate('jsonapi-resources.exceptions.record_not_found.title',
76
+ default: 'Record not found'),
77
+ detail: I18n.translate('jsonapi-resources.exceptions.record_not_found.detail',
78
+ default: "The record identified by #{id} could not be found.", id: id))]
79
+ end
80
+ end
81
+
82
+ class UnsupportedMediaTypeError < Error
83
+ attr_accessor :media_type
84
+
85
+ def initialize(media_type, error_object_overrides = {})
86
+ @media_type = media_type
87
+ super(error_object_overrides)
88
+ end
89
+
90
+ def errors
91
+ [create_error_object(code: JSONAPI::UNSUPPORTED_MEDIA_TYPE,
92
+ status: :unsupported_media_type,
93
+ title: I18n.translate('jsonapi-resources.exceptions.unsupported_media_type.title',
94
+ default: 'Unsupported media type'),
95
+ detail: I18n.translate('jsonapi-resources.exceptions.unsupported_media_type.detail',
96
+ default: "All requests that create or update must use the '#{JSONAPI::MEDIA_TYPE}' Content-Type. This request specified '#{media_type}'.",
97
+ needed_media_type: JSONAPI::MEDIA_TYPE,
98
+ media_type: media_type))]
99
+ end
100
+ end
101
+
102
+ class NotAcceptableError < Error
103
+ attr_accessor :media_type
104
+
105
+ def initialize(media_type, error_object_overrides = {})
106
+ @media_type = media_type
107
+ super(error_object_overrides)
108
+ end
109
+
110
+ def errors
111
+ [create_error_object(code: JSONAPI::NOT_ACCEPTABLE,
112
+ status: :not_acceptable,
113
+ title: I18n.translate('jsonapi-resources.exceptions.not_acceptable.title',
114
+ default: 'Not acceptable'),
115
+ detail: I18n.translate('jsonapi-resources.exceptions.not_acceptable.detail',
116
+ default: "All requests must use the '#{JSONAPI::MEDIA_TYPE}' Accept without media type parameters. This request specified '#{media_type}'.",
117
+ needed_media_type: JSONAPI::MEDIA_TYPE,
118
+ media_type: media_type))]
119
+ end
120
+ end
121
+
122
+
123
+ class HasManyRelationExists < Error
124
+ attr_accessor :id
125
+
126
+ def initialize(id, error_object_overrides = {})
127
+ @id = id
128
+ super(error_object_overrides)
129
+ end
130
+
131
+ def errors
132
+ [create_error_object(code: JSONAPI::RELATION_EXISTS,
133
+ status: :bad_request,
134
+ title: I18n.translate('jsonapi-resources.exceptions.has_many_relation.title',
135
+ default: 'Relation exists'),
136
+ detail: I18n.translate('jsonapi-resources.exceptions.has_many_relation.detail',
137
+ default: "The relation to #{id} already exists.",
138
+ id: id))]
139
+ end
140
+ end
141
+
142
+ class BadRequest < Error
143
+ def initialize(exception)
144
+ @exception = exception
145
+ end
146
+
147
+ def errors
148
+ [JSONAPI::Error.new(code: JSONAPI::BAD_REQUEST,
149
+ status: :bad_request,
150
+ title: I18n.translate('jsonapi-resources.exceptions.bad_request.title',
151
+ default: 'Bad Request'),
152
+ detail: I18n.translate('jsonapi-resources.exceptions.bad_request.detail',
153
+ default: @exception))]
154
+ end
155
+ end
156
+
157
+ class InvalidRequestFormat < Error
158
+ def errors
159
+ [JSONAPI::Error.new(code: JSONAPI::BAD_REQUEST,
160
+ status: :bad_request,
161
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_request_format.title',
162
+ default: 'Bad Request'),
163
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_request_format.detail',
164
+ default: 'Request must be a hash'))]
165
+ end
166
+ end
167
+
168
+ class ToManySetReplacementForbidden < Error
169
+ def errors
170
+ [create_error_object(code: JSONAPI::FORBIDDEN,
171
+ status: :forbidden,
172
+ title: I18n.translate('jsonapi-resources.exceptions.to_many_set_replacement_forbidden.title',
173
+ default: 'Complete replacement forbidden'),
174
+ detail: I18n.translate('jsonapi-resources.exceptions.to_many_set_replacement_forbidden.detail',
175
+ default: 'Complete replacement forbidden for this relationship'))]
176
+ end
177
+ end
178
+
179
+ class InvalidFiltersSyntax < Error
180
+ attr_accessor :filters
181
+
182
+ def initialize(filters, error_object_overrides = {})
183
+ @filters = filters
184
+ super(error_object_overrides)
185
+ end
186
+
187
+ def errors
188
+ [create_error_object(code: JSONAPI::INVALID_FILTERS_SYNTAX,
189
+ status: :bad_request,
190
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_filter_syntax.title',
191
+ default: 'Invalid filters syntax'),
192
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_filter_syntax.detail',
193
+ default: "#{filters} is not a valid syntax for filtering.",
194
+ filters: filters))]
195
+ end
196
+ end
197
+
198
+ class FilterNotAllowed < Error
199
+ attr_accessor :filter
200
+
201
+ def initialize(filter, error_object_overrides = {})
202
+ @filter = filter
203
+ super(error_object_overrides)
204
+ end
205
+
206
+ def errors
207
+ [create_error_object(code: JSONAPI::FILTER_NOT_ALLOWED,
208
+ status: :bad_request,
209
+ title: I18n.translate('jsonapi-resources.exceptions.filter_not_allowed.title',
210
+ default: 'Filter not allowed'),
211
+ detail: I18n.translate('jsonapi-resources.exceptions.filter_not_allowed.detail',
212
+ default: "#{filter} is not allowed.", filter: filter))]
213
+ end
214
+ end
215
+
216
+ class InvalidFilterValue < Error
217
+ attr_accessor :filter, :value
218
+
219
+ def initialize(filter, value, error_object_overrides = {})
220
+ @filter = filter
221
+ @value = value
222
+ super(error_object_overrides)
223
+ end
224
+
225
+ def errors
226
+ [create_error_object(code: JSONAPI::INVALID_FILTER_VALUE,
227
+ status: :bad_request,
228
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_filter_value.title',
229
+ default: 'Invalid filter value'),
230
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_filter_value.detail',
231
+ default: "#{value} is not a valid value for #{filter}.",
232
+ value: value, filter: filter))]
233
+ end
234
+ end
235
+
236
+ class InvalidFieldValue < Error
237
+ attr_accessor :field, :value
238
+
239
+ def initialize(field, value, error_object_overrides = {})
240
+ @field = field
241
+ @value = value
242
+ super(error_object_overrides)
243
+ end
244
+
245
+ def errors
246
+ [create_error_object(code: JSONAPI::INVALID_FIELD_VALUE,
247
+ status: :bad_request,
248
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_field_value.title',
249
+ default: 'Invalid field value'),
250
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_field_value.detail',
251
+ default: "#{value} is not a valid value for #{field}.",
252
+ value: value, field: field))]
253
+ end
254
+ end
255
+
256
+ class InvalidFieldFormat < Error
257
+ def errors
258
+ [create_error_object(code: JSONAPI::INVALID_FIELD_FORMAT,
259
+ status: :bad_request,
260
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_field_format.title',
261
+ default: 'Invalid field format'),
262
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_field_format.detail',
263
+ default: 'Fields must specify a type.'))]
264
+ end
265
+ end
266
+
267
+ class InvalidDataFormat < Error
268
+ def errors
269
+ [create_error_object(code: JSONAPI::INVALID_DATA_FORMAT,
270
+ status: :bad_request,
271
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_data_format.title',
272
+ default: 'Invalid data format'),
273
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_data_format.detail',
274
+ default: 'Data must be a hash.'))]
275
+ end
276
+ end
277
+
278
+ class InvalidLinksObject < Error
279
+ def errors
280
+ [create_error_object(code: JSONAPI::INVALID_LINKS_OBJECT,
281
+ status: :bad_request,
282
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_links_object.title',
283
+ default: 'Invalid Links Object'),
284
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_links_object.detail',
285
+ default: 'Data is not a valid Links Object.'))]
286
+ end
287
+ end
288
+
289
+ class TypeMismatch < Error
290
+ attr_accessor :type
291
+
292
+ def initialize(type, error_object_overrides = {})
293
+ @type = type
294
+ super(error_object_overrides)
295
+ end
296
+
297
+ def errors
298
+ [create_error_object(code: JSONAPI::TYPE_MISMATCH,
299
+ status: :bad_request,
300
+ title: I18n.translate('jsonapi-resources.exceptions.type_mismatch.title',
301
+ default: 'Type Mismatch'),
302
+ detail: I18n.translate('jsonapi-resources.exceptions.type_mismatch.detail',
303
+ default: "#{type} is not a valid type for this operation.", type: type))]
304
+ end
305
+ end
306
+
307
+ class InvalidField < Error
308
+ attr_accessor :field, :type
309
+
310
+ def initialize(type, field, error_object_overrides = {})
311
+ @field = field
312
+ @type = type
313
+ super(error_object_overrides)
314
+ end
315
+
316
+ def errors
317
+ [create_error_object(code: JSONAPI::INVALID_FIELD,
318
+ status: :bad_request,
319
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_field.title',
320
+ default: 'Invalid field'),
321
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_field.detail',
322
+ default: "#{field} is not a valid field for #{type}.",
323
+ field: field, type: type))]
324
+ end
325
+ end
326
+
327
+ class InvalidInclude < Error
328
+ attr_accessor :relationship, :resource
329
+
330
+ def initialize(resource, relationship, error_object_overrides = {})
331
+ @resource = resource
332
+ @relationship = relationship
333
+ super(error_object_overrides)
334
+ end
335
+
336
+ def errors
337
+ [create_error_object(code: JSONAPI::INVALID_INCLUDE,
338
+ status: :bad_request,
339
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_include.title',
340
+ default: 'Invalid field'),
341
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_include.detail',
342
+ default: "#{relationship} is not a valid relationship of #{resource}",
343
+ relationship: relationship, resource: resource))]
344
+ end
345
+ end
346
+
347
+ class InvalidSortCriteria < Error
348
+ attr_accessor :sort_criteria, :resource
349
+
350
+ def initialize(resource, sort_criteria, error_object_overrides = {})
351
+ @resource = resource
352
+ @sort_criteria = sort_criteria
353
+ super(error_object_overrides)
354
+ end
355
+
356
+ def errors
357
+ [create_error_object(code: JSONAPI::INVALID_SORT_CRITERIA,
358
+ status: :bad_request,
359
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_sort_criteria.title',
360
+ default: 'Invalid sort criteria'),
361
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_sort_criteria.detail',
362
+ default: "#{sort_criteria} is not a valid sort criteria for #{resource}",
363
+ sort_criteria: sort_criteria, resource: resource))]
364
+ end
365
+ end
366
+
367
+ class ParametersNotAllowed < Error
368
+ attr_accessor :params
369
+
370
+ def initialize(params, error_object_overrides = {})
371
+ @params = params
372
+ super(error_object_overrides)
373
+ end
374
+
375
+ def errors
376
+ params.collect do |param|
377
+ create_error_object(code: JSONAPI::PARAM_NOT_ALLOWED,
378
+ status: :bad_request,
379
+ title: I18n.translate('jsonapi-resources.exceptions.parameters_not_allowed.title',
380
+ default: 'Param not allowed'),
381
+ detail: I18n.translate('jsonapi-resources.exceptions.parameters_not_allowed.detail',
382
+ default: "#{param} is not allowed.", param: param))
383
+
384
+ end
385
+ end
386
+ end
387
+
388
+ class ParameterMissing < Error
389
+ attr_accessor :param
390
+
391
+ def initialize(param, error_object_overrides = {})
392
+ @param = param
393
+ super(error_object_overrides)
394
+ end
395
+
396
+ def errors
397
+ [create_error_object(code: JSONAPI::PARAM_MISSING,
398
+ status: :bad_request,
399
+ title: I18n.translate('jsonapi-resources.exceptions.parameter_missing.title',
400
+ default: 'Missing Parameter'),
401
+ detail: I18n.translate('jsonapi-resources.exceptions.parameter_missing.detail',
402
+ default: "The required parameter, #{param}, is missing.", param: param))]
403
+ end
404
+ end
405
+
406
+ class KeyNotIncludedInURL < Error
407
+ attr_accessor :key
408
+
409
+ def initialize(key, error_object_overrides = {})
410
+ @key = key
411
+ super(error_object_overrides)
412
+ end
413
+
414
+ def errors
415
+ [create_error_object(code: JSONAPI::KEY_NOT_INCLUDED_IN_URL,
416
+ status: :bad_request,
417
+ title: I18n.translate('jsonapi-resources.exceptions.key_not_included_in_url.title',
418
+ default: 'Key is not included in URL'),
419
+ detail: I18n.translate('jsonapi-resources.exceptions.key_not_included_in_url.detail',
420
+ default: "The URL does not support the key #{key}",
421
+ key: key))]
422
+ end
423
+ end
424
+
425
+ class MissingKey < Error
426
+ def errors
427
+ [create_error_object(code: JSONAPI::KEY_ORDER_MISMATCH,
428
+ status: :bad_request,
429
+ title: I18n.translate('jsonapi-resources.exceptions.missing_key.title',
430
+ default: 'A key is required'),
431
+ detail: I18n.translate('jsonapi-resources.exceptions.missing_key.detail',
432
+ default: 'The resource object does not contain a key.'))]
433
+ end
434
+ end
435
+
436
+ class RecordLocked < Error
437
+ attr_accessor :message
438
+
439
+ def initialize(message, error_object_overrides = {})
440
+ @message = message
441
+ super(error_object_overrides)
442
+ end
443
+
444
+ def errors
445
+ [create_error_object(code: JSONAPI::LOCKED,
446
+ status: :locked,
447
+ title: I18n.translate('jsonapi-resources.exceptions.record_locked.title',
448
+ default: 'Locked resource'),
449
+ detail: "#{message}")]
450
+ end
451
+ end
452
+
453
+ class ValidationErrors < Error
454
+ attr_reader :error_messages, :error_metadata, :resource_relationships
455
+
456
+ def initialize(resource, error_object_overrides = {})
457
+ @error_messages = resource.model_error_messages
458
+ @error_metadata = resource.validation_error_metadata
459
+ @resource_relationships = resource.class._relationships.keys
460
+ @key_formatter = JSONAPI.configuration.key_formatter
461
+ super(error_object_overrides)
462
+ end
463
+
464
+ def format_key(key)
465
+ @key_formatter.format(key)
466
+ end
467
+
468
+ def errors
469
+ error_messages.flat_map do |attr_key, messages|
470
+ messages.map { |message| json_api_error(attr_key, message) }
471
+ end
472
+ end
473
+
474
+ private
475
+
476
+ def json_api_error(attr_key, message)
477
+ create_error_object(code: JSONAPI::VALIDATION_ERROR,
478
+ status: :unprocessable_entity,
479
+ title: message,
480
+ detail: "#{format_key(attr_key)} - #{message}",
481
+ source: { pointer: pointer(attr_key) },
482
+ meta: metadata_for(attr_key, message))
483
+ end
484
+
485
+ def metadata_for(attr_key, message)
486
+ return if error_metadata.nil?
487
+ error_metadata[attr_key] ? error_metadata[attr_key][message] : nil
488
+ end
489
+
490
+ def pointer(attr_or_relationship_name)
491
+ formatted_attr_or_relationship_name = format_key(attr_or_relationship_name)
492
+ if resource_relationships.include?(attr_or_relationship_name)
493
+ "/data/relationships/#{formatted_attr_or_relationship_name}"
494
+ else
495
+ "/data/attributes/#{formatted_attr_or_relationship_name}"
496
+ end
497
+ end
498
+ end
499
+
500
+ class SaveFailed < Error
501
+ def errors
502
+ [create_error_object(code: JSONAPI::SAVE_FAILED,
503
+ status: :unprocessable_entity,
504
+ title: I18n.translate('jsonapi-resources.exceptions.save_failed.title',
505
+ default: 'Save failed or was cancelled'),
506
+ detail: I18n.translate('jsonapi-resources.exceptions.save_failed.detail',
507
+ default: 'Save failed or was cancelled'))]
508
+ end
509
+ end
510
+
511
+ class InvalidPageObject < Error
512
+ def errors
513
+ [create_error_object(code: JSONAPI::INVALID_PAGE_OBJECT,
514
+ status: :bad_request,
515
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_page_object.title',
516
+ default: 'Invalid Page Object'),
517
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_page_object.detail',
518
+ default: 'Invalid Page Object.'))]
519
+ end
520
+ end
521
+
522
+ class PageParametersNotAllowed < Error
523
+ attr_accessor :params
524
+
525
+ def initialize(params, error_object_overrides = {})
526
+ @params = params
527
+ super(error_object_overrides)
528
+ end
529
+
530
+ def errors
531
+ params.collect do |param|
532
+ create_error_object(code: JSONAPI::PARAM_NOT_ALLOWED,
533
+ status: :bad_request,
534
+ title: I18n.translate('jsonapi-resources.exceptions.page_parameters_not_allowed.title',
535
+ default: 'Page parameter not allowed'),
536
+ detail: I18n.translate('jsonapi-resources.exceptions.page_parameters_not_allowed.detail',
537
+ default: "#{param} is not an allowed page parameter.",
538
+ param: param))
539
+ end
540
+ end
541
+ end
542
+
543
+ class InvalidPageValue < Error
544
+ attr_accessor :page, :value
545
+
546
+ def initialize(page, value, error_object_overrides = {})
547
+ @page = page
548
+ @value = value
549
+ super(error_object_overrides)
550
+ end
551
+
552
+ def errors
553
+ [create_error_object(code: JSONAPI::INVALID_PAGE_VALUE,
554
+ status: :bad_request,
555
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_page_value.title',
556
+ default: 'Invalid page value'),
557
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_page_value.detail',
558
+ default: "#{value} is not a valid value for #{page} page parameter.",
559
+ value: value, page: page))]
560
+ end
561
+ end
562
+ end
563
+ end