filter_param 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +138 -0
- data/lib/filter_param/ast/attribute.rb +15 -0
- data/lib/filter_param/ast/expressions.rb +37 -0
- data/lib/filter_param/ast/group.rb +13 -0
- data/lib/filter_param/ast/literal.rb +26 -0
- data/lib/filter_param/ast/literals/boolean.rb +38 -0
- data/lib/filter_param/ast/literals/date.rb +33 -0
- data/lib/filter_param/ast/literals/date_time.rb +36 -0
- data/lib/filter_param/ast/literals/decimal.rb +29 -0
- data/lib/filter_param/ast/literals/integer.rb +39 -0
- data/lib/filter_param/ast/literals/null.rb +19 -0
- data/lib/filter_param/ast/literals/string.rb +43 -0
- data/lib/filter_param/ast/node.rb +15 -0
- data/lib/filter_param/ast/scope.rb +12 -0
- data/lib/filter_param/definition.rb +157 -0
- data/lib/filter_param/field.rb +45 -0
- data/lib/filter_param/operator.rb +46 -0
- data/lib/filter_param/operators/and.rb +13 -0
- data/lib/filter_param/operators/case_insensitive_equal.rb +16 -0
- data/lib/filter_param/operators/contains.rb +18 -0
- data/lib/filter_param/operators/ends_with.rb +18 -0
- data/lib/filter_param/operators/equal.rb +21 -0
- data/lib/filter_param/operators/field_filter_operator.rb +40 -0
- data/lib/filter_param/operators/greater_than.rb +16 -0
- data/lib/filter_param/operators/greater_than_equal.rb +16 -0
- data/lib/filter_param/operators/group.rb +17 -0
- data/lib/filter_param/operators/less_than.rb +16 -0
- data/lib/filter_param/operators/less_than_equal.rb +16 -0
- data/lib/filter_param/operators/not.rb +16 -0
- data/lib/filter_param/operators/not_equal.rb +19 -0
- data/lib/filter_param/operators/or.rb +13 -0
- data/lib/filter_param/operators/present.rb +21 -0
- data/lib/filter_param/operators/starts_with.rb +18 -0
- data/lib/filter_param/parser.rb +144 -0
- data/lib/filter_param/scope.rb +24 -0
- data/lib/filter_param/transformer.rb +37 -0
- data/lib/filter_param/transpiler.rb +114 -0
- data/lib/filter_param/version.rb +5 -0
- data/lib/filter_param.rb +70 -0
- 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
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,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,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,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,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
|