api_only_pagination 0.1.4 → 0.1.5

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: 826fde0fb392a394358a65a5759171b45cc47f449785bf2877d6728f7dfb4f0e
4
- data.tar.gz: 1b13d3e52a7cdac0de53a8a54c5751283fb5e6419fdf0ac85457b74124b0e600
3
+ metadata.gz: f9b67d605d6da1e12e6ecde2eb1dda4d48b6e9b22223c8e5f92085b639af3e28
4
+ data.tar.gz: b0a68efae6b619150cd9c2d123f3f4c2d616bfcec632a9a785b27d5c59ea7c3d
5
5
  SHA512:
6
- metadata.gz: 14eb5e9588e159cc148149135bdc6ef7b2b6879394300d4aa71d6da36fc7fe8049357aab703c9f2e3488fc7290c9671bede4eda9c9b2562fbb875d962b25935a
7
- data.tar.gz: 47d8acd2be2b1f6ea72a689124156e9728404be2214232782fa733b01912e8a6875f0c62bd67eecba7c4b784d21d92ad0d5697b4f1cde2975d47e95eb3872bd5
6
+ metadata.gz: 80bd7f472ee99ce2da2018761bc2c028a2fb77762b1dead34e07f78a8fdbee0e7c24999892cb77403355c12e415801b60c7aa509bd85ed7db9bd3192382cbdd1
7
+ data.tar.gz: b21c4af4ac4ecb4d609d052e1be7ad4436f064706c8b29fe34f27483d0771e77a820cac12b49f3cc9560cbf82c3fea1e2b6d933031c6183c13b6251e25293f68
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml CHANGED
@@ -11,3 +11,7 @@ Style/StringLiteralsInInterpolation:
11
11
 
12
12
  Layout/LineLength:
13
13
  Max: 120
14
+
15
+ Metrics/BlockLength:
16
+ Exclude:
17
+ - "spec/**/*"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.5] - 2026-10-30
4
+
5
+ - Fix: negative `page`/`per_page` values produced invalid SQL; any value that isn't a positive integer now falls back to the configured default
6
+ - Fix: paginating relations with `group` or a custom `select` raised errors while counting
7
+ - Fix: `last_page?` returned false for empty results and for pages past the end
8
+ - Remove a no-op `ActiveRecord::Relation.include`; chained calls such as `User.where(...).do_paginate` are unaffected
9
+ - Add a real spec suite (in-memory SQLite) and CI across Ruby 3.2–3.4 and ActiveRecord 6.1–8.1
10
+
3
11
  ## [0.1.0] - 2023-03-23
4
12
 
5
13
  - Initial release
