filterameter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +96 -0
- data/Rakefile +17 -0
- data/lib/filterameter/configuration.rb +32 -0
- data/lib/filterameter/controller_filters.rb +110 -0
- data/lib/filterameter/declarative_filters.rb +46 -0
- data/lib/filterameter/exceptions/cannot_determine_model_error.rb +25 -0
- data/lib/filterameter/exceptions/undeclared_parameter_error.rb +21 -0
- data/lib/filterameter/exceptions/validation_error.rb +21 -0
- data/lib/filterameter/exceptions.rb +12 -0
- data/lib/filterameter/filter_declaration.rb +40 -0
- data/lib/filterameter/filter_factory.rb +41 -0
- data/lib/filterameter/filters/attribute_filter.rb +18 -0
- data/lib/filterameter/filters/conditional_scope_filter.rb +20 -0
- data/lib/filterameter/filters/nested_filter.rb +21 -0
- data/lib/filterameter/filters/scope_filter.rb +18 -0
- data/lib/filterameter/log_subscriber.rb +24 -0
- data/lib/filterameter/parameters_base.rb +45 -0
- data/lib/filterameter/version.rb +5 -0
- data/lib/filterameter.rb +30 -0
- data/lib/tasks/filterameter_tasks.rake +5 -0
- metadata +134 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 97bc00d12d3a953cbedcfa92a1b8c469ef442936ebaac28157fd4a88cb8dc62c
|
4
|
+
data.tar.gz: f7233773047377d725a047e3b01b159f097ecbece898b83d824b197e6146db61
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e19b7c776809225651165bf7714ca7820cb5c3824138ff798842d4b99cf67c58d92eb077070d9959874aa6ebf2352c2b5cf34f6c223241c64b36d4dc7b2ee59e
|
7
|
+
data.tar.gz: d24c6b1abe68ef70cbb03c8abecda1147f1dc889b2b7cd90062c35245f1c442d07ff6562866cb4d097b0bc2e3572cc02c5c5a406a146f7af91fa85b3abc883c9
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Todd Kummer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# Filterameter
|
2
|
+
Declarative Filter Parameters for Rails Controllers.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
Declare filters at the top of controllers to increase readability and reduce boilerplate code. Filters can be declared for attributes, scopes, or attributes from singular associations (`belongs_to` or `has_one`). Validations can also be assigned.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
filter :color
|
9
|
+
filter :size, validates: { inclusion: { in: %w[Small Medium Large] }, unless: -> { size.is_a? Array } }
|
10
|
+
filter :brand_name, association: :brand, name: :name
|
11
|
+
filter :on_sale, association: :price, validates: [{ numericality: { greater_than: 0 } },
|
12
|
+
{ numericality: { less_than: 100 } }]
|
13
|
+
```
|
14
|
+
|
15
|
+
### Filtering Options
|
16
|
+
|
17
|
+
The following options can be specified for each filter.
|
18
|
+
|
19
|
+
#### name
|
20
|
+
If the name of the parameter is different than the name of the attribute or scope, then use the name parameter to specify the name of the attribute or scope. For example, if the attribute name is `current_status` but the filter is exposed simply as `status` use the following:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
filter :status, name: :current_status
|
24
|
+
```
|
25
|
+
|
26
|
+
#### association
|
27
|
+
If the attribute or scope is nested, it can be referenced by naming the association. Only singular associations are valid. For example, if the manager_id attribute lives on an employee's department record, use the following:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
filter :manager_id, association: :department
|
31
|
+
```
|
32
|
+
|
33
|
+
#### validates
|
34
|
+
If the filter value should be validated, use the `validates` option along with [ActiveModel validations](https://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validates). Here's an example of the inclusion validator being used to restrict sizes:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
filter :size, validates: { inclusion: { in: %w[Small Medium Large] }, unless: -> { size.is_a? Array } }
|
38
|
+
```
|
39
|
+
|
40
|
+
Note that the `inclusion` validator does not allow arrays to be specified. If the filter should allow multiple values to be specified, then the validation needs to be disabled when the value an array.
|
41
|
+
|
42
|
+
### Configuring Controllers
|
43
|
+
|
44
|
+
Rails conventions are used to determine the controller's model as well as the name of the instance variable to apply the filters to. For example, the PhotosController will use the variable `@photos` to store a query against the Photo model. If the conventions do not provide the correct info, they can be overridden with the following two methods:
|
45
|
+
|
46
|
+
#### filter_model
|
47
|
+
Provide the name of the model. This method also allows the variable name to be optionally provided as the second parameter.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
filter_model 'Picture'
|
51
|
+
```
|
52
|
+
|
53
|
+
#### filter_query_var_name
|
54
|
+
Provide the name of the instance variable. For example, if the query is stored as `@data`, use the following:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
filter_query_var_name :data
|
58
|
+
```
|
59
|
+
|
60
|
+
## Installation
|
61
|
+
Add this line to your application's Gemfile:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
gem 'filterameter'
|
65
|
+
```
|
66
|
+
|
67
|
+
And then execute:
|
68
|
+
```bash
|
69
|
+
$ bundle
|
70
|
+
```
|
71
|
+
|
72
|
+
Or install it yourself as:
|
73
|
+
```bash
|
74
|
+
$ gem install filterameter
|
75
|
+
```
|
76
|
+
|
77
|
+
|
78
|
+
## Running Tests
|
79
|
+
|
80
|
+
Tests are written in RSpec and the dummy app uses a docker database. First, start the database and prepare it from the dummy folder.
|
81
|
+
|
82
|
+
```bash
|
83
|
+
cd spec/dummy
|
84
|
+
docker-compose up -d
|
85
|
+
bundle exec rails db:test:prepare
|
86
|
+
cd ../..
|
87
|
+
```
|
88
|
+
|
89
|
+
Run the tests from the main directory
|
90
|
+
|
91
|
+
```bash
|
92
|
+
bundle exec rspec
|
93
|
+
```
|
94
|
+
|
95
|
+
## License
|
96
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Filterameter'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
# = Configuration
|
5
|
+
#
|
6
|
+
# Class Configuration stores the following settings:
|
7
|
+
# - action_on_undeclared_parameters
|
8
|
+
# - action_on_validation_failure
|
9
|
+
#
|
10
|
+
# == Action on Undeclared Parameters
|
11
|
+
# Occurs when the filter parameter contains any keys that are not defined. Valid actions are :log, :raise, and
|
12
|
+
# false (do not take action). By default, development will log, test will raise, and production will do nothing.
|
13
|
+
#
|
14
|
+
# == Action on Validation Failure
|
15
|
+
# Occurs when a filter parameter fails a validation. Valid actions are :log, :raise, and false (do not take action).
|
16
|
+
# By default, development will log, test will raise, and production will do nothing.
|
17
|
+
class Configuration
|
18
|
+
attr_accessor :action_on_undeclared_parameters, :action_on_validation_failure
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@action_on_undeclared_parameters =
|
22
|
+
@action_on_validation_failure =
|
23
|
+
if Rails.env.development?
|
24
|
+
:log
|
25
|
+
elsif Rails.env.test?
|
26
|
+
:raise
|
27
|
+
else
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/inflector'
|
4
|
+
|
5
|
+
require 'active_support/rails'
|
6
|
+
require 'action_dispatch'
|
7
|
+
require 'action_controller/metal/live'
|
8
|
+
require 'action_controller/metal/strong_parameters'
|
9
|
+
|
10
|
+
require 'filterameter/filter_factory'
|
11
|
+
require 'filterameter/filter_declaration'
|
12
|
+
require 'filterameter/log_subscriber'
|
13
|
+
require 'filterameter/parameters_base'
|
14
|
+
|
15
|
+
module Filterameter
|
16
|
+
# = Controller Filters
|
17
|
+
#
|
18
|
+
# Class ControllerFilters stores the configuration declared via class-level method calls such as the list of
|
19
|
+
# filters and the optionally declared model class. Each controller will have one instance of the controller
|
20
|
+
# declarations stored as a class variable.
|
21
|
+
class ControllerFilters
|
22
|
+
attr_writer :query_variable_name
|
23
|
+
|
24
|
+
def initialize(controller_name, controller_path)
|
25
|
+
@controller_name = controller_name
|
26
|
+
@controller_path = controller_path
|
27
|
+
@declarations = {}
|
28
|
+
@filters = Hash.new { |hash, key| hash[key] = filter_factory.build(@declarations[key]) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def model_class=(model_class)
|
32
|
+
@model_class = model_class.is_a?(String) ? model_class.constantize : model_class
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_filter(parameter_name, options)
|
36
|
+
@declarations[parameter_name.to_s] = Filterameter::FilterDeclaration.new(parameter_name, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
def query_variable_name
|
40
|
+
@query_variable_name ||= model_class.model_name.plural
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_query(filter_params)
|
44
|
+
valid_filters(filter_params).reduce(model_class.all) do |query, (name, value)|
|
45
|
+
@filters[name].apply(query, value)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def model_class
|
52
|
+
@model_class ||= @controller_name.classify.safe_constantize ||
|
53
|
+
@controller_path.classify.safe_constantize ||
|
54
|
+
raise(Filterameter::Exceptions::CannotDetermineModelError.new(@controller_name,
|
55
|
+
@controller_path))
|
56
|
+
end
|
57
|
+
|
58
|
+
# lazy so that model_class can be optionally set
|
59
|
+
def filter_factory
|
60
|
+
@filter_factory ||= Filterameter::FilterFactory.new(model_class)
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid_filters(filter_params)
|
64
|
+
remove_invalid_values(
|
65
|
+
remove_undeclared_filters(filter_params)
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def remove_undeclared_filters(filter_params)
|
70
|
+
filter_params.slice(*declared_parameter_names).tap do |declared_parameters|
|
71
|
+
handle_undeclared_parameters(filter_params) if declared_parameters.size != filter_params.size
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_undeclared_parameters(filter_params)
|
76
|
+
action = Filterameter.configuration.action_on_undeclared_parameters
|
77
|
+
return unless action
|
78
|
+
|
79
|
+
undeclared_parameter_names = filter_params.keys - declared_parameter_names
|
80
|
+
case action
|
81
|
+
when :log
|
82
|
+
ActiveSupport::Notifications.instrument('undeclared_parameters.filterameter', keys: undeclared_parameter_names)
|
83
|
+
when :raise
|
84
|
+
raise Filterameter::Exceptions::UndeclaredParameterError, undeclared_parameter_names
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def remove_invalid_values(filter_params)
|
89
|
+
validator = validator_class.new(filter_params)
|
90
|
+
return filter_params if validator.valid?
|
91
|
+
|
92
|
+
case Filterameter.configuration.action_on_validation_failure
|
93
|
+
when :log
|
94
|
+
ActiveSupport::Notifications.instrument('validation_failure.filterameter', errors: validator.errors)
|
95
|
+
when :raise
|
96
|
+
raise Filterameter::Exceptions::ValidationError, validator.errors
|
97
|
+
end
|
98
|
+
|
99
|
+
filter_params.except(*validator.errors.keys.map(&:to_s))
|
100
|
+
end
|
101
|
+
|
102
|
+
def declared_parameter_names
|
103
|
+
@declared_parameter_names ||= @declarations.keys
|
104
|
+
end
|
105
|
+
|
106
|
+
def validator_class
|
107
|
+
@validator_class ||= Filterameter::ParametersBase.build_sub_class(@declarations.values)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'filterameter/controller_filters'
|
4
|
+
|
5
|
+
module Filterameter
|
6
|
+
# = Declarative Filters
|
7
|
+
#
|
8
|
+
# module DeclarativeFilters provides a controller DSL to declare filters along with any validations.
|
9
|
+
module DeclarativeFilters
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
before_action :build_filtered_query, only: :index
|
14
|
+
end
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
def filter_model(model_class, query_var_name = nil)
|
18
|
+
controller_filters.model_class = model_class
|
19
|
+
filter_query_var_name(query_var_name) if query_var_name.present?
|
20
|
+
end
|
21
|
+
|
22
|
+
def filter_query_var_name(query_variable_name)
|
23
|
+
controller_filters.query_variable_name = query_variable_name
|
24
|
+
end
|
25
|
+
|
26
|
+
def filter(name, options = {})
|
27
|
+
controller_filters.add_filter(name, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def filters(*names)
|
31
|
+
names.each { |name| filter(name) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def controller_filters
|
35
|
+
@controller_filters ||= Filterameter::ControllerFilters.new(controller_name, controller_path)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def build_filtered_query
|
42
|
+
instance_variable_set("@#{self.class.controller_filters.query_variable_name}",
|
43
|
+
self.class.controller_filters.build_query(params.to_unsafe_h.fetch(:filter, {})))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Exceptions
|
5
|
+
# = Cannot Determine Model Error
|
6
|
+
#
|
7
|
+
# Class CannotDetermineModelError is raised when the model class cannot be determined from either the controller
|
8
|
+
# name or controller path. This is a setup issue; the resolution is for the controller to specify the model class
|
9
|
+
# explicitly by adding a call to `filter_model`.
|
10
|
+
class CannotDetermineModelError < FilterameterError
|
11
|
+
def initialize(name, path)
|
12
|
+
super "Cannot determine model name from controller name #{value_and_classify(name)} " \
|
13
|
+
"or path #{value_and_classify(path)}. Declare the model explicitly with filter_model."
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def value_and_classify(value)
|
19
|
+
"(#{value} => #{value.classify})"
|
20
|
+
rescue StandardError
|
21
|
+
value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Exceptions
|
5
|
+
# = Undeclared Parameter Error
|
6
|
+
#
|
7
|
+
# Class UndeclaredParameterError is raised when a request contains filter parameters that have not been declared.
|
8
|
+
# Configuration setting `action_on_undeclared_parameters` determines whether or not the exception is raised.
|
9
|
+
class UndeclaredParameterError < FilterameterError
|
10
|
+
attr_reader :keys
|
11
|
+
|
12
|
+
def initialize(keys)
|
13
|
+
@keys = keys
|
14
|
+
end
|
15
|
+
|
16
|
+
def message
|
17
|
+
"The following filter parameter(s) have not been declared: #{keys}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Exceptions
|
5
|
+
# = Validation Error
|
6
|
+
#
|
7
|
+
# Class ValidationError is raised when a specified parameter fails a validation. Configuration setting
|
8
|
+
# `action_on_validation_failure` determines whether or not the exception is raised.
|
9
|
+
class ValidationError < FilterameterError
|
10
|
+
attr_reader :errors
|
11
|
+
|
12
|
+
def initialize(errors)
|
13
|
+
@errors = errors
|
14
|
+
end
|
15
|
+
|
16
|
+
def message
|
17
|
+
"The following parameter(s) failed validation: #{errors.full_messages}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Exceptions
|
5
|
+
class FilterameterError < StandardError
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'filterameter/exceptions/cannot_determine_model_error'
|
11
|
+
require 'filterameter/exceptions/validation_error'
|
12
|
+
require 'filterameter/exceptions/undeclared_parameter_error'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/array/wrap'
|
4
|
+
|
5
|
+
module Filterameter
|
6
|
+
# = Filter Declaration
|
7
|
+
#
|
8
|
+
# Class FilterDeclaration captures the filter declaration within the controller.
|
9
|
+
class FilterDeclaration
|
10
|
+
attr_reader :name, :parameter_name, :association, :validations
|
11
|
+
|
12
|
+
def initialize(parameter_name, options)
|
13
|
+
@parameter_name = parameter_name.to_s
|
14
|
+
|
15
|
+
validate_options(options)
|
16
|
+
@name = options.fetch(:name, parameter_name).to_s
|
17
|
+
@association = options[:association]
|
18
|
+
@filter_on_empty = options.fetch(:filter_on_empty, false)
|
19
|
+
@validations = Array.wrap(options[:validates])
|
20
|
+
end
|
21
|
+
|
22
|
+
def nested?
|
23
|
+
@association.present?
|
24
|
+
end
|
25
|
+
|
26
|
+
def validations?
|
27
|
+
!@validations.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def filter_on_empty?
|
31
|
+
@filter_on_empty
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def validate_options(options)
|
37
|
+
options.assert_valid_keys(:name, :association, :filter_on_empty, :validates)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'filterameter/filters/attribute_filter'
|
4
|
+
require 'filterameter/filters/conditional_scope_filter'
|
5
|
+
require 'filterameter/filters/nested_filter'
|
6
|
+
require 'filterameter/filters/scope_filter'
|
7
|
+
|
8
|
+
module Filterameter
|
9
|
+
# = Filter Factory
|
10
|
+
#
|
11
|
+
# Class FilterFactory builds a filter from a FilterDeclaration.
|
12
|
+
class FilterFactory
|
13
|
+
def initialize(model_class)
|
14
|
+
@model_class = model_class
|
15
|
+
end
|
16
|
+
|
17
|
+
def build(declaration)
|
18
|
+
model = declaration.nested? ? model_from_association(declaration.association) : @model_class
|
19
|
+
filter = build_filter(model, declaration.name)
|
20
|
+
|
21
|
+
declaration.nested? ? Filterameter::Filters::NestedFilter.new(declaration.association, model, filter) : filter
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def build_filter(model, name)
|
27
|
+
# checking dangerous_class_method? excludes any names that cannot be scope names, such as "name"
|
28
|
+
if model.respond_to?(name) && !model.dangerous_class_method?(name)
|
29
|
+
Filterameter::Filters::ScopeFilter.new(name)
|
30
|
+
else
|
31
|
+
Filterameter::Filters::AttributeFilter.new(name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO: rescue then raise custom error with cause
|
36
|
+
def model_from_association(association)
|
37
|
+
[association].flatten.reduce(@model_class) { |memo, name| memo.reflect_on_association(name).klass }
|
38
|
+
# rescue StandardError => e
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Filters
|
5
|
+
# = Attribute Filter
|
6
|
+
#
|
7
|
+
# Class AttributeFilter leverages ActiveRecord's where query method to add criteria for an attribute.
|
8
|
+
class AttributeFilter
|
9
|
+
def initialize(attribute_name)
|
10
|
+
@attribute_name = attribute_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def apply(query, value)
|
14
|
+
query.where(@attribute_name => value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Filters
|
5
|
+
# = Conditional Scope Filter
|
6
|
+
#
|
7
|
+
# Class ConditionalScopeFilter applies the scope if the parameter is not false.
|
8
|
+
class ConditionalScopeFilter
|
9
|
+
def initialize(scope_name)
|
10
|
+
@scope_name = scope_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def apply(query, value)
|
14
|
+
return query unless ActiveModel::Type::Boolean.new.cast(value)
|
15
|
+
|
16
|
+
query.send(@scope_name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Filters
|
5
|
+
# = Nested Attribute Filter
|
6
|
+
#
|
7
|
+
# Class NestedFilter joins the nested table(s) then merges the filter to the association's model.
|
8
|
+
class NestedFilter
|
9
|
+
def initialize(joins_values, association_model, attribute_filter)
|
10
|
+
@joins_values = joins_values
|
11
|
+
@association_model = association_model
|
12
|
+
@attribute_filter = attribute_filter
|
13
|
+
end
|
14
|
+
|
15
|
+
def apply(query, value)
|
16
|
+
query.joins(@joins_values)
|
17
|
+
.merge(@attribute_filter.apply(@association_model, value))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Filters
|
5
|
+
# = Scope Filter
|
6
|
+
#
|
7
|
+
# Class ScopeFilter applies the named scope passing in the parameter value.
|
8
|
+
class ScopeFilter
|
9
|
+
def initialize(scope_name)
|
10
|
+
@scope_name = scope_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def apply(query, value)
|
14
|
+
query.send(@scope_name, value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
# = Log Subscriber
|
5
|
+
#
|
6
|
+
# Class LogSubscriber provides instrumentation for events.
|
7
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
8
|
+
def validation_failure(event)
|
9
|
+
debug do
|
10
|
+
errors = event.payload[:errors]
|
11
|
+
([' The following filter validation errors occurred:'] + errors.full_messages).join("\n - ")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def undeclared_parameters(event)
|
16
|
+
debug do
|
17
|
+
keys = event.payload[:keys]
|
18
|
+
" Undeclared filter parameter#{'s' if keys.size > 1}: #{keys.map { |e| ":#{e}" }.join(', ')}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
Filterameter::LogSubscriber.attach_to :filterameter
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model/attribute_assignment'
|
4
|
+
require 'active_model/validations'
|
5
|
+
|
6
|
+
module Filterameter
|
7
|
+
# = Parameters
|
8
|
+
#
|
9
|
+
# Class Parameters is sub-classed to provide controller-specific validations.
|
10
|
+
class ParametersBase
|
11
|
+
include ActiveModel::Validations
|
12
|
+
|
13
|
+
def self.build_sub_class(declarations)
|
14
|
+
Class.new(self).tap do |sub_class|
|
15
|
+
declarations.select(&:validations?).each do |declaration|
|
16
|
+
sub_class.add_validation(declaration.parameter_name, declaration.validations)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.name
|
22
|
+
'ControllerParameters'
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.add_validation(parameter_name, validations)
|
26
|
+
attr_accessor parameter_name
|
27
|
+
|
28
|
+
default_options = { allow_nil: true }
|
29
|
+
validations.each do |validation|
|
30
|
+
validates parameter_name, default_options.merge(validation)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(attributes)
|
35
|
+
attributes.each { |k, v| assign_attribute(k, v) }
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def assign_attribute(key, value)
|
41
|
+
setter = :"#{key}="
|
42
|
+
public_send(setter, value) if respond_to?(setter)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/filterameter.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'filterameter/configuration'
|
4
|
+
require 'filterameter/declarative_filters'
|
5
|
+
require 'filterameter/exceptions'
|
6
|
+
|
7
|
+
# = Filterameter
|
8
|
+
#
|
9
|
+
# Module Filterameter can be mixed into a controller to provide the DSL to describe each controller's filters.
|
10
|
+
#
|
11
|
+
# The model class must be declared if it cannot be derived. It can be derived if (A) the model is not namespaced and its
|
12
|
+
# name matches the controller name (for example BrandsController -> Brand) or (B) both the controller and model share
|
13
|
+
# the same namespace and name.
|
14
|
+
module Filterameter
|
15
|
+
class << self
|
16
|
+
attr_writer :configuration
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.configuration
|
20
|
+
@configuration ||= Configuration.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.reset
|
24
|
+
@configuration = Configuration.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.configure
|
28
|
+
yield(configuration)
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: filterameter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Todd Kummer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-04-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.2.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.2.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pg
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec-rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.9'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.9'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.77'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.77'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Enable filter parameters to be declared in controllers.
|
84
|
+
email:
|
85
|
+
- todd@rockridgesolutions.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- MIT-LICENSE
|
91
|
+
- README.md
|
92
|
+
- Rakefile
|
93
|
+
- lib/filterameter.rb
|
94
|
+
- lib/filterameter/configuration.rb
|
95
|
+
- lib/filterameter/controller_filters.rb
|
96
|
+
- lib/filterameter/declarative_filters.rb
|
97
|
+
- lib/filterameter/exceptions.rb
|
98
|
+
- lib/filterameter/exceptions/cannot_determine_model_error.rb
|
99
|
+
- lib/filterameter/exceptions/undeclared_parameter_error.rb
|
100
|
+
- lib/filterameter/exceptions/validation_error.rb
|
101
|
+
- lib/filterameter/filter_declaration.rb
|
102
|
+
- lib/filterameter/filter_factory.rb
|
103
|
+
- lib/filterameter/filters/attribute_filter.rb
|
104
|
+
- lib/filterameter/filters/conditional_scope_filter.rb
|
105
|
+
- lib/filterameter/filters/nested_filter.rb
|
106
|
+
- lib/filterameter/filters/scope_filter.rb
|
107
|
+
- lib/filterameter/log_subscriber.rb
|
108
|
+
- lib/filterameter/parameters_base.rb
|
109
|
+
- lib/filterameter/version.rb
|
110
|
+
- lib/tasks/filterameter_tasks.rake
|
111
|
+
homepage: https://github.com/RockSolt/filterameter
|
112
|
+
licenses:
|
113
|
+
- MIT
|
114
|
+
metadata: {}
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubygems_version: 3.0.8
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: Declarative Filter Parameters for Rails Controllers
|
134
|
+
test_files: []
|