filterameter 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f44f6d30fd92e15bc7652df41d75e2d620740fb19f146cbb253274460c3686f2
4
- data.tar.gz: 84109580ba0079787e3bcc0e20054a71c2eb2ae7efc512d511124172f19c69fc
3
+ metadata.gz: 8b2ac8a6c211408669122ab5bf2a131ade58736fa145508305f5ebe3dc9c0bdb
4
+ data.tar.gz: 9216ddecfedab830f9a3a82cf9c1936f040e64664b23b2d16a4ccd71f1f4923e
5
5
  SHA512:
6
- metadata.gz: 4b8606c20e87f96b25876da9b670f555b1c95b40bc5fa13368174d1e4264ea1a535a93ea714111171710de12e301876aa59081f327a9cbd8e4f5a6735f2c0406
7
- data.tar.gz: 1f848be8b42e68845cebaabadd04b0e9019dab54d9cfdfa2628048215cb6ed5c5e37360fa99a473208cf4539a5963a065f0790a75ecc5d795e45037ae35f7347
6
+ metadata.gz: ad640c098660b98a33f54378f16dced7d3c2dd278c6d6b6db120d2026cb7f03bda5f0d5033653e697af153eff4bdb3c9c84e39d263a18eedba5075098b7354ef
7
+ data.tar.gz: 3e775bbe1d6ed769bdfb1a83ac3f4a74c69128517a5bf6ea3fc140e50a8b025214d87d544c9263ad4bf0495f34d477f7a2128836f81e7b6a82d4ec037b9df938
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  Declarative filter parameters provide clean and clear filters for Rails controllers.
8
8
 
9
9
  ## Usage
10
- Declare filters in 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.
10
+ Declare filters in controllers to increase readability and reduce boilerplate code. Filters can be declared for attributes or scopes, either directly on the model or on an associated model. Validations can also be assigned.
11
11
 
12
12
  ```ruby
13
13
  filter :color
@@ -28,13 +28,25 @@ If the name of the parameter is different than the name of the attribute or scop
28
28
  filter :status, name: :current_status
29
29
  ```
30
30
 
31
+ This option can also be helpful with nested filters so that the query parameter can be prefixed with the model name. See the `association` option for an example.
32
+
31
33
  #### association
32
- 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:
34
+ If the attribute or scope is nested, it can be referenced by naming the association. For example, if the manager_id attribute lives on an employee's department record, use the following:
33
35
 
34
36
  ```ruby
35
37
  filter :manager_id, association: :department
36
38
  ```
37
39
 
40
+ The attribute or scope can be nested more than one level. Declare the filter with an array specifying the associations in order. For example, if an employee belongs to a department and a department belongs to a business unit, use the following to query on the business unit name:
41
+
42
+ ```ruby
43
+ filter :business_unit_name, name: :name, association: [:department, :business_unit]
44
+ ```
45
+
46
+ If an association is a `has_many` [the distinct method](https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-distinct) is called on the query.
47
+
48
+ _Limitation:_ If there is more than one association to the same table _and_ both associations can be part of the query, then you cannot use a nested filter directly. Instead, build a scope that disambiguates the associations then build a filter against that scope.
49
+
38
50
  #### validates
39
51
  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:
40
52
 
@@ -16,7 +16,7 @@ module Filterameter
16
16
 
17
17
  validate_options(options)
18
18
  @name = options.fetch(:name, parameter_name).to_s
19
- @association = options[:association]
19
+ @association = Array.wrap(options[:association]).presence
20
20
  @filter_on_empty = options.fetch(:filter_on_empty, false)
21
21
  @validations = Array.wrap(options[:validates])
22
22
  @raw_partial_options = options.fetch(:partial, false)
@@ -24,7 +24,7 @@ module Filterameter
24
24
  end
25
25
 
26
26
  def nested?
27
- @association.present?
27
+ !@association.nil?
28
28
  end
29
29
 
30
30
  def validations?
@@ -10,14 +10,23 @@ module Filterameter
10
10
  end
11
11
 
12
12
  def build(declaration)
13
- model = declaration.nested? ? model_from_association(declaration.association) : @model_class
14
- filter = build_filter(model, declaration)
15
-
16
- declaration.nested? ? Filterameter::Filters::NestedFilter.new(declaration.association, model, filter) : filter
13
+ if declaration.nested?
14
+ build_nested_filter(declaration)
15
+ else
16
+ build_filter(@model_class, declaration)
17
+ end
17
18
  end
18
19
 
19
20
  private
20
21
 
22
+ def build_nested_filter(declaration)
23
+ model = model_from_association(declaration.association)
24
+ filter = build_filter(model, declaration)
25
+ clazz = filter_class(declaration.association)
26
+
27
+ clazz.new(declaration.association, model, filter)
28
+ end
29
+
21
30
  def build_filter(model, declaration) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