data/CLAUDE.md ADDED
@@ -0,0 +1,40 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project
6
+
7
+ `api_only_pagination` is a small Ruby gem that adds offset pagination to ActiveRecord models and relations for API-only apps. It returns a plain response object (data + page metadata) instead of view helpers. Depends on `activerecord >= 5.0`; targets Ruby >= 2.6.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ bin/setup # install dependencies
13
+ bin/console # IRB with the gem loaded
14
+ bundle exec rake # specs + RuboCop (what CI runs)
15
+ bundle exec rubocop # lint
16
+ bundle exec rspec # run specs
17
+ bundle exec rspec spec/api_only_pagination_spec.rb:13 # single example
18
+ bundle exec rake install # install gem locally
19
+ bundle exec rake release # tag + push to rubygems (bump lib/api_only_pagination/version.rb first)
20
+ ```
21
+
22
+ Specs run against an in-memory SQLite database; `spec/spec_helper.rb` defines the schema and a `User` model, and resets records and configuration before each example.
23
+
24
+ CI (`.github/workflows/ci.yml`) runs specs against every Gemfile in `gemfiles/` (one per ActiveRecord version) on several Rubies. To reproduce one locally: `BUNDLE_GEMFILE=gemfiles/activerecord_7.2.gemfile bundle install && BUNDLE_GEMFILE=gemfiles/activerecord_7.2.gemfile bundle exec rspec`. When adding a Rails version, add a gemfile and a matrix entry.
25
+
26
+ ## Architecture
27
+
28
+ The call flow spans several small files in `lib/api_only_pagination/`:
29
+
30
+ 1. **Hooking into ActiveRecord** (`lib/api_only_pagination.rb`): an `ActiveSupport.on_load(:active_record)` block includes `ActiveRecordPaginate` into `ActiveRecord::Base`. Chained calls like `User.where(...).do_paginate` still work, because a relation passes unknown methods to its model class while applying its own scope, so `all` inside the method returns the scoped relation.
31
+ 2. **`ActiveRecordPaginate`**: defines three aliases, `do_paginate`, `paginate`, and `paginated_response`, each taking `(page, per_page)`. They wrap `all` in a `Pagination` and build a `PaginatedData`.
32
+ 3. **`Pagination`**: runs `collection.count(:all)` once when it's constructed (for grouped relations it counts the groups), then applies `limit`/`offset` from the metadata in `results`. `results` is still a lazy relation.
33
+ 4. **`Paginate`**: the metadata and normalization logic. Any page/per_page value that isn't a positive integer (nil, zero, negative, non-numeric) falls back to the configured default, and `per_page` is capped at `max_per_page`. It also computes `offset`, `total_pages`, and next/previous helpers.
34
+ 5. **`PaginatedData`**: a plain object with `data`, `per_page`, `current_page`, `total_pages`, and `total_records`. The README shows the result as a hash, but it's actually this object.
35
+
36
+ **Configuration** (`Configuration` + `ApiOnlyPagination.configure`/`.reset`): `default_page_size` (20), `max_per_page` (100), `default_page` (1). Values are read when each call runs, so changes take effect right away.
37
+
38
+ ## Style
39
+
40
+ RuboCop (`.rubocop.yml`): double-quoted strings, max line length 120, `TargetRubyVersion` 2.6. Every file starts with `# frozen_string_literal: true`.
data/Gemfile CHANGED
@@ -8,3 +8,5 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rubocop", "~> 1.21"
11
+
12
+ gem "sqlite3", "~> 1.4"
data/Gemfile.lock CHANGED
@@ -1,25 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- api_only_pagination (0.1.4)
4
+ api_only_pagination (0.1.5)
5
5
  activerecord (>= 5.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- actionpack (6.1.7.3)
11
- actionview (= 6.1.7.3)
12
- activesupport (= 6.1.7.3)
13
- rack (~> 2.0, >= 2.0.9)
14
- rack-test (>= 0.6.3)
15
- rails-dom-testing (~> 2.0)
16
- rails-html-sanitizer (~> 1.0, >= 1.2.0)
17
- actionview (6.1.7.3)
18
- activesupport (= 6.1.7.3)
19
- builder (~> 3.1)
20
- erubi (~> 1.4)
21
- rails-dom-testing (~> 2.0)
22
- rails-html-sanitizer (~> 1.1, >= 1.2.0)
23
10
  activemodel (6.1.7.3)
24
11
  activesupport (= 6.1.7.3)
25
12
  activerecord (6.1.7.3)
@@ -32,39 +19,17 @@ GEM
32
19
  tzinfo (~> 2.0)
33
20
  zeitwerk (~> 2.3)
34
21
  ast (2.4.2)
35
- builder (3.2.4)
36
22
  concurrent-ruby (1.2.2)
37
- crass (1.0.6)
38
23
  diff-lcs (1.5.0)
39
- erubi (1.12.0)
40
24
  i18n (1.13.0)
41
25
  concurrent-ruby (~> 1.0)
42
26
  json (2.6.1)
43
- loofah (2.20.0)
44
- crass (~> 1.0.2)
45
- nokogiri (>= 1.5.9)
46
- method_source (1.0.0)
47
27
  minitest (5.18.0)
48
- nokogiri (1.13.10-x86_64-darwin)
49
- racc (~> 1.4)
50
28
  parallel (1.21.0)
51
- parser (3.2.1.1)
29
+ parser (3.3.13.0)
52
30
  ast (~> 2.4.1)
53
- racc (1.6.2)
54
- rack (2.2.7)
55
- rack-test (2.1.0)
56
- rack (>= 1.3)
57
- rails-dom-testing (2.0.3)
58
- activesupport (>= 4.2.0)
59
- nokogiri (>= 1.6)
60
- rails-html-sanitizer (1.5.0)
61
- loofah (~> 2.19, >= 2.19.1)
62
- railties (6.1.7.3)
63
- actionpack (= 6.1.7.3)
64
- activesupport (= 6.1.7.3)
65
- method_source
66
- rake (>= 12.2)
67
- thor (~> 1.0)
31
+ racc
32
+ racc (1.8.1)
68
33
  rainbow (3.1.1)
69
34
  rake (13.0.6)
70
35
  regexp_parser (2.3.0)
@@ -81,14 +46,6 @@ GEM
81
46
  rspec-mocks (3.10.2)
82
47
  diff-lcs (>= 1.2.0, < 2.0)
83
48
  rspec-support (~> 3.10.0)
84
- rspec-rails (4.0.2)
85
- actionpack (>= 4.2)
86
- activesupport (>= 4.2)
87
- railties (>= 4.2)
88
- rspec-core (~> 3.10)
89
- rspec-expectations (~> 3.10)
90
- rspec-mocks (~> 3.10)
91
- rspec-support (~> 3.10)
92
49
  rspec-support (3.10.2)
93
50
  rubocop (1.48.1)
94
51
  json (~> 2.3)
@@ -103,21 +60,25 @@ GEM
103
60
  rubocop-ast (1.27.0)
104
61
  parser (>= 3.2.1.0)
105
62
  ruby-progressbar (1.11.0)
106
- thor (1.2.1)
63
+ sqlite3 (1.7.3-arm64-darwin)
64
+ sqlite3 (1.7.3-x86_64-darwin)
65
+ sqlite3 (1.7.3-x86_64-linux)
107
66
  tzinfo (2.0.6)
108
67
  concurrent-ruby (~> 1.0)
109
68
  unicode-display_width (2.4.2)
110
69
  zeitwerk (2.6.8)
111
70
 
112
71
  PLATFORMS
72
+ arm64-darwin-25
113
73
  x86_64-darwin-20
74
+ x86_64-linux
114
75
 
115
76
  DEPENDENCIES
116
77
  api_only_pagination!
117
78
  rake (~> 13.0)
118
79
  rspec (~> 3.6)
119
- rspec-rails (~> 4.0)
120
80
  rubocop (~> 1.21)
81
+ sqlite3 (~> 1.4)
121
82
 
122
83
  BUNDLED WITH
123
84
  2.4.9
data/Rakefile CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
4
5
  require "rubocop/rake_task"
5
6
 
7
+ RSpec::Core::RakeTask.new(:spec)
6
8
  RuboCop::RakeTask.new
7
9
 
8
- task default: :rubocop
10
+ task default: %i[spec rubocop]
@@ -31,7 +31,6 @@ Gem::Specification.new do |spec|
31
31
  spec.require_paths = ["lib"]
32
32
  spec.add_dependency "activerecord", ">= 5.0"
33
33
  spec.add_development_dependency "rspec", "~> 3.6"
34
- spec.add_development_dependency "rspec-rails", "~> 4.0"
35
34
  # Uncomment to register a new dependency of your gem
36
35
  # spec.add_dependency "example-gem", "~> 1.0"
37
36
 
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec path: ".."
6
+
7
+ gem "activerecord", "~> 6.1.0"
8
+ gem "rake", "~> 13.0"
9
+ gem "sqlite3", "~> 1.4"
10
+
11
+ # Rails < 7.1 breaks with concurrent-ruby 1.3.5+ and relies on gems Ruby 3.4 no longer ships by default
12
+ gem "base64"
13
+ gem "bigdecimal"
14
+ gem "concurrent-ruby", "< 1.3.5"
15
+ gem "logger"
16
+ gem "mutex_m"
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec path: ".."
6
+
7
+ gem "activerecord", "~> 7.0.0"
8
+ gem "rake", "~> 13.0"
9
+ gem "sqlite3", "~> 1.4"
10
+
11
+ # Rails < 7.1 breaks with concurrent-ruby 1.3.5+ and relies on gems Ruby 3.4 no longer ships by default
12
+ gem "base64"
13
+ gem "bigdecimal"
14
+ gem "concurrent-ruby", "< 1.3.5"
15
+ gem "logger"
16
+ gem "mutex_m"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec path: ".."
6
+
7
+ gem "activerecord", "~> 7.1.0"
8
+ gem "rake", "~> 13.0"
9
+ gem "sqlite3", "~> 1.6", ">= 1.6.6"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec path: ".."
6
+
7
+ gem "activerecord", "~> 7.2.0"
8
+ gem "rake", "~> 13.0"
9
+ gem "sqlite3", ">= 1.6.6"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec path: ".."
6
+
7
+ gem "activerecord", "~> 8.0.0"
8
+ gem "rake", "~> 13.0"
9
+ gem "sqlite3", ">= 2.1"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec path: ".."
6
+
7
+ gem "activerecord", "~> 8.1.0"
8
+ gem "rake", "~> 13.0"
9
+ gem "sqlite3", ">= 2.1"
@@ -3,11 +3,15 @@
3
3
  require "active_record"
4
4
 
5
5
  module ApiOnlyPagination
6
+ # Adds do_paginate (and its aliases) to every ActiveRecord model and, through delegation, to relations
6
7
  module ActiveRecordPaginate
7
8
  extend ActiveSupport::Concern
9
+
10
+ # Pagination entry points available on models and relations
8
11
  module ClassMethods
9
12
  %i[do_paginate paginate paginated_response].each do |access_method|
10
- define_method(access_method) do |page = ApiOnlyPagination.configuration.default_page, per_page = ApiOnlyPagination.configuration.default_page_size| # or even |*args|
13
+ # nil page/per_page fall back to the configured defaults in Paginate
14
+ define_method(access_method) do |page = nil, per_page = nil|
11
15
  pagination = ApiOnlyPagination::Pagination.new(all, { page: page, per_page: per_page })
12
16
  meta_data = pagination.metadata
13
17
 
@@ -18,7 +22,7 @@ module ApiOnlyPagination
18
22
  total_pages: meta_data.total_pages,
19
23
  total_records: meta_data.count)
