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.
- 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
|