toller 1.1.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 +17 -0
- data/lib/toller/filters/column_handler.rb +29 -7
- data/lib/toller/filters/mutators/common/range.rb +1 -1
- data/lib/toller/filters/scope_handler.rb +1 -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 +31 -6
- data/lib/toller/sorts/scope_handler.rb +1 -1
- data/lib/toller/version.rb +1 -1
- data/lib/toller.rb +10 -2
- metadata +2 -1
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(
|
|
@@ -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
|
|
@@ -9,21 +9,20 @@ module Toller
|
|
|
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, the
|
|
13
|
-
# logged and skipped instead of raising
|
|
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.
|
|
14
15
|
#
|
|
15
16
|
# @param collection [ActiveRecord::Relation] the collection to filter
|
|
16
17
|
# @param type [Symbol] the filter type (e.g. :string, :integer, :boolean)
|
|
17
18
|
# @param value [Object] the raw filter param value
|
|
18
19
|
# @param properties [Hash] the filter's properties, used to resolve `:field`
|
|
19
|
-
# @return [ActiveRecord::Relation] the filtered collection, or +collection+ unchanged if `:field` is
|
|
20
|
+
# @return [ActiveRecord::Relation] the filtered collection, or +collection+ unchanged if `:field` is
|
|
21
|
+
# unknown or its actual column type doesn't match `type`
|
|
20
22
|
def call(collection, type, value, properties)
|
|
21
23
|
field_name = properties[:field]
|
|
22
24
|
|
|
23
|
-
unless
|
|
24
|
-
Rails.logger.warn("[Toller] Skipping filter: #{collection.klass} has no column `#{field_name}`")
|
|
25
|
-
return collection
|
|
26
|
-
end
|
|
25
|
+
return collection unless resolvable_column?(collection, field_name, type)
|
|
27
26
|
|
|
28
27
|
mutated_value = value_mutator(type, value)
|
|
29
28
|
|
|
@@ -32,6 +31,29 @@ module Toller
|
|
|
32
31
|
|
|
33
32
|
private
|
|
34
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
|
+
|
|
35
57
|
##
|
|
36
58
|
# Value mutator
|
|
37
59
|
#
|
|
@@ -12,7 +12,7 @@ module Toller
|
|
|
12
12
|
# `call(value)` that returns +value+ unchanged, or a Range when +value+ contains
|
|
13
13
|
# Ruby range syntax (`..` for inclusive, `...` for exclusive).
|
|
14
14
|
#
|
|
15
|
-
#
|
|
15
|
+
# Methods here must refer to Ruby's core Range class as `::Range` - a bare
|
|
16
16
|
# `Range` would resolve to this module (Mutators::Common::Range) instead, since
|
|
17
17
|
# Ruby's constant lookup checks the lexical scope before the top level.
|
|
18
18
|
module Range
|
|
@@ -19,7 +19,7 @@ module Toller
|
|
|
19
19
|
def call(collection, value, properties)
|
|
20
20
|
scoped_name = properties[:scope_name] || properties[:field]
|
|
21
21
|
|
|
22
|
-
unless collection.klass
|
|
22
|
+
unless ScopeResolver.own_class_method?(collection.klass, scoped_name)
|
|
23
23
|
Rails.logger.warn("[Toller] Skipping filter: #{collection.klass} has no scope `#{scoped_name}`")
|
|
24
24
|
return collection
|
|
25
25
|
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::ColumnHandler.new.call(collection, direction, properties)
|
|
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
|
|
@@ -9,22 +9,47 @@ module Toller
|
|
|
9
9
|
##
|
|
10
10
|
# Applies a plain `order` clause to +collection+ for a non-scope sort.
|
|
11
11
|
#
|
|
12
|
-
# If +field+ isn't a real column on the collection's model, the
|
|
13
|
-
# logged and skipped instead of raising
|
|
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.
|
|
14
15
|
#
|
|
15
16
|
# @param collection [ActiveRecord::Relation] the collection to sort
|
|
17
|
+
# @param type [Symbol] the sort type (e.g. :string, :integer, :boolean)
|
|
16
18
|
# @param direction [Symbol] the sort direction, :asc or :desc
|
|
17
19
|
# @param properties [Hash] the sort's properties, used to resolve `:field`
|
|
18
|
-
# @return [ActiveRecord::Relation] the sorted collection, or +collection+ unchanged if `:field` is
|
|
19
|
-
|
|
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)
|
|
20
23
|
field_name = properties[:field]
|
|
21
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)
|
|
22
41
|
unless collection.klass.column_names.include?(field_name.to_s)
|
|
23
42
|
Rails.logger.warn("[Toller] Skipping sort: #{collection.klass} has no column `#{field_name}`")
|
|
24
|
-
return
|
|
43
|
+
return false
|
|
25
44
|
end
|
|
26
45
|
|
|
27
|
-
collection.
|
|
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
|
|
28
53
|
end
|
|
29
54
|
end
|
|
30
55
|
end
|
|
@@ -19,7 +19,7 @@ module Toller
|
|
|
19
19
|
def call(collection, direction, properties)
|
|
20
20
|
scoped_name = properties[:scope_name] || properties[:field]
|
|
21
21
|
|
|
22
|
-
unless collection.klass
|
|
22
|
+
unless ScopeResolver.own_class_method?(collection.klass, scoped_name)
|
|
23
23
|
Rails.logger.warn("[Toller] Skipping sort: #{collection.klass} has no scope `#{scoped_name}`")
|
|
24
24
|
return collection
|
|
25
25
|
end
|
data/lib/toller/version.rb
CHANGED
data/lib/toller.rb
CHANGED
|
@@ -10,6 +10,7 @@ require "toller/filters/mutators/integer"
|
|
|
10
10
|
require "toller/filters/mutators/time"
|
|
11
11
|
require "toller/filters/scope_handler"
|
|
12
12
|
require "toller/retriever"
|
|
13
|
+
require "toller/scope_resolver"
|
|
13
14
|
require "toller/sort"
|
|
14
15
|
require "toller/sorts/column_handler"
|
|
15
16
|
require "toller/sorts/scope_handler"
|
|
@@ -21,6 +22,9 @@ require "toller/sorts/scope_handler"
|
|
|
21
22
|
module Toller
|
|
22
23
|
extend ActiveSupport::Concern
|
|
23
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
|
+
|
|
24
28
|
##
|
|
25
29
|
# Coerces a raw string value into a boolean, using the same rule Toller's
|
|
26
30
|
# own `type: :boolean` filters use internally. Handy inside a model scope
|
|
@@ -78,15 +82,19 @@ module Toller
|
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
##
|
|
85
|
+
# Filter params
|
|
86
|
+
#
|
|
81
87
|
# @return [Hash] the current request's filter params, keyed by filter parameter name
|
|
82
88
|
def filter_params
|
|
83
|
-
params.fetch(filter_param_key.to_sym, {})
|
|
89
|
+
params.fetch(filter_param_key.to_sym, {}).transform_keys { |key| key.to_s.strip.downcase }
|
|
84
90
|
end
|
|
85
91
|
|
|
86
92
|
##
|
|
93
|
+
# Sort param split
|
|
94
|
+
#
|
|
87
95
|
# @return [Array<String>] the current request's sort params, e.g. ['-published_at', 'title']
|
|
88
96
|
def sort_params
|
|
89
|
-
params.fetch(sort_param_key.to_sym, "").split(",")
|
|
97
|
+
params.fetch(sort_param_key.to_sym, "").split(",").map { |param| param.strip.downcase }.reject(&:empty?)
|
|
90
98
|
end
|
|
91
99
|
|
|
92
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
|
|
@@ -46,6 +46,7 @@ files:
|
|
|
46
46
|
- lib/toller/filters/mutators/time.rb
|
|
47
47
|
- lib/toller/filters/scope_handler.rb
|
|
48
48
|
- lib/toller/retriever.rb
|
|
49
|
+
- lib/toller/scope_resolver.rb
|
|
49
50
|
- lib/toller/sort.rb
|
|
50
51
|
- lib/toller/sorts/column_handler.rb
|
|
51
52
|
- lib/toller/sorts/scope_handler.rb
|