20
24
  end
21
- end
25
+ end
22
26
  end
23
27
  end
24
28
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ApiOnlyPagination
4
+ # Global settings, changed through ApiOnlyPagination.configure
4
5
  class Configuration
5
6
  attr_accessor :default_page_size, :max_per_page, :default_page
6
7
 
@@ -1,27 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ApiOnlyPagination
4
+ # Normalizes page/per_page input and computes page metadata from a record count
4
5
  class Paginate
5
6
  attr_reader :page, :count, :per_page
6
7
 
7
8
  def initialize(page, per_page, count)
8
- @page = (page || ApiOnlyPagination.configuration.default_page).to_i
9
+ config = ApiOnlyPagination.configuration
10
+ @page = positive_integer(page) || config.default_page
9
11
  @count = count
10
- @per_page = [(per_page.to_i || ApiOnlyPagination.configuration.default_page_size),
11
- ApiOnlyPagination.configuration.max_per_page].min
12
-
13
- validate_params
14
- end
15
-
16
- def validate_params
17
- @page = @page.zero? ? ApiOnlyPagination.configuration.default_page : @page
18
- @per_page = @per_page.zero? ? ApiOnlyPagination.configuration.default_page_size : @per_page
12
+ @per_page = [positive_integer(per_page) || config.default_page_size, config.max_per_page].min
19
13
  end