22
31
  # checking dangerous_class_method? excludes any names that cannot be scope names, such as "name"
23
32
  if model.respond_to?(declaration.name) && !model.dangerous_class_method?(declaration.name)
@@ -33,9 +42,28 @@ module Filterameter
33
42
  end
34
43
  end
35
44
 
45
+ def filter_class(association_names)
46
+ if any_collections?(association_names)
47
+ Filters::NestedCollectionFilter
48
+ else
49
+ Filters::NestedFilter
50
+ end
51
+ end
52
+
53
+ def any_collections?(association_names)
54
+ association_names.reduce(@model_class) do |model, name|
55
+ association = model.reflect_on_association(name)
56
+ return true if association.collection?
57
+
58
+ association.klass
59
+ end
60
+
61
+ false
62
+ end
63
+
36
64
  # TODO: rescue then raise custom error with cause
37
65
  def model_from_association(association)
38
- [association].flatten.reduce(@model_class) { |memo, name| memo.reflect_on_association(name).klass }
66
+ association.flatten.reduce(@model_class) { |memo, name| memo.reflect_on_association(name).klass }
39
67
  # rescue StandardError => e
40
68
  end
41
69
  end
@@ -51,8 +51,8 @@ module Filterameter
51
51
  end
52
52
 
53
53
  def add_range_maximum(parameter_name, options)
54
- parameter_name_min = "#{parameter_name}_max"
55
- @declarations[parameter_name_min] = Filterameter::FilterDeclaration.new(parameter_name_min,
54
+ parameter_name_max = "#{parameter_name}_max"
55
+ @declarations[parameter_name_max] = Filterameter::FilterDeclaration.new(parameter_name_max,
56
56
  options.merge(range: :max_only))
57
57
  end
58
58
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Filterameter
4
+ module Filters
5
+ # = Nested Collection Filter
6
+ #
7
+ # Class NestedCollectionFilter joins the nested table(s), merges the filter to the association's model, then makes
8
+ # the results distinct.
9
+ class NestedCollectionFilter < NestedFilter
10
+ def apply(*)
11
+ super.distinct
12
+ end
13
+ end
14
+ end
15
+ end
@@ -6,8 +6,8 @@ module Filterameter
6
6
  #
7
7
  # Class NestedFilter joins the nested table(s) then merges the filter to the association's model.
8
8
  class NestedFilter
9
- def initialize(joins_values, association_model, attribute_filter)
10
- @joins_values = joins_values
9
+ def initialize(association_names, association_model, attribute_filter)
10
+ @joins_values = build_joins_values_argument(association_names)
11
11
  @association_model = association_model
12
12
  @attribute_filter = attribute_filter
13
13
  end
@@ -16,6 +16,20 @@ module Filterameter
16
16
  query.joins(@joins_values)
17
17
  .merge(@attribute_filter.apply(@association_model, value))
18
18
  end
19
+
20
+ private
21
+
22
+ def build_joins_values_argument(association_names)
23
+ return association_names.first if association_names.size == 1
24
+
25
+ convert_to_nested_hash(association_names)
26
+ end
27
+
28
+ def convert_to_nested_hash(association_names)
29
+ {}.tap do |nested_hash|
30
+ association_names.reduce(nested_hash) { |memo, name| memo.store(name, {}) }
31
+ end
32
+ end
19
33
  end
20
34
  end
21
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Filterameter
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Todd Kummer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-14 00:00:00.000000000 Z
11
+ date: 2024-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -114,14 +114,14 @@ dependencies:
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: 1.60.2
117
+ version: '1.64'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: 1.60.2
124
+ version: '1.64'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: rubocop-packaging
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -142,14 +142,14 @@ dependencies:
142
142
  requirements:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: 2.23.1
145
+ version: '2.25'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: 2.23.1
152
+ version: '2.25'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: simplecov
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -164,7 +164,8 @@ dependencies:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0.18'
167
- description: Enable filter parameters to be declared in query classes or controllers.
167
+ description: Declare filters in Rails controllers to increase readability and reduce
168
+ boilerplate code.
168
169
  email:
169
170
  - todd@rockridgesolutions.com
170
171
  executables: []
@@ -191,6 +192,7 @@ files:
191
192
  - lib/filterameter/filters/matches_filter.rb
192
193
  - lib/filterameter/filters/maximum_filter.rb
193
194
  - lib/filterameter/filters/minimum_filter.rb
195
+ - lib/filterameter/filters/nested_collection_filter.rb
194
196
  - lib/filterameter/filters/nested_filter.rb
195
197
  - lib/filterameter/filters/scope_filter.rb
196
198
  - lib/filterameter/log_subscriber.rb