filter_param 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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