20
14
 
21
15
  def offset
22
- return 0 if page == 1
23
-
24
- per_page * (page.to_i - 1)
16
+ per_page * (page - 1)
25
17
  end
26
18
 
27
19
  def next_page
@@ -41,7 +33,7 @@ module ApiOnlyPagination
41
33
  end
42
34
 
43
35
  def last_page?
44
- page == total_pages
36
+ page >= total_pages
45
37
  end
46
38
 
47
39
  def first_page?
@@ -51,5 +43,13 @@ module ApiOnlyPagination
51
43
  def total_pages
52
44
  (count / per_page.to_f).ceil
53
45
  end
46
+
47
+ private
48
+
49
+ # nil, zero, negative and non-numeric values (e.g. "abc") all become nil
50
+ def positive_integer(value)
51
+ integer = value.to_i
52
+ integer if integer.positive?
53
+ end
54
54
  end
55
55
  end
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ApiOnlyPagination
4
+ # Counts a relation and applies limit/offset for the requested page
4
5
  class Pagination
5
6
  attr_reader :collection, :params
6
7
 
7
8
  def initialize(collection, params = {})
8
9
  @collection = collection
9
- @params = params.merge(count: collection.count)
10
+ @params = params.merge(count: total_count)
10
11
  end
11
12
 
