filterameter 0.1.3 → 0.1.4

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: d8110e021be072bc1f37dd324c42e590e3c91a94fbadd934fdc0758f1f85d700
4
- data.tar.gz: 7ee31589a1fb66717647a1085658ab4dd5b9ba7deb58d847102fc72be1d9dffe
3
+ metadata.gz: 0baada12bc6ad382e146635629a89c47f935e92f1e4c72597889440305c46ebf
4
+ data.tar.gz: 427ecf17bd6458fbe76af104ab5b1eb32a4edb2adc3c0b73ac4fb329f1eba0f9
5
5
  SHA512:
6
- metadata.gz: 83117da59a74ba92bf255cdb8ccd025f071f5549dfeb315af5894c12dc8829928fef6aae0eb6d94eaec7abfd5f2a85aa1d085bd7b3ec26d0b476d0506febd7a4
7
- data.tar.gz: 3f777e21518ce18d2f62f36912ba3b532839a9f5d03bc73dd7796648d1fcec81992701b07ad639dbe021f7765ab5d5baa66d4d82bf92edc7264a2463df4044ff
6
+ metadata.gz: 24ce1e8ca752befea919c57f5e79af75d80f77569d0171a052b1b1a14c65af5d0e6ad234127767e3f42bddb4d058feafdee58130cf38102cd4b10eef2bdc3ca9
7
+ data.tar.gz: 5e0ad2d3348d84df2d7198bc38611f0a3bb8ea52ce4882c61ca0289f48239afc11ec49f4b293fc79810cda25a92f8e236ad00b73bb3d4769e4d4912b99939651
data/README.md CHANGED
@@ -1,3 +1,6 @@
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
+
1
4
  # Filterameter
2
5
  Declarative Filter Parameters for Rails Controllers.
3
6
 
@@ -57,6 +60,24 @@ The `match` options defines where you are searching (which then controls where t
57
60
  - from_start: adds a wildcard at the end, for example 'blue%'
58
61
  - dynamic: adds no wildcards; this enables the client to fully control the search string
59
62
 
63
+ #### range
64
+ 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.)
65
+
66
+ Here are the available options:
67
+ - true: enable ranges, minimum values, and/or maximum values
68
+ - min_only: enables minimum values
69
+ - max_only: enables maximum values
70
+
71
+ 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>.
72
+
73
+ ```ruby
74
+ filter :price, range: true
75
+ filter :approved_at, range: :min_only
76
+ filter :sale_price, range: :max_only
77
+ ```
78
+
79
+ In the first example, query parameters could include <tt>price</tt>, <tt>price_min</tt>, and <tt>price_max</tt>.
80
+
60
81
  ### Configuring Controllers
61
82
 
62
83
  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:
@@ -25,6 +25,7 @@ module Filterameter
25
25
  @controller_name = controller_name
26
26
  @controller_path = controller_path
27
27
  @declarations = {}
28
+ @ranges = {}
28
29
  @filters = Hash.new { |hash, key| hash[key] = filter_factory.build(@declarations[key]) }
29
30
  end
30
31
 
@@ -33,7 +34,10 @@ module Filterameter
33
34
  end
34
35
 
35
36
  def add_filter(parameter_name, options)
36
- @declarations[parameter_name.to_s] = Filterameter::FilterDeclaration.new(parameter_name, options)
37
+ @declarations[parameter_name.to_s] =
38
+ Filterameter::FilterDeclaration.new(parameter_name, options).tap do |fd|
39
+ add_declarations_for_range(fd, options, parameter_name) if fd.range_enabled?
40
+ end
37
41
  end
38
42
 
39
43
  def query_variable_name
@@ -41,7 +45,9 @@ module Filterameter
41
45
  end
42
46
 
43
47
  def build_query(filter_params, starting_query)
44
- valid_filters(filter_params).reduce(starting_query || model_class.all) do |query, (name, value)|
48
+ valid_filters(filter_params)
49
+ .tap { |parameters| convert_min_and_max_to_range(parameters) }
50
+ .reduce(starting_query || model_class.all) do |query, (name, value)|
45
51
  @filters[name].apply(query, value)
46
52
  end
47
53
  end
@@ -66,6 +72,16 @@ module Filterameter
66
72
  )
