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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 565b78efab21039ddd7025adad94e0ac2e045943acef17e97374aac58b0b3b10
4
- data.tar.gz: a73035fb270e002dfd8cfe8ed38dff7317dbbf033e4aac922e5b409fc9aad92b
3
+ metadata.gz: 14ead1892ebc47f8a2dc0ee254545de4a013e7140d738654b9ad42dfd5c05799
4
+ data.tar.gz: 39fa2fa6acd9fed9e002b78f020e3b95ed89a920384481b24271b1898f955057
5
5
  SHA512:
6
- metadata.gz: b6a9f24aeec9383b065c39c3430c63ab90bd75cd963b414418e552d3fdd22a151e0c1da7ece697c92116958d294781c83330145889266d6be739e7ba2698b78a
7
- data.tar.gz: b816afc011e2070a7adfeee999702ecdf2ca04af45e50a4ec7966d98ef1fd5486aaa1bcffd4c48a443db9aafdc43a60c5679c7b4dd20e5ff918304b6b38232bd
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.0'
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
- `-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`.
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::WhereHandler.new.call(collection, type, value, properties)
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
- # Where handler for filter
8
- class WhereHandler
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
- module_function
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
- module_function
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
- module_function
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
- module_function
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
@@ -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.parameter, nil)
53
+ filter_params.fetch(downcased_parameter(retrieval), nil)
54
54
  else
55
- sort_params.include?("-#{retrieval.parameter}") ? :desc : :asc
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.parameter, nil).present?
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.parameter.to_s
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::OrderHandler.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
@@ -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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Toller
4
4
  # @return [String] the current gem version
5
- VERSION = "1.0.0"
5
+ VERSION = "1.2.0"
6
6
  end
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/order_handler"
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.0.0
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/order_handler.rb
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