filter_param 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE +21 -0
  4. data/README.md +138 -0
  5. data/lib/filter_param/ast/attribute.rb +15 -0
  6. data/lib/filter_param/ast/expressions.rb +37 -0
  7. data/lib/filter_param/ast/group.rb +13 -0
  8. data/lib/filter_param/ast/literal.rb +26 -0
  9. data/lib/filter_param/ast/literals/boolean.rb +38 -0
  10. data/lib/filter_param/ast/literals/date.rb +33 -0
  11. data/lib/filter_param/ast/literals/date_time.rb +36 -0
  12. data/lib/filter_param/ast/literals/decimal.rb +29 -0
  13. data/lib/filter_param/ast/literals/integer.rb +39 -0
  14. data/lib/filter_param/ast/literals/null.rb +19 -0
  15. data/lib/filter_param/ast/literals/string.rb +43 -0
  16. data/lib/filter_param/ast/node.rb +15 -0
  17. data/lib/filter_param/ast/scope.rb +12 -0
  18. data/lib/filter_param/definition.rb +157 -0
  19. data/lib/filter_param/field.rb +45 -0
  20. data/lib/filter_param/operator.rb +46 -0
  21. data/lib/filter_param/operators/and.rb +13 -0
  22. data/lib/filter_param/operators/case_insensitive_equal.rb +16 -0
  23. data/lib/filter_param/operators/contains.rb +18 -0
  24. data/lib/filter_param/operators/ends_with.rb +18 -0
  25. data/lib/filter_param/operators/equal.rb +21 -0
  26. data/lib/filter_param/operators/field_filter_operator.rb +40 -0
  27. data/lib/filter_param/operators/greater_than.rb +16 -0
  28. data/lib/filter_param/operators/greater_than_equal.rb +16 -0
  29. data/lib/filter_param/operators/group.rb +17 -0
  30. data/lib/filter_param/operators/less_than.rb +16 -0
  31. data/lib/filter_param/operators/less_than_equal.rb +16 -0
  32. data/lib/filter_param/operators/not.rb +16 -0
  33. data/lib/filter_param/operators/not_equal.rb +19 -0
  34. data/lib/filter_param/operators/or.rb +13 -0
  35. data/lib/filter_param/operators/present.rb +21 -0
  36. data/lib/filter_param/operators/starts_with.rb +18 -0
  37. data/lib/filter_param/parser.rb +144 -0
  38. data/lib/filter_param/scope.rb +24 -0
  39. data/lib/filter_param/transformer.rb +37 -0
  40. data/lib/filter_param/transpiler.rb +114 -0
  41. data/lib/filter_param/version.rb +5 -0
  42. data/lib/filter_param.rb +70 -0
  43. metadata +143 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ff8a89676339b2fb5deed26115ac897d11e77b554ddd10156c746b8313e9626e
