wor-paginate 0.4.1 → 0.5.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
  SHA256:
3
- metadata.gz: 2382535974a5955b672ae5a22548b2100126afa69237fd5f76e7b5c18f6cd9e1
4
- data.tar.gz: 977102640aba1c02a50d36181d2a4cc2fa3ab5e0bfe57b86587ae24fc4ada4ca
3
+ metadata.gz: decca93406ee541c53c128fb024c0be0ba58576b47a0c9cde5e834e33365c8e2
4
+ data.tar.gz: baae1708bf393145b589b872e3bf2a4ba44f3215efecc1a3bae34025cac0c9c4
5
5
  SHA512:
6
- metadata.gz: acfa22f022acd7468c674cf723ca74b366deddf18d58320570951bd1067a36573a039468134ea4e1ee7586ea61e5fb685451e6fd1eb9f0a182d716a324177d2c
7
- data.tar.gz: 2e1397984c3da7db0defdbe42e05f14e8692496d0411e9318e83f020ce3a3fe7e72ecfbfe7c748c3fd93263725d3752b89b8eda7e09fb3e729f3d6dacebf0cfb
6
+ metadata.gz: 75eb5ddfc44ed747a69fa797125776a4a2cdbe5e40ba9a3e7502a9af1685b844cad52d50c910d3856d591e8a31b7f6f1495a42542eabfb50a057992ba183d717
7
+ data.tar.gz: a140a0337bdb012c9821217f34cd2ea2099c0f8407cab21e8042c80ab5ee2c0ca8a375ad83d8931ed159f855cbc8ffc72458252c2c006694a9fa3e7da7d858a9
data/.gitignore CHANGED
@@ -1,5 +1,7 @@
1
1
  spec/dummy/db/*.sqlite3
2
2
  spec/dummy/db/*.sqlite3-journal
3
+ spec/dummy/db/*.sqlite3-shm
4
+ spec/dummy/db/*.sqlite3-wal
3
5
  /test.sqlite3
4
6
  spec/dummy/log/*.log
5
7
  spec/dummy/tmp/
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Change log
2
2
 
3
+ ### V0.5.0
4
+ * [#6](https://github.com/icoluccio/wor-paginate/pull/6) Add block serialization to `render_paginated` - [@icoluccio](https://github.com/icoluccio).
5
+
6
+ ### V0.4.2
7
+ * [#3](https://github.com/icoluccio/wor-paginate/pull/3) Add Pagy adapter - [@icoluccio](https://github.com/icoluccio).
8
+ * [#4](https://github.com/icoluccio/wor-paginate/pull/4) Remove remaining rubocop:disable/:enable comments - [@icoluccio](https://github.com/icoluccio).
9
+ * [#5](https://github.com/icoluccio/wor-paginate/pull/5) Document Kaminari/will_paginate Model.page collision - [@icoluccio](https://github.com/icoluccio).
10
+
3
11
  ### V0.4.1
4
12
  * [#2](https://github.com/icoluccio/wor-paginate/pull/2) Remove remaining dead Wolox links - [@icoluccio](https://github.com/icoluccio).
5
13
 
data/Gemfile CHANGED
@@ -23,6 +23,7 @@ group :development, :test do
23
23
  gem 'generator_spec', '~> 0.10'
24
24
  gem 'kaminari', '~> 1.2'
25
25
  gem 'overcommit', '~> 0.71'
26
+ gem 'pagy', '~> 43.0'
26
27
  gem 'panko_serializer', '~> 0.8.5'
27
28
  gem 'puma', '~> 6.0'
28
29
  gem 'rake', '~> 13.0'
data/README.md CHANGED
@@ -11,11 +11,14 @@ Wor::Paginate
11
11
  - [Basic usage](#basic-usage)
12
12
  - [Customizing output](#customizing-output)
13
13
  - [Custom serializers](#custom-serializers)
14
+ - [Block serialization](#block-serialization)
14
15
  - [Custom options](#custom-options)
15
16
  - [Custom formatters](#custom-formatters)
16
17
  - [Custom adapters](#custom-adapters)
17
- - [Working with Kaminari or will_paginate](#working-with-kaminari-or-will_paginate)
18
+ - [Working with Kaminari, will_paginate, or Pagy](#working-with-kaminari-will_paginate-or-pagy)
19
+ - [Kaminari and will_paginate both define `Model.page`](#kaminari-and-will_paginate-both-define-modelpage)
18
20
  - [Test helpers](#test-helpers)
21
+ - [Field value chains](#field-value-chains)
19
22
  - [Contributing](#contributing)
20
23
  - [Releases](#releases)
21
24
  - [About](#about)
@@ -108,6 +111,19 @@ render_paginated DummyModel, each_serializer: CustomDummyModelSerializer
108
111
  ```
109
112
  where the serializer is just an [`ActiveModel::Serializer`](https://github.com/rails-api/active_model_serializers).
110
113
 
114
+ #### Block serialization
115
+ When you need a custom per-record payload that doesn't fit a serializer class, pass a block to `render_paginated`. The block receives each record and its return value is used as the serialized entry:
116
+
117
+ ```ruby
118
+ def index
119
+ render_paginated(DummyModel) { |record| { id: record.id, label: record.name.upcase } }
120
+ end
121
+ ```
122
+
123
+ All pagination metadata (`count`, `total_pages`, `next_page_url`, etc.) is still computed and returned normally. The block only controls the content of the `page` array.
124
+
125
+ The block form works with any adapter and takes precedence over `each_serializer` when both are supplied.
126
+
111
127
  #### Custom options
112
128
  ##### max_limit
113
129
  The max amount of items is passed through the `max_limit` option. You can set the value in the initializer or in the `render_paginated` method (If none is supplied, take the default value configured in the initializer). Default is 50.
@@ -263,11 +279,42 @@ There are also helper methods available to dynamically operate the gem's adapter
263
279
  When the gem paginates, it tries to adapt the content to the first adapter that is "adaptable" for the content (unless a custom adapter has been passed to render_paginated or a default_adapter has been defined in the initializer). So beware of which adapters (and in which order) you are leaving in the `Wor::Paginate::Config.adapters` array, because depending on those, the gem will try to adapt the content.
264
280
 
265
281
 
266
- ### Working with Kaminari or will_paginate
267
- If either Kaminari or will_paginate is required in the project, Wor::Paginate will use them for pagination with no code or configuration change.
282
+ ### Working with Kaminari, will_paginate, or Pagy
283
+ If Kaminari, will_paginate, or [Pagy](https://github.com/ddnexus/pagy) is required in the project, Wor::Paginate will use it for pagination with no code or configuration change.
284
+
285
+ If more than one is available, Wor::Paginate prefers will_paginate, then Kaminari, then Pagy (the order they're checked in `Config::DEFAULT_ADAPTERS`). To opt out of one, for example if Pagy happens to be in your bundle for unrelated reasons, call `Wor::Paginate::Config.remove_adapter(Wor::Paginate::Adapters::Pagy)` in the initializer.
286
+
287
+ #### Kaminari and will_paginate both define `Model.page`
288
+ Kaminari and will_paginate each add their own `page` class method to your models, for the same purpose but with a different return value. Whichever gem's Rails initializer happens to run last silently overwrites the other's definition. This isn't affected by Gemfile order.
289
+
290
+ Wor::Paginate's own adapter selection already routes around this (see the precedence above). If you specifically need Kaminari's own pagination behavior while will_paginate is also installed, rename Kaminari's method and subclass the built-in Kaminari adapter, overriding only the two methods that call `.page` literally:
291
+
292
+ ```ruby
293
+ # config/initializers/kaminari.rb
294
+ Kaminari.configure do |config|
295
+ config.page_method_name = :kpage
296
+ end
297
+ ```
298
+
299
+ ```ruby
300
+ # app/adapters/kaminari_renamed_adapter.rb
301
+ class KaminariRenamedAdapter < Wor::Paginate::Adapters::Kaminari
302
+ def required_methods
303
+ %i[kpage]
304
+ end
305
+
306
+ def paginated_content
307
+ @paginated_content ||= @content.kpage(@page).per(@limit)
308
+ end
309
+ end
310
+ ```
311
+
312
+ ```ruby
313
+ render_paginated SomeModel, adapter: KaminariRenamedAdapter
314
+ ```
268
315
 
269
316
  ### Test helpers
270
- You can use the `be_paginated` matcher to test your endpoints. It also accepts the `with` chain method to receive a formatter.
317
+ You can use the `be_paginated` matcher to test your endpoints.
271
318
 
272
319
  You only need to add this in your rails_helper.rb
273
320
 
@@ -300,6 +347,22 @@ describe YourController do
300
347
  end
301
348
  ```
302
349
 
350
+ #### Field value chains
351
+
352
+ You can assert specific pagination field values by chaining matchers:
353
+
354
+ ```ruby
355
+ expect(response_body).to be_paginated
356
+ .with_total_count(42)
357
+ .with_current_page(1)
358
+ .with_next_page(2)
359
+ .with_previous_page(nil)
360
+ .with_total_pages(3)
361
+ .with_count(25)
362
+ ```
363
+
364
+ Each chain is optional and independent. When a chain assertion fails, the error message names the specific field and the expected vs actual value.
365
+
303
366
  ### Working with panko-serializer
304
367
 
305
368
  The default formatter is [Active Model Serializer](https://github.com/rails-api/active_model_serializers).
@@ -327,7 +390,7 @@ and next, pass the specific serializer that you can use in the specific endpoint
327
390
  5. Commit your changes (`git commit -am 'Add some feature'`)
328
391
  6. Run RuboCop lint (`bundle exec rubocop lib spec --format simple`)
329
392
  7. Run rspec tests (`BUNDLE_GEMFILE=gemfiles/rails_8.1.gemfile bundle exec rspec`)
330
- 8. Push your branch (`git push origin my-new-feature`) the pre-push hook re-verifies both automatically
393
+ 8. Push your branch (`git push origin my-new-feature`) - the pre-push hook re-verifies both automatically
331
394
  9. Create a new Pull Request to `main` branch
332
395
 
333
396
  ## Releases
@@ -20,6 +20,7 @@ Wor::Paginate.configure do |config|
20
20
  # Wor::Paginate::Adapters::WillPaginateAlreadyPaginated
21
21
  # Wor::Paginate::Adapters::WillPaginate
22
22
  # Wor::Paginate::Adapters::Kaminari
23
+ # Wor::Paginate::Adapters::Pagy
23
24
  # Wor::Paginate::Adapters::ActiveRecord
24
25
  # Wor::Paginate::Adapters::Enumerable
25
26
 
@@ -30,6 +31,7 @@ Wor::Paginate.configure do |config|
30
31
  # Adapters::WillPaginateAlreadyPaginated,
31
32
  # Adapters::WillPaginate,
32
33
  # Adapters::Kaminari,
34
+ # Adapters::Pagy,
33
35
  # Adapters::ActiveRecord,
34
36
  # Adapters::Enumerable
35
37
  # ]
@@ -0,0 +1,46 @@
1
+ require_relative 'helpers/total_count'
2
+
3
+ # Used when render_paginated is called with an ActiveRecord directly, with the
4
+ # pagy gem loaded. Something like
5
+ ### render_paginated DummyModel
6
+ module Wor
7
+ module Paginate
8
+ module Adapters
9
+ class Pagy < Base
10
+ include Helpers::TotalCount
11
+
12
+ def adapt?
13
+ defined?(::Pagy::Offset) && super
14
+ end
15
+
16
+ def required_methods
17
+ %i[offset limit table_name]
18
+ end
19
+
20
+ def paginated_content
21
+ @paginated_content ||= pagy.records(@content)
22
+ end
23
+
24
+ delegate :count, to: :paginated_content
25
+
26
+ def total_pages
27
+ pagy.pages
28
+ end
29
+
30
+ def next_page
31
+ pagy.next
32
+ end
33
+
34
+ def previous_page
35
+ pagy.previous
36
+ end
37
+
38
+ private
39
+
40
+ def pagy
41
+ @pagy ||= ::Pagy::Offset.new(count: total_count, page: @page, limit: @limit)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -16,6 +16,7 @@ module Wor
16
16
  will_paginate_paginated: Adapters::WillPaginateAlreadyPaginated,
17
17
  will_paginate: Adapters::WillPaginate,
18
18
  kaminari: Adapters::Kaminari,
19
+ pagy: Adapters::Pagy,
19
20
  active_record: Adapters::ActiveRecord,
20
21
  enumerable: Adapters::Enumerable
21
22
  }.freeze
@@ -5,16 +5,17 @@ module Wor
5
5
  module Formatters
6
6
  class Base
7
7
  include Utils::UriHelper
8
- attr_accessor :adapter, :content, :formatter, :options
8
+ attr_accessor :adapter, :formatter, :options
9
9
 
10
- def initialize(adapter, options = {})
10
+ def initialize(adapter, options = {}, &block)
11
11
  @adapter = adapter
12
12
  @options = options
13
+ @serializer_block = block
13
14
  end
14
15
 
15
16
  def format # rubocop: disable Metrics/MethodLength
16
17
  {
17
- page: serialized_content,
18
+ page: serialized_page,
18
19
  count: count,
19
20
  total_pages: total_pages,
20
21
  total_count: options[:total_count] || total_count,
@@ -35,9 +36,13 @@ module Wor
35
36
  end
36
37
 
37
38
  def paginated_content
38
- # rubocop:disable Naming/MemoizedInstanceVariableName -- @content matches the public `content` attr_accessor above; renaming it would silently change what `content` returns.
39
- @content ||= adapter.paginated_content
40
- # rubocop:enable Naming/MemoizedInstanceVariableName
39
+ @paginated_content ||= adapter.paginated_content
40
+ end
41
+
42
+ def serialized_page
43
+ return paginated_content.map(&@serializer_block) if @serializer_block
44
+
45
+ serialized_content
41
46
  end
42
47
 
43
48
  def serialized_content
@@ -2,13 +2,13 @@ require_relative 'utils/preserve_records_helper'
2
2
 
3
3
  module Wor
4
4
  module Paginate
5
- def render_paginated(content, options = {})
6
- return render_paginate_with_include(content, options) if includes?(options)
5
+ def render_paginated(content, options = {}, &block)
6
+ return render_paginate_with_include(content, options, &block) if includes?(options)
7
7
 
8
- render json: paginate(content, options)
8
+ render json: paginate(content, options, &block)
9
9
  end
10
10
 
11
- def paginate(content, options = {})
11
+ def paginate(content, options = {}, &block)
12
12
  current_url = request.original_url
13
13
  if (preserve_records = options[:preserve_records])
14
14
  content, current_url = Wor::Paginate::Utils::PreserveRecordsHelper
@@ -18,11 +18,12 @@ module Wor
18
18
  adapter = find_adapter_for_content(content, options)
19
19
  raise Exceptions::NoPaginationAdapter if adapter.blank?
20
20
 
21
- formatter_class(options).new(adapter, options.merge(_current_url: current_url)).format
21
+ merged = options.merge(_current_url: current_url)
22
+ formatter_class(options).new(adapter, merged, &block).format
22
23
  end
23
24
 
24
- def render_paginate_with_include(content, options)
25
- render json: paginate(content, options), include: options[:include]
25
+ def render_paginate_with_include(content, options, &block)
26
+ render json: paginate(content, options, &block), include: options[:include]
26
27
  end
27
28
 
28
29
  def formatter_class(options)
@@ -1,3 +1,5 @@
1
+ CHAINABLE_FIELDS = %w[total_count count total_pages current_page next_page previous_page].freeze
2
+
1
3
  class MockedAdapter < Wor::Paginate::Adapters::Base
2
4
  def initialize
3
5
  super(nil, 1, 1)
@@ -28,28 +30,63 @@ class MockedAdapter < Wor::Paginate::Adapters::Base
28
30
  end
29
31
  end
30
32
 
33
+ class PaginatedAssertion
34
+ attr_reader :failures, :formatted_keys
35
+
36
+ def initialize(response, formatter, expectations)
37
+ @response = response
38
+ formatted = formatter.new(MockedAdapter.new, _current_url: 'http://example.com/')
39
+ @formatted_keys = formatted.format.as_json.keys
40
+ @failures = key_failures + field_failures(expectations)
41
+ end
42
+
43
+ def pass?
44
+ @failures.empty?
45
+ end
46
+
47
+ private
48
+
49
+ def key_failures
50
+ return [] if @response.keys == @formatted_keys
51
+
52
+ ["expected keys #{@formatted_keys} but got #{@response.keys}"]
53
+ end
54
+
55
+ def field_failures(expectations)
56
+ CHAINABLE_FIELDS.filter_map do |field|
57
+ expected = expectations[field]
58
+ next if expected.nil?
59
+
60
+ actual = @response[field]
61
+ next if actual == expected
62
+
63
+ "expected #{field} to be #{expected.inspect} but got #{actual.inspect}"
64
+ end
65
+ end
66
+ end
67
+
31
68
  RSpec::Matchers.define :be_paginated do
32
69
  match do |actual_response|
33
- response = parse_response(actual_response)
34
70
  formatter = @custom_formatter || Wor::Paginate::Formatters::Base
35
- @formatted_keys = formatter.new(MockedAdapter.new, _current_url: 'http://exaple.com/').format.as_json.keys
36
- response.keys == @formatted_keys
71
+ expectations = CHAINABLE_FIELDS.index_with { |f| instance_variable_get(:"@expected_#{f}") }
72
+ @assertion = PaginatedAssertion.new(parse_response(actual_response), formatter, expectations)
73
+ @assertion.pass?
37
74
  end
38
75
 
39
76
  def parse_response(response)
40
77
  response.is_a?(Hash) ? response : JSON.parse(response.body)
41
78
  end
42
79
 
43
- chain :with do |custom_formatter|
44
- @custom_formatter = custom_formatter
45
- end
80
+ chain(:with) { |formatter| @custom_formatter = formatter }
46
81
 
47
- failure_message do |actual_response|
48
- "expected that #{parse_response(actual_response)} to be paginated with keys #{@formatted_keys}"
82
+ CHAINABLE_FIELDS.each do |field|
83
+ chain(:"with_#{field}") { |value| instance_variable_set(:"@expected_#{field}", value) }
49
84
  end
50
85
 
86
+ failure_message { |_| @assertion.failures.join("\n") }
87
+
51
88
  failure_message_when_negated do |actual_response|
52
89
  "expected that #{parse_response(actual_response)} not " \
53
- "to be paginated with keys #{@formatted_keys}"
90
+ "to be paginated with keys #{@assertion.formatted_keys}"
54
91
  end
55
92
  end
@@ -1,5 +1,5 @@
1
1
  module Wor
2
2
  module Paginate
3
- VERSION = '0.4.1'.freeze
3
+ VERSION = '0.5.0'.freeze
4
4
  end
5
5
  end
data/lib/wor/paginate.rb CHANGED
@@ -3,6 +3,7 @@ require_relative 'paginate/adapters/active_record'
3
3
  require_relative 'paginate/adapters/enumerable'
4
4
  require_relative 'paginate/adapters/kaminari'
5
5
  require_relative 'paginate/adapters/will_paginate'
6
+ require_relative 'paginate/adapters/pagy'
6
7
  require_relative 'paginate/adapters/kaminari_already_paginated'
7
8
  require_relative 'paginate/adapters/will_paginate_already_paginated'
8
9
  require_relative 'paginate/exceptions/dependency_error'
data/wor-paginate.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |s|
12
12
  s.email = ["ignacio.coluccio@gmail.com", "martin.mallea@wolox.com.ar", "ramiro.rojo@wolox.com.ar", "lucas.voboril@wolox.com.ar"]
13
13
  s.homepage = "https://github.com/icoluccio/wor-paginate"
14
14
  s.summary = "Simplified pagination for Rails API controllers"
15
- s.description = "Wor::Paginate is a gem for Rails that simplifies pagination, particularly for controller methods, while standardizing JSON output for APIs. It's meant to work both as a standalone pagination gem and as an extra layer over Kaminari and will_paginate"
15
+ s.description = "Wor::Paginate is a gem for Rails that simplifies pagination, particularly for controller methods, while standardizing JSON output for APIs. It's meant to work both as a standalone pagination gem and as an extra layer over Kaminari, will_paginate, or Pagy"
16
16
  s.license = "MIT"
17
17
 
18
18
  s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec)/}) }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wor-paginate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - icoluccio
@@ -10,7 +10,7 @@ authors:
10
10
  - lucasVoboril
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-08-01 00:00:00.000000000 Z
13
+ date: 2026-08-30 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: railties
@@ -54,8 +54,8 @@ dependencies:
54
54
  version: '9'
55
55
  description: Wor::Paginate is a gem for Rails that simplifies pagination, particularly
56
56
  for controller methods, while standardizing JSON output for APIs. It's meant to
57
- work both as a standalone pagination gem and as an extra layer over Kaminari and
58
- will_paginate
57
+ work both as a standalone pagination gem and as an extra layer over Kaminari, will_paginate,
58
+ or Pagy
59
59
  email:
60
60
  - ignacio.coluccio@gmail.com
61
61
  - martin.mallea@wolox.com.ar
@@ -84,6 +84,7 @@ files:
84
84
  - lib/wor/paginate/adapters/helpers/total_count.rb
85
85
  - lib/wor/paginate/adapters/kaminari.rb
86
86
  - lib/wor/paginate/adapters/kaminari_already_paginated.rb
87
+ - lib/wor/paginate/adapters/pagy.rb
87
88
  - lib/wor/paginate/adapters/will_paginate.rb
88
89
  - lib/wor/paginate/adapters/will_paginate_already_paginated.rb
89
90
  - lib/wor/paginate/config.rb
@@ -120,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
121
  - !ruby/object:Gem::Version
121
122
  version: '0'
122
123
  requirements: []
123
- rubygems_version: 4.0.6
124
+ rubygems_version: 3.6.9
124
125
  specification_version: 4
125
126
  summary: Simplified pagination for Rails API controllers
126
127
  test_files: []