toller 1.0.0 → 1.2.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/README.md +2 -2
- data/lib/toller/filter.rb +18 -1
- data/lib/toller/filters/{where_handler.rb → column_handler.rb} +33 -3
- data/lib/toller/filters/mutators/boolean.rb +1 -1
- data/lib/toller/filters/mutators/common/range.rb +59 -0
- data/lib/toller/filters/mutators/date.rb +1 -37
- data/lib/toller/filters/mutators/datetime.rb +1 -37
- data/lib/toller/filters/mutators/integer.rb +1 -45
- data/lib/toller/filters/mutators/time.rb +1 -37
- data/lib/toller/filters/scope_handler.rb +9 -1
- data/lib/toller/retriever.rb +14 -4
- data/lib/toller/scope_resolver.rb +25 -0
- data/lib/toller/sort.rb +18 -1
- data/lib/toller/sorts/column_handler.rb +56 -0
- data/lib/toller/sorts/scope_handler.rb +9 -1
- data/lib/toller/version.rb +1 -1
- data/lib/toller.rb +25 -4
- metadata +5 -3
- data/lib/toller/sorts/order_handler.rb +0 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 14ead1892ebc47f8a2dc0ee254545de4a013e7140d738654b9ad42dfd5c05799
|
|
4
|
+
data.tar.gz: 39fa2fa6acd9fed9e002b78f020e3b95ed89a920384481b24271b1898f955057
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71f8f591c8b0d40a0e67cacfb3e9953d4a47d9ad8a542f7327f05e67f6deec12c1a8580547a47acf5d9851c016cae0588e0f20d62c1ae93245d1c0ade14b8807
|
|
7
|
+
data.tar.gz: 128e6e2f34d0bf6381b57987d73d485eeeb70628c96aba9ce557ba251395e9cfc96c5b12bb6b23f1546c822642af46ca8260a6ea5b46390f2840a9090c970697
|
data/README.md
CHANGED
|
@@ -16,7 +16,7 @@ See the [wiki](https://github.com/dfreerksen/toller/wiki) for usage information.
|
|
|
16
16
|
Add this line to your application's Gemfile:
|
|
17
17
|
|
|
18
18
|
```ruby
|
|
19
|
-
gem 'toller', '~> 1.
|
|
19
|
+
gem 'toller', '~> 1.2'
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
And then execute:
|
|
@@ -64,7 +64,7 @@ To run tests with Appraisal, run
|
|
|
64
64
|
$ bundle exec appraisal rspec
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
Without any flags, Appraisal2 defaults to running 2 appraisals in parallel. The test database is in-memory (`database: ":memory:"`), so each appraisal subprocess gets its own isolated database and parallel runs don't conflict. Pass `-n 1` to force one Rails version at a time instead.
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
$ bundle exec appraisal rails-6-0 rspec
|
data/lib/toller/filter.rb
CHANGED
|
@@ -21,7 +21,10 @@ module Toller
|
|
|
21
21
|
# @option options [Boolean] :default whether this filter applies automatically when no filter params were sent
|
|
22
22
|
# @option options [Symbol] :scope_name for type: :scope, the model scope to call; defaults to +parameter+
|
|
23
23
|
# @return [Toller::Filter] a new instance of Filter
|
|
24
|
+
# @raise [ArgumentError] if +type+ isn't one of {Toller::VALID_TYPES}
|
|
24
25
|
def initialize(parameter, type, options)
|
|
26
|
+
validate_type!(parameter, type)
|
|
27
|
+
|
|
25
28
|
@parameter = parameter
|
|
26
29
|
@type = type
|
|
27
30
|
@properties = options.reverse_merge(
|
|
@@ -42,7 +45,7 @@ module Toller
|
|
|
42
45
|
if type == :scope
|
|
43
46
|
Filters::ScopeHandler.new.call(collection, value, properties)
|
|
44
47
|
else
|
|
45
|
-
Filters::
|
|
48
|
+
Filters::ColumnHandler.new.call(collection, type, value, properties)
|
|
46
49
|
end
|
|
47
50
|
end
|
|
48
51
|
|
|
@@ -51,5 +54,19 @@ module Toller
|
|
|
51
54
|
def default
|
|
52
55
|
properties[:default]
|
|
53
56
|
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# @param parameter [Symbol] the public filter param name, used in the error message
|
|
62
|
+
# @param type [Symbol] the filter type to validate
|
|
63
|
+
# @return [nil]
|
|
64
|
+
# @raise [ArgumentError] if +type+ isn't one of {Toller::VALID_TYPES}
|
|
65
|
+
def validate_type!(parameter, type)
|
|
66
|
+
return if Toller::VALID_TYPES.include?(type)
|
|
67
|
+
|
|
68
|
+
raise ArgumentError, "Toller: unknown type `#{type.inspect}` for filter `#{parameter}` " \
|
|
69
|
+
"(expected one of #{Toller::VALID_TYPES.join(', ')})"
|
|
70
|
+
end
|
|
54
71
|
end
|
|
55
72
|
end
|
|
@@ -4,19 +4,26 @@ module Toller
|
|
|
4
4
|
# :nodoc:
|
|
5
5
|
module Filters
|
|
6
6
|
##
|
|
7
|
-
#
|
|
8
|
-
class
|
|
7
|
+
# Column handler for filter
|
|
8
|
+
class ColumnHandler
|
|
9
9
|
##
|
|
10
10
|
# Applies a plain `where` clause to +collection+ for a non-scope filter.
|
|
11
11
|
#
|
|
12
|
+
# If +field+ isn't a real column on the collection's model, or the column's actual type
|
|
13
|
+
# doesn't match the declared `type:`, the filter is logged and skipped instead of raising
|
|
14
|
+
# (or silently applying a mismatched mutator) once the relation is evaluated.
|
|
15
|
+
#
|
|
12
16
|
# @param collection [ActiveRecord::Relation] the collection to filter
|
|
13
17
|
# @param type [Symbol] the filter type (e.g. :string, :integer, :boolean)
|
|
14
18
|
# @param value [Object] the raw filter param value
|
|
15
19
|
# @param properties [Hash] the filter's properties, used to resolve `:field`
|
|
16
|
-
# @return [ActiveRecord::Relation] the filtered collection
|
|
20
|
+
# @return [ActiveRecord::Relation] the filtered collection, or +collection+ unchanged if `:field` is
|
|
21
|
+
# unknown or its actual column type doesn't match `type`
|
|
17
22
|
def call(collection, type, value, properties)
|
|
18
23
|
field_name = properties[:field]
|
|
19
24
|
|
|
25
|
+
return collection unless resolvable_column?(collection, field_name, type)
|
|
26
|
+
|
|
20
27
|
mutated_value = value_mutator(type, value)
|
|
21
28
|
|
|
22
29
|
collection.where(field_name => mutated_value)
|
|
@@ -24,6 +31,29 @@ module Toller
|
|
|
24
31
|
|
|
25
32
|
private
|
|
26
33
|
|
|
34
|
+
##
|
|
35
|
+
# Confirms +field_name+ is a real column on +collection+'s model whose actual type matches
|
|
36
|
+
# +type+, logging a warning and returning false otherwise.
|
|
37
|
+
#
|
|
38
|
+
# @param collection [ActiveRecord::Relation] the collection to filter
|
|
39
|
+
# @param field_name [Symbol] the column to look up
|
|
40
|
+
# @param type [Symbol] the filter's declared type
|
|
41
|
+
# @return [Boolean] whether +field_name+ exists and its actual column type matches +type+
|
|
42
|
+
def resolvable_column?(collection, field_name, type)
|
|
43
|
+
unless collection.klass.column_names.include?(field_name.to_s)
|
|
44
|
+
Rails.logger.warn("[Toller] Skipping filter: #{collection.klass} has no column `#{field_name}`")
|
|
45
|
+
return false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
actual_type = collection.klass.columns_hash[field_name.to_s].type
|
|
49
|
+
return true if actual_type == type
|
|
50
|
+
|
|
51
|
+
Rails.logger.warn(
|
|
52
|
+
"[Toller] Skipping filter: #{collection.klass}##{field_name} is `#{actual_type}`, declared as `#{type}`"
|
|
53
|
+
)
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
|
|
27
57
|
##
|
|
28
58
|
# Value mutator
|
|
29
59
|
#
|
|
@@ -16,7 +16,7 @@ module Toller
|
|
|
16
16
|
# @param value [String] the raw filter param value
|
|
17
17
|
# @return [Boolean] true if +value+ is one of "1", "t", "true", "y", "yes"; false otherwise
|
|
18
18
|
def call(value)
|
|
19
|
-
%w[1 t true y yes].include?(value)
|
|
19
|
+
%w[1 t true y yes].include?(value.to_s.downcase)
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toller
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module Filters
|
|
6
|
+
# :nodoc:
|
|
7
|
+
module Mutators
|
|
8
|
+
# :nodoc:
|
|
9
|
+
module Common
|
|
10
|
+
##
|
|
11
|
+
# Shared range-parsing behavior for filter mutators. Extend this module to get a
|
|
12
|
+
# `call(value)` that returns +value+ unchanged, or a Range when +value+ contains
|
|
13
|
+
# Ruby range syntax (`..` for inclusive, `...` for exclusive).
|
|
14
|
+
#
|
|
15
|
+
# Methods here must refer to Ruby's core Range class as `::Range` - a bare
|
|
16
|
+
# `Range` would resolve to this module (Mutators::Common::Range) instead, since
|
|
17
|
+
# Ruby's constant lookup checks the lexical scope before the top level.
|
|
18
|
+
module Range
|
|
19
|
+
##
|
|
20
|
+
# Coerces a raw filter param into its original value or a Range.
|
|
21
|
+
#
|
|
22
|
+
# @param value [String] the raw filter param value
|
|
23
|
+
# @return [String,::Range] the original value, or a Range when the value contains range syntax
|
|
24
|
+
# (`..` or `...`)
|
|
25
|
+
def call(value)
|
|
26
|
+
range_dots = inclusive_or_exclusive_range(value)
|
|
27
|
+
|
|
28
|
+
return value if range_dots.blank?
|
|
29
|
+
|
|
30
|
+
range(value, range_dots)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# Builds a Range by splitting +value+ on the given range dots.
|
|
35
|
+
#
|
|
36
|
+
# @param value [String] the raw range string, e.g. "1..10"
|
|
37
|
+
# @param dots [String] the range separator, either ".." or "..."
|
|
38
|
+
# @return [::Range] the resulting range, exclusive when +dots+ is "..."
|
|
39
|
+
def range(value, dots)
|
|
40
|
+
::Range.new(*value.split(dots), dots == "...")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# Detects whether +value+ contains inclusive or exclusive range syntax.
|
|
45
|
+
#
|
|
46
|
+
# @param value [String] the raw filter param value
|
|
47
|
+
# @return [String,nil] "..." for an exclusive range, ".." for an inclusive range, or nil if +value+ is not
|
|
48
|
+
# a range
|
|
49
|
+
def inclusive_or_exclusive_range(value)
|
|
50
|
+
return "..." if value.include?("...")
|
|
51
|
+
return ".." if value.include?("..")
|
|
52
|
+
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -8,43 +8,7 @@ module Toller
|
|
|
8
8
|
##
|
|
9
9
|
# Date filter mutator
|
|
10
10
|
module Date
|
|
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 `...`)
|
|
18
|
-
def call(value)
|
|
19
|
-
range_dots = inclusive_or_exclusive_range(value)
|
|
20
|
-
|
|
21
|
-
return value if range_dots.blank?
|
|
22
|
-
|
|
23
|
-
range(value, range_dots)
|
|
24
|
-
end
|
|
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
|
|
32
|
-
def range(value, dots)
|
|
33
|
-
Range.new(*value.split(dots))
|
|
34
|
-
end
|
|
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
|
|
42
|
-
def inclusive_or_exclusive_range(value)
|
|
43
|
-
return "..." if value.include?("...")
|
|
44
|
-
return ".." if value.include?("..")
|
|
45
|
-
|
|
46
|
-
nil
|
|
47
|
-
end
|
|
11
|
+
extend Common::Range
|
|
48
12
|
end
|
|
49
13
|
end
|
|
50
14
|
end
|
|
@@ -8,43 +8,7 @@ module Toller
|
|
|
8
8
|
##
|
|
9
9
|
# Datetime filter mutator
|
|
10
10
|
module Datetime
|
|
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 `...`)
|
|
18
|
-
def call(value)
|
|
19
|
-
range_dots = inclusive_or_exclusive_range(value)
|
|
20
|
-
|
|
21
|
-
return value if range_dots.blank?
|
|
22
|
-
|
|
23
|
-
range(value, range_dots)
|
|
24
|
-
end
|
|
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
|
|
32
|
-
def range(value, dots)
|
|
33
|
-
Range.new(*value.split(dots))
|
|
34
|
-
end
|
|
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
|
|
42
|
-
def inclusive_or_exclusive_range(value)
|
|
43
|
-
return "..." if value.include?("...")
|
|
44
|
-
return ".." if value.include?("..")
|
|
45
|
-
|
|
46
|
-
nil
|
|
47
|
-
end
|
|
11
|
+
extend Common::Range
|
|
48
12
|
end
|
|
49
13
|
end
|
|
50
14
|
end
|
|
@@ -8,51 +8,7 @@ module Toller
|
|
|
8
8
|
##
|
|
9
9
|
# Integer filter mutator
|
|
10
10
|
module Integer
|
|
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 `...`)
|
|
18
|
-
def call(value)
|
|
19
|
-
return value unless range?(value)
|
|
20
|
-
|
|
21
|
-
range(value)
|
|
22
|
-
end
|
|
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 `...`
|
|
29
|
-
def range?(value)
|
|
30
|
-
range_dots = inclusive_or_exclusive_range(value)
|
|
31
|
-
|
|
32
|
-
range_dots.present?
|
|
33
|
-
end
|
|
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
|
|
40
|
-
def range(value)
|
|
41
|
-
Range.new(*value.split(inclusive_or_exclusive_range(value)))
|
|
42
|
-
end
|
|
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
|
|
50
|
-
def inclusive_or_exclusive_range(value)
|
|
51
|
-
return "..." if value.include?("...")
|
|
52
|
-
return ".." if value.include?("..")
|
|
53
|
-
|
|
54
|
-
nil
|
|
55
|
-
end
|
|
11
|
+
extend Common::Range
|
|
56
12
|
end
|
|
57
13
|
end
|
|
58
14
|
end
|
|
@@ -8,43 +8,7 @@ module Toller
|
|
|
8
8
|
##
|
|
9
9
|
# Time filter mutator
|
|
10
10
|
module Time
|
|
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 `...`)
|
|
18
|
-
def call(value)
|
|
19
|
-
range_dots = inclusive_or_exclusive_range(value)
|
|
20
|
-
|
|
21
|
-
return value if range_dots.blank?
|
|
22
|
-
|
|
23
|
-
range(value, range_dots)
|
|
24
|
-
end
|
|
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
|
|
32
|
-
def range(value, dots)
|
|
33
|
-
Range.new(*value.split(dots))
|
|
34
|
-
end
|
|
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
|
|
42
|
-
def inclusive_or_exclusive_range(value)
|
|
43
|
-
return "..." if value.include?("...")
|
|
44
|
-
return ".." if value.include?("..")
|
|
45
|
-
|
|
46
|
-
nil
|
|
47
|
-
end
|
|
11
|
+
extend Common::Range
|
|
48
12
|
end
|
|
49
13
|
end
|
|
50
14
|
end
|
|
@@ -9,13 +9,21 @@ module Toller
|
|
|
9
9
|
##
|
|
10
10
|
# Applies a named scope to +collection+ for a `type: :scope` filter.
|
|
11
11
|
#
|
|
12
|
+
# If the resolved scope doesn't exist on the collection's model, the
|
|
13
|
+
# filter is logged and skipped instead of raising a NoMethodError.
|
|
14
|
+
#
|
|
12
15
|
# @param collection [ActiveRecord::Relation] the collection to filter
|
|
13
16
|
# @param value [Object] the filter param value to pass to the scope
|
|
14
17
|
# @param properties [Hash] the filter's properties, used to resolve `:scope_name` (falling back to `:field`)
|
|
15
|
-
# @return [ActiveRecord::Relation] the scoped collection
|
|
18
|
+
# @return [ActiveRecord::Relation] the scoped collection, or +collection+ unchanged if the scope is unknown
|
|
16
19
|
def call(collection, value, properties)
|
|
17
20
|
scoped_name = properties[:scope_name] || properties[:field]
|
|
18
21
|
|
|
22
|
+
unless ScopeResolver.own_class_method?(collection.klass, scoped_name)
|
|
23
|
+
Rails.logger.warn("[Toller] Skipping filter: #{collection.klass} has no scope `#{scoped_name}`")
|
|
24
|
+
return collection
|
|
25
|
+
end
|
|
26
|
+
|
|
19
27
|
collection.public_send(scoped_name, value)
|
|
20
28
|
end
|
|
21
29
|
end
|
data/lib/toller/retriever.rb
CHANGED
|
@@ -50,9 +50,9 @@ module Toller
|
|
|
50
50
|
def filter
|
|
51
51
|
active_retrievals.reduce(collection) do |items, retrieval|
|
|
52
52
|
param_value = if retrieval.is_a?(Filter)
|
|
53
|
-
filter_params.fetch(retrieval
|
|
53
|
+
filter_params.fetch(downcased_parameter(retrieval), nil)
|
|
54
54
|
else
|
|
55
|
-
sort_params.include?("-#{retrieval
|
|
55
|
+
sort_params.include?("-#{downcased_parameter(retrieval)}") ? :desc : :asc
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
retrieval.apply!(items, param_value)
|
|
@@ -75,7 +75,7 @@ module Toller
|
|
|
75
75
|
def filtering_activated?(retrieval)
|
|
76
76
|
return true if filter_params.blank? && retrieval.default
|
|
77
77
|
|
|
78
|
-
filter_params.fetch(retrieval
|
|
78
|
+
filter_params.fetch(downcased_parameter(retrieval), nil).present?
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
##
|
|
@@ -84,9 +84,19 @@ module Toller
|
|
|
84
84
|
def sorting_activated?(retrieval)
|
|
85
85
|
return true if sort_params.blank? && retrieval.default
|
|
86
86
|
|
|
87
|
-
string_parameter = retrieval
|
|
87
|
+
string_parameter = downcased_parameter(retrieval)
|
|
88
88
|
|
|
89
89
|
sort_params.include?(string_parameter) || sort_params.include?("-#{string_parameter}")
|
|
90
90
|
end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# `filter_params`/`sort_params` are downcased at the request level, so `retrieval.parameter`
|
|
94
|
+
# (declared as-is by `filter_on`/`sort_on`) must be downcased the same way before comparison.
|
|
95
|
+
#
|
|
96
|
+
# @param retrieval [Toller::Filter,Toller::Sort] the filter/sort to check
|
|
97
|
+
# @return [String] +retrieval.parameter+ downcased
|
|
98
|
+
def downcased_parameter(retrieval)
|
|
99
|
+
retrieval.parameter.to_s.downcase
|
|
100
|
+
end
|
|
91
101
|
end
|
|
92
102
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toller
|
|
4
|
+
##
|
|
5
|
+
# Shared lookup used by the `type: :scope` filter/sort handlers to confirm a resolved
|
|
6
|
+
# `scope_name`/`field` is actually declared on the model (a real `scope`, or a plain
|
|
7
|
+
# `def self.foo`), rather than a method inherited from ActiveRecord itself. Without this,
|
|
8
|
+
# a typo'd `scope_name:` that happens to collide with a built-in ActiveRecord class method
|
|
9
|
+
# (`delete_all`, `where`, `sum`, etc.) would silently `public_send` it with the raw request
|
|
10
|
+
# value instead of being treated as an unresolvable scope.
|
|
11
|
+
module ScopeResolver
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# @param klass [Class] the ActiveRecord model class to check
|
|
16
|
+
# @param name [Symbol,String] the method name to look for
|
|
17
|
+
# @return [Boolean] whether +name+ is defined by +klass+ or one of its own ancestors,
|
|
18
|
+
# rather than inherited from ActiveRecord::Base itself
|
|
19
|
+
def own_class_method?(klass, name)
|
|
20
|
+
klass.singleton_class.ancestors
|
|
21
|
+
.take_while { |mod| mod != ActiveRecord::Base.singleton_class }
|
|
22
|
+
.any? { |mod| mod.public_method_defined?(name.to_sym, false) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/toller/sort.rb
CHANGED
|
@@ -21,7 +21,10 @@ module Toller
|
|
|
21
21
|
# @option options [Boolean] :default whether this sort applies automatically when no sort params were sent
|
|
22
22
|
# @option options [Symbol] :scope_name for type: :scope, the model scope to call; defaults to +parameter+
|
|
23
23
|
# @return [Toller::Sort] a new instance of Sort
|
|
24
|
+
# @raise [ArgumentError] if +type+ isn't one of {Toller::VALID_TYPES}
|
|
24
25
|
def initialize(parameter, type, options)
|
|
26
|
+
validate_type!(parameter, type)
|
|
27
|
+
|
|
25
28
|
@parameter = parameter
|
|
26
29
|
@type = type
|
|
27
30
|
@properties = options.reverse_merge(
|
|
@@ -42,7 +45,7 @@ module Toller
|
|
|
42
45
|
if type == :scope
|
|
43
46
|
Sorts::ScopeHandler.new.call(collection, direction, properties)
|
|
44
47
|
else
|
|
45
|
-
Sorts::
|
|
48
|
+
Sorts::ColumnHandler.new.call(collection, type, direction, properties)
|
|
46
49
|
end
|
|
47
50
|
end
|
|
48
51
|
|
|
@@ -51,5 +54,19 @@ module Toller
|
|
|
51
54
|
def default
|
|
52
55
|
properties[:default]
|
|
53
56
|
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# @param parameter [Symbol] the public sort param name, used in the error message
|
|
62
|
+
# @param type [Symbol] the sort type to validate
|
|
63
|
+
# @return [nil]
|
|
64
|
+
# @raise [ArgumentError] if +type+ isn't one of {Toller::VALID_TYPES}
|
|
65
|
+
def validate_type!(parameter, type)
|
|
66
|
+
return if Toller::VALID_TYPES.include?(type)
|
|
67
|
+
|
|
68
|
+
raise ArgumentError, "Toller: unknown type `#{type.inspect}` for sort `#{parameter}` " \
|
|
69
|
+
"(expected one of #{Toller::VALID_TYPES.join(', ')})"
|
|
70
|
+
end
|
|
54
71
|
end
|
|
55
72
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toller
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module Sorts
|
|
6
|
+
##
|
|
7
|
+
# Column handler for filter
|
|
8
|
+
class ColumnHandler
|
|
9
|
+
##
|
|
10
|
+
# Applies a plain `order` clause to +collection+ for a non-scope sort.
|
|
11
|
+
#
|
|
12
|
+
# If +field+ isn't a real column on the collection's model, or the column's actual type
|
|
13
|
+
# doesn't match the declared `type:`, the sort is logged and skipped instead of raising
|
|
14
|
+
# once the relation is evaluated.
|
|
15
|
+
#
|
|
16
|
+
# @param collection [ActiveRecord::Relation] the collection to sort
|
|
17
|
+
# @param type [Symbol] the sort type (e.g. :string, :integer, :boolean)
|
|
18
|
+
# @param direction [Symbol] the sort direction, :asc or :desc
|
|
19
|
+
# @param properties [Hash] the sort's properties, used to resolve `:field`
|
|
20
|
+
# @return [ActiveRecord::Relation] the sorted collection, or +collection+ unchanged if `:field` is
|
|
21
|
+
# unknown or its actual column type doesn't match `type`
|
|
22
|
+
def call(collection, type, direction, properties)
|
|
23
|
+
field_name = properties[:field]
|
|
24
|
+
|
|
25
|
+
return collection unless resolvable_column?(collection, field_name, type)
|
|
26
|
+
|
|
27
|
+
collection.order(field_name => direction)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Confirms +field_name+ is a real column on +collection+'s model whose actual type matches
|
|
34
|
+
# +type+, logging a warning and returning false otherwise.
|
|
35
|
+
#
|
|
36
|
+
# @param collection [ActiveRecord::Relation] the collection to sort
|
|
37
|
+
# @param field_name [Symbol] the column to look up
|
|
38
|
+
# @param type [Symbol] the sort's declared type
|
|
39
|
+
# @return [Boolean] whether +field_name+ exists and its actual column type matches +type+
|
|
40
|
+
def resolvable_column?(collection, field_name, type)
|
|
41
|
+
unless collection.klass.column_names.include?(field_name.to_s)
|
|
42
|
+
Rails.logger.warn("[Toller] Skipping sort: #{collection.klass} has no column `#{field_name}`")
|
|
43
|
+
return false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
actual_type = collection.klass.columns_hash[field_name.to_s].type
|
|
47
|
+
return true if actual_type == type
|
|
48
|
+
|
|
49
|
+
Rails.logger.warn(
|
|
50
|
+
"[Toller] Skipping sort: #{collection.klass}##{field_name} is `#{actual_type}`, declared as `#{type}`"
|
|
51
|
+
)
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -9,13 +9,21 @@ module Toller
|
|
|
9
9
|
##
|
|
10
10
|
# Applies a named scope to +collection+ for a `type: :scope` sort.
|
|
11
11
|
#
|
|
12
|
+
# If the resolved scope doesn't exist on the collection's model, the
|
|
13
|
+
# sort is logged and skipped instead of raising a NoMethodError.
|
|
14
|
+
#
|
|
12
15
|
# @param collection [ActiveRecord::Relation] the collection to sort
|
|
13
16
|
# @param direction [Symbol] the sort direction, :asc or :desc, passed to the scope
|
|
14
17
|
# @param properties [Hash] the sort's properties, used to resolve `:scope_name` (falling back to `:field`)
|
|
15
|
-
# @return [ActiveRecord::Relation] the scoped collection
|
|
18
|
+
# @return [ActiveRecord::Relation] the scoped collection, or +collection+ unchanged if the scope is unknown
|
|
16
19
|
def call(collection, direction, properties)
|
|
17
20
|
scoped_name = properties[:scope_name] || properties[:field]
|
|
18
21
|
|
|
22
|
+
unless ScopeResolver.own_class_method?(collection.klass, scoped_name)
|
|
23
|
+
Rails.logger.warn("[Toller] Skipping sort: #{collection.klass} has no scope `#{scoped_name}`")
|
|
24
|
+
return collection
|
|
25
|
+
end
|
|
26
|
+
|
|
19
27
|
collection.public_send(scoped_name, direction)
|
|
20
28
|
end
|
|
21
29
|
end
|
data/lib/toller/version.rb
CHANGED
data/lib/toller.rb
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "toller/filter"
|
|
4
|
+
require "toller/filters/column_handler"
|
|
4
5
|
require "toller/filters/mutators/boolean"
|
|
6
|
+
require "toller/filters/mutators/common/range"
|
|
5
7
|
require "toller/filters/mutators/date"
|
|
6
8
|
require "toller/filters/mutators/datetime"
|
|
7
9
|
require "toller/filters/mutators/integer"
|
|
8
10
|
require "toller/filters/mutators/time"
|
|
9
11
|
require "toller/filters/scope_handler"
|
|
10
|
-
require "toller/filters/where_handler"
|
|
11
12
|
require "toller/retriever"
|
|
13
|
+
require "toller/scope_resolver"
|
|
12
14
|
require "toller/sort"
|
|
13
|
-
require "toller/sorts/
|
|
15
|
+
require "toller/sorts/column_handler"
|
|
14
16
|
require "toller/sorts/scope_handler"
|
|
15
17
|
|
|
16
18
|
##
|
|
@@ -20,6 +22,21 @@ require "toller/sorts/scope_handler"
|
|
|
20
22
|
module Toller
|
|
21
23
|
extend ActiveSupport::Concern
|
|
22
24
|
|
|
25
|
+
# @return [Array<Symbol>] the types `filter_on`/`sort_on` recognize for their `type:` option
|
|
26
|
+
VALID_TYPES = %i[string text integer boolean date datetime time scope].freeze
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# Coerces a raw string value into a boolean, using the same rule Toller's
|
|
30
|
+
# own `type: :boolean` filters use internally. Handy inside a model scope
|
|
31
|
+
# backing a `type: :scope` filter, which receives the raw param value
|
|
32
|
+
# unmutated.
|
|
33
|
+
#
|
|
34
|
+
# @param value [String] the raw value
|
|
35
|
+
# @return [Boolean] true if +value+ is one of "1", "t", "true", "y", "yes"; false otherwise
|
|
36
|
+
def self.truthy(value)
|
|
37
|
+
Filters::Mutators::Boolean.call(value)
|
|
38
|
+
end
|
|
39
|
+
|
|
23
40
|
##
|
|
24
41
|
# Applies every active filter/sort for the current request to +collection+.
|
|
25
42
|
#
|
|
@@ -65,15 +82,19 @@ module Toller
|
|
|
65
82
|
end
|
|
66
83
|
|
|
67
84
|
##
|
|
85
|
+
# Filter params
|
|
86
|
+
#
|
|
68
87
|
# @return [Hash] the current request's filter params, keyed by filter parameter name
|
|
69
88
|
def filter_params
|
|
70
|
-
params.fetch(filter_param_key.to_sym, {})
|
|
89
|
+
params.fetch(filter_param_key.to_sym, {}).transform_keys { |key| key.to_s.strip.downcase }
|
|
71
90
|
end
|
|
72
91
|
|
|
73
92
|
##
|
|
93
|
+
# Sort param split
|
|
94
|
+
#
|
|
74
95
|
# @return [Array<String>] the current request's sort params, e.g. ['-published_at', 'title']
|
|
75
96
|
def sort_params
|
|
76
|
-
params.fetch(sort_param_key.to_sym, "").split(",")
|
|
97
|
+
params.fetch(sort_param_key.to_sym, "").split(",").map { |param| param.strip.downcase }.reject(&:empty?)
|
|
77
98
|
end
|
|
78
99
|
|
|
79
100
|
##
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: toller
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Freerksen
|
|
@@ -37,16 +37,18 @@ files:
|
|
|
37
37
|
- Rakefile
|
|
38
38
|
- lib/toller.rb
|
|
39
39
|
- lib/toller/filter.rb
|
|
40
|
+
- lib/toller/filters/column_handler.rb
|
|
40
41
|
- lib/toller/filters/mutators/boolean.rb
|
|
42
|
+
- lib/toller/filters/mutators/common/range.rb
|
|
41
43
|
- lib/toller/filters/mutators/date.rb
|
|
42
44
|
- lib/toller/filters/mutators/datetime.rb
|
|
43
45
|
- lib/toller/filters/mutators/integer.rb
|
|
44
46
|
- lib/toller/filters/mutators/time.rb
|
|
45
47
|
- lib/toller/filters/scope_handler.rb
|
|
46
|
-
- lib/toller/filters/where_handler.rb
|
|
47
48
|
- lib/toller/retriever.rb
|
|
49
|
+
- lib/toller/scope_resolver.rb
|
|
48
50
|
- lib/toller/sort.rb
|
|
49
|
-
- lib/toller/sorts/
|
|
51
|
+
- lib/toller/sorts/column_handler.rb
|
|
50
52
|
- lib/toller/sorts/scope_handler.rb
|
|
51
53
|
- lib/toller/version.rb
|
|
52
54
|
homepage: https://github.com/dfreerksen/toller
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Toller
|
|
4
|
-
# :nodoc:
|
|
5
|
-
module Sorts
|
|
6
|
-
##
|
|
7
|
-
# Order handler for filter
|
|
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
|
|
16
|
-
def call(collection, direction, properties)
|
|
17
|
-
field_name = properties[:field]
|
|
18
|
-
|
|
19
|
-
collection.order(field_name => direction)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|