12
13
  def metadata
@@ -18,5 +19,13 @@ module ApiOnlyPagination
18
19
  .limit(metadata.per_page)
19
20
  .offset(metadata.offset)
20
21
  end
22
+
23
+ private
24
+
25
+ # count(:all) ignores custom selects; grouped relations return a Hash of group => count
26
+ def total_count
27
+ count = collection.count(:all)
28
+ count.is_a?(Hash) ? count.size : count
29
+ end
21
30
  end
22
31
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ApiOnlyPagination
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
@@ -8,6 +8,7 @@ require_relative "api_only_pagination/pagination"
8
8
  require_relative "api_only_pagination/paginated_data"
9
9
  require_relative "api_only_pagination/active_record_paginate"
10
10
 
11
+ # Offset pagination for ActiveRecord that returns data plus page metadata for API responses
11
12
  module ApiOnlyPagination
12
13
  class << self
13
14
  def configuration
@@ -26,5 +27,4 @@ end
26
27
 
27
28
  ActiveSupport.on_load(:active_record) do
28
29
  include ApiOnlyPagination::ActiveRecordPaginate
29
- ActiveRecord::Relation.include(ApiOnlyPagination::ActiveRecordPaginate)
30
30
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: api_only_pagination
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ashok
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-13 00:00:00.000000000 Z
11
+ date: 2026-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '3.6'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec-rails
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '4.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '4.0'
55
41
  description: pagination with api response only
56
42
  email:
57
43
  - kingkoirala1@gmail.com
@@ -59,8 +45,10 @@ executables: []
59
45
  extensions: []
60
46
  extra_rdoc_files: []
61
47
  files:
48
+ - ".rspec"
62
49
  - ".rubocop.yml"
63
50
  - CHANGELOG.md
51
+ - CLAUDE.md
64
52
  - CODE_OF_CONDUCT.md
65
53
  - Gemfile
66
54
  - Gemfile.lock
@@ -68,6 +56,12 @@ files:
68
56
  - README.md
69
57
  - Rakefile
70
58
  - api_only_pagination.gemspec
59
+ - gemfiles/activerecord_6.1.gemfile
60
+ - gemfiles/activerecord_7.0.gemfile
61
+ - gemfiles/activerecord_7.1.gemfile
62
+ - gemfiles/activerecord_7.2.gemfile
63
+ - gemfiles/activerecord_8.0.gemfile
64
+ - gemfiles/activerecord_8.1.gemfile
71
65
  - lib/api_only_pagination.rb
72
66
  - lib/api_only_pagination/active_record_paginate.rb
73
67
  - lib/api_only_pagination/configuration.rb
@@ -97,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
91
  - !ruby/object:Gem::Version
98
92
  version: '0'
99
93
  requirements: []
100
- rubygems_version: 3.2.16
94
+ rubygems_version: 3.5.3
101
95
  signing_key:
102
96
  specification_version: 4
103
97
  summary: pagination with api response only