filterameter 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +103 -7
- data/lib/filterameter/coordinators/base.rb +43 -0
- data/lib/filterameter/coordinators/controller_coordinator.rb +42 -0
- data/lib/filterameter/coordinators/query_coordinator.rb +18 -0
- data/lib/filterameter/declarative_controller_filters.rb +40 -0
- data/lib/filterameter/declarative_filters.rb +12 -31
- data/lib/filterameter/exceptions/cannot_determine_model_error.rb +1 -1
- data/lib/filterameter/exceptions/undeclared_parameter_error.rb +5 -4
- data/lib/filterameter/exceptions/validation_error.rb +1 -0
- data/lib/filterameter/filter_declaration.rb +37 -1
- data/lib/filterameter/filter_factory.rb +15 -5
- data/lib/filterameter/filter_registry.rb +62 -0
- data/lib/filterameter/filterable.rb +55 -0
- data/lib/filterameter/filters/arel_filter.rb +14 -0
- data/lib/filterameter/filters/matches_filter.rb +22 -0
- data/lib/filterameter/filters/maximum_filter.rb +14 -0
- data/lib/filterameter/filters/minimum_filter.rb +14 -0
- data/lib/filterameter/options/partial_options.rb +82 -0
- data/lib/filterameter/parameters_base.rb +2 -0
- data/lib/filterameter/query_builder.rb +74 -0
- data/lib/filterameter/validators/inclusion_validator.rb +29 -0
- data/lib/filterameter/version.rb +1 -1
- data/lib/filterameter.rb +1 -0
- metadata +84 -17
- data/lib/filterameter/controller_filters.rb +0 -110
- data/lib/tasks/filterameter_tasks.rake +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e753f63174bbf6eb6a0ad352fc518782bc092f5e9075bcd596d540c76ceb6ab1
|
4
|
+
data.tar.gz: 14f544522f30cbf4d2d6fe54fb5e8a5e0310172fcc75007c087e9402359d6ae3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 13b2457bf17e0cdd1d9b11075d4a5df015179b46000a2b3418d10ab5885bb5d2fc0435c59bfdae7668c2cf165973084ccd2a1403e95f32e1e40cef1a67849dfb
|
7
|
+
data.tar.gz: 26b14da4bdc7e786dbd025af086dc9aadac131306c3ef82ea7ef8f73e72e7518e4de825d34ab80b51ed2b3052705060ebf87b6ae5dce56a8adc697f051928258
|
data/README.md
CHANGED
@@ -1,12 +1,17 @@
|
|
1
|
+
[![Gem Version](https://badge.fury.io/rb/filterameter.svg)](https://badge.fury.io/rb/filterameter)
|
2
|
+
[![RuboCop](https://github.com/RockSolt/filterameter/workflows/RuboCop/badge.svg)](https://github.com/RockSolt/filterameter/actions?query=workflow%3ARuboCop)
|
3
|
+
[![RSpec](https://github.com/RockSolt/filterameter/workflows/RSpec/badge.svg)](https://github.com/RockSolt/filterameter/actions?query=workflow%3ARSpec)
|
4
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/d9d87f9ce8020eb6e656/maintainability)](https://codeclimate.com/github/RockSolt/filterameter/maintainability)
|
5
|
+
|
1
6
|
# Filterameter
|
2
|
-
Declarative
|
7
|
+
Declarative filter parameters provide provide clean and clear filters for queries.
|
3
8
|
|
4
9
|
## Usage
|
5
|
-
Declare filters
|
10
|
+
Declare filters in query classes or 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
11
|
|
7
12
|
```ruby
|
8
13
|
filter :color
|
9
|
-
filter :size, validates: { inclusion: { in: %w[Small Medium Large]
|
14
|
+
filter :size, validates: { inclusion: { in: %w[Small Medium Large], allow_multiple_values: true } }
|
10
15
|
filter :brand_name, association: :brand, name: :name
|
11
16
|
filter :on_sale, association: :price, validates: [{ numericality: { greater_than: 0 } },
|
12
17
|
{ numericality: { less_than: 100 } }]
|
@@ -34,14 +39,87 @@ filter :manager_id, association: :department
|
|
34
39
|
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
40
|
|
36
41
|
```ruby
|
37
|
-
filter :size, validates: { inclusion: { in: %w[Small Medium Large] }
|
42
|
+
filter :size, validates: { inclusion: { in: %w[Small Medium Large] } }
|
43
|
+
```
|
44
|
+
|
45
|
+
The `inclusion` validator has been overridden to provide the additional option `allow_multiple_values`. When true, the value can be an array and each entry in the array will be validated. Use this when the filter can specify one or more values.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
filter :size, validates: { inclusion: { in: %w[Small Medium Large], allow_multiple_values: true } }
|
49
|
+
```
|
50
|
+
|
51
|
+
|
52
|
+
#### partial
|
53
|
+
Specify the partial option if the filter should do a partial search (SQL's `LIKE`). The partial option accepts a hash to specify the search behavior. Here are the available options:
|
54
|
+
- match: anywhere (default), from_start, dynamic
|
55
|
+
- case_sensitive: true, false (default)
|
56
|
+
|
57
|
+
There are two shortcuts: : the partial option can be declared with `true`, which just uses the defaults; or the partial option can be declared with the match option directly, such as `partial: :from_start`.
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
filter :description, partial: true
|
61
|
+
filter :department_name, partial: :from_start
|
62
|
+
filter :reason, partial: { match: :dynamic, case_sensitive: true }
|
63
|
+
```
|
64
|
+
|
65
|
+
The `match` options defines where you are searching (which then controls where the wildcard(s) appear):
|
66
|
+
- anywhere: adds wildcards at the start and end, for example '%blue%'
|
67
|
+
- from_start: adds a wildcard at the end, for example 'blue%'
|
68
|
+
- dynamic: adds no wildcards; this enables the client to fully control the search string
|
69
|
+
|
70
|
+
#### range
|
71
|
+
Specify the range option to enable searches by ranges, minimum values, or maximum values. (All of these are inclusive. A search for a minimum value of $10.00 would include all items priced at $10.00.)
|
72
|
+
|
73
|
+
Here are the available options:
|
74
|
+
- true: enable ranges, minimum values, and/or maximum values
|
75
|
+
- min_only: enables minimum values
|
76
|
+
- max_only: enables maximum values
|
77
|
+
|
78
|
+
Using the range option means that _in addition to the attribute filter_ minimum and maximum query parameters may also be specified. The parameter names are the attribute name plus the suffix <tt>_min</tt> or <tt>_max</tt>.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
filter :price, range: true
|
82
|
+
filter :approved_at, range: :min_only
|
83
|
+
filter :sale_price, range: :max_only
|
84
|
+
```
|
85
|
+
|
86
|
+
In the first example, query parameters could include <tt>price</tt>, <tt>price_min</tt>, and <tt>price_max</tt>.
|
87
|
+
|
88
|
+
### Query Classes
|
89
|
+
|
90
|
+
Include module `Filterameter::DeclarativeFilters` in the query class. The model must be declared using `model`, and a default query can optionally be declared using `default_query`. If no default query is provided, then the default is `.all`.
|
91
|
+
|
92
|
+
#### Example
|
93
|
+
|
94
|
+
Here's what a query class for the Widgets model with filters on size and color might look like:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
class WidgetQuery
|
98
|
+
include Filterameter::DeclarativeFilters
|
99
|
+
|
100
|
+
model Widget
|
101
|
+
filter :size
|
102
|
+
filter :color
|
103
|
+
end
|
38
104
|
```
|
39
105
|
|
40
|
-
|
106
|
+
Build the query using class method `build_query`. The method takes two parameters:
|
107
|
+
|
108
|
+
- filter: the hash of filter parameters
|
109
|
+
- starting_query: any scope to build on (if not provided, the default query is the starting point)
|
110
|
+
|
111
|
+
Here's how the query might be invoked:
|
41
112
|
|
42
|
-
|
113
|
+
```ruby
|
114
|
+
filters = { size: 'large', color: 'blue' }
|
115
|
+
widgets = WidgetQuery.build_query(filters, Widget.limit(10))
|
116
|
+
```
|
43
117
|
|
44
|
-
|
118
|
+
### Controllers
|
119
|
+
|
120
|
+
Include module `Filterameter::DeclarativeControllerFilters` in the controller. Add before action callback `build_filtered_query` for controller actions that should build the query.
|
121
|
+
|
122
|
+
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
123
|
|
46
124
|
#### filter_model
|
47
125
|
Provide the name of the model. This method also allows the variable name to be optionally provided as the second parameter.
|
@@ -57,6 +135,24 @@ Provide the name of the instance variable. For example, if the query is stored a
|
|
57
135
|
filter_query_var_name :data
|
58
136
|
```
|
59
137
|
|
138
|
+
#### Example
|
139
|
+
|
140
|
+
In the happy path, the WidgetsController serves Widgets and can filter on size and color. Here's what the controller might look like:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
class WidgetController < ApplicationController
|
144
|
+
include Filterameter::DeclarativeControllerFilters
|
145
|
+
before_action :build_filtered_query, only: :index
|
146
|
+
|
147
|
+
filter :size
|
148
|
+
filter :color
|
149
|
+
|
150
|
+
def index
|
151
|
+
render json: @widgets
|
152
|
+
end
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
60
156
|
## Installation
|
61
157
|
Add this line to your application's Gemfile:
|
62
158
|
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'filterameter/filter_declaration'
|
4
|
+
require 'filterameter/filter_factory'
|
5
|
+
require 'filterameter/filter_registry'
|
6
|
+
require 'filterameter/log_subscriber'
|
7
|
+
require 'filterameter/parameters_base'
|
8
|
+
require 'filterameter/query_builder'
|
9
|
+
|
10
|
+
module Filterameter
|
11
|
+
module Coordinators
|
12
|
+
# = Coordinators Base
|
13
|
+
#
|
14
|
+
# The main responsibility of the Coordinators classes is to keep the namespace clean for controllers and query
|
15
|
+
# objects that implement filter parameters. The coordinators encapsulate references to the Query Builder and
|
16
|
+
# Filter Registry.
|
17
|
+
class Base
|
18
|
+
attr_reader :model_class
|
19
|
+
|
20
|
+
delegate :add_filter, to: :registry
|
21
|
+
delegate :build_query, to: :query_builder
|
22
|
+
|
23
|
+
def model_class=(model_class)
|
24
|
+
@model_class = model_class.is_a?(String) ? model_class.constantize : model_class
|
25
|
+
end
|
26
|
+
|
27
|
+
def query_builder
|
28
|
+
@query_builder ||= Filterameter::QueryBuilder.new(default_query, registry)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def default_query
|
34
|
+
model_class.all
|
35
|
+
end
|
36
|
+
|
37
|
+
# lazy so that model_class can be optionally set
|
38
|
+
def registry
|
39
|
+
@registry ||= Filterameter::FilterRegistry.new(Filterameter::FilterFactory.new(model_class))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,42 @@
|
|
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/coordinators/base'
|
11
|
+
|
12
|
+
module Filterameter
|
13
|
+
module Coordinators
|
14
|
+
# = Controller Filters
|
15
|
+
#
|
16
|
+
# Class ControllerFilters stores the configuration declared via class-level method calls such as the list of
|
17
|
+
# filters and the optionally declared model class. Each controller will have one instance of the controller
|
18
|
+
# declarations stored as a class variable.
|
19
|
+
class ControllerCoordinator < Filterameter::Coordinators::Base
|
20
|
+
attr_writer :query_variable_name
|
21
|
+
|
22
|
+
def initialize(controller_name, controller_path)
|
23
|
+
@controller_name = controller_name
|
24
|
+
@controller_path = controller_path
|
25
|
+
super()
|
26
|
+
end
|
27
|
+
|
28
|
+
def query_variable_name
|
29
|
+
@query_variable_name ||= model_class.model_name.plural
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def model_class
|
35
|
+
@model_class ||= @controller_name.classify.safe_constantize ||
|
36
|
+
@controller_path.classify.safe_constantize ||
|
37
|
+
raise(Filterameter::Exceptions::CannotDetermineModelError.new(@controller_name,
|
38
|
+
@controller_path))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Coordinators
|
5
|
+
# = Query Coordinator
|
6
|
+
#
|
7
|
+
# Class QueryCoordinator coordinates the filter logic for query classes.
|
8
|
+
class QueryCoordinator < Filterameter::Coordinators::Base
|
9
|
+
attr_writer :default_query
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def default_query
|
14
|
+
@default_query || super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'filterameter/filterable'
|
4
|
+
require 'filterameter/coordinators/controller_coordinator'
|
5
|
+
|
6
|
+
module Filterameter
|
7
|
+
# = Declarative Controller Filters
|
8
|
+
#
|
9
|
+
# Mixin DeclarativeControllerFilters can included in controllers to enable the filter DSL.
|
10
|
+
module DeclarativeControllerFilters
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
include Filterameter::Filterable
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
def filter_model(model_class, query_var_name = nil)
|
16
|
+
filter_coordinator.model_class = model_class
|
17
|
+
filter_query_var_name(query_var_name) if query_var_name.present?
|
18
|
+
end
|
19
|
+
|
20
|
+
def filter_query_var_name(query_variable_name)
|
21
|
+
filter_coordinator.query_variable_name = query_variable_name
|
22
|
+
end
|
23
|
+
|
24
|
+
def filter_coordinator
|
25
|
+
@filter_coordinator ||= Filterameter::Coordinators::ControllerCoordinator.new(controller_name, controller_path)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def build_filtered_query
|
32
|
+
var_name = "@#{self.class.filter_coordinator.query_variable_name}"
|
33
|
+
instance_variable_set(
|
34
|
+
var_name,
|
35
|
+
self.class.filter_coordinator.build_query(params.to_unsafe_h.fetch(:filter, {}),
|
36
|
+
instance_variable_get(var_name))
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,50 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'filterameter/
|
3
|
+
require 'filterameter/coordinators/query_coordinator'
|
4
|
+
require 'filterameter/filterable'
|
5
|
+
require 'filterameter/query_builder'
|
4
6
|
|
5
7
|
module Filterameter
|
6
8
|
# = Declarative Filters
|
7
9
|
#
|
8
|
-
#
|
10
|
+
# Mixin DeclarativeFilters is included to build query classes using the filter DSL.
|
9
11
|
module DeclarativeFilters
|
10
12
|
extend ActiveSupport::Concern
|
11
|
-
|
12
|
-
included do
|
13
|
-
before_action :build_filtered_query, only: :index
|
14
|
-
end
|
13
|
+
include Filterameter::Filterable
|
15
14
|
|
16
15
|
class_methods do
|
17
|
-
|
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
|
16
|
+
delegate :build_query, to: :filter_coordinator
|
25
17
|
|
26
|
-
def
|
27
|
-
|
18
|
+
def model(model_class)
|
19
|
+
filter_coordinator.model_class = model_class
|
28
20
|
end
|
29
21
|
|
30
|
-
def
|
31
|
-
|
22
|
+
def default_query(query)
|
23
|
+
filter_coordinator.default_query = query
|
32
24
|
end
|
33
25
|
|
34
|
-
def
|
35
|
-
@
|
26
|
+
def filter_coordinator
|
27
|
+
@filter_coordinator ||= Filterameter::Coordinators::QueryCoordinator.new
|
36
28
|
end
|
37
29
|
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def build_filtered_query
|
42
|
-
var_name = "@#{self.class.controller_filters.query_variable_name}"
|
43
|
-
instance_variable_set(
|
44
|
-
var_name,
|
45
|
-
self.class.controller_filters.build_query(params.to_unsafe_h.fetch(:filter, {}),
|
46
|
-
instance_variable_get(var_name))
|
47
|
-
)
|
48
|
-
end
|
49
30
|
end
|
50
31
|
end
|
@@ -10,7 +10,7 @@ module Filterameter
|
|
10
10
|
class CannotDetermineModelError < FilterameterError
|
11
11
|
def initialize(name, path)
|
12
12
|
super "Cannot determine model name from controller name #{value_and_classify(name)} " \
|
13
|
-
|
13
|
+
"or path #{value_and_classify(path)}. Declare the model explicitly with filter_model."
|
14
14
|
end
|
15
15
|
|
16
16
|
private
|
@@ -7,14 +7,15 @@ module Filterameter
|
|
7
7
|
# Class UndeclaredParameterError is raised when a request contains filter parameters that have not been declared.
|
8
8
|
# Configuration setting `action_on_undeclared_parameters` determines whether or not the exception is raised.
|
9
9
|
class UndeclaredParameterError < FilterameterError
|
10
|
-
attr_reader :
|
10
|
+
attr_reader :key
|
11
11
|
|
12
|
-
def initialize(
|
13
|
-
|
12
|
+
def initialize(key)
|
13
|
+
super
|
14
|
+
@key = key
|
14
15
|
end
|
15
16
|
|
16
17
|
def message
|
17
|
-
"The following filter parameter
|
18
|
+
"The following filter parameter has not been declared: #{key}"
|
18
19
|
end
|
19
20
|
end
|
20
21
|
end
|
@@ -1,12 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'active_support/core_ext/array/wrap'
|
4
|
+
require 'filterameter/options/partial_options'
|
4
5
|
|
5
6
|
module Filterameter
|
6
7
|
# = Filter Declaration
|
7
8
|
#
|
8
9
|
# Class FilterDeclaration captures the filter declaration within the controller.
|
9
10
|
class FilterDeclaration
|
11
|
+
VALID_RANGE_OPTIONS = [true, :min_only, :max_only].freeze
|
12
|
+
|
10
13
|
attr_reader :name, :parameter_name, :association, :validations
|
11
14
|
|
12
15
|
def initialize(parameter_name, options)
|
@@ -17,6 +20,8 @@ module Filterameter
|
|
17
20
|
@association = options[:association]
|
18
21
|
@filter_on_empty = options.fetch(:filter_on_empty, false)
|
19
22
|
@validations = Array.wrap(options[:validates])
|
23
|
+
@raw_partial_options = options.fetch(:partial, false)
|
24
|
+
@raw_range = options[:range]
|
20
25
|
end
|
21
26
|
|
22
27
|
def nested?
|
@@ -31,10 +36,41 @@ module Filterameter
|
|
31
36
|
@filter_on_empty
|
32
37
|
end
|
33
38
|
|
39
|
+
def partial_search?
|
40
|
+
partial_options.present?
|
41
|
+
end
|
42
|
+
|
43
|
+
def partial_options
|
44
|
+
@partial_options ||= @raw_partial_options ? Options::PartialOptions.new(@raw_partial_options) : nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def range_enabled?
|
48
|
+
@raw_range.present?
|
49
|
+
end
|
50
|
+
|
51
|
+
def range?
|
52
|
+
@raw_range == true
|
53
|
+
end
|
54
|
+
|
55
|
+
def minimum?
|
56
|
+
@raw_range == :min_only
|
57
|
+
end
|
58
|
+
|
59
|
+
def maximum?
|
60
|
+
@raw_range == :max_only
|
61
|
+
end
|
62
|
+
|
34
63
|
private
|
35
64
|
|
36
65
|
def validate_options(options)
|
37
|
-
options.assert_valid_keys(:name, :association, :filter_on_empty, :validates)
|
66
|
+
options.assert_valid_keys(:name, :association, :filter_on_empty, :validates, :partial, :range)
|
67
|
+
validate_range(options[:range]) if options.key?(:range)
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate_range(range)
|
71
|
+
return if VALID_RANGE_OPTIONS.include?(range)
|
72
|
+
|
73
|
+
raise ArgumentError, "Invalid range option: #{range}"
|
38
74
|
end
|
39
75
|
end
|
40
76
|
end
|
@@ -1,7 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'filterameter/filters/arel_filter'
|
3
4
|
require 'filterameter/filters/attribute_filter'
|
4
5
|
require 'filterameter/filters/conditional_scope_filter'
|
6
|
+
require 'filterameter/filters/matches_filter'
|
7
|
+
require 'filterameter/filters/maximum_filter'
|
8
|
+
require 'filterameter/filters/minimum_filter'
|
5
9
|
require 'filterameter/filters/nested_filter'
|
6
10
|
require 'filterameter/filters/scope_filter'
|
7
11
|
|
@@ -16,19 +20,25 @@ module Filterameter
|
|
16
20
|
|
17
21
|
def build(declaration)
|
18
22
|
model = declaration.nested? ? model_from_association(declaration.association) : @model_class
|
19
|
-
filter = build_filter(model, declaration
|
23
|
+
filter = build_filter(model, declaration)
|
20
24
|
|
21
25
|
declaration.nested? ? Filterameter::Filters::NestedFilter.new(declaration.association, model, filter) : filter
|
22
26
|
end
|
23
27
|
|
24
28
|
private
|
25
29
|
|
26
|
-
def build_filter(model,
|
30
|
+
def build_filter(model, declaration) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
27
31
|
# 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)
|
32
|
+
if model.respond_to?(declaration.name) && !model.dangerous_class_method?(declaration.name)
|
33
|
+
Filterameter::Filters::ScopeFilter.new(declaration.name)
|
34
|
+
elsif declaration.partial_search?
|
35
|
+
Filterameter::Filters::MatchesFilter.new(declaration.name, declaration.partial_options)
|
36
|
+
elsif declaration.minimum?
|
37
|
+
Filterameter::Filters::MinimumFilter.new(model, declaration.name)
|
38
|
+
elsif declaration.maximum?
|
39
|
+
Filterameter::Filters::MaximumFilter.new(model, declaration.name)
|
30
40
|
else
|
31
|
-
Filterameter::Filters::AttributeFilter.new(name)
|
41
|
+
Filterameter::Filters::AttributeFilter.new(declaration.name)
|
32
42
|
end
|
33
43
|
end
|
34
44
|
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
# Filters
|
5
|
+
#
|
6
|
+
# Class FilterRegistry is a collection of the filters. It captures the filter declarations when classes are loaded,
|
7
|
+
# then uses the injected FilterFactory to build the filters on demand as they are needed.
|
8
|
+
class FilterRegistry
|
9
|
+
attr_reader :ranges
|
10
|
+
|
11
|
+
def initialize(filter_factory)
|
12
|
+
@filter_factory = filter_factory
|
13
|
+
@declarations = {}
|
14
|
+
@ranges = {}
|
15
|
+
@filters = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_filter(parameter_name, options)
|
19
|
+
@declarations[parameter_name.to_s] = Filterameter::FilterDeclaration.new(parameter_name, options).tap do |fd|
|
20
|
+
add_declarations_for_range(fd, options, parameter_name) if fd.range_enabled?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch(name)
|
25
|
+
name = name.to_s
|
26
|
+
@filters.fetch(name) do
|
27
|
+
raise Filterameter::Exceptions::UndeclaredParameterError, name unless @declarations.keys.include?(name)
|
28
|
+
|
29
|
+
@filters[name] = @filter_factory.build(@declarations[name])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def filter_declarations
|
34
|
+
@declarations.values
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# if range is enabled, then in addition to the attribute filter this also adds min and/or max filters
|
40
|
+
def add_declarations_for_range(attribute_declaration, options, parameter_name)
|
41
|
+
add_range_minimum(parameter_name, options) if attribute_declaration.range? || attribute_declaration.minimum?
|
42
|
+
add_range_maximum(parameter_name, options) if attribute_declaration.range? || attribute_declaration.maximum?
|
43
|
+
capture_range_declaration(parameter_name) if attribute_declaration.range?
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_range_minimum(parameter_name, options)
|
47
|
+
parameter_name_min = "#{parameter_name}_min"
|
48
|
+
@declarations[parameter_name_min] = Filterameter::FilterDeclaration.new(parameter_name_min,
|
49
|
+
options.merge(range: :min_only))
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_range_maximum(parameter_name, options)
|
53
|
+
parameter_name_min = "#{parameter_name}_max"
|
54
|
+
@declarations[parameter_name_min] = Filterameter::FilterDeclaration.new(parameter_name_min,
|
55
|
+
options.merge(range: :max_only))
|
56
|
+
end
|
57
|
+
|
58
|
+
def capture_range_declaration(name)
|
59
|
+
@ranges[name] = { min: "#{name}_min", max: "#{name}_max" }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
# = Declarative Filters
|
5
|
+
#
|
6
|
+
# Mixin Filterable provides class methods <tt>filter</tt> and <tt>filters</tt>.
|
7
|
+
module Filterable
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
# Declares a filter that can be read from the parameters and applied to the ActiveRecord query. The <tt>name</tt>
|
12
|
+
# identifies the name of the parameter and is the default value to determine the criteria to be applied. The name
|
13
|
+
# can be either an attribute or a scope.
|
14
|
+
#
|
15
|
+
# === Options
|
16
|
+
#
|
17
|
+
# [:name]
|
18
|
+
# Specify the attribute or scope name if the parameter name is not the same. The default value
|
19
|
+
# is the parameter name, so if the two match this can be left out.
|
20
|
+
#
|
21
|
+
# [:association]
|
22
|
+
# Specify the name of the association if the attribute or scope is nested.
|
23
|
+
#
|
24
|
+
# [:validates]
|
25
|
+
# Specify a validation if the parameter value should be validated. This uses ActiveModel validations;
|
26
|
+
# please review those for types of validations and usage.
|
27
|
+
#
|
28
|
+
# [:partial]
|
29
|
+
# Specify the partial option if the filter should do a partial search (SQL's `LIKE`). The partial
|
30
|
+
# option accepts a hash to specify the search behavior. Here are the available options:
|
31
|
+
# - match: anywhere (default), from_start, dynamic
|
32
|
+
# - case_sensitive: true, false (default)
|
33
|
+
#
|
34
|
+
# There are two shortcuts: : the partial option can be declared with `true`, which just uses the
|
35
|
+
# defaults; or the partial option can be declared with the match option directly,
|
36
|
+
# such as `partial: :from_start`.
|
37
|
+
#
|
38
|
+
# [:range]
|
39
|
+
# Specify a range option if the filter also allows ranges to be searched. The range option accepts
|
40
|
+
# the following options:
|
41
|
+
# - true: enables two additional parameters with attribute name plus suffixes <tt>_min</tt> and <tt>_max</tt>
|
42
|
+
# - :min_only: enables additional parameter with attribute name plus suffix <tt>_min</tt>
|
43
|
+
# - :max_only: enables additional parameter with attribute name plus suffix <tt>_max</tt>
|
44
|
+
def filter(name, options = {})
|
45
|
+
filter_coordinator.add_filter(name, options)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Declares a list of filters that can be read from the parameters and applied to the query. The name can be either
|
49
|
+
# an attribute or a scope. Declare filters individually with <tt>filter</tt> if more options are required.
|
50
|
+
def filters(*names)
|
51
|
+
names.each { |name| filter(name) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Filters
|
5
|
+
# = Arel Filter
|
6
|
+
#
|
7
|
+
# Class ArelFilter is a base class for arel queries. It does not implement <tt>apply</tt>.
|
8
|
+
class ArelFilter
|
9
|
+
def initialize(model, attribute_name)
|
10
|
+
@arel_attribute = model.arel_table[attribute_name]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Filters
|
5
|
+
# = Matches Filter
|
6
|
+
#
|
7
|
+
# Class MatchesFilter uses arel's `matches` to generate a LIKE query.
|
8
|
+
class MatchesFilter
|
9
|
+
def initialize(attribute_name, options)
|
10
|
+
@attribute_name = attribute_name
|
11
|
+
@prefix = options.match_anywhere? ? '%' : nil
|
12
|
+
@suffix = options.match_anywhere? || options.match_from_start? ? '%' : nil
|
13
|
+
@case_sensitive = options.case_sensitive?
|
14
|
+
end
|
15
|
+
|
16
|
+
def apply(query, value)
|
17
|
+
arel = query.arel_table[@attribute_name].matches("#{@prefix}#{value}#{@suffix}", false, @case_sensitive)
|
18
|
+
query.where(arel)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Filters
|
5
|
+
# = Maximum Filter
|
6
|
+
#
|
7
|
+
# Class MaximumFilter adds criteria for all values greater than or equal to a maximum.
|
8
|
+
class MaximumFilter < ArelFilter
|
9
|
+
def apply(query, value)
|
10
|
+
query.where(@arel_attribute.lteq(value))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Filters
|
5
|
+
# = Minimum Filter
|
6
|
+
#
|
7
|
+
# Class MinimumFilter adds criteria for all values greater than or equal to a minimum.
|
8
|
+
class MinimumFilter < ArelFilter
|
9
|
+
def apply(query, value)
|
10
|
+
query.where(@arel_attribute.gteq(value))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Options
|
4
|
+
# = Partial Options
|
5
|
+
#
|
6
|
+
# Class PartialOptions parses the options passed in as partial, then exposes those. Here are the options along with
|
7
|
+
# their valid values:
|
8
|
+
# - match: anywhere (default), from_start, dynamic
|
9
|
+
# - case_sensitive: true, false (default)
|
10
|
+
#
|
11
|
+
# Options may be specified by passing a hash with the option keys:
|
12
|
+
#
|
13
|
+
# partial: { match: :from_start, case_sensitive: true }
|
14
|
+
#
|
15
|
+
# There are two shortcuts: the partial option can be declared with `true`, which just uses the defaults; or the
|
16
|
+
# partial option can be declared with the match option directly, such as partial: :from_start.
|
17
|
+
class PartialOptions
|
18
|
+
VALID_OPTIONS = %i[match case_sensitive].freeze
|
19
|
+
VALID_MATCH_OPTIONS = %w[anywhere from_start dynamic].freeze
|
20
|
+
|
21
|
+
def initialize(options)
|
22
|
+
@match = 'anywhere'
|
23
|
+
@case_sensitive = false
|
24
|
+
|
25
|
+
case options
|
26
|
+
when TrueClass
|
27
|
+
nil
|
28
|
+
when Hash
|
29
|
+
evaluate_hash(options)
|
30
|
+
when String, Symbol
|
31
|
+
assign_match(options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def case_sensitive?
|
36
|
+
@case_sensitive
|
37
|
+
end
|
38
|
+
|
39
|
+
def match_anywhere?
|
40
|
+
@match == 'anywhere'
|
41
|
+
end
|
42
|
+
|
43
|
+
def match_from_start?
|
44
|
+
@match == 'from_start'
|
45
|
+
end
|
46
|
+
|
47
|
+
def match_dynamically?
|
48
|
+
@match == 'dynamic'
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def evaluate_hash(options)
|
54
|
+
options.assert_valid_keys(:match, :case_sensitive)
|
55
|
+
assign_match(options[:match]) if options.key?(:match)
|
56
|
+
assign_case_sensitive(options[:case_sensitive]) if options.key?(:case_sensitive)
|
57
|
+
end
|
58
|
+
|
59
|
+
def assign_match(value)
|
60
|
+
validate_match(value)
|
61
|
+
@match = value.to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate_match(value)
|
65
|
+
return if VALID_MATCH_OPTIONS.include? value.to_s
|
66
|
+
|
67
|
+
raise ArgumentError,
|
68
|
+
"Invalid match option for partial: #{value}. Valid options are #{VALID_MATCH_OPTIONS.to_sentence}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def assign_case_sensitive(value)
|
72
|
+
validate_case_sensitive(value)
|
73
|
+
@case_sensitive = value
|
74
|
+
end
|
75
|
+
|
76
|
+
def validate_case_sensitive(value)
|
77
|
+
return if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
78
|
+
|
79
|
+
raise ArgumentError, "Invalid case_sensitive option for partial: #{value}. Valid options are true and false."
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'active_model/attribute_assignment'
|
4
4
|
require 'active_model/validations'
|
5
|
+
require 'filterameter/validators/inclusion_validator'
|
5
6
|
|
6
7
|
module Filterameter
|
7
8
|
# = Parameters
|
@@ -9,6 +10,7 @@ module Filterameter
|
|
9
10
|
# Class Parameters is sub-classed to provide controller-specific validations.
|
10
11
|
class ParametersBase
|
11
12
|
include ActiveModel::Validations
|
13
|
+
include Filterameter::Validators
|
12
14
|
|
13
15
|
def self.build_sub_class(declarations)
|
14
16
|
Class.new(self).tap do |sub_class|
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
# = Query Builder
|
5
|
+
#
|
6
|
+
# Class Query Builder turns filter parameters into a query.
|
7
|
+
class QueryBuilder
|
8
|
+
def initialize(default_query, filter_registry)
|
9
|
+
@default_query = default_query
|
10
|
+
@registry = filter_registry
|
11
|
+
end
|
12
|
+
|
13
|
+
def build_query(filter_params, starting_query = nil)
|
14
|
+
valid_filters(filter_params)
|
15
|
+
.tap { |parameters| convert_min_and_max_to_range(parameters) }
|
16
|
+
.reduce(starting_query || @default_query) do |query, (name, value)|
|
17
|
+
add_filter_parameter_to_query(query, name, value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def add_filter_parameter_to_query(query, filter_name, parameter_value)
|
24
|
+
@registry.fetch(filter_name).apply(query, parameter_value)
|
25
|
+
rescue Filterameter::Exceptions::UndeclaredParameterError => e
|
26
|
+
handle_undeclared_parameter(e)
|
27
|
+
query
|
28
|
+
end
|
29
|
+
|
30
|
+
def valid_filters(filter_params)
|
31
|
+
remove_invalid_values(filter_params)
|
32
|
+
end
|
33
|
+
|
34
|
+
# if both min and max are present in the query parameters, replace with range
|
35
|
+
def convert_min_and_max_to_range(parameters)
|
36
|
+
@registry.ranges.each do |attribute_name, min_max_names|
|
37
|
+
next unless min_max_names.values.all? { |min_max_name| parameters[min_max_name].present? }
|
38
|
+
|
39
|
+
parameters[attribute_name] = Range.new(parameters.delete(min_max_names[:min]),
|
40
|
+
parameters.delete(min_max_names[:max]))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def handle_undeclared_parameter(exception)
|
45
|
+
action = Filterameter.configuration.action_on_undeclared_parameters
|
46
|
+
return unless action
|
47
|
+
|
48
|
+
case action
|
49
|
+
when :log
|
50
|
+
ActiveSupport::Notifications.instrument('undeclared_parameters.filterameter', key: exception.key)
|
51
|
+
when :raise
|
52
|
+
raise exception
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def remove_invalid_values(filter_params)
|
57
|
+
validator = validator_class.new(filter_params)
|
58
|
+
return filter_params if validator.valid?
|
59
|
+
|
60
|
+
case Filterameter.configuration.action_on_validation_failure
|
61
|
+
when :log
|
62
|
+
ActiveSupport::Notifications.instrument('validation_failure.filterameter', errors: validator.errors)
|
63
|
+
when :raise
|
64
|
+
raise Filterameter::Exceptions::ValidationError, validator.errors
|
65
|
+
end
|
66
|
+
|
67
|
+
filter_params.except(*validator.errors.attribute_names.map(&:to_s))
|
68
|
+
end
|
69
|
+
|
70
|
+
def validator_class
|
71
|
+
@validator_class ||= Filterameter::ParametersBase.build_sub_class(@registry.filter_declarations)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Filterameter
|
4
|
+
module Validators
|
5
|
+
# = Inclusion Validator
|
6
|
+
#
|
7
|
+
# Class InclusionValidator extends ActiveModel::Validations::InclusionValidator to enable validations of multiple
|
8
|
+
# values.
|
9
|
+
#
|
10
|
+
# == Example
|
11
|
+
#
|
12
|
+
# validates: { inclusion: { in: %w[Small Medium Large], allow_multiple_values: true } }
|
13
|
+
#
|
14
|
+
class InclusionValidator < ActiveModel::Validations::InclusionValidator
|
15
|
+
def validate_each(record, attribute, value)
|
16
|
+
return super unless allow_multiple_values?
|
17
|
+
|
18
|
+
# any? just provides a mechanism to stop after first error
|
19
|
+
Array.wrap(value).any? { |v| super(record, attribute, v) }
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def allow_multiple_values?
|
25
|
+
@options.fetch(:allow_multiple_values, false)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/filterameter/version.rb
CHANGED
data/lib/filterameter.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: filterameter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Todd Kummer
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-12-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -24,20 +24,62 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 5.2.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: guard
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.16'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.16'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: guard-rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.7'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.7'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: guard-rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.5.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.5.0
|
27
69
|
- !ruby/object:Gem::Dependency
|
28
70
|
name: pg
|
29
71
|
requirement: !ruby/object:Gem::Requirement
|
30
72
|
requirements:
|
31
|
-
- - "
|
73
|
+
- - "~>"
|
32
74
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
75
|
+
version: 1.1.4
|
34
76
|
type: :development
|
35
77
|
prerelease: false
|
36
78
|
version_requirements: !ruby/object:Gem::Requirement
|
37
79
|
requirements:
|
38
|
-
- - "
|
80
|
+
- - "~>"
|
39
81
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
82
|
+
version: 1.1.4
|
41
83
|
- !ruby/object:Gem::Dependency
|
42
84
|
name: rspec-rails
|
43
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -58,28 +100,42 @@ dependencies:
|
|
58
100
|
requirements:
|
59
101
|
- - "~>"
|
60
102
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
103
|
+
version: 1.22.3
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 1.22.3
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-rails
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 2.8.1
|
62
118
|
type: :development
|
63
119
|
prerelease: false
|
64
120
|
version_requirements: !ruby/object:Gem::Requirement
|
65
121
|
requirements:
|
66
122
|
- - "~>"
|
67
123
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
124
|
+
version: 2.8.1
|
69
125
|
- !ruby/object:Gem::Dependency
|
70
126
|
name: simplecov
|
71
127
|
requirement: !ruby/object:Gem::Requirement
|
72
128
|
requirements:
|
73
|
-
- - "
|
129
|
+
- - "~>"
|
74
130
|
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
131
|
+
version: '0.18'
|
76
132
|
type: :development
|
77
133
|
prerelease: false
|
78
134
|
version_requirements: !ruby/object:Gem::Requirement
|
79
135
|
requirements:
|
80
|
-
- - "
|
136
|
+
- - "~>"
|
81
137
|
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
138
|
+
version: '0.18'
|
83
139
|
description: Enable filter parameters to be declared in controllers.
|
84
140
|
email:
|
85
141
|
- todd@rockridgesolutions.com
|
@@ -92,7 +148,10 @@ files:
|
|
92
148
|
- Rakefile
|
93
149
|
- lib/filterameter.rb
|
94
150
|
- lib/filterameter/configuration.rb
|
95
|
-
- lib/filterameter/
|
151
|
+
- lib/filterameter/coordinators/base.rb
|
152
|
+
- lib/filterameter/coordinators/controller_coordinator.rb
|
153
|
+
- lib/filterameter/coordinators/query_coordinator.rb
|
154
|
+
- lib/filterameter/declarative_controller_filters.rb
|
96
155
|
- lib/filterameter/declarative_filters.rb
|
97
156
|
- lib/filterameter/exceptions.rb
|
98
157
|
- lib/filterameter/exceptions/cannot_determine_model_error.rb
|
@@ -100,19 +159,27 @@ files:
|
|
100
159
|
- lib/filterameter/exceptions/validation_error.rb
|
101
160
|
- lib/filterameter/filter_declaration.rb
|
102
161
|
- lib/filterameter/filter_factory.rb
|
162
|
+
- lib/filterameter/filter_registry.rb
|
163
|
+
- lib/filterameter/filterable.rb
|
164
|
+
- lib/filterameter/filters/arel_filter.rb
|
103
165
|
- lib/filterameter/filters/attribute_filter.rb
|
104
166
|
- lib/filterameter/filters/conditional_scope_filter.rb
|
167
|
+
- lib/filterameter/filters/matches_filter.rb
|
168
|
+
- lib/filterameter/filters/maximum_filter.rb
|
169
|
+
- lib/filterameter/filters/minimum_filter.rb
|
105
170
|
- lib/filterameter/filters/nested_filter.rb
|
106
171
|
- lib/filterameter/filters/scope_filter.rb
|
107
172
|
- lib/filterameter/log_subscriber.rb
|
173
|
+
- lib/filterameter/options/partial_options.rb
|
108
174
|
- lib/filterameter/parameters_base.rb
|
175
|
+
- lib/filterameter/query_builder.rb
|
176
|
+
- lib/filterameter/validators/inclusion_validator.rb
|
109
177
|
- lib/filterameter/version.rb
|
110
|
-
- lib/tasks/filterameter_tasks.rake
|
111
178
|
homepage: https://github.com/RockSolt/filterameter
|
112
179
|
licenses:
|
113
180
|
- MIT
|
114
181
|
metadata: {}
|
115
|
-
post_install_message:
|
182
|
+
post_install_message:
|
116
183
|
rdoc_options: []
|
117
184
|
require_paths:
|
118
185
|
- lib
|
@@ -128,7 +195,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
195
|
version: '0'
|
129
196
|
requirements: []
|
130
197
|
rubygems_version: 3.0.8
|
131
|
-
signing_key:
|
198
|
+
signing_key:
|
132
199
|
specification_version: 4
|
133
200
|
summary: Declarative Filter Parameters for Rails Controllers
|
134
201
|
test_files: []
|
@@ -1,110 +0,0 @@
|
|
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, starting_query)
|
44
|
-
valid_filters(filter_params).reduce(starting_query || 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
|