toller 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35290d3f9b21adb7834fbf214f71655f6d71628742e2e18ddee3de86e6b4b45f
4
- data.tar.gz: 390975016fc9236a125340538fbd3a565924392ab6a3c68c6794928ceb38522d
3
+ metadata.gz: 565b78efab21039ddd7025adad94e0ac2e045943acef17e97374aac58b0b3b10
4
+ data.tar.gz: a73035fb270e002dfd8cfe8ed38dff7317dbbf033e4aac922e5b409fc9aad92b
5
5
  SHA512:
6
- metadata.gz: 2907a0aafb8c1fbb9e6c0b021778081874fd10263a7f970c9a0142f9f706decf433837fea12b9408de8983759417c9417eab02a008c969359aabf6336df777fa
7
- data.tar.gz: 3e788653d93fff2e613501c48a1dde8eec9514676481a8680b4e28bfcafef341e8dc29a75bbe1bbfb60c905681ed18fb5c36895549118e81063d52ca6fcc5fc8
6
+ metadata.gz: b6a9f24aeec9383b065c39c3430c63ab90bd75cd963b414418e552d3fdd22a151e0c1da7ece697c92116958d294781c83330145889266d6be739e7ba2698b78a
7
+ data.tar.gz: b816afc011e2070a7adfeee999702ecdf2ca04af45e50a4ec7966d98ef1fd5486aaa1bcffd4c48a443db9aafdc43a60c5679c7b4dd20e5ff918304b6b38232bd
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2020 David Freerksen
1
+ Copyright 2026 David Freerksen
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,13 +1,22 @@
1
1
  # Toller
2
2
 
3
- URL based filtering and sorting. See the wiki for usage information.
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:
@@ -38,26 +47,79 @@ More information is [available in the wiki](https://github.com/dfreerksen/toller
38
47
  $ bin/test
39
48
  ```
40
49
 
50
+ ### Appraisal
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
+
56
+ ```bash
57
+ $ bundle exec appraisal install
58
+ $ bundle exec appraisal generate-install
59
+ ```
60
+
61
+ To run tests with Appraisal, run
62
+
63
+ ```bash
64
+ $ bundle exec appraisal rspec
65
+ ```
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`.
68
+
69
+ ```bash
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
77
+ ```
78
+
41
79
  ## Release
42
80
 
43
- 1. Bump the gem version in `lib/toller/version.rb`
44
- 2. Build the gem with
81
+ See [Release.md](docs/Release.md)
82
+
83
+ ## Code Analysis
45
84
 
46
- ```
47
- $ bundle exec rake build
48
- ```
85
+ Various tools are used to ensure code is linted and formatted correctly.
49
86
 
50
- This will create a new .gem file in `pkg/`. Fix any errors or warnings that come up.
51
- 3. Commit the version change to git with a commit message similar to "Release [X.Y.Z]"
52
- 4. Create the gem, tag it in Github and release to Rubygems
87
+ ### RuboCop
53
88
 
54
- ```
55
- $ bundle exec rake release
56
- ```
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
+ ```
102
+
103
+ ## Documentation
104
+
105
+ [Yard](https://github.com/lsegal/yard) is used to generate documentation. [Online documentation is available](http://www.rubydoc.info/github/dfreerksen/toller/master)
106
+
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
+ ```
57
119
 
58
120
  ## Contributing
59
121
 
60
- 1. Fork it ([https://github.com/dfrerksen/recieve/fork](https://github.com/dfrerksen/recieve/fork))
122
+ 1. Fork it ([https://github.com/dfrerksen/toller/fork](https://github.com/dfrerksen/toller/fork))
61
123
  2. Create your feature branch (`git checkout -b my-new-feature`)
62
124
  3. Commit your changes (`git commit -am 'Add some feature'`)
63
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 'bundler/setup'
4
+ require "bundler/setup"
5
5
  rescue LoadError
6
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
7
7
  end
8
8
 
9
- require 'bundler/gem_tasks'
9
+ require "bundler/gem_tasks"
10
10
 
11
- APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
12
- load 'rails/tasks/engine.rake'
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
- attr_reader :parameter, :properties, :type
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 '...' if value.include?('...')
26
- return '..' if value.include?('..')
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 '...' if value.include?('...')
26
- return '..' if value.include?('..')
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 '...' if value.include?('...')
30
- return '..' if value.include?('..')
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 '...' if value.include?('...')
26
- return '..' if value.include?('..')
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
@@ -3,14 +3,38 @@
3
3
  module Toller
4
4
  ##
5
5
  # Retriever
6
- #
7
6
  class Retriever
8
- attr_reader :collection, :filter_params, :retrievals, :sort_params
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
- attr_reader :parameter, :properties, :type
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
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Toller
4
- VERSION = '0.4.0'
4
+ # @return [String] the current gem version
5
+ VERSION = "1.0.0"
5
6
  end
data/lib/toller.rb CHANGED
@@ -1,26 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
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'
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, '').split(',')
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,36 +1,31 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toller
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.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: 2020-09-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: 6.0.3
20
16
  - - ">="
21
17
  - !ruby/object:Gem::Version
22
- version: 6.0.3.2
18
+ version: '6.0'
23
19
  type: :runtime
24
20
  prerelease: false
25
21
  version_requirements: !ruby/object:Gem::Requirement
26
22
  requirements:
27
- - - "~>"
28
- - !ruby/object:Gem::Version
29
- version: 6.0.3
30
23
  - - ">="
31
24
  - !ruby/object:Gem::Version
32
- version: 6.0.3.2
33
- 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.
34
29
  email:
35
30
  - dfreerksen@gmail.com
36
31
  executables: []
@@ -57,8 +52,13 @@ files:
57
52
  homepage: https://github.com/dfreerksen/toller
58
53
  licenses:
59
54
  - MIT
60
- metadata: {}
61
- post_install_message:
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
62
62
  rdoc_options: []
63
63
  require_paths:
64
64
  - lib
@@ -66,15 +66,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
66
66
  requirements:
67
67
  - - ">="
68
68
  - !ruby/object:Gem::Version
69
- version: '0'
69
+ version: '3.0'
70
70
  required_rubygems_version: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
74
  version: '0'
75
75
  requirements: []
76
- rubygems_version: 3.0.3
77
- signing_key:
76
+ rubygems_version: 4.0.16
78
77
  specification_version: 4
79
- summary: Summary of Toller.
78
+ summary: URL-query-param-based filtering and sorting for Rails controllers.
80
79
  test_files: []