67
73
  end
68
74
 
75
+ # if both min and max are present in the query parameters, replace with range
76
+ def convert_min_and_max_to_range(parameters)
77
+ @ranges.each do |attribute_name, min_max_names|
78
+ next unless min_max_names.values.all? { |min_max_name| parameters[min_max_name].present? }
79
+
80
+ parameters[attribute_name] = Range.new(parameters.delete(min_max_names[:min]),
81
+ parameters.delete(min_max_names[:max]))
82
+ end
83
+ end
84
+
69
85
  def remove_undeclared_filters(filter_params)
70
86
  filter_params.slice(*declared_parameter_names).tap do |declared_parameters|
71
87
  handle_undeclared_parameters(filter_params) if declared_parameters.size != filter_params.size
@@ -106,5 +122,27 @@ module Filterameter
106
122
  def validator_class
107
123
  @validator_class ||= Filterameter::ParametersBase.build_sub_class(@declarations.values)
108
124
  end
125
+
126
+ # if range is enabled, then in addition to the attribute filter this also adds min and/or max filters
127
+ def add_declarations_for_range(attribute_declaration, options, parameter_name)
128
+ add_range_minimum(parameter_name, options) if attribute_declaration.range? || attribute_declaration.minimum?
129
+ add_range_maximum(parameter_name, options) if attribute_declaration.range? || attribute_declaration.maximum?
130
+ capture_range_declaration(parameter_name) if attribute_declaration.range?
131
+ end
132
+
133
+ def add_range_minimum(parameter_name, options)
134
+ @declarations["#{parameter_name}_min"] = Filterameter::FilterDeclaration.new(parameter_name,
135
+ options.merge(range: :min_only))
136
+ end
137
+
138
+ def add_range_maximum(parameter_name, options)
139
+ @declarations["#{parameter_name}_max"] = Filterameter::FilterDeclaration.new(parameter_name,
140
+ options.merge(range: :max_only))
141
+ end
142
+
143
+ # memoizing these makes it easier to spot and replace ranges in query parameters; see convert_min_and_max_to_range
144
+ def capture_range_declaration(name)
145
+ @ranges[name] = { min: "#{name}_min", max: "#{name}_max" }
146
+ end
109
147
  end
110
148
  end
@@ -19,6 +19,39 @@ module Filterameter
19
19
  controller_filters.query_variable_name = query_variable_name
20
20
  end
21
21
 
22
+ # Declares a filter that can be read from the parameters and applied to the ActiveRecord query. The <tt>name</tt>
23
+ # identifies the name of the parameter and is the default value to determine the criteria to be applied. The name
24
+ # can be either an attribute or a scope.
25
+ #
26
+ # === Options
27
+ #
28
+ # [:name]
29
+ # Specify the attribute or scope name if the parameter name is not the same. The default value
30
+ # is the parameter name, so if the two match this can be left out.
31
+ #
32
+ # [:association]
33
+ # Specify the name of the association if the attribute or scope is nested.
34
+ #
35
+ # [:validates]
36
+ # Specify a validation if the parameter value should be validated. This uses ActiveModel validations;
37
+ # please review those for types of validations and usage.
38
+ #
39
+ # [:partial]
40
+ # Specify the partial option if the filter should do a partial search (SQL's `LIKE`). The partial
41
+ # option accepts a hash to specify the search behavior. Here are the available options:
42
+ # - match: anywhere (default), from_start, dynamic
43
+ # - case_sensitive: true, false (default)
44
+ #
45
+ # There are two shortcuts: : the partial option can be declared with `true`, which just uses the
46
+ # defaults; or the partial option can be declared with the match option directly,
47
+ # such as `partial: :from_start`.
48
+ #
49
+ # [:range]
50
+ # Specify a range option if the filter also allows ranges to be searched. The range option accepts
51
+ # the following options:
52
+ # - true: enables two additional parameters with attribute name plus suffixes <tt>_min</tt> and <tt>_max</tt>
53
+ # - :min_only: enables additional parameter with attribute name plus suffix <tt>_min</tt>
54
+ # - :max_only: enables additional parameter with attribute name plus suffix <tt>_max</tt>
22
55
  def filter(name, options = {})
23
56
  controller_filters.add_filter(name, options)
