next_page 0.2.0 → 1.0.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: 816ed22df71f4838cfe99457bec6a62c2b0e01318436a1fa8f03eb927dfb8939
4
- data.tar.gz: 170578dcc7f6ee852c461fb43597bc0d3a64231016c74498ad674da847296690
3
+ metadata.gz: 38c9e0beeee5d1e9d2e9a4a85e25fdf5011c6d9116dcb35ffdaaddfa795a3cca
4
+ data.tar.gz: 408c859f1330a3ce2a577d43f8af4bbf390031d4a5aca33c1a40f7d2ba29465b
5
5
  SHA512:
6
- metadata.gz: 47b95e0134da9e2ff7032e35ca485d0da63fd2c84c8959833e7e1f6338879968d20458ed244553d60d9406a5880f4f8216ad8e5a218b047fcb27fdf113d65a51
7
- data.tar.gz: 5860bb0ce08448a20625d051bdae952c45a04310a46b10de82d0a758cc0812b4b14e688a9d2732f68a2c11b973d99f08eaf59a7a8da283bf91788c96830fbf9c
6
+ metadata.gz: b2ef11040267561b0daf6174b766c374b998a3effe8c2de9e76b13980386d98bb3b4562e2858a3e70a700a2d45154f6dcd381951dbc997f20af1c99d9435b98e
7
+ data.tar.gz: 7267d87c88104d59d007b6b986fd398853f2591a2b87c25293986fa0fd5559dd7bc08bca4b0a33683f34656165ac3b6490a86d86c7d0f2d647950d558da6dc56
data/README.md CHANGED
@@ -4,43 +4,72 @@
4
4
  [![Maintainability](https://api.codeclimate.com/v1/badges/0efe1a9b66a0bf161536/maintainability)](https://codeclimate.com/github/RockSolt/next_page/maintainability)
5
5
 
6
6
  # NextPage
7
- Basic pagination for Rails controllers.
8
7
 
9
- ## Usage
10
- Module Pagination provides pagination for index methods. It assigns a limit and offset to the resource query and extends the relation with mixin NextPage::PaginationAttributes to provide helper methods for generating links.
8
+ NextPage provides simple pagination with no frills in less than 100 lines of code. It reads request
9
+ parameters, provides an offset and limit to the query, and decorates the ActiveRecord relation
10
+ with pagination attributes.
11
11
 
12
- ### Include the Module
13
- Add an include statement for the module into any controller that needs pagination:
12
+ No more, no less.
14
13
 
15
- ```ruby
16
- include NextPage::Pagination
17
14
  ```
15
+ def index
16
+ @widgets = paginate_resource(Widget.all)
17
+ end
18
+ ```
19
+
20
+ ## Table of Contents
21
+
22
+ - [Getting Started](#getting-started)
23
+ - [Usage](#usage)
24
+ - [Include the Module](#include-the-module)
25
+ - [Invoking Pagination](#invoking-pagination)
26
+ - [Link Helpers](#link-helpers)
27
+ - [Count Query](#count-query)
28
+ - [Request Parameters](#request-parameters)
29
+ - [Default Results per Action](#default-results-per-action)
30
+ - [Configuration](#configuration)
31
+ - [Contribute](#contribute)
32
+ - [Running Tests](#running-tests)
33
+ - [License](#license)
18
34
 
19
- There are two ways to paginate: using a before filter or by calling `paginate_resource` explicitly.
35
+ ## Getting Started
20
36
 
21
- ### Before Filter
22
- Here's an example of using the before filter in a controller:
37
+ This gem requires Rails 7.1+ and works with ActiveRecord.
38
+
39
+ ### Installation
40
+
41
+ Add this line to your application's Gemfile:
23
42
 
24
43
  ```ruby
25
- before_action :apply_next_page_pagination, only: :index
44
+ gem 'next_page'
26
45
  ```
27
46
 
28
- This entry point uses the following conventions to apply pagination:
29
- - the name of the instance variable is the sames as the component (for example PhotosController -> @photos)
30
- - the name of the models is the controller name singularized (for example PhotosController -> Photo)
47
+ And then execute:
48
+ ```bash
49
+ $ bundle
50
+ ```
31
51
 
32
- Either can be overridden by calling method `paginate_with` in the controller. The two override options are
33
- `instance_variable_name` and `model_class`. For example, if the PhotosController used the model Picture and the
34
- instance variable name @photographs, the controller declares it as follows:
52
+ Or install it yourself as:
53
+ ```bash
54
+ $ gem install next_page
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ Module NextPage::Pagination provides pagination controllers. It assigns a limit and offset to the
60
+ resource query and extends the relation with mixin NextPage::PaginationAttributes to provide helper
61
+ methods for generating links.
62
+
63
+ ### Include the Module
64
+
65
+ Add an include statement for the module into any controller that needs pagination (or the ApplicationController):
35
66
 
36
67
  ```ruby
37
- paginate_with instance_variable_name: :photographs, model_class: 'Picture'
68
+ include NextPage::Pagination
38
69
  ```
39
70
 
40
- If the before filter is used, it will populate an instance variable. The action should NOT reset the variable, as
41
- that removes pagination.
71
+ ### Invoking Pagination
42
72
 
43
- ### Invoking Pagination Directly
44
73
  To paginate a resource pass the resource into method `paginate_resource` then store the return value back in the
45
74
  resource:
46
75
 
@@ -48,98 +77,82 @@ resource:
48
77
  @photos = paginate_resource(@photos)
49
78
  ```
50
79
 
51
- ### Sorting
52
- Requests can specify sort order using the parameter `sort` with an attribute name or scope. Sorts can be prefixed with `+` or `-` to indicate ascending or descending. Multiple sorts can be specified either as a comma separated list or via bracket notation.
53
-
54
- /photos?sort=-created_at
55
- /photos?sort=location,-created_by
56
- /photos?sort[]=location&photos[]=-created_by
80
+ The resource is decorated, so this needs to be the last step before rendering.
57
81
 
58
- The default sort order is primary key descending. It can be overridden by using the `default_sort` option of `paginate_with`. Use a string formatted just as url parameter would be formatted.
82
+ ### Link Helpers
59
83
 
60
- ```ruby
61
- paginate_with default_sort: '-created_at'
62
- ```
84
+ This gem does not do any rendering. It does provide helper methods for generating links. The resource will include the following additional methods:
85
+ - previous_page
86
+ - current_page
87
+ - next_page
88
+ - total_pages
89
+ - total_count
90
+ - per_page
63
91
 
64
- #### Nested Sorts
92
+ The `previous_page` and `last_page` readers will return `nil` on the first and last page, respectively.
65
93
 
66
- Nested attributes and scopes can be indicated by providing the association names separated by periods.
94
+ #### Count Query
67
95
 
68
- /photos?sort=user.name
69
- /photos?sort=-user.address.state
96
+ In some cases (such as grouping), calling count on the query does not provide an accurate representation. If that is the case, then there are two ways to override the default behavior:
97
+ - provide a count_query that can resolve the attributes
98
+ - specify the following attributes manually: current_page, total_count, and per_page
70
99
 
71
- #### Directional Scope Sorts
100
+ ### Request Parameters
72
101
 
73
- In order to use directions (`+` or `-`) with a scope, the scope must be defined as a class method and take a single parameter. The scope will receive either `'asc'` or `'desc'`. Here is an example of a valid directional scope.
102
+ In order to control pagingation, the request should pass the `size` and `number` parameters under the
103
+ `page` key:
74
104
 
75
- ```ruby
76
- def self.status(direction)
77
- order("CASE status WHEN 'new' THEN 1 WHEN 'in progress' THEN 2 ELSE 3 END #{direction}")
78
- end
105
+ ```
106
+ ?page[size]=10&page[number]=2
79
107
  ```
80
108
 
81
- #### Scope Prefix / Suffix
82
-
83
- In order to keep the peace between frontend and backend developers, scope names can include a prefix or suffix that the front end can ignore. For example, given a scope that sorts on a derived attribute (such as status in the _Direction Scope Sorts_ example), the backend developer might prefer to name the scope status_sort or sort_by_status, as a class method that shares the same name as an attribute might be unclear. However, the frontend developer does not want a query parameter that says <tt>sort=sort_by_status</tt>; it is an exception because it doesn't match the name of the attribute (and it's not pretty).
84
-
85
- The configuration allows a prefix and or suffix to be specified. If either is specified, then in addition to looking for a scope that matches the parameter name, it will also look for a scope that matches the prefixed and/or suffixed name. Prefixes are defined by configuration option <tt>sort_scope_prefix</tt> and suffixes are defined by <tt>sort_scope_suffix</tt>.
109
+ ### Default Results per Action
86
110
 
87
- For example, if the backend developer prefers <tt>sort_by_status</tt> then the following configuration can be used:
111
+ The default number of results per page can be overridden by specifying a new default with the call:
88
112
 
89
113
  ```ruby
90
- NextPage.configure do |config|
91
- config.sort_scope_prefix = 'sort_by_'
92
- end
93
- ```
94
- This allows the query parameter to be the following:
114
+ @photos = paginate_resource(@photos, default_limit: 25)
115
+ ```
95
116
 
96
- sort=status
117
+ ## Configuration
97
118
 
119
+ There is one configuration option: `default_per_page`.
98
120
 
99
- ### Default Limit
100
- The default size limit can be overridden with the `paginate_with` method for either type of paginagion. Pass option
101
- `default_limit` to specify an override:
121
+ The option can be set directly...
102
122
 
103
- ```ruby
104
- paginate_with default_limit: 25
105
- ```
123
+ `NextPage.configuration.default_per_page = 25`
106
124
 
107
- All the options can be mixed and matches when calling `paginate_with`:
125
+ ...or the configuration can be yielded:
108
126
 
109
- ```ruby
110
- paginate_with model_class: 'Photo', default_limit: 12
111
- paginate_with default_limit: 12, instance_variable_name: 'data'
127
+ ```
128
+ NextPage.configure do |config|
129
+ config.default_per_page = 25
130
+ end
112
131
  ```
113
132
 
114
- ### Link Helpers
115
- This gem does not do any rendering. It does provide helper methods for generating links. The resource will include the following additional methods (when the request header Accept is `'application/vnd.api+json'`):
116
- - current_page
117
- - next_page
118
- - total_pages
119
- - per_page
133
+ **Results per Page**
120
134
 
121
- #### Count Query
122
- In some cases (such as grouping), calling count on the query does not provide an accurate representation. If that is the case, then there are two ways to override the default behavior:
123
- - provide a count_query that can resolve the attributes
124
- - specify the following attributes manually: current_page, total_count, and per_page
135
+ If not specified in the configuration, the default value for results per page is 12.
125
136
 
137
+ ## Contribute
126
138
 
127
- ## Installation
128
- Add this line to your application's Gemfile:
139
+ Feedback, feature requests, proposed changes, and bug reports are welcomed. Please use the
140
+ [issue tracker](https://github.com/RockSolt/next_page/issues) for feedback and feature requests. To
141
+ propose a change directly, please fork the repo and open a pull request. Keep an eye on the actions
142
+ to make sure the tests and Rubocop are passing. [Code Climate](https://codeclimate.com/github/RockSolt/next_page)
143
+ is also used manually to assess the codeline.
129
144
 
130
- ```ruby
131
- gem 'next_page'
132
- ```
145
+ ### Running Tests
133
146
 
134
- And then execute:
135
- ```bash
136
- $ bundle
137
- ```
147
+ Tests are written in RSpec and the dummy app uses a docker database.
148
+
149
+ The tests can also be run across all the ruby and Rails combinations using appraisal. The install is a one-time step.
138
150
 
139
- Or install it yourself as:
140
151
  ```bash
141
- $ gem install next_page
152
+ bundle exec appraisal install
153
+ bundle exec appraisal rspec
142
154
  ```
143
155
 
144
156
  ## License
157
+
145
158
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,38 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NextPage
4
- # = Configuration
4
+ # # Configuration
5
5
  #
6
6
  # Class Configuration stores the following settings:
7
- # - sort_scope_prefix
8
- # - sort_scope_suffix
9
- #
10
- # == Sort Scope Prefix
11
- # Enables client to sort request to be mapped to a scope with a more specific name. For example, given a derived
12
- # attribute named <tt>status</tt>, the query parameter can be <tt>sort=status</tt> but would map to a more explicitly
13
- # named scope, such as <tt>sort_by_status</tt> (assuming the <tt>sort_scope_prefix</tt> value is 'sort_by_').
14
- #
15
- # == Sort Scope Suffix
16
- # Enables client to sort request to be mapped to a scope with a more specific name. For example, given a derived
17
- # attribute named <tt>status</tt>, the query parameter can be <tt>sort=status</tt> but would map to a more explicitly
18
- # named scope, such as <tt>status_sort</tt> (assuming the <tt>sort_scope_suffix</tt> value is '_sort').
7
+ # - default_per_page
19
8
  class Configuration
20
- attr_reader :sort_scope_prefix, :sort_scope_suffix
21
-
22
- def sort_scope_prefix=(value)
23
- @sort_scope_prefix = value.to_s
24
- end
25
-
26
- def sort_scope_prefix?
27
- @sort_scope_prefix.present?
28
- end
29
-
30
- def sort_scope_suffix=(value)
31
- @sort_scope_suffix = value.to_s
32
- end
9
+ attr_accessor :default_per_page
33
10
 
34
- def sort_scope_suffix?
35
- @sort_scope_suffix.present?
11
+ def initialize
12
+ @default_per_page = 12
36
13
  end
37
14
  end
38
15
  end
@@ -1,80 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NextPage
4
- # = Pagination
4
+ # # Pagination
5
5
  #
6
- # Module Pagination provides pagination for index methods. It assigns a limit and offset
6
+ # Module Pagination provides pagination for ActiveRecord queries. It assigns a limit and offset
7
7
  # to the resource query and extends the relation with mixin NextPage::PaginationAttributes.
8
8
  #
9
- # There are two ways to paginate: using a before filter or by calling `paginate_resource` explicitly.
10
9
  #
11
- # == Before Filter
12
- # Here's an example of using the before filter in a controller:
13
- # before_action :apply_next_page_pagination, only: :index
10
+ # ## Invoking Pagination
14
11
  #
15
- # This entry point uses the following conventions to apply pagination:
16
- # - the name of the instance variable is the same as the component (for example PhotosController -> @photos)
17
- # - the name of the model is the controller name singularized (for example PhotosController -> Photo)
18
- #
19
- # Either can be overridden by calling method `paginate_with` in the controller. The two override options are
20
- # `instance_variable_name` and `model_class`. For example, if the PhotosController used the model Picture and the
21
- # instance variable name @photographs, the controller declares it as follows:
22
- # paginate_with instance_variable_name: :photographs, model_class: 'Picture'
23
- #
24
- # If the before filter is used, it will populate an instance variable. The action should NOT reset the variable, as
25
- # that removes pagination.
26
- #
27
- # == Invoking Pagination Directly
28
12
  # To paginate a resource pass the resource into method `paginate_resource` then store the return value back in the
29
13
  # resource:
30
14
  #
31
15
  # @photos = paginate_resource(@photos)
32
16
  #
33
- # == Default Limit
34
- # The default size limit can be overridden with the `paginate_with` method for either type of pagination. Pass option
35
- # `default_limit` to specify an override:
36
- #
37
- # paginate_with default_limit: 25
38
- #
39
- # All the options can be mixed and matches when calling `paginate_with`:
40
- #
41
- # paginate_with model_class: 'Photo', default_limit: 12
42
- # paginate_with default_limit: 12, instance_variable_name: 'data'
43
17
  module Pagination
44
18
  extend ActiveSupport::Concern
45
19
 
46
20
  class_methods do
47
21
  def next_page_paginator # :nodoc:
48
- @next_page_paginator ||= NextPage::Paginator.new(controller_name, controller_path)
22
+ @next_page_paginator ||= NextPage::Paginator.new
49
23
  end
50
-
51
- # Configure pagination with any of the following options:
52
- # - instance_variable_name: explicitly name the variable if it does not follow the convention
53
- # - model_class: explicitly specify the model name if it does not follow the convention
54
- # - default_limit: specify an alternate default
55
- # - default_sort: sort parameter if none provided, use same format as url: created_at OR -updated_at
56
- def paginate_with(instance_variable_name: nil, model_class: nil, default_limit: nil, default_sort: nil)
57
- next_page_paginator.paginate_with(instance_variable_name, model_class, default_limit, default_sort)
58
- end
59
- end
60
-
61
- # Called with before_action in order to automatically paginate the resource.
62
- def apply_next_page_pagination
63
- self.class.next_page_paginator.paginate(self, params.slice(:page, :sort))
64
24
  end
65
25
 
66
- # Invokes pagination directly, the result must be stored as the resource itself is not modified.
67
- def paginate_resource(resource)
68
- self.class.next_page_paginator.paginate_resource(resource, params.slice(:page, :sort))
69
- end
70
-
71
- def render(*args) # :nodoc:
72
- return super unless action_name == 'index' && request.headers[:Accept] == 'application/vnd.api+json'
73
-
74
- self.class.next_page_paginator.decorate_meta!(args.first)
75
- super
76
- rescue StandardError
77
- super
26
+ def paginate_resource(resource, default_limit: nil)
27
+ self.class.next_page_paginator.paginate_resource(resource, params.fetch(:page, {}), default_limit)
78
28
  end
79
29
  end
80
30
  end
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NextPage
4
- # = Pagination Attributes
4
+ # # Pagination Attributes
5
5
  #
6
- # Module PaginationAttributes adds in methods required for pagination links: previous_page, current_page, next_page,
6
+ # Module PaginationAttributes adds in methods to help with pagination links: previous_page, current_page, next_page,
7
7
  # and total_pages. It reads the offset and limit on the query to determine the values.
8
8
  #
9
9
  # In some cases the query will not support count. In that case, there are two ways to override the default behavior:
10
10
  # - provide a count_query that can resolve the attributes
11
11
  # - specify the following attributes manually: current_page, total_count, and per_page
12
+ #
13
+ # These can be completed after the call to `paginate_resource`.
12
14
  module PaginationAttributes
13
15
  attr_writer :count_query, :current_page, :total_count, :per_page
14
16
 
@@ -1,96 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NextPage
4
- # = Paginator
4
+ # # Paginator
5
5
  #
6
6
  # Class Paginator uses the controller information to determine the model and variable name for the
7
7
  # request, then applies a limit and offset to the query based upon the parameters or the defaults. It also extends
8
8
  # the resource with the NextPage::PaginationAttributes mixin.
9
- #
10
- # Configuration can be specified in the controller by calling `paginate_with`. The following overrides can be
11
- # specified if necessary:
12
- # - default_limit: limit to use if request does not specify (default value is 10)
13
- # - instance_variable_name: default value is the controller name; for example, @photos in PhotosController
14
- # - model_class: default derived from controller name (or path if nested); for example, Photo for PhotosController
15
9
  class Paginator
16
- DEFAULT_LIMIT = 10
17
-
18
- def initialize(controller_name, controller_path)
19
- @controller_name = controller_name
20
- @controller_path = controller_path
21
-
22
- @default_limit = DEFAULT_LIMIT
23
- end
24
-
25
- def paginate_with(instance_variable_name, model_class, default_limit, default_sort)
26
- @default_limit = default_limit if default_limit.present?
27
- @instance_variable_name = instance_variable_name
28
- @model_class = model_class.is_a?(String) ? model_class.constantize : model_class
29
- @default_sort = default_sort
30
- end
10
+ def paginate_resource(data, params, default_limit)
11
+ default_limit ||= NextPage.configuration.default_per_page
31
12
 
32
- def paginate(controller, page_params)
33
- name = "@#{instance_variable_name}"
34
- data = controller.instance_variable_get(name) || model_class.all
13
+ assign_pagination_attributes(
14
+ data,
15
+ per_page: params[:size]&.to_i || default_limit,
16
+ current_page: params[:number]&.to_i || 1
17
+ )
35
18
 
36
- controller.instance_variable_set(name, paginate_resource(data, page_params))
37
- end
38
-
39
- def paginate_resource(data, params)
40
- assign_pagination_attributes(data, params)
41
-
42
- data = sorter.sort(data, params.fetch(:sort, default_sort))
43
19
  data.limit(data.per_page).offset((data.current_page - 1) * data.per_page)
44
20
  end
45
21
 
46
- def decorate_meta!(options)
47
- return unless options.is_a?(Hash) && options.key?(:json) && !options[:json].is_a?(Hash)
48
-
49
- resource = options[:json]
50
- options[:meta] = options.fetch(:meta, {}).merge!(total_pages: resource.total_pages,
51
- total_count: resource.total_count)
52
- end
53
-
54
22
  private
55
23
 
56
- def model_class
57
- @model_class ||= @controller_name.classify.safe_constantize ||
58
- @controller_path.classify.safe_constantize ||
59
- raise('Could not determine model for pagination; please specify using `paginate_with` options.')
60
- end
61
-
62
- def instance_variable_name
63
- @instance_variable_name ||= @controller_name
64
- end
65
-
66
- def default_sort
67
- @default_sort ||= "-#{@model_class.primary_key}"
68
- end
69
-
70
- def assign_pagination_attributes(data, params)
24
+ def assign_pagination_attributes(data, per_page:, current_page:)
71
25
  data.extend(NextPage::PaginationAttributes)
72
- data.per_page = page_size(params[:page])
73
- data.current_page = page_number(params[:page])
74
- end
75
-
76
- def page_size(page)
77
- if page.present? && page[:size].present?
78
- page[:size]&.to_i
79
- else
80
- @default_limit
81
- end
82
- end
83
-
84
- def page_number(page)
85
- if page.present? && page[:number].present?
86
- page[:number]&.to_i
87
- else
88
- 1
89
- end
90
- end
91
26
 
92
- def sorter
93
- @sorter ||= NextPage::Sorter.new(model_class)
27
+ data.per_page = per_page
28
+ data.current_page = current_page
94
29
  end
95
30
  end
96
31
  end
@@ -1,3 +1,3 @@
1
1
  module NextPage
2
- VERSION = '0.2.0'
2
+ VERSION = '1.0.0'.freeze
3
3
  end
data/lib/next_page.rb CHANGED
@@ -1,18 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'next_page/configuration'
4
- require 'next_page/exceptions'
5
4
  require 'next_page/pagination'
6
5
  require 'next_page/pagination_attributes'
7
- require 'next_page/sorter'
8
6
  require 'next_page/paginator'
9
7
 
10
8
  # = Next Page
11
9
  module NextPage
12
- class << self
13
- attr_writer :configuration
14
- end
15
-
16
10
  def self.configuration
17
11
  @configuration ||= Configuration.new
18
12
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: next_page
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Todd Kummer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-21 00:00:00.000000000 Z
11
+ date: 2025-04-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 6.1.7
19
+ version: 7.1.5
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 6.1.7
26
+ version: 7.1.5
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: appraisal
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,42 +44,42 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 2.18.0
47
+ version: '2.19'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 2.18.0
54
+ version: '2.19'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: guard-rspec
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 4.7.3
61
+ version: '4.7'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 4.7.3
68
+ version: '4.7'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: guard-rubocop
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 1.5.0
75
+ version: '1.5'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 1.5.0
82
+ version: '1.5'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: pg
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -100,42 +100,42 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 5.1.2
103
+ version: '7.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 5.1.2
110
+ version: '7.0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rubocop
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: 1.60.1
117
+ version: '1.75'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: 1.60.1
124
+ version: '1.75'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: simplecov
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0.18'
131
+ version: '0.19'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '0.18'
138
+ version: '0.19'
139
139
  description: Provide basic pagination, including page size and number as well as helpers
140
140
  for generating links.
141
141
  email:
@@ -149,18 +149,10 @@ files:
149
149
  - Rakefile
150
150
  - lib/next_page.rb
151
151
  - lib/next_page/configuration.rb
152
- - lib/next_page/exceptions.rb
153
- - lib/next_page/exceptions/invalid_nested_sort.rb
154
- - lib/next_page/exceptions/invalid_sort_parameter.rb
155
152
  - lib/next_page/pagination.rb
156
153
  - lib/next_page/pagination_attributes.rb
157
154
  - lib/next_page/paginator.rb
158
- - lib/next_page/sort/name_evaluator.rb
159
- - lib/next_page/sort/segment_parser.rb
160
- - lib/next_page/sort/sort_builder.rb
161
- - lib/next_page/sorter.rb
162
155
  - lib/next_page/version.rb
163
- - lib/tasks/next_page_tasks.rake
164
156
  homepage: https://github.com/RockSolt/next_page
165
157
  licenses:
166
158
  - MIT
@@ -173,14 +165,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
173
165
  requirements:
174
166
  - - ">="
175
167
  - !ruby/object:Gem::Version
176
- version: '0'
168
+ version: 3.1.0
177
169
  required_rubygems_version: !ruby/object:Gem::Requirement
178
170
  requirements:
179
171
  - - ">="
180
172
  - !ruby/object:Gem::Version
181
173
  version: '0'
182
174
  requirements: []
183
- rubygems_version: 3.4.17
175
+ rubygems_version: 3.5.7
184
176
  signing_key:
185
177
  specification_version: 4
186
178
  summary: Pagination for Rails Controllers
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module NextPage
4
- module Exceptions
5
- # = Invalid Nested Sort
6
- class InvalidNestedSort < NextPage::Exceptions::NextPageError
7
- def initialize(model, association)
8
- @model = model
9
- @association = association
10
- super("Invalid nested sort: Unable to find association #{@association} on model #{@model}")
11
- end
12
- end
13
- end
14
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module NextPage
4
- module Exceptions
5
- # = Invalid Sort Parameter
6
- class InvalidSortParameter < NextPage::Exceptions::NextPageError
7
- def initialize(segment)
8
- @segment = segment
9
- super("Invalid sort parameter (#{@segment}). Must be an attribute or scope.")
10
- end
11
- end
12
- end
13
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module NextPage
4
- module Exceptions
5
- class NextPageError < StandardError
6
- end
7
- end
8
- end
9
-
10
- require 'next_page/exceptions/invalid_nested_sort'
11
- require 'next_page/exceptions/invalid_sort_parameter'
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module NextPage
4
- module Sort
5
- # = Name Evaluator
6
- #
7
- # Determines if a name represents an attribute or a scope, provides mapping if the scope requires a prefix or
8
- # suffix. Scope is checked first, allowing it to override an attribute of the same name. For scopes, method
9
- # <tt>directional_scope?</tt> checks to see if the scope is a class method that accepts one parameter; if so, then
10
- # the scope will be invoked with the direction.
11
- class NameEvaluator
12
- attr_reader :scope_name
13
-
14
- def initialize(model, name)
15
- @model = model
16
- @name = name.to_s
17
- evaluate_for_scope
18
- end
19
-
20
- def scope?
21
- @scope_name.present?
22
- end
23
-
24
- # true when scope is class method with one parameter
25
- def directional_scope?
26
- return false unless scope?
27
-
28
- @model.method(@scope_name).arity == 1
29
- end
30
-
31
- def valid_attribute_name?
32
- @model.attribute_names.include?(@name)
33
- end
34
-
35
- private
36
-
37
- def evaluate_for_scope
38
- assign_scope(@name) || assign_prefixed_scope || assign_suffixed_scope
39
- end
40
-
41
- def assign_scope(potential_scope)
42
- return if @model.dangerous_class_method?(potential_scope) || !@model.respond_to?(potential_scope)
43
-
44
- @scope_name = potential_scope
45
- end
46
-
47
- def assign_prefixed_scope
48
- return unless NextPage.configuration.sort_scope_prefix?
49
-
50
- assign_scope("#{NextPage.configuration.sort_scope_prefix}#{@name}")
51
- end
52
-
53
- def assign_suffixed_scope
54
- return unless NextPage.configuration.sort_scope_suffix?
55
-
56
- assign_scope("#{@name}#{NextPage.configuration.sort_scope_suffix}")
57
- end
58
- end
59
- end
60
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module NextPage
4
- module Sort
5
- # = Segment Parser
6
- #
7
- # Parses each sort segment to provide direction, associations, and name.
8
- class SegmentParser
9
- attr_reader :direction, :associations, :name
10
-
11
- SEGMENT_REGEX = /(?<sign>[+|-]?)(?<names>.+)/
12
-
13
- def initialize(segment)
14
- @segment = segment
15
- parsed = segment.match SEGMENT_REGEX
16
- @direction = parsed['sign'] == '-' ? 'desc' : 'asc'
17
- *@associations, @name = *parsed['names'].split('.')
18
- end
19
-
20
- def to_s
21
- @segment
22
- end
23
-
24
- def attribute_with_direction
25
- { @name => @direction }
26
- end
27
-
28
- def nested?
29
- @associations.present?
30
- end
31
- end
32
- end
33
- end
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module NextPage
4
- module Sort
5
- # = Sort Builder
6
- class SortBuilder
7
- def initialize(model)
8
- @model = model
9
- end
10
-
11
- # TODO: support passing direction to scope
12
- def build(segment)
13
- @parser = NextPage::Sort::SegmentParser.new(segment)
14
-
15
- if @parser.nested?
16
- build_nested
17
- else
18
- build_non_nested
19
- end
20
- end
21
-
22
- private
23
-
24
- def build_nested
25
- sort_model = dig_association_model(@parser.associations)
26
- joins = build_joins(@parser.associations)
27
- evaluator = NextPage::Sort::NameEvaluator.new(sort_model, @parser.name)
28
-
29
- if evaluator.scope?
30
- build_nested_scope_sort(sort_model, joins, evaluator)
31
- elsif evaluator.valid_attribute_name?
32
- nested_attribute_sort(sort_model, joins, @parser.attribute_with_direction)
33
- else
34
- raise NextPage::Exceptions::InvalidSortParameter, @parser
35
- end
36
- end
37
-
38
- def build_nested_scope_sort(sort_model, joins, evaluator)
39
- if evaluator.directional_scope?
40
- nested_directional_scope_sort(sort_model, joins, evaluator.scope_name, @parser.direction)
41
- else
42
- nested_scope_sort(sort_model, joins, evaluator.scope_name)
43
- end
44
- end
45
-
46
- def nested_attribute_sort(model, joins, attribute_with_direction)
47
- ->(query) { query.joins(joins).merge(model.order(attribute_with_direction)) }
48
- end
49
-
50
- def nested_scope_sort(model, joins, scope_name)
51
- ->(query) { query.joins(joins).merge(model.public_send(scope_name)) }
52
- end
53
-
54
- def nested_directional_scope_sort(model, joins, scope_name, direction)
55
- ->(query) { query.joins(joins).merge(model.public_send(scope_name, direction)) }
56
- end
57
-
58
- def build_non_nested
59
- evaluator = NextPage::Sort::NameEvaluator.new(@model, @parser.name)
60
-
61
- if evaluator.scope?
62
- build_non_nested_scope_sort(evaluator)
63
- elsif evaluator.valid_attribute_name?
64
- attribute_sort(@parser.attribute_with_direction)
65
- else
66
- raise NextPage::Exceptions::InvalidSortParameter, @parser
67
- end
68
- end
69
-
70
- def build_non_nested_scope_sort(evaluator)
71
- if evaluator.directional_scope?
72
- directional_scope_sort(evaluator.scope_name, @parser.direction)
73
- else
74
- scope_sort(evaluator.scope_name)
75
- end
76
- end
77
-
78
- def attribute_sort(attribute_with_direction)
79
- ->(query) { query.order(attribute_with_direction) }
80
- end
81
-
82
- def scope_sort(scope_name)
83
- ->(query) { query.send(scope_name) }
84
- end
85
-
86
- def directional_scope_sort(scope_name, direction)
87
- ->(query) { query.send(scope_name, direction) }
88
- end
89
-
90
- # traverse nested associations to find last association's model
91
- def dig_association_model(associations)
92
- associations.reduce(@model) do |model, association_name|
93
- association = model.reflect_on_association(association_name)
94
- raise NextPage::Exceptions::InvalidNestedSort.new(model, association_name) if association.nil?
95
-
96
- association.klass
97
- end
98
- end
99
-
100
- # transform associations array to nested hash
101
- # ['team'] => [:team]
102
- # ['team', 'coach'] => { team: :coach }
103
- def build_joins(associations)
104
- associations.map(&:to_sym)
105
- .reverse
106
- .reduce { |memo, association| memo.nil? ? association.to_sym : { association => memo } }
107
- end
108
-
109
- # TODO: consider prefix / suffix
110
- def named_scope(sort_model)
111
- # checking dangerous_class_method? excludes any names that cannot be scope names, such as "name"
112
- return unless sort_model.respond_to?(@parser.name) && !sort_model.dangerous_class_method?(@parser.name)
113
-
114
- @parser.name
115
- end
116
- end
117
- end
118
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'next_page/sort/name_evaluator'
4
- require 'next_page/sort/segment_parser'
5
- require 'next_page/sort/sort_builder'
6
-
7
- module NextPage
8
- # = Sorter
9
- #
10
- # Class Sorter reads the sort parameter and applies the related ordering. Results for each parameter string are
11
- # cached so evaluation only occurs once.
12
- class Sorter
13
- # Initializes a new sorter. The given model is used to validate sort attributes as well as build nested sorts.
14
- def initialize(model)
15
- @model = model
16
- @cache = Hash.new { |hash, key| hash[key] = build_sort(key) }
17
- end
18
-
19
- # Adds sorting to given query based upon the param. Returns a new query; the existing query is NOT modified.
20
- #
21
- # The +query+ parameter is an ActiveRecord arel or model.
22
- #
23
- # The +sort_fields+ parameter is a string that conforms to the JSON-API specification for sorting fields:
24
- # https://jsonapi.org/format/#fetching-sorting
25
- def sort(query, sort_fields)
26
- return from_array(query, sort_fields.split(',')) if sort_fields.include?(',')
27
- return from_array(query, sort_fields) if sort_fields.is_a? Array
28
-
29
- apply_sort(query, sort_fields)
30
- end
31
-
32
- private
33
-
34
- def apply_sort(query, key)
35
- @cache[key].call(query)
36
- end
37
-
38
- def from_array(query, param)
39
- param.reduce(query) { |memo, key| apply_sort(memo, key) }
40
- end
41
-
42
- # returns a lambda that applies the appropriate sort, either from a scope, nested attribute, or attribute
43
- def build_sort(key)
44
- ActiveSupport::Notifications.instrument('build_sort.next_page', { key: key }) do
45
- NextPage::Sort::SortBuilder.new(@model).build(key)
46
- end
47
- end
48
- end
49
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
- # desc "Explaining what the task does"
3
- # task :next_page do
4
- # # Task goes here
5
- # end