4
+ data.tar.gz: 7f1f7f22bd1f106fe4818edf3dcc454f34897a0e0f9b73a9110f026424872008
5
+ SHA512:
6
+ metadata.gz: f2cc08c9fdbae1de993f41b9d4204592a1bf80fd35586e45eb684cb5cd1478977e0d55a35b53d9b7d14b49514db93268d8baa9bf5a26be59ea8b4bfe2aa41c5a
7
+ data.tar.gz: 1aec98d47ae4177b0061fcaec187ce894cfda9d9d288b6fd0d5c15e9327619139066767fce65489bf0dccc5e84e5b29eae069c10f8f4ae2c0a2a94e4fd08f27b
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-06-15
4
+
5
+ - Initial release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Jayson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # FilterParam
2
+
3
+ ### Record Filtering for apps built on Rails/ActiveRecord
4
+
5
+ Quickly implement record filtering in your APIs using a filter expression inspired by [SCIM Query](https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.2):
6
+
7
+ ```ruby
8
+ https://{some origin}/users?filter="first_name eq 'John' and last_name pr and
9
+ not (active eq false and (birth_date gt '1991-01-01' or birth_date eq null))"
10
+ ```
11
+
12
+ **TL;DR** See [ sample usage for Rails here ](#rails-usage).
13
+
14
+ ## Features
15
+
16
+ * Transpilation of the filter expression into SQL
17
+ * Whitelisting of allowed filter attributes
18
+ * Column name aliasing / expose a different attribute name in the API
19
+ * Pre-processing of filter values/literals
20
+ * Type validation of filter values (e.g., date and datetime literals should be in standard ISO 8601 format)
21
+ * Allows custom filter operators
22
+ * Expression grouping
23
+ * Supports **MySQL**, **PostgreSQL**, and **SQLite**
24
+ * Supports **Rails 4.2** and above
25
+
26
+
27
+ ## Installation
28
+
29
+ Add this line to your application's Gemfile:
30
+
31
+ ```ruby
32
+ gem 'filter_param'
33
+ ```
34
+
35
+ And then execute:
36
+
37
+ ```sh
38
+ bundle install
39
+ ```
40
+
41
+ Or install it yourself as:
42
+
43
+ ```sh
44
+ gem install filter_param
45
+ ```
46
+
47
+ ## Usage
48
+ ### Basic
49
+
50
+ #### 1. Whitelist/define the filter fields
51
+
52
+ ```ruby
53
+ fitler_param = FilterParam.define do
54
+ field :first_name, type: :string
55
+ field :last_name, rename: "family_name"
56
+ field :birth_date, type: :date
57
+ field :member_since, type: :datetime
58
+ field :active, type: :boolean
59
+ end
60
+ ```
61
+
62
+
63
+ This is is equivalent to:
64
+
65
+ ```ruby
66
+ filter_param = FilterParam::Definition.new
67
+ .field(:first_name, type: :string)
68
+ .field(:last_name, rename: "family_name")
69
+ .field(:birth_date, type: :date)
70
+ .field(:member_since, type: :datetime)
71
+ .field(:active, type: :boolean)
72
+ ```
73
+
74
+ `field` method accepts the filter field name as the first argument. Any other configuration such as `:type` follows the name.
75
+
76
+ #### 2. Filter records using a filter expression
77
+
78
+ The `filter!` method accepts an `ActiveRecord_Relation` and the filter expression string from your API's request parameter. This method then transpiles the filter expression into SQL and returns a new `ActiveRecord_Relation` with the SQL conditions applied.
79
+
80
+ ```ruby
81
+ rel = filter_param.filter!(User.all, "first_name eq 'John' and last_name pr and not (active eq false and (birth_date gt '1991-01-
82
+ 01' or birth_date eq null))")
83
+ ```
84
+
85
+ To see the SQL that will be executed in the ActiveRecord relation:
86
+
87
+ ```ruby
88
+ rel.to_sql
89
+ => "SELECT \"users\".* FROM \"users\" WHERE (first_name = 'John' AND
90
+ (family_name IS NOT NULL AND TRIM(family_name) != '') AND
91
+ NOT (active = 0 AND (birth_date > '1991-01-01' OR birth_date IS NULL)))"
92
+ ```
93
+
94
+ ### Errors
95
+
96
+ | Class | Description |
97
+ | ----------- | ----------- |
98
+ | `FilterParam::UnknownField` | A filter field in the given filter expression is not whitelisted in the filter definition. |
99
+ | `FilterParam::ParseError` | The given filter expression can't be parsed possibly due to malformed expression or syntax issue. |
100
+ | `FilterParam::InvalidLiteral` | A filter field value in the given filter expression is invalie (e.g., date and datetime should be in ISO 8601 format) |
101
+ | `FilterParam::ExpressionError` | Generic error caused by the given filter expression. |
102
+ | `FilterParam::UnknownType` | Configured `:type` of a filter field in the definition is invalid. |
103
+
104
+ ## Development
105
+
106
+ 1. If testing/developing for MySQL or PG, create the database first:<br/>
107
+
108
+ ###### MySQL
109
+ ```sh
110
+ mysql> CREATE DATABASE filter_param;
111
+ ```
112
+
113
+ ###### PostgreSQL
114
+ ```sh
115
+ $ createdb filter_param
116
+ ```
117
+
118
+ 2. After checking out the repo, run `bin/setup` to install dependencies.
119
+ 3. Run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. Use the environment variables below to target the database<br/><br/>
120
+
121
+ By default, SQLite and the latest stable Rails version are used in tests and console. Refer to the environment variables below to change this:
122
+
123
+ | Environment Variable | Values | Example |
124
+ | ----------- | ----------- |----------- |
125
+ | `DB_ADAPTER` | **Default: :sqlite**. `sqlite`,`mysql2`, or `postgresql` | ```DB_ADAPTER=postgresql bundle exec rspec```<br/><br/> ```DB_ADAPTER=postgresql ./bin/console``` |
126
+ | `RAILS_VERSION` | **Default: 7-0** <br/><br/> `4-2`,`5-0`,`5-1`,`5-2`,`6-0`,`6-1`,`7-0` |```RAILS_VERSION=5-2 ./bin/setup```<br/><br/>```RAILS_VERSION=5-2 bundle exec rspec```<br/><br/> ```RAILS_VERSION=5-2 ./bin/console```|
127
+
128
+
129
+ <br/><br/>
130
+ To install this gem onto your local machine, run `bundle exec rake install`.
131
+
132
+ ## Contributing
133
+
134
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jsonb-uy/rotulus.
135
+
136
+ ## License
137
+
138
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,15 @@
1
+ module FilterParam
2
+ module AST
3
+ class Attribute < Node
4
+ attr_reader :name
5
+
6
+ def initialize(name)
7
+ super()
8
+
9
+ @name = name
10
+ end
11
+
12
+ alias to_s name
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ module FilterParam
2
+ module AST
3
+ module Expressions
4
+ class Expression < Node
5
+ attr_reader :operator, :operands
6
+
7
+ def initialize(operator, *operands)
8
+ super()
9
+
10
+ @operator = operator.to_sym
11
+ @operands = operands
12
+ end
13
+ end
14
+
15
+ class UnaryExpression < Expression
16
+ attr_reader :operand
17
+
18
+ def initialize(operator, operand)
19
+ super(operator, operand)
20
+
21
+ @operand = operand
22
+ end
23
+ end
24
+
25
+ class BinaryExpression < Expression
26
+ attr_reader :left_operand, :right_operand
27
+
28
+ def initialize(operator, left_operand, right_operand)
29
+ super(operator, left_operand, right_operand)
30
+
31
+ @left_operand = left_operand
32
+ @right_operand = right_operand
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,13 @@
1
+ module FilterParam
2
+ module AST
3
+ class Group < Expressions::UnaryExpression
4
+ attr_reader :expression
5
+
6
+ def initialize(expression)
7
+ super(:group, expression)
8
+
9
+ @expression = expression
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ module FilterParam
2
+ module AST
3
+ class Literal < Node
4
+ attr_accessor :value
5
+
6
+ def initialize(value = nil)
7
+ @value = value
8
+ end
9
+
10
+ def type_cast(type)
11
+ return self if type.blank?
12
+
13
+ cast_method = "to_#{type}"
14
+ return send(cast_method) if respond_to?(cast_method, true)
15
+
16
+ raise InvalidLiteral.new("Cannot cast '#{value}' to #{type}")
17
+ end
18
+
19
+ private
20
+
21
+ def visit_method
22
+ :visit_literal
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,38 @@
1
+ module FilterParam
2
+ module AST
3
+ module Literals
4
+ class Boolean < Literal
5
+ def initialize(value)
6
+ @value = (value.to_s == "true")
7
+ end
8
+
9
+ def data_type
10
+ :boolean
11
+ end
12
+
13
+ private_class_method :new
14
+
15
+ TRUE = new("true")
16
+ FALSE = new("false")
17
+
18
+ private
19
+
20
+ def to_boolean
21
+ self
22
+ end
23
+
24
+ def to_string
25
+ Literals::String.new(value)
26
+ end
27
+
28
+ def to_integer
29
+ Literals::Integer.new(value ? 1 : 0)
30
+ end
31
+
32
+ def to_decimal
33
+ Literals::Decimal.new(value ? 1.0 : 0.0)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ require "date"
2
+
3
+ module FilterParam
4
+ module AST
5
+ module Literals
6
+ class Date < Literal
7
+ def initialize(value)
8
+ @value = ::Date.iso8601(value.to_s)
9
+ rescue ::Date::Error
10
+ raise FilterParam::InvalidLiteral.new("Invalid ISO8601 Date: #{value}")
11
+ end
12
+
13
+ def data_type
14
+ :date
15
+ end
16
+
17
+ private
18
+
19
+ def to_string
20
+ Literals::String.new(value)
21
+ end
22
+
23
+ def to_date
24
+ self
25
+ end
26
+
27
+ def to_datetime
28
+ Literals::DateTime.new(value)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ require "date"
2
+
3
+ module FilterParam
4
+ module AST
5
+ module Literals
6
+ class DateTime < Date
7
+ def initialize(value)
8
+ @raw_value = value
9
+ @value = ::DateTime.iso8601(value.to_s)
10
+ rescue ::Date::Error
11
+ raise FilterParam::InvalidLiteral.new("Invalid ISO8601 Datetime: #{value}")
12
+ end
13
+
14
+ def data_type
15
+ :datetime
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :raw_value
21
+
22
+ def to_string
23
+ Literals::String.new(raw_value)
24
+ end
25
+
26
+ def to_date
27
+ Literals::Date.new(value)
28
+ end
29
+
30
+ def to_datetime
31
+ self
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ require "bigdecimal"
2
+
3
+ module FilterParam
4
+ module AST
5
+ module Literals
6
+ class Decimal < Integer
7
+ def initialize(value)
8
+ @value = BigDecimal(value.to_s)
9
+ rescue ArgumentError
10
+ raise InvalidLiteral.new("Invalid Decimal: #{value}")
11
+ end
12
+
13
+ def data_type
14
+ :decimal
15
+ end
16
+
17
+ private
18
+
19
+ def to_integer
20
+ Literals::Integer.new(value.to_i)
21
+ end
22
+
23
+ def to_decimal
24
+ self
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ module FilterParam
2
+ module AST
3
+ module Literals
4
+ class Integer < Literal
5
+ def initialize(value)
6
+ whole_num = value.to_s.split(".").first
7
+
8
+ @value = Integer(whole_num)
9
+ rescue ArgumentError
10
+ raise InvalidLiteral.new("Invalid Integer: #{value}")
11
+ end
12
+
13
+ def data_type
14
+ :integer
15
+ end
16
+
17
+ private
18
+
19
+ def to_boolean
20
+ return Literals::Boolean::FALSE if value.zero?
21
+
22
+ Literals::Boolean::TRUE
23
+ end
24
+
25
+ def to_string
26
+ Literals::String.new(value)
27
+ end
28
+
29
+ def to_integer
30
+ self
31
+ end
32
+
33
+ def to_decimal
34
+ Literals::Decimal.new(value)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,19 @@
1
+ require "singleton"
2
+
3
+ module FilterParam
4
+ module AST
5
+ module Literals
6
+ class Null < Literal
7
+ include Singleton
8
+
9
+ def data_type
10
+ :null
11
+ end
12
+
13
+ def type_cast(type)
14
+ self
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ module FilterParam
2
+ module AST
3
+ module Literals
4
+ class String < Literal
5
+ def initialize(value)
6
+ @value = value.to_s
7
+ end
8
+
9
+ def data_type
10
+ :string
11
+ end
12
+
13
+ private
14
+
15
+ def to_boolean
16
+ return Literals::Boolean::TRUE if value.downcase == "true"
17
+
18
+ Literals::Boolean::FALSE
19
+ end
20
+
21
+ def to_string
22
+ self
23
+ end
24
+
25
+ def to_integer
26
+ Literals::Integer.new(value)
27
+ end
28
+
29
+ def to_decimal
30
+ Literals::Decimal.new(value)
31
+ end
32
+
33
+ def to_date
34
+ Literals::Date.new(value)
35
+ end
36
+
37
+ def to_datetime
38
+ Literals::DateTime.new(value)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ module FilterParam
2
+ module AST
3
+ class Node
4
+ def accept(visitor)
5
+ visitor.send(visit_method, self)
6
+ end
7
+
8
+ private
9
+
10
+ def visit_method
11
+ "visit_#{self.class.name.demodulize.underscore}".to_sym
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module FilterParam
2
+ module AST
3
+ class Scope < Node
4
+ attr_reader :name, :args
5
+
6
+ def initialize(name, args)
7
+ @name = name.to_s
8
+ @args = args
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,157 @@
1
+ module FilterParam
2
+ # FilterParam definition that whitelists the columns that are allowed to
3
+ # filtered (i.e. used in SQL WHERE condition) and the allowed scopes.
4
+ class Definition
5
+ # Allows whitelisting columns using a block
6
+ #
7
+ # @param [Proc] block Field definition block
8
+ #
9
+ # @return [self] Definition instance
10
+ def define(&block)
11
+ raise ArgumentError.new("Missing block") unless block_given?
12
+
13
+ instance_eval(&block)
14
+
15
+ self
16
+ end
17
+
18
+ # Whitelist a column
19
+ #
20
+ # @param [String, Symbol] name column name
21
+ # @param [Hash] options column options:
22
+ # * type [Symbol] expected field type:
23
+ # :string (default), :int, :decimal :boolean, :date, :datetime
24
+ # * rename [String, Proc] rename field in the formatted output.
25
+ # This can be a Proc code block that receives the :name as argument and
26
+ # returns a transformed field name.
27
+ # * value [Proc] pre-process literal operand values. This receives the :value
28
+ # argument parsed from the expression string and returns a transformed field value.
29
+ #
30
+ # @return [self] Definition instance
31
+ def field(name, **options)
32
+ name = name.to_s
33
+ return if name.blank?
34
+
35
+ fields_hash[name] = Field.new(name, options[:type], options)
36
+
37
+ self
38
+ end
39
+
40
+ # Whitelist multiple columns with the same column options.
41
+ #
42
+ # @param [Array<String, Symbol>] names list of column names
43
+ # @param [Hash] options column configuration options
44
+ #
45
+ # @see #field
46
+ #
47
+ # @return [self] Definition instance
48
+ #
49
+ def fields(*names, **options)
50
+ restrict_string_rename!(options[:rename])
51
+
52
+ names.each { |name| field(name, **options) }
53
+
54
+ self
55
+ end
56
+
57
+ # Whitelist a scope name
58
+ #
59
+ # @param [String, Symbol] name scope name. ar_relation passed to `#filter!` must expose this scope.
60
+ # @param [Hash] options column options:
61
+ # * rename [String, Proc] rename to actual scope name.
62
+ # This can be a Proc code block that receives the :name as argument and
63
+ # returns a transformed scope name.
64
+ def scope(name, **options)
65
+ name = name.to_s
66
+ return if name.blank?
67
+
68
+ scopes_hash[name] = Scope.new(name, options)
69
+
70
+ self
71
+ end
72
+
73
+ # Whitelist multiple scope names with the same scope options.
74
+ #
75
+ # @param [Array<String, Symbol>] names list of scope names
76
+ # @param [Hash] options scope configuration options
77
+ #
78
+ # @see #scope
79
+ #
80
+ # @return [self] Definition instance
81
+ #
82
+ def scopes(*names, **options)
83
+ restrict_string_rename!(options[:rename])
84
+
85
+ names.each { |name| scope(name, **options) }
86
+
87
+ self
88
+ end
89
+
90
+ # Filters an :ar_relation by the filter :expression
91
+ #
92
+ # @param [ActiveRecord::Relation] ar_relation Relation to filter
93
+ # @param [String] expression Filter expression.
94
+ #
95
+ def filter!(ar_relation, expression)
96
+ transpiler = Transpiler.new(ar_relation, self)
97
+
98
+ ar_relation.where(
99
+ transpiler.transpile!(expression)
100
+ )
101
+ end
102
+
103
+ # Returns the declared Field instance
104
+ #
105
+ # @param [String, Symbol] field_name
106
+ #
107
+ # @return [Field]
108
+ def find_field!(field_name)
109
+ field = fields_hash[field_name.to_s].presence
110
+ return field if field
111
+
112
+ raise UnknownField.new("Unknown field: '#{field_name}'")
113
+ end
114
+
115
+ # Returns the declared Scope instance
116
+ #
117
+ # @param [String, Symbol] scope_name
118
+ #
119
+ # @return [Field]
120
+ def find_scope!(scope_name)
121
+ scope = scopes_hash[scope_name.to_s].presence
122
+ return scope if scope
123
+
124
+ raise UnknownScope.new("Unknown scope: '#{scope_name}'")
125
+ end
126
+
127
+ # Returns the declared Field names
128
+ #
129
+ # @return [Array<String>]
130
+ def field_names
131
+ fields_hash.keys
132
+ end
133
+
134
+ # Returns the declared Scope names
135
+ #
136
+ # @return [Array<String>]
137
+ def scope_names
138
+ scopes_hash.keys
139
+ end
140
+
141
+ private
142
+
143
+ def fields_hash
144
+ @fields_hash ||= {}
145
+ end
146
+
147
+ def scopes_hash
148
+ @scopes_hash ||= {}
149
+ end
150
+
151
+ def restrict_string_rename!(rename)
152
+ return if rename.blank? || rename.is_a?(Proc)
153
+
154
+ raise ArgumentError.new(":rename should be a Proc")
155
+ end
156
+ end
157
+ end