24
57
  end
@@ -8,6 +8,8 @@ module Filterameter
8
8
  #
9
9
  # Class FilterDeclaration captures the filter declaration within the controller.
10
10
  class FilterDeclaration
11
+ VALID_RANGE_OPTIONS = [true, :min_only, :max_only].freeze
12
+
11
13
  attr_reader :name, :parameter_name, :association, :validations
12
14
 
13
15
  def initialize(parameter_name, options)
@@ -19,6 +21,7 @@ module Filterameter
19
21
  @filter_on_empty = options.fetch(:filter_on_empty, false)
20
22
  @validations = Array.wrap(options[:validates])
21
23
  @raw_partial_options = options.fetch(:partial, false)
24
+ @raw_range = options[:range]
22
25
  end
23
26
 
24
27
  def nested?
@@ -41,10 +44,33 @@ module Filterameter
41
44
  @partial_options ||= @raw_partial_options ? Options::PartialOptions.new(@raw_partial_options) : nil
42
45
  end
43
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
+
44
63
  private
45
64
 
46
65
  def validate_options(options)
47
- options.assert_valid_keys(:name, :association, :filter_on_empty, :validates, :partial)
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}"
48
74
  end
49
75
  end
50
76
  end
@@ -1,8 +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'
5
6
  require 'filterameter/filters/matches_filter'
7
+ require 'filterameter/filters/maximum_filter'
8
+ require 'filterameter/filters/minimum_filter'
6
9
  require 'filterameter/filters/nested_filter'
7
10
  require 'filterameter/filters/scope_filter'
8
11
 
@@ -24,12 +27,16 @@ module Filterameter
24
27
 
25
28
  private
26
29
 
27
- def build_filter(model, declaration)
30
+ def build_filter(model, declaration) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
28
31
  # checking dangerous_class_method? excludes any names that cannot be scope names, such as "name"
29
32
  if model.respond_to?(declaration.name) && !model.dangerous_class_method?(declaration.name)
30
33
  Filterameter::Filters::ScopeFilter.new(declaration.name)
31
34
  elsif declaration.partial_search?
32
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)
33
40
  else
34
41
  Filterameter::Filters::AttributeFilter.new(declaration.name)
35
42
  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,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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Filterameter
4
- VERSION = '0.1.3'
4
+ VERSION = '0.1.4'
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.1.3
4
+ version: 0.1.4
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: 2020-05-09 00:00:00.000000000 Z
11
+ date: 2020-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -142,21 +142,23 @@ files:
142
142
  - lib/filterameter/exceptions/validation_error.rb
143
143
  - lib/filterameter/filter_declaration.rb
144
144
  - lib/filterameter/filter_factory.rb
145
+ - lib/filterameter/filters/arel_filter.rb
145
146
  - lib/filterameter/filters/attribute_filter.rb
146
147
  - lib/filterameter/filters/conditional_scope_filter.rb
147
148
  - lib/filterameter/filters/matches_filter.rb
149
+ - lib/filterameter/filters/maximum_filter.rb
150
+ - lib/filterameter/filters/minimum_filter.rb
148
151
  - lib/filterameter/filters/nested_filter.rb
149
152
  - lib/filterameter/filters/scope_filter.rb
150
153
  - lib/filterameter/log_subscriber.rb
151
154
  - lib/filterameter/options/partial_options.rb
152
155
  - lib/filterameter/parameters_base.rb
153
156
  - lib/filterameter/version.rb
154
- - lib/tasks/filterameter_tasks.rake
155
157
  homepage: https://github.com/RockSolt/filterameter
156
158
  licenses:
157
159
  - MIT
158
160
  metadata: {}
159
- post_install_message:
161
+ post_install_message:
160
162
  rdoc_options: []
161
163
  require_paths:
162
164
  - lib
@@ -172,7 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
172
174
  version: '0'
173
175
  requirements: []
174
176
  rubygems_version: 3.0.8
175
- signing_key:
177
+ signing_key:
176
178
  specification_version: 4
177
179
  summary: Declarative Filter Parameters for Rails Controllers
178
180
  test_files: []
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
- # desc "Explaining what the task does"
3
- # task :filterameter do
4
- # # Task goes here
5
- # end