toller 0.5.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 +4 -4
- data/MIT-LICENSE +1 -1
- data/README.md +65 -19
- data/Rakefile +5 -5
- data/lib/toller/filter.rb +25 -2
- data/lib/toller/filters/mutators/boolean.rb +7 -1
- data/lib/toller/filters/mutators/date.rb +21 -3
- data/lib/toller/filters/mutators/datetime.rb +21 -3
- data/lib/toller/filters/mutators/integer.rb +25 -3
- data/lib/toller/filters/mutators/time.rb +21 -3
- data/lib/toller/filters/scope_handler.rb +8 -1
- data/lib/toller/filters/where_handler.rb +42 -1
- data/lib/toller/retriever.rb +39 -2
- data/lib/toller/sort.rb +25 -2
- data/lib/toller/sorts/order_handler.rb +8 -1
- data/lib/toller/sorts/scope_handler.rb +8 -1
- data/lib/toller/version.rb +2 -1
- data/lib/toller.rb +54 -14
- metadata +17 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 565b78efab21039ddd7025adad94e0ac2e045943acef17e97374aac58b0b3b10
|
|
4
|
+
data.tar.gz: a73035fb270e002dfd8cfe8ed38dff7317dbbf033e4aac922e5b409fc9aad92b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6a9f24aeec9383b065c39c3430c63ab90bd75cd963b414418e552d3fdd22a151e0c1da7ece697c92116958d294781c83330145889266d6be739e7ba2698b78a
|
|
7
|
+
data.tar.gz: b816afc011e2070a7adfeee999702ecdf2ca04af45e50a4ec7966d98ef1fd5486aaa1bcffd4c48a443db9aafdc43a60c5679c7b4dd20e5ff918304b6b38232bd
|
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
# Toller
|
|
2
2
|
|
|
3
|
-
URL
|
|
3
|
+
URL-query-param-based filtering and sorting for Rails controllers.
|
|
4
|
+
|
|
5
|
+
Toller is a Rails engine that lets controllers declare filter_on/sort_on directives, then applies whichever filters and sorts are active for the current request to an ActiveRecord relation based on URL query parameters.
|
|
6
|
+
|
|
7
|
+
See the [wiki](https://github.com/dfreerksen/toller/wiki) for usage information.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
* Ruby >= 3.3.0
|
|
12
|
+
* Rails >= 6.0
|
|
4
13
|
|
|
5
14
|
## Installation
|
|
6
15
|
|
|
7
16
|
Add this line to your application's Gemfile:
|
|
8
17
|
|
|
9
18
|
```ruby
|
|
10
|
-
gem 'toller'
|
|
19
|
+
gem 'toller', '~> 1.0'
|
|
11
20
|
```
|
|
12
21
|
|
|
13
22
|
And then execute:
|
|
@@ -40,40 +49,77 @@ $ bin/test
|
|
|
40
49
|
|
|
41
50
|
### Appraisal
|
|
42
51
|
|
|
52
|
+
Uses [Appraisal2](https://github.com/appraisal-rb/appraisal2) (a maintained fork of [Appraisal](https://github.com/thoughtbot/appraisal), still exposing the `appraisal` executable) to ensure various dependency versions work as expected
|
|
53
|
+
|
|
54
|
+
When dependencies change, run
|
|
55
|
+
|
|
43
56
|
```bash
|
|
44
57
|
$ bundle exec appraisal install
|
|
45
|
-
$ bundle exec appraisal
|
|
46
|
-
$ bundle exec appraisal rails-6 bin/test
|
|
58
|
+
$ bundle exec appraisal generate-install
|
|
47
59
|
```
|
|
48
60
|
|
|
49
|
-
|
|
61
|
+
To run tests with Appraisal, run
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
$ bundle exec appraisal rspec
|
|
65
|
+
```
|
|
50
66
|
|
|
51
|
-
|
|
67
|
+
`-n 1` forces Appraisal2 to run one Rails version at a time. Without it, Appraisal2 defaults to running 2 appraisals in parallel, and since every appraisal shares the same `test/dummy/db/test.sqlite3` file, concurrent runs can intermittently fail with `SQLite3::BusyException: database is locked`.
|
|
52
68
|
|
|
53
69
|
```bash
|
|
54
|
-
$
|
|
70
|
+
$ bundle exec appraisal rails-6-0 rspec
|
|
71
|
+
$ bundle exec appraisal rails-6-1 rspec
|
|
72
|
+
$ bundle exec appraisal rails-7-0 rspec
|
|
73
|
+
$ bundle exec appraisal rails-7-1 rspec
|
|
74
|
+
$ bundle exec appraisal rails-7-2 rspec
|
|
75
|
+
$ bundle exec appraisal rails-8-0 rspec
|
|
76
|
+
$ bundle exec appraisal rails-8-1 rspec
|
|
55
77
|
```
|
|
56
78
|
|
|
57
79
|
## Release
|
|
58
80
|
|
|
59
|
-
|
|
60
|
-
|
|
81
|
+
See [Release.md](docs/Release.md)
|
|
82
|
+
|
|
83
|
+
## Code Analysis
|
|
84
|
+
|
|
85
|
+
Various tools are used to ensure code is linted and formatted correctly.
|
|
86
|
+
|
|
87
|
+
### RuboCop
|
|
88
|
+
|
|
89
|
+
[RuboCop](https://github.com/bbatsov/rubocop) is a Ruby static code analyzer.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
$ rubocop
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### YARD-Lint
|
|
96
|
+
|
|
97
|
+
[YARD-Lint](https://github.com/mensfeld/yard-lint) is a linter for YARD documentation.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
$ bundle exec yard-lint
|
|
101
|
+
```
|
|
61
102
|
|
|
62
|
-
|
|
63
|
-
$ bundle exec rake build
|
|
64
|
-
```
|
|
103
|
+
## Documentation
|
|
65
104
|
|
|
66
|
-
|
|
67
|
-
3. Commit the version change to git with a commit message similar to "Release [X.Y.Z]"
|
|
68
|
-
4. Create the gem, tag it in Github and release to Rubygems
|
|
105
|
+
[Yard](https://github.com/lsegal/yard) is used to generate documentation. [Online documentation is available](http://www.rubydoc.info/github/dfreerksen/toller/master)
|
|
69
106
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
107
|
+
Build the documentation with one of the following
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
$ yard
|
|
111
|
+
$ yard doc
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Build the documentation and list all undocumented objects
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
$ yard stats --list-undoc
|
|
118
|
+
```
|
|
73
119
|
|
|
74
120
|
## Contributing
|
|
75
121
|
|
|
76
|
-
1. Fork it ([https://github.com/dfrerksen/
|
|
122
|
+
1. Fork it ([https://github.com/dfrerksen/toller/fork](https://github.com/dfrerksen/toller/fork))
|
|
77
123
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
78
124
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
79
125
|
4. Push to the branch (`git push origin my-new-feature`)
|
data/Rakefile
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
begin
|
|
4
|
-
require
|
|
4
|
+
require "bundler/setup"
|
|
5
5
|
rescue LoadError
|
|
6
|
-
puts
|
|
6
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
require
|
|
9
|
+
require "bundler/gem_tasks"
|
|
10
10
|
|
|
11
|
-
APP_RAKEFILE = File.expand_path(
|
|
12
|
-
load
|
|
11
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
12
|
+
load "rails/tasks/engine.rake"
|
data/lib/toller/filter.rb
CHANGED
|
@@ -3,10 +3,24 @@
|
|
|
3
3
|
module Toller
|
|
4
4
|
##
|
|
5
5
|
# Filter
|
|
6
|
-
#
|
|
7
6
|
class Filter
|
|
8
|
-
|
|
7
|
+
# @return [Symbol] the public filter param name
|
|
8
|
+
attr_reader :parameter
|
|
9
9
|
|
|
10
|
+
# @return [Hash] the filter's resolved options (:field, :default, :scope_name, etc.)
|
|
11
|
+
attr_reader :properties
|
|
12
|
+
|
|
13
|
+
# @return [Symbol] the filter type, e.g. :string, :integer, :scope
|
|
14
|
+
attr_reader :type
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# @param parameter [Symbol] the public filter param name
|
|
18
|
+
# @param type [Symbol] the filter type, e.g. :string, :integer, :scope
|
|
19
|
+
# @param options [Hash] filter options; merged over defaults for :field, :default, and :scope_name
|
|
20
|
+
# @option options [Symbol] :field the column/attribute to query; defaults to +parameter+
|
|
21
|
+
# @option options [Boolean] :default whether this filter applies automatically when no filter params were sent
|
|
22
|
+
# @option options [Symbol] :scope_name for type: :scope, the model scope to call; defaults to +parameter+
|
|
23
|
+
# @return [Toller::Filter] a new instance of Filter
|
|
10
24
|
def initialize(parameter, type, options)
|
|
11
25
|
@parameter = parameter
|
|
12
26
|
@type = type
|
|
@@ -17,6 +31,13 @@ module Toller
|
|
|
17
31
|
)
|
|
18
32
|
end
|
|
19
33
|
|
|
34
|
+
##
|
|
35
|
+
# Applies this filter to +collection+, dispatching to the scope or
|
|
36
|
+
# where handler based on +type+.
|
|
37
|
+
#
|
|
38
|
+
# @param collection [ActiveRecord::Relation] the collection to filter
|
|
39
|
+
# @param value [Object] the active filter param value
|
|
40
|
+
# @return [ActiveRecord::Relation] the filtered collection
|
|
20
41
|
def apply!(collection, value)
|
|
21
42
|
if type == :scope
|
|
22
43
|
Filters::ScopeHandler.new.call(collection, value, properties)
|
|
@@ -25,6 +46,8 @@ module Toller
|
|
|
25
46
|
end
|
|
26
47
|
end
|
|
27
48
|
|
|
49
|
+
##
|
|
50
|
+
# @return [Boolean] whether this filter applies automatically when no filter params were sent at all
|
|
28
51
|
def default
|
|
29
52
|
properties[:default]
|
|
30
53
|
end
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Toller
|
|
4
|
+
# :nodoc:
|
|
4
5
|
module Filters
|
|
6
|
+
# :nodoc:
|
|
5
7
|
module Mutators
|
|
6
8
|
##
|
|
7
9
|
# Boolean filter mutator
|
|
8
|
-
#
|
|
9
10
|
module Boolean
|
|
10
11
|
module_function
|
|
11
12
|
|
|
13
|
+
##
|
|
14
|
+
# Coerces a raw filter param into a boolean.
|
|
15
|
+
#
|
|
16
|
+
# @param value [String] the raw filter param value
|
|
17
|
+
# @return [Boolean] true if +value+ is one of "1", "t", "true", "y", "yes"; false otherwise
|
|
12
18
|
def call(value)
|
|
13
19
|
%w[1 t true y yes].include?(value)
|
|
14
20
|
end
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Toller
|
|
4
|
+
# :nodoc:
|
|
4
5
|
module Filters
|
|
6
|
+
# :nodoc:
|
|
5
7
|
module Mutators
|
|
6
8
|
##
|
|
7
9
|
# Date filter mutator
|
|
8
|
-
#
|
|
9
10
|
module Date
|
|
10
11
|
module_function
|
|
11
12
|
|
|
13
|
+
##
|
|
14
|
+
# Coerces a raw filter param into a date value or range.
|
|
15
|
+
#
|
|
16
|
+
# @param value [String] the raw filter param value
|
|
17
|
+
# @return [String,Range] the original value, or a Range when the value contains range syntax (`..` or `...`)
|
|
12
18
|
def call(value)
|
|
13
19
|
range_dots = inclusive_or_exclusive_range(value)
|
|
14
20
|
|
|
@@ -17,13 +23,25 @@ module Toller
|
|
|
17
23
|
range(value, range_dots)
|
|
18
24
|
end
|
|
19
25
|
|
|
26
|
+
##
|
|
27
|
+
# Builds a Range by splitting +value+ on the given range dots.
|
|
28
|
+
#
|
|
29
|
+
# @param value [String] the raw range string, e.g. "2024-01-01..2024-12-31"
|
|
30
|
+
# @param dots [String] the range separator, either ".." or "..."
|
|
31
|
+
# @return [Range] the resulting range
|
|
20
32
|
def range(value, dots)
|
|
21
33
|
Range.new(*value.split(dots))
|
|
22
34
|
end
|
|
23
35
|
|
|
36
|
+
##
|
|
37
|
+
# Detects whether +value+ contains inclusive or exclusive range syntax.
|
|
38
|
+
#
|
|
39
|
+
# @param value [String] the raw filter param value
|
|
40
|
+
# @return [String,nil] "..." for an exclusive range, ".." for an inclusive range, or nil if +value+ is not
|
|
41
|
+
# a range
|
|
24
42
|
def inclusive_or_exclusive_range(value)
|
|
25
|
-
return
|
|
26
|
-
return
|
|
43
|
+
return "..." if value.include?("...")
|
|
44
|
+
return ".." if value.include?("..")
|
|
27
45
|
|
|
28
46
|
nil
|
|
29
47
|
end
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Toller
|
|
4
|
+
# :nodoc:
|
|
4
5
|
module Filters
|
|
6
|
+
# :nodoc:
|
|
5
7
|
module Mutators
|
|
6
8
|
##
|
|
7
9
|
# Datetime filter mutator
|
|
8
|
-
#
|
|
9
10
|
module Datetime
|
|
10
11
|
module_function
|
|
11
12
|
|
|
13
|
+
##
|
|
14
|
+
# Coerces a raw filter param into a datetime value or range.
|
|
15
|
+
#
|
|
16
|
+
# @param value [String] the raw filter param value
|
|
17
|
+
# @return [String,Range] the original value, or a Range when the value contains range syntax (`..` or `...`)
|
|
12
18
|
def call(value)
|
|
13
19
|
range_dots = inclusive_or_exclusive_range(value)
|
|
14
20
|
|
|
@@ -17,13 +23,25 @@ module Toller
|
|
|
17
23
|
range(value, range_dots)
|
|
18
24
|
end
|
|
19
25
|
|
|
26
|
+
##
|
|
27
|
+
# Builds a Range by splitting +value+ on the given range dots.
|
|
28
|
+
#
|
|
29
|
+
# @param value [String] the raw range string, e.g. "2024-01-01..2024-12-31"
|
|
30
|
+
# @param dots [String] the range separator, either ".." or "..."
|
|
31
|
+
# @return [Range] the resulting range
|
|
20
32
|
def range(value, dots)
|
|
21
33
|
Range.new(*value.split(dots))
|
|
22
34
|
end
|
|
23
35
|
|
|
36
|
+
##
|
|
37
|
+
# Detects whether +value+ contains inclusive or exclusive range syntax.
|
|
38
|
+
#
|
|
39
|
+
# @param value [String] the raw filter param value
|
|
40
|
+
# @return [String,nil] "..." for an exclusive range, ".." for an inclusive range, or nil if +value+ is not
|
|
41
|
+
# a range
|
|
24
42
|
def inclusive_or_exclusive_range(value)
|
|
25
|
-
return
|
|
26
|
-
return
|
|
43
|
+
return "..." if value.include?("...")
|
|
44
|
+
return ".." if value.include?("..")
|
|
27
45
|
|
|
28
46
|
nil
|
|
29
47
|
end
|
|
@@ -1,33 +1,55 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Toller
|
|
4
|
+
# :nodoc:
|
|
4
5
|
module Filters
|
|
6
|
+
# :nodoc:
|
|
5
7
|
module Mutators
|
|
6
8
|
##
|
|
7
9
|
# Integer filter mutator
|
|
8
|
-
#
|
|
9
10
|
module Integer
|
|
10
11
|
module_function
|
|
11
12
|
|
|
13
|
+
##
|
|
14
|
+
# Coerces a raw filter param into an integer value or range.
|
|
15
|
+
#
|
|
16
|
+
# @param value [String] the raw filter param value
|
|
17
|
+
# @return [String, Range] the original value, or a Range when the value contains range syntax (`..` or `...`)
|
|
12
18
|
def call(value)
|
|
13
19
|
return value unless range?(value)
|
|
14
20
|
|
|
15
21
|
range(value)
|
|
16
22
|
end
|
|
17
23
|
|
|
24
|
+
##
|
|
25
|
+
# Checks whether +value+ contains range syntax.
|
|
26
|
+
#
|
|
27
|
+
# @param value [String] the raw filter param value
|
|
28
|
+
# @return [Boolean] true if +value+ contains `..` or `...`
|
|
18
29
|
def range?(value)
|
|
19
30
|
range_dots = inclusive_or_exclusive_range(value)
|
|
20
31
|
|
|
21
32
|
range_dots.present?
|
|
22
33
|
end
|
|
23
34
|
|
|
35
|
+
##
|
|
36
|
+
# Builds a Range by splitting +value+ on its range dots.
|
|
37
|
+
#
|
|
38
|
+
# @param value [String] the raw range string, e.g. "1..10"
|
|
39
|
+
# @return [Range] the resulting range
|
|
24
40
|
def range(value)
|
|
25
41
|
Range.new(*value.split(inclusive_or_exclusive_range(value)))
|
|
26
42
|
end
|
|
27
43
|
|
|
44
|
+
##
|
|
45
|
+
# Detects whether +value+ contains inclusive or exclusive range syntax.
|
|
46
|
+
#
|
|
47
|
+
# @param value [String] the raw filter param value
|
|
48
|
+
# @return [String,nil] "..." for an exclusive range, ".." for an inclusive range, or nil if +value+ is not
|
|
49
|
+
# a range
|
|
28
50
|
def inclusive_or_exclusive_range(value)
|
|
29
|
-
return
|
|
30
|
-
return
|
|
51
|
+
return "..." if value.include?("...")
|
|
52
|
+
return ".." if value.include?("..")
|
|
31
53
|
|
|
32
54
|
nil
|
|
33
55
|
end
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Toller
|
|
4
|
+
# :nodoc:
|
|
4
5
|
module Filters
|
|
6
|
+
# :nodoc:
|
|
5
7
|
module Mutators
|
|
6
8
|
##
|
|
7
9
|
# Time filter mutator
|
|
8
|
-
#
|
|
9
10
|
module Time
|
|
10
11
|
module_function
|
|
11
12
|
|
|
13
|
+
##
|
|
14
|
+
# Coerces a raw filter param into a time value or range.
|
|
15
|
+
#
|
|
16
|
+
# @param value [String] the raw filter param value
|
|
17
|
+
# @return [String,Range] the original value, or a Range when the value contains range syntax (`..` or `...`)
|
|
12
18
|
def call(value)
|
|
13
19
|
range_dots = inclusive_or_exclusive_range(value)
|
|
14
20
|
|
|
@@ -17,13 +23,25 @@ module Toller
|
|
|
17
23
|
range(value, range_dots)
|
|
18
24
|
end
|
|
19
25
|
|
|
26
|
+
##
|
|
27
|
+
# Builds a Range by splitting +value+ on the given range dots.
|
|
28
|
+
#
|
|
29
|
+
# @param value [String] the raw range string, e.g. "09:00..17:00"
|
|
30
|
+
# @param dots [String] the range separator, either ".." or "..."
|
|
31
|
+
# @return [Range] the resulting range
|
|
20
32
|
def range(value, dots)
|
|
21
33
|
Range.new(*value.split(dots))
|
|
22
34
|
end
|
|
23
35
|
|
|
36
|
+
##
|
|
37
|
+
# Detects whether +value+ contains inclusive or exclusive range syntax.
|
|
38
|
+
#
|
|
39
|
+
# @param value [String] the raw filter param value
|
|
40
|
+
# @return [String,nil] "..." for an exclusive range, ".." for an inclusive range, or nil if +value+ is not
|
|
41
|
+
# a range
|
|
24
42
|
def inclusive_or_exclusive_range(value)
|
|
25
|
-
return
|
|
26
|
-
return
|
|
43
|
+
return "..." if value.include?("...")
|
|
44
|
+
return ".." if value.include?("..")
|
|
27
45
|
|
|
28
46
|
nil
|
|
29
47
|
end
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Toller
|
|
4
|
+
# :nodoc:
|
|
4
5
|
module Filters
|
|
5
6
|
##
|
|
6
7
|
# Scope handler for filter
|
|
7
|
-
#
|
|
8
8
|
class ScopeHandler
|
|
9
|
+
##
|
|
10
|
+
# Applies a named scope to +collection+ for a `type: :scope` filter.
|
|
11
|
+
#
|
|
12
|
+
# @param collection [ActiveRecord::Relation] the collection to filter
|
|
13
|
+
# @param value [Object] the filter param value to pass to the scope
|
|
14
|
+
# @param properties [Hash] the filter's properties, used to resolve `:scope_name` (falling back to `:field`)
|
|
15
|
+
# @return [ActiveRecord::Relation] the scoped collection
|
|
9
16
|
def call(collection, value, properties)
|
|
10
17
|
scoped_name = properties[:scope_name] || properties[:field]
|
|
11
18
|
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Toller
|
|
4
|
+
# :nodoc:
|
|
4
5
|
module Filters
|
|
5
6
|
##
|
|
6
7
|
# Where handler for filter
|
|
7
|
-
#
|
|
8
8
|
class WhereHandler
|
|
9
|
+
##
|
|
10
|
+
# Applies a plain `where` clause to +collection+ for a non-scope filter.
|
|
11
|
+
#
|
|
12
|
+
# @param collection [ActiveRecord::Relation] the collection to filter
|
|
13
|
+
# @param type [Symbol] the filter type (e.g. :string, :integer, :boolean)
|
|
14
|
+
# @param value [Object] the raw filter param value
|
|
15
|
+
# @param properties [Hash] the filter's properties, used to resolve `:field`
|
|
16
|
+
# @return [ActiveRecord::Relation] the filtered collection
|
|
9
17
|
def call(collection, type, value, properties)
|
|
10
18
|
field_name = properties[:field]
|
|
11
19
|
|
|
@@ -16,28 +24,61 @@ module Toller
|
|
|
16
24
|
|
|
17
25
|
private
|
|
18
26
|
|
|
27
|
+
##
|
|
28
|
+
# Value mutator
|
|
29
|
+
#
|
|
30
|
+
# Runs +value+ through the type-specific mutator, if one exists for +type+.
|
|
31
|
+
#
|
|
32
|
+
# @param type [Symbol] the filter type
|
|
33
|
+
# @param value [Object] the raw filter param value
|
|
34
|
+
# @return [Object] the mutated value, or the original +value+ if +type+ has no mutator
|
|
19
35
|
def value_mutator(type, value)
|
|
20
36
|
return value unless %i[boolean date datetime integer time].include?(type)
|
|
21
37
|
|
|
22
38
|
send("#{type}_mutator", value)
|
|
23
39
|
end
|
|
24
40
|
|
|
41
|
+
##
|
|
42
|
+
# Boolean mutator
|
|
43
|
+
#
|
|
44
|
+
# @param value [String] the raw filter param value
|
|
45
|
+
# @return [Boolean] the mutated boolean value
|
|
25
46
|
def boolean_mutator(value)
|
|
26
47
|
Mutators::Boolean.call(value)
|
|
27
48
|
end
|
|
28
49
|
|
|
50
|
+
##
|
|
51
|
+
# Integer mutator
|
|
52
|
+
#
|
|
53
|
+
# @param value [String] the raw filter param value
|
|
54
|
+
# @return [String, Range] the mutated integer value or range
|
|
29
55
|
def integer_mutator(value)
|
|
30
56
|
Mutators::Integer.call(value)
|
|
31
57
|
end
|
|
32
58
|
|
|
59
|
+
##
|
|
60
|
+
# Date mutator
|
|
61
|
+
#
|
|
62
|
+
# @param value [String] the raw filter param value
|
|
63
|
+
# @return [String, Range] the mutated date value or range
|
|
33
64
|
def date_mutator(value)
|
|
34
65
|
Mutators::Date.call(value)
|
|
35
66
|
end
|
|
36
67
|
|
|
68
|
+
##
|
|
69
|
+
# Time mutator
|
|
70
|
+
#
|
|
71
|
+
# @param value [String] the raw filter param value
|
|
72
|
+
# @return [String, Range] the mutated time value or range
|
|
37
73
|
def time_mutator(value)
|
|
38
74
|
Mutators::Time.call(value)
|
|
39
75
|
end
|
|
40
76
|
|
|
77
|
+
##
|
|
78
|
+
# DateTime mutator
|
|
79
|
+
#
|
|
80
|
+
# @param value [String] the raw filter param value
|
|
81
|
+
# @return [String, Range] the mutated datetime value or range
|
|
41
82
|
def datetime_mutator(value)
|
|
42
83
|
Mutators::Datetime.call(value)
|
|
43
84
|
end
|
data/lib/toller/retriever.rb
CHANGED
|
@@ -3,14 +3,38 @@
|
|
|
3
3
|
module Toller
|
|
4
4
|
##
|
|
5
5
|
# Retriever
|
|
6
|
-
#
|
|
7
6
|
class Retriever
|
|
8
|
-
|
|
7
|
+
# @return [ActiveRecord::Relation] the collection being filtered/sorted
|
|
8
|
+
attr_reader :collection
|
|
9
9
|
|
|
10
|
+
# @return [Hash] the request's filter params, keyed by filter parameter name
|
|
11
|
+
attr_reader :filter_params
|
|
12
|
+
|
|
13
|
+
# @return [Array<Toller::Filter,Toller::Sort>] every Filter and Sort registered on the
|
|
14
|
+
# including class's ancestor chain
|
|
15
|
+
attr_reader :retrievals
|
|
16
|
+
|
|
17
|
+
# @return [Array<String>] the request's sort params, e.g. ['-published_at', 'title']
|
|
18
|
+
attr_reader :sort_params
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Builds a Retriever and applies all active filters/sorts to +collection+.
|
|
22
|
+
#
|
|
23
|
+
# @param collection [ActiveRecord::Relation] the collection to filter/sort
|
|
24
|
+
# @param filter_params [Hash] the request's filter params
|
|
25
|
+
# @param sort_params [Array<String>] the request's sort params
|
|
26
|
+
# @param retrievals [Array<Toller::Filter,Toller::Sort>] every registered Filter and Sort
|
|
27
|
+
# @return [ActiveRecord::Relation] the filtered and sorted collection
|
|
10
28
|
def self.filter(collection, filter_params, sort_params, retrievals)
|
|
11
29
|
new(collection, filter_params, sort_params, retrievals).filter
|
|
12
30
|
end
|
|
13
31
|
|
|
32
|
+
##
|
|
33
|
+
# @param collection [ActiveRecord::Relation] the collection to filter/sort
|
|
34
|
+
# @param filter_params [Hash] the request's filter params
|
|
35
|
+
# @param sort_params [Array<String>] the request's sort params
|
|
36
|
+
# @param retrievals [Array<Toller::Filter,Toller::Sort>] every registered Filter and Sort
|
|
37
|
+
# @return [Toller::Retriever] a new instance of Retriever
|
|
14
38
|
def initialize(collection, filter_params, sort_params, retrievals)
|
|
15
39
|
@collection = collection
|
|
16
40
|
@filter_params = filter_params
|
|
@@ -18,6 +42,11 @@ module Toller
|
|
|
18
42
|
@retrievals = retrievals
|
|
19
43
|
end
|
|
20
44
|
|
|
45
|
+
##
|
|
46
|
+
# Reduces over the active filters/sorts, chaining each one's `apply!`
|
|
47
|
+
# onto the collection returned by the previous one.
|
|
48
|
+
#
|
|
49
|
+
# @return [ActiveRecord::Relation] the filtered and sorted collection
|
|
21
50
|
def filter
|
|
22
51
|
active_retrievals.reduce(collection) do |items, retrieval|
|
|
23
52
|
param_value = if retrieval.is_a?(Filter)
|
|
@@ -32,18 +61,26 @@ module Toller
|
|
|
32
61
|
|
|
33
62
|
private
|
|
34
63
|
|
|
64
|
+
##
|
|
65
|
+
# @return [Array<Toller::Filter,Toller::Sort>] the subset of +retrievals+ that are active for the current request
|
|
35
66
|
def active_retrievals
|
|
36
67
|
retrievals.select do |retrieval|
|
|
37
68
|
retrieval.is_a?(Filter) ? filtering_activated?(retrieval) : sorting_activated?(retrieval)
|
|
38
69
|
end
|
|
39
70
|
end
|
|
40
71
|
|
|
72
|
+
##
|
|
73
|
+
# @param retrieval [Toller::Filter] the filter to check
|
|
74
|
+
# @return [Boolean] whether +retrieval+ is active for the current request
|
|
41
75
|
def filtering_activated?(retrieval)
|
|
42
76
|
return true if filter_params.blank? && retrieval.default
|
|
43
77
|
|
|
44
78
|
filter_params.fetch(retrieval.parameter, nil).present?
|
|
45
79
|
end
|
|
46
80
|
|
|
81
|
+
##
|
|
82
|
+
# @param retrieval [Toller::Sort] the sort to check
|
|
83
|
+
# @return [Boolean] whether +retrieval+ is active for the current request
|
|
47
84
|
def sorting_activated?(retrieval)
|
|
48
85
|
return true if sort_params.blank? && retrieval.default
|
|
49
86
|
|
data/lib/toller/sort.rb
CHANGED
|
@@ -3,10 +3,24 @@
|
|
|
3
3
|
module Toller
|
|
4
4
|
##
|
|
5
5
|
# Sort
|
|
6
|
-
#
|
|
7
6
|
class Sort
|
|
8
|
-
|
|
7
|
+
# @return [Symbol] the public sort param name
|
|
8
|
+
attr_reader :parameter
|
|
9
9
|
|
|
10
|
+
# @return [Hash] the sort's resolved options (:field, :default, :scope_name, etc.)
|
|
11
|
+
attr_reader :properties
|
|
12
|
+
|
|
13
|
+
# @return [Symbol] the sort type, e.g. :string, :integer, :scope
|
|
14
|
+
attr_reader :type
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# @param parameter [Symbol] the public sort param name
|
|
18
|
+
# @param type [Symbol] the sort type, e.g. :string, :integer, :scope
|
|
19
|
+
# @param options [Hash] sort options; merged over defaults for :field, :default, and :scope_name
|
|
20
|
+
# @option options [Symbol] :field the column/attribute to sort on; defaults to +parameter+
|
|
21
|
+
# @option options [Boolean] :default whether this sort applies automatically when no sort params were sent
|
|
22
|
+
# @option options [Symbol] :scope_name for type: :scope, the model scope to call; defaults to +parameter+
|
|
23
|
+
# @return [Toller::Sort] a new instance of Sort
|
|
10
24
|
def initialize(parameter, type, options)
|
|
11
25
|
@parameter = parameter
|
|
12
26
|
@type = type
|
|
@@ -17,6 +31,13 @@ module Toller
|
|
|
17
31
|
)
|
|
18
32
|
end
|
|
19
33
|
|
|
34
|
+
##
|
|
35
|
+
# Applies this sort to +collection+, dispatching to the scope or
|
|
36
|
+
# order handler based on +type+.
|
|
37
|
+
#
|
|
38
|
+
# @param collection [ActiveRecord::Relation] the collection to sort
|
|
39
|
+
# @param direction [Symbol] the sort direction, :asc or :desc
|
|
40
|
+
# @return [ActiveRecord::Relation] the sorted collection
|
|
20
41
|
def apply!(collection, direction = :asc)
|
|
21
42
|
if type == :scope
|
|
22
43
|
Sorts::ScopeHandler.new.call(collection, direction, properties)
|
|
@@ -25,6 +46,8 @@ module Toller
|
|
|
25
46
|
end
|
|
26
47
|
end
|
|
27
48
|
|
|
49
|
+
##
|
|
50
|
+
# @return [Boolean] whether this sort applies automatically when no sort params were sent at all
|
|
28
51
|
def default
|
|
29
52
|
properties[:default]
|
|
30
53
|
end
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Toller
|
|
4
|
+
# :nodoc:
|
|
4
5
|
module Sorts
|
|
5
6
|
##
|
|
6
7
|
# Order handler for filter
|
|
7
|
-
#
|
|
8
8
|
class OrderHandler
|
|
9
|
+
##
|
|
10
|
+
# Applies a plain `order` clause to +collection+ for a non-scope sort.
|
|
11
|
+
#
|
|
12
|
+
# @param collection [ActiveRecord::Relation] the collection to sort
|
|
13
|
+
# @param direction [Symbol] the sort direction, :asc or :desc
|
|
14
|
+
# @param properties [Hash] the sort's properties, used to resolve `:field`
|
|
15
|
+
# @return [ActiveRecord::Relation] the sorted collection
|
|
9
16
|
def call(collection, direction, properties)
|
|
10
17
|
field_name = properties[:field]
|
|
11
18
|
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Toller
|
|
4
|
+
# :nodoc:
|
|
4
5
|
module Sorts
|
|
5
6
|
##
|
|
6
7
|
# Scope handler for sort
|
|
7
|
-
#
|
|
8
8
|
class ScopeHandler
|
|
9
|
+
##
|
|
10
|
+
# Applies a named scope to +collection+ for a `type: :scope` sort.
|
|
11
|
+
#
|
|
12
|
+
# @param collection [ActiveRecord::Relation] the collection to sort
|
|
13
|
+
# @param direction [Symbol] the sort direction, :asc or :desc, passed to the scope
|
|
14
|
+
# @param properties [Hash] the sort's properties, used to resolve `:scope_name` (falling back to `:field`)
|
|
15
|
+
# @return [ActiveRecord::Relation] the scoped collection
|
|
9
16
|
def call(collection, direction, properties)
|
|
10
17
|
scoped_name = properties[:scope_name] || properties[:field]
|
|
11
18
|
|
data/lib/toller/version.rb
CHANGED
data/lib/toller.rb
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require
|
|
10
|
-
require
|
|
11
|
-
require
|
|
12
|
-
require
|
|
13
|
-
require
|
|
14
|
-
require
|
|
3
|
+
require "toller/filter"
|
|
4
|
+
require "toller/filters/mutators/boolean"
|
|
5
|
+
require "toller/filters/mutators/date"
|
|
6
|
+
require "toller/filters/mutators/datetime"
|
|
7
|
+
require "toller/filters/mutators/integer"
|
|
8
|
+
require "toller/filters/mutators/time"
|
|
9
|
+
require "toller/filters/scope_handler"
|
|
10
|
+
require "toller/filters/where_handler"
|
|
11
|
+
require "toller/retriever"
|
|
12
|
+
require "toller/sort"
|
|
13
|
+
require "toller/sorts/order_handler"
|
|
14
|
+
require "toller/sorts/scope_handler"
|
|
15
15
|
|
|
16
16
|
##
|
|
17
17
|
# Toller
|
|
18
18
|
#
|
|
19
19
|
# Query param based filtering and sorting
|
|
20
|
-
#
|
|
21
20
|
module Toller
|
|
22
21
|
extend ActiveSupport::Concern
|
|
23
22
|
|
|
23
|
+
##
|
|
24
|
+
# Applies every active filter/sort for the current request to +collection+.
|
|
25
|
+
#
|
|
26
|
+
# @param collection [ActiveRecord::Relation] the collection to filter/sort
|
|
27
|
+
# @return [ActiveRecord::Relation] the filtered and sorted collection
|
|
24
28
|
def retrieve(collection)
|
|
25
29
|
Retriever.filter(collection, filter_params, sort_params, retrievals)
|
|
26
30
|
end
|
|
@@ -30,37 +34,73 @@ module Toller
|
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
class_methods do
|
|
37
|
+
##
|
|
38
|
+
# Declares a filter on the including controller.
|
|
39
|
+
#
|
|
40
|
+
# @param parameter [Symbol] the public filter param name
|
|
41
|
+
# @param type [Symbol] the filter type, e.g. :string, :integer, :scope
|
|
42
|
+
# @param options [Hash] filter options, e.g. :field, :scope_name, :default
|
|
43
|
+
# @return [Array<Toller::Filter,Toller::Sort>] the class's updated `_filters` list
|
|
33
44
|
def filter_on(parameter, type:, **options)
|
|
34
45
|
_filters << Filter.new(parameter, type, options)
|
|
35
46
|
end
|
|
36
47
|
|
|
48
|
+
##
|
|
49
|
+
# Declares a sort on the including controller.
|
|
50
|
+
#
|
|
51
|
+
# @param parameter [Symbol] the public sort param name
|
|
52
|
+
# @param type [Symbol] the sort type, e.g. :string, :integer, :scope
|
|
53
|
+
# @param options [Hash] sort options, e.g. :field, :scope_name, :default
|
|
54
|
+
# @return [Array<Toller::Filter,Toller::Sort>] the class's updated `_filters` list
|
|
37
55
|
def sort_on(parameter, type:, **options)
|
|
38
56
|
_filters << Sort.new(parameter, type, options)
|
|
39
57
|
end
|
|
40
58
|
|
|
59
|
+
##
|
|
60
|
+
# @return [Array<Toller::Filter,Toller::Sort>] the filters/sorts declared directly on this class
|
|
61
|
+
# via `filter_on`/`sort_on`
|
|
41
62
|
def _filters
|
|
42
63
|
@_filters ||= []
|
|
43
64
|
end
|
|
44
65
|
end
|
|
45
66
|
|
|
67
|
+
##
|
|
68
|
+
# @return [Hash] the current request's filter params, keyed by filter parameter name
|
|
46
69
|
def filter_params
|
|
47
70
|
params.fetch(filter_param_key.to_sym, {})
|
|
48
71
|
end
|
|
49
72
|
|
|
73
|
+
##
|
|
74
|
+
# @return [Array<String>] the current request's sort params, e.g. ['-published_at', 'title']
|
|
50
75
|
def sort_params
|
|
51
|
-
params.fetch(sort_param_key.to_sym,
|
|
76
|
+
params.fetch(sort_param_key.to_sym, "").split(",")
|
|
52
77
|
end
|
|
53
78
|
|
|
79
|
+
##
|
|
80
|
+
# Override in an including controller to change the query param Toller
|
|
81
|
+
# reads filters from.
|
|
82
|
+
#
|
|
83
|
+
# @return [Symbol] the query param key holding filter params
|
|
54
84
|
def filter_param_key
|
|
55
85
|
:filters
|
|
56
86
|
end
|
|
57
87
|
|
|
88
|
+
##
|
|
89
|
+
# Override in an including controller to change the query param Toller
|
|
90
|
+
# reads sorts from.
|
|
91
|
+
#
|
|
92
|
+
# @return [Symbol] the query param key holding sort params
|
|
58
93
|
def sort_param_key
|
|
59
94
|
:sort
|
|
60
95
|
end
|
|
61
96
|
|
|
62
97
|
private
|
|
63
98
|
|
|
99
|
+
##
|
|
100
|
+
# Retrievals
|
|
101
|
+
#
|
|
102
|
+
# @return [Array<Toller::Filter,Toller::Sort>] every Filter and Sort registered via `filter_on`/`sort_on` across the
|
|
103
|
+
# including class's ancestor chain
|
|
64
104
|
def retrievals
|
|
65
105
|
self.class.ancestors.flat_map { |klass| klass.try(:_filters) }.compact
|
|
66
106
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: toller
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Freerksen
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -16,29 +15,17 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '6.0'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
requirements:
|
|
31
|
-
- - "~>"
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: 2.3.0
|
|
34
|
-
type: :development
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - "~>"
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: 2.3.0
|
|
41
|
-
description: Description of Toller.
|
|
25
|
+
version: '6.0'
|
|
26
|
+
description: Toller is a Rails engine that lets controllers declare filter_on/sort_on
|
|
27
|
+
directives, then applies whichever filters and sorts are active for the current
|
|
28
|
+
request to an ActiveRecord relation based on URL query parameters.
|
|
42
29
|
email:
|
|
43
30
|
- dfreerksen@gmail.com
|
|
44
31
|
executables: []
|
|
@@ -65,8 +52,13 @@ files:
|
|
|
65
52
|
homepage: https://github.com/dfreerksen/toller
|
|
66
53
|
licenses:
|
|
67
54
|
- MIT
|
|
68
|
-
metadata:
|
|
69
|
-
|
|
55
|
+
metadata:
|
|
56
|
+
rubygems_mfa_required: 'true'
|
|
57
|
+
bug_tracker_uri: https://github.com/dfreerksen/toller/issues
|
|
58
|
+
documentation_uri: https://www.rubydoc.info/github/dfreerksen/toller/master
|
|
59
|
+
homepage_uri: https://github.com/dfreerksen/toller
|
|
60
|
+
source_code_uri: https://github.com/dfreerksen/toller
|
|
61
|
+
wiki_uri: https://github.com/dfreerksen/toller/wiki
|
|
70
62
|
rdoc_options: []
|
|
71
63
|
require_paths:
|
|
72
64
|
- lib
|
|
@@ -74,15 +66,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
74
66
|
requirements:
|
|
75
67
|
- - ">="
|
|
76
68
|
- !ruby/object:Gem::Version
|
|
77
|
-
version:
|
|
69
|
+
version: '3.0'
|
|
78
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
71
|
requirements:
|
|
80
72
|
- - ">="
|
|
81
73
|
- !ruby/object:Gem::Version
|
|
82
74
|
version: '0'
|
|
83
75
|
requirements: []
|
|
84
|
-
rubygems_version:
|
|
85
|
-
signing_key:
|
|
76
|
+
rubygems_version: 4.0.16
|
|
86
77
|
specification_version: 4
|
|
87
|
-
summary:
|
|
78
|
+
summary: URL-query-param-based filtering and sorting for Rails controllers.
|
|
88
79
|
test_files: []
|