kiroshi 0.2.0 → 0.3.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 +4 -4
- data/.codacy.yml +13 -0
- data/.markdownlint.json +6 -0
- data/README.md +73 -5
- data/lib/kiroshi/filter.rb +27 -10
- data/lib/kiroshi/filter_query/exact.rb +3 -3
- data/lib/kiroshi/filter_query/like.rb +30 -6
- data/lib/kiroshi/filter_query.rb +4 -4
- data/lib/kiroshi/filter_runner.rb +4 -4
- data/lib/kiroshi/filters/class_methods.rb +36 -33
- data/lib/kiroshi/filters.rb +27 -15
- data/lib/kiroshi/version.rb +1 -1
- data/lib/kiroshi.rb +0 -1
- data/spec/lib/kiroshi/filter_query/exact_spec.rb +40 -0
- data/spec/lib/kiroshi/filter_query/like_spec.rb +52 -7
- data/spec/lib/kiroshi/filter_runner_spec.rb +42 -1
- data/spec/lib/kiroshi/filters/class_methods_spec.rb +45 -1
- data/spec/lib/kiroshi/filters_spec.rb +211 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/db/schema.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5748e560809cbfa28ea2b9d6ad06578ad1f4d0c67038bed6aca1a1978f27aa7c
|
4
|
+
data.tar.gz: b15225c3e02cbb634ec4fec69c240cf7bb623e8d89d9fa216c0c5521e98f08a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e7ad5fe225a10041b672270a6f4407985c4be7b9761f9ae2ad5bfcebd45318c3caa03633dc066faf845e5b818fc36e8408e5991abf125d394b29b8af96ade5a3
|
7
|
+
data.tar.gz: b7ba3b33bda40558ebc76e9a1cf4c9fd803fb4881e28eac16f38e64f5562ca6748bbdb206a12a416ab19376f619be26d52986339bedd9b929dc33b65fa68a36a
|
data/.codacy.yml
ADDED
data/.markdownlint.json
ADDED
data/README.md
CHANGED
@@ -7,16 +7,16 @@
|
|
7
7
|
|
8
8
|
## Yard Documentation
|
9
9
|
|
10
|
-
[https://www.rubydoc.info/gems/kiroshi/0.
|
10
|
+
[https://www.rubydoc.info/gems/kiroshi/0.3.0](https://www.rubydoc.info/gems/kiroshi/0.3.0)
|
11
11
|
|
12
12
|
Kiroshi has been designed to make filtering ActiveRecord queries easier
|
13
13
|
by providing a flexible and reusable filtering system. It allows you to
|
14
14
|
define filter sets that can be applied to any ActiveRecord scope,
|
15
15
|
supporting both exact matches and partial matching using SQL LIKE operations.
|
16
16
|
|
17
|
-
Current Release: [0.
|
17
|
+
Current Release: [0.3.0](https://github.com/darthjee/kiroshi/tree/0.3.0)
|
18
18
|
|
19
|
-
[Next release](https://github.com/darthjee/kiroshi/compare/0.
|
19
|
+
[Next release](https://github.com/darthjee/kiroshi/compare/0.3.0...master)
|
20
20
|
|
21
21
|
## Installation
|
22
22
|
|
@@ -67,6 +67,9 @@ Kiroshi supports two types of matching:
|
|
67
67
|
- `:exact` - Exact match (default)
|
68
68
|
- `:like` - Partial match using SQL LIKE
|
69
69
|
|
70
|
+
<details>
|
71
|
+
<summary>Specifying filter types</summary>
|
72
|
+
|
70
73
|
```ruby
|
71
74
|
class UserFilters < Kiroshi::Filters
|
72
75
|
filter_by :email, match: :like # Partial matching
|
@@ -78,11 +81,15 @@ filters = UserFilters.new(email: 'admin', role: 'moderator')
|
|
78
81
|
filtered_users = filters.apply(User.all)
|
79
82
|
# Generates: WHERE email LIKE '%admin%' AND role = 'moderator'
|
80
83
|
```
|
84
|
+
</details>
|
81
85
|
|
82
86
|
#### Advanced Examples
|
83
87
|
|
84
88
|
##### Multiple Filter Types
|
85
89
|
|
90
|
+
<details>
|
91
|
+
<summary>Applying only some filters</summary>
|
92
|
+
|
86
93
|
```ruby
|
87
94
|
class ProductFilters < Kiroshi::Filters
|
88
95
|
filter_by :name, match: :like
|
@@ -96,9 +103,13 @@ filters = ProductFilters.new(name: 'laptop', category: 'electronics')
|
|
96
103
|
products = filters.apply(Product.all)
|
97
104
|
# Only name and category filters are applied, price and brand are ignored
|
98
105
|
```
|
106
|
+
</details>
|
99
107
|
|
100
108
|
##### Controller Integration
|
101
109
|
|
110
|
+
<details>
|
111
|
+
<summary>Using filters in Rails controllers</summary>
|
112
|
+
|
102
113
|
```ruby
|
103
114
|
# URL: /documents?filter[name]=report&filter[status]=published&filter[author]=john
|
104
115
|
class DocumentsController < ApplicationController
|
@@ -125,9 +136,13 @@ class DocumentFilters < Kiroshi::Filters
|
|
125
136
|
filter_by :author, match: :like
|
126
137
|
end
|
127
138
|
```
|
139
|
+
</details>
|
128
140
|
|
129
141
|
##### Nested Resource Filtering
|
130
142
|
|
143
|
+
<details>
|
144
|
+
<summary>Filtering nested resources</summary>
|
145
|
+
|
131
146
|
```ruby
|
132
147
|
# URL: /users/123/articles?filter[title]=ruby&filter[published]=true&filter[tag]=tutorial
|
133
148
|
class ArticleFilters < Kiroshi::Filters
|
@@ -146,9 +161,13 @@ def article_filters
|
|
146
161
|
ArticleFilters.new(params[:filter]&.permit(:title, :published, :tag))
|
147
162
|
end
|
148
163
|
```
|
164
|
+
</details>
|
149
165
|
|
150
166
|
##### Joined Tables and Table Qualification
|
151
167
|
|
168
|
+
<details>
|
169
|
+
<summary>Working with joined tables</summary>
|
170
|
+
|
152
171
|
When working with joined tables that have columns with the same name, you can specify which table to filter on using the `table` parameter:
|
153
172
|
|
154
173
|
```ruby
|
@@ -165,9 +184,13 @@ filters = DocumentFilters.new(tag_name: 'ruby', status: 'published')
|
|
165
184
|
filtered_documents = filters.apply(scope)
|
166
185
|
# Generates: WHERE tags.name LIKE '%ruby%' AND documents.status = 'published'
|
167
186
|
```
|
187
|
+
</details>
|
168
188
|
|
169
189
|
###### Table Qualification Examples
|
170
190
|
|
191
|
+
<details>
|
192
|
+
<summary>Advanced table qualification scenarios</summary>
|
193
|
+
|
171
194
|
```ruby
|
172
195
|
# Filter documents by tag name and document status
|
173
196
|
class DocumentTagFilters < Kiroshi::Filters
|
@@ -201,9 +224,54 @@ result = filters.apply(scope)
|
|
201
224
|
```
|
202
225
|
|
203
226
|
The `table` parameter accepts both symbols and strings, and helps resolve column name ambiguity in complex joined queries.
|
227
|
+
</details>
|
228
|
+
|
229
|
+
##### Custom Column Mapping
|
230
|
+
|
231
|
+
<details>
|
232
|
+
<summary>Using different filter keys from database columns</summary>
|
233
|
+
|
234
|
+
Sometimes you may want to use a different filter key name from the database column name. The `column` parameter allows you to specify which database column to query while keeping a descriptive filter key:
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
class UserFilters < Kiroshi::Filters
|
238
|
+
filter_by :full_name, column: :name, match: :like # Filter key 'full_name' queries 'name' column
|
239
|
+
filter_by :user_email, column: :email, match: :like # Filter key 'user_email' queries 'email' column
|
240
|
+
filter_by :account_status, column: :status # Filter key 'account_status' queries 'status' column
|
241
|
+
end
|
242
|
+
|
243
|
+
filters = UserFilters.new(full_name: 'John', user_email: 'admin', account_status: 'active')
|
244
|
+
result = filters.apply(User.all)
|
245
|
+
# Generates: WHERE name LIKE '%John%' AND email LIKE '%admin%' AND status = 'active'
|
246
|
+
```
|
247
|
+
</details>
|
248
|
+
|
249
|
+
###### Column Mapping with Table Qualification
|
250
|
+
|
251
|
+
<details>
|
252
|
+
<summary>Combining column mapping with table qualification</summary>
|
253
|
+
|
254
|
+
You can combine `column` and `table` parameters for complex scenarios:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
class DocumentFilters < Kiroshi::Filters
|
258
|
+
filter_by :author_name, column: :name, table: :users, match: :like # Filter key 'author_name' queries 'users.name'
|
259
|
+
filter_by :doc_title, column: :title, table: :documents, match: :like # Filter key 'doc_title' queries 'documents.title'
|
260
|
+
filter_by :tag_label, column: :name, table: :tags, match: :like # Filter key 'tag_label' queries 'tags.name'
|
261
|
+
end
|
262
|
+
|
263
|
+
scope = Document.joins(:user, :tags)
|
264
|
+
filters = DocumentFilters.new(author_name: 'John', doc_title: 'Ruby', tag_label: 'tutorial')
|
265
|
+
result = filters.apply(scope)
|
266
|
+
# Generates: WHERE users.name LIKE '%John%' AND documents.title LIKE '%Ruby%' AND tags.name LIKE '%tutorial%'
|
267
|
+
```
|
268
|
+
|
269
|
+
This feature is particularly useful when:
|
270
|
+
- Creating more descriptive filter parameter names for APIs
|
271
|
+
- Avoiding naming conflicts between filter keys and existing method names
|
272
|
+
- Building user-friendly filter interfaces with intuitive parameter names
|
273
|
+
</details>
|
204
274
|
|
205
275
|
## API Reference
|
206
276
|
|
207
277
|
Kiroshi provides a simple, clean API focused on the `Kiroshi::Filters` class. Individual filters are handled internally and don't require direct interaction in most use cases.
|
208
|
-
|
209
|
-
For detailed API documentation, see the [YARD documentation](https://www.rubydoc.info/gems/kiroshi/0.2.0).
|
data/lib/kiroshi/filter.rb
CHANGED
@@ -23,14 +23,14 @@ module Kiroshi
|
|
23
23
|
#
|
24
24
|
# @since 0.1.0
|
25
25
|
class Filter
|
26
|
-
attr_reader :
|
26
|
+
attr_reader :filter_key, :match, :table_name
|
27
27
|
|
28
|
-
# @!method
|
28
|
+
# @!method filter_key
|
29
29
|
# @api private
|
30
30
|
#
|
31
|
-
# Returns the
|
31
|
+
# Returns the filter key name to identify this filter
|
32
32
|
#
|
33
|
-
# @return [Symbol] the
|
33
|
+
# @return [Symbol] the filter key name to identify this filter
|
34
34
|
|
35
35
|
# @!method match
|
36
36
|
# @api private
|
@@ -42,15 +42,16 @@ module Kiroshi
|
|
42
42
|
# @!method table_name
|
43
43
|
# @api private
|
44
44
|
#
|
45
|
-
# Returns the table name to qualify the
|
45
|
+
# Returns the table name to qualify the column
|
46
46
|
#
|
47
47
|
# @return [String, String, nil] the table name or nil if not specified
|
48
48
|
|
49
49
|
# Creates a new Filter instance
|
50
50
|
#
|
51
|
-
# @param
|
51
|
+
# @param filter_key [Symbol] the filter key name to identify this filter
|
52
52
|
# @param match [Symbol] the matching type, defaults to :exact
|
53
|
-
# @param table [String, Symbol, nil] the table name to qualify the
|
53
|
+
# @param table [String, Symbol, nil] the table name to qualify the column, defaults to nil
|
54
|
+
# @param column [Symbol] the column name to use in database queries, defaults to filter_key
|
54
55
|
# @option match [Symbol] :exact performs exact matching (default)
|
55
56
|
# @option match [Symbol] :like performs partial matching using SQL LIKE
|
56
57
|
#
|
@@ -63,11 +64,27 @@ module Kiroshi
|
|
63
64
|
# @example Creating a filter with table qualification
|
64
65
|
# filter = Kiroshi::Filter.new(:name, table: 'documents')
|
65
66
|
#
|
66
|
-
# @
|
67
|
-
|
68
|
-
|
67
|
+
# @example Creating a filter with custom column name
|
68
|
+
# filter = Kiroshi::Filter.new(:user_name, column: :full_name)
|
69
|
+
#
|
70
|
+
# @since 0.3.0
|
71
|
+
def initialize(filter_key, match: :exact, table: nil, column: nil)
|
72
|
+
@filter_key = filter_key
|
69
73
|
@match = match
|
70
74
|
@table_name = table
|
75
|
+
@column = column
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the column name to use in database queries
|
79
|
+
#
|
80
|
+
# Uses lazy initialization - defaults to filter_key if no column was specified.
|
81
|
+
# This allows for flexible column mapping while maintaining backward compatibility.
|
82
|
+
#
|
83
|
+
# @return [Symbol] the column name to use in database queries
|
84
|
+
#
|
85
|
+
# @since 0.3.0
|
86
|
+
def column
|
87
|
+
@column ||= filter_key
|
71
88
|
end
|
72
89
|
|
73
90
|
# Applies the filter to the given scope
|
@@ -13,14 +13,14 @@ module Kiroshi
|
|
13
13
|
# @example Applying exact match query
|
14
14
|
# query = Kiroshi::FilterQuery::Exact.new(filter_runner)
|
15
15
|
# query.apply
|
16
|
-
# # Generates: WHERE
|
16
|
+
# # Generates: WHERE table_name.column = value
|
17
17
|
#
|
18
18
|
# @since 0.1.1
|
19
19
|
class Exact < FilterQuery
|
20
20
|
# Applies exact match filtering to the scope
|
21
21
|
#
|
22
22
|
# This method generates a WHERE clause with exact equality matching
|
23
|
-
# for the filter's
|
23
|
+
# for the filter's column and value.
|
24
24
|
#
|
25
25
|
# @return [ActiveRecord::Relation] the filtered scope with exact match
|
26
26
|
#
|
@@ -31,7 +31,7 @@ module Kiroshi
|
|
31
31
|
#
|
32
32
|
# @since 0.1.1
|
33
33
|
def apply
|
34
|
-
scope.where(table_name => {
|
34
|
+
scope.where(table_name => { column => value })
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
@@ -13,7 +13,7 @@ module Kiroshi
|
|
13
13
|
# @example Applying LIKE match query
|
14
14
|
# query = Kiroshi::FilterQuery::Like.new(filter_runner)
|
15
15
|
# query.apply
|
16
|
-
# # Generates: WHERE table_name.
|
16
|
+
# # Generates: WHERE "table_name"."column" LIKE '%value%'
|
17
17
|
#
|
18
18
|
# @since 0.1.1
|
19
19
|
class Like < FilterQuery
|
@@ -28,14 +28,38 @@ module Kiroshi
|
|
28
28
|
# @example Applying LIKE match
|
29
29
|
# query = Like.new(filter_runner)
|
30
30
|
# query.apply
|
31
|
-
# # Generates: WHERE documents.name LIKE '%ruby%'
|
31
|
+
# # Generates: WHERE "documents"."name" LIKE '%ruby%'
|
32
32
|
#
|
33
33
|
# @since 0.1.1
|
34
34
|
def apply
|
35
|
-
scope.where(
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
scope.where(sql_query, "%#{value}%")
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# @api private
|
41
|
+
# @private
|
42
|
+
#
|
43
|
+
# Builds the SQL query string for LIKE matching
|
44
|
+
#
|
45
|
+
# This method constructs the SQL fragment with proper table and column
|
46
|
+
# qualification using double quotes to avoid conflicts with reserved words.
|
47
|
+
#
|
48
|
+
# @return [String] the SQL query fragment for LIKE matching
|
49
|
+
#
|
50
|
+
# @example Generated SQL fragment
|
51
|
+
# sql_query # => ' # Constructs the parameterized SQL query string for column matching
|
52
|
+
#
|
53
|
+
# @return [String] The SQL query string with placeholders
|
54
|
+
# @example
|
55
|
+
# sql_query
|
56
|
+
# # Returns: '"table_name"."column" LIKE ?''
|
57
|
+
#
|
58
|
+
# @since 0.3.0
|
59
|
+
def sql_query
|
60
|
+
<<~SQL.squish
|
61
|
+
"#{table_name}"."#{column}" LIKE ?
|
62
|
+
SQL
|
39
63
|
end
|
40
64
|
end
|
41
65
|
end
|
data/lib/kiroshi/filter_query.rb
CHANGED
@@ -94,7 +94,7 @@ module Kiroshi
|
|
94
94
|
#
|
95
95
|
# @return [Kiroshi::FilterRunner] the filter runner instance
|
96
96
|
|
97
|
-
delegate :scope, :
|
97
|
+
delegate :scope, :column, :table_name, :value, to: :filter_runner
|
98
98
|
|
99
99
|
# @!method scope
|
100
100
|
# @api private
|
@@ -103,12 +103,12 @@ module Kiroshi
|
|
103
103
|
#
|
104
104
|
# @return [ActiveRecord::Relation] the scope
|
105
105
|
|
106
|
-
# @!method
|
106
|
+
# @!method column
|
107
107
|
# @api private
|
108
108
|
#
|
109
|
-
# Returns the
|
109
|
+
# Returns the column name to use in database queries
|
110
110
|
#
|
111
|
-
# @return [Symbol] the
|
111
|
+
# @return [Symbol] the column name
|
112
112
|
|
113
113
|
# @!method table_name
|
114
114
|
# @api private
|
@@ -136,16 +136,16 @@ module Kiroshi
|
|
136
136
|
#
|
137
137
|
# @return [Kiroshi::Filter] the filter configuration
|
138
138
|
|
139
|
-
delegate :
|
139
|
+
delegate :column, to: :filter
|
140
140
|
delegate :table_name, to: :scope, prefix: true
|
141
141
|
delegate :table_name, to: :filter, prefix: true
|
142
142
|
|
143
|
-
# @!method
|
143
|
+
# @!method column
|
144
144
|
# @api private
|
145
145
|
#
|
146
|
-
# Returns the
|
146
|
+
# Returns the column name to use in database queries
|
147
147
|
#
|
148
|
-
# @return [Symbol] the
|
148
|
+
# @return [Symbol] the column name to use in database queries
|
149
149
|
|
150
150
|
# @!method scope_table_name
|
151
151
|
# @api private
|
@@ -21,8 +21,8 @@ module Kiroshi
|
|
21
21
|
# end
|
22
22
|
#
|
23
23
|
# @example Accessing filter configurations
|
24
|
-
# DocumentFilters.filter_configs.keys # => [
|
25
|
-
# DocumentFilters.filter_configs[
|
24
|
+
# DocumentFilters.filter_configs.keys # => ["name", "status", "category"]
|
25
|
+
# DocumentFilters.filter_configs["name"].match # => :like
|
26
26
|
#
|
27
27
|
# @since 0.2.0
|
28
28
|
# @author darthjee
|
@@ -38,37 +38,39 @@ module Kiroshi
|
|
38
38
|
# options to handle complex database queries with joins and ambiguous
|
39
39
|
# column names.
|
40
40
|
#
|
41
|
-
# @overload filter_by(
|
42
|
-
# @param
|
41
|
+
# @overload filter_by(filter_key, **options)
|
42
|
+
# @param filter_key [Symbol] the filter key name to identify this filter
|
43
43
|
# @param options [Hash] additional options passed to {Filter#initialize}
|
44
44
|
# @option options [Symbol] :match (:exact) the matching type
|
45
45
|
# - +:exact+ for exact matching (default)
|
46
46
|
# - +:like+ for partial matching using SQL LIKE with wildcards
|
47
|
-
# @option options [String, Symbol, nil] :table (nil) the table name to qualify the
|
47
|
+
# @option options [String, Symbol, nil] :table (nil) the table name to qualify the column
|
48
48
|
# when dealing with joined tables that have conflicting column names
|
49
|
+
# @option options [Symbol, nil] :column (nil) the column name to use in database queries,
|
50
|
+
# defaults to filter_key if not specified
|
49
51
|
#
|
50
52
|
# @return (see Filters.filter_by)
|
51
53
|
# @example (see Filters.filter_by)
|
52
54
|
# @note (see Filters.filter_by)
|
53
55
|
# @see (see Filters.filter_by)
|
54
56
|
# @since (see Filters.filter_by)
|
55
|
-
def filter_by(
|
56
|
-
Filter.new(
|
57
|
-
filter_configs[
|
57
|
+
def filter_by(filter_key, **options)
|
58
|
+
Filter.new(filter_key, **options).tap do |filter|
|
59
|
+
filter_configs[filter_key.to_s] = filter
|
58
60
|
end
|
59
61
|
end
|
60
62
|
|
61
63
|
# @api private
|
62
|
-
# Returns the filter configuration for a specific
|
64
|
+
# Returns the filter configuration for a specific filter key
|
63
65
|
#
|
64
66
|
# This method provides a convenient way to retrieve a specific filter
|
65
|
-
# by its
|
67
|
+
# by its filter key name. It's a shorthand for accessing the filter_configs
|
66
68
|
# hash directly and is used internally by the filtering system.
|
67
69
|
#
|
68
|
-
# @param
|
70
|
+
# @param filter_key [Symbol, String] the filter key name to look up
|
69
71
|
#
|
70
|
-
# @return [Filter, nil] the filter instance for the given
|
71
|
-
# or nil if no filter is configured for that
|
72
|
+
# @return [Filter, nil] the filter instance for the given filter key,
|
73
|
+
# or nil if no filter is configured for that filter key
|
72
74
|
#
|
73
75
|
# @example Retrieving a specific filter
|
74
76
|
# class MyFilters < Kiroshi::Filters
|
@@ -76,16 +78,17 @@ module Kiroshi
|
|
76
78
|
# filter_by :status
|
77
79
|
# end
|
78
80
|
#
|
79
|
-
# MyFilters.filter_for(:name) # => #<Kiroshi::Filter:0x... @
|
80
|
-
# MyFilters.filter_for(:status) # => #<Kiroshi::Filter:0x... @
|
81
|
+
# MyFilters.filter_for(:name) # => #<Kiroshi::Filter:0x... @filter_key=:name @match=:like>
|
82
|
+
# MyFilters.filter_for(:status) # => #<Kiroshi::Filter:0x... @filter_key=:status @match=:exact>
|
81
83
|
# MyFilters.filter_for(:unknown) # => nil
|
82
84
|
#
|
83
85
|
# @see .filter_configs for accessing the complete filter registry
|
84
86
|
# @see Filters#apply for how this method is used during filtering
|
85
87
|
#
|
86
|
-
# @since 0.
|
87
|
-
def filter_for(
|
88
|
-
|
88
|
+
# @since 0.3.0
|
89
|
+
def filter_for(filter_key)
|
90
|
+
filter_key_string = filter_key.to_s
|
91
|
+
filter_configs[filter_key_string] || inherited_filter_for(filter_key_string)
|
89
92
|
end
|
90
93
|
|
91
94
|
private
|
@@ -96,17 +99,17 @@ module Kiroshi
|
|
96
99
|
# Searches for a filter in the inheritance chain
|
97
100
|
#
|
98
101
|
# This method looks up the inheritance chain to find a filter configuration
|
99
|
-
# for the given
|
102
|
+
# for the given filter key. It only searches in superclasses that inherit
|
100
103
|
# from Kiroshi::Filters, stopping when it reaches a non-Filters class.
|
101
104
|
#
|
102
|
-
# @param
|
105
|
+
# @param filter_key_string [String] the filter key name to look up
|
103
106
|
# @return [Filter, nil] the filter instance from a parent class, or nil if not found
|
104
107
|
#
|
105
|
-
# @since 0.
|
106
|
-
def inherited_filter_for(
|
108
|
+
# @since 0.3.0
|
109
|
+
def inherited_filter_for(filter_key_string)
|
107
110
|
return nil unless superclass < Kiroshi::Filters
|
108
111
|
|
109
|
-
superclass.filter_for(
|
112
|
+
superclass.filter_for(filter_key_string)
|
110
113
|
end
|
111
114
|
|
112
115
|
# @api private
|
@@ -116,7 +119,7 @@ module Kiroshi
|
|
116
119
|
#
|
117
120
|
# This method provides access to the internal registry of filters
|
118
121
|
# that have been configured using {.filter_by}. The returned hash
|
119
|
-
# contains {Filter} instances keyed by their
|
122
|
+
# contains {Filter} instances keyed by their filter key names, allowing
|
120
123
|
# for efficient O(1) lookup during filter application.
|
121
124
|
#
|
122
125
|
# This method is primarily used internally by {Filters#apply} to
|
@@ -124,8 +127,8 @@ module Kiroshi
|
|
124
127
|
# While marked as private API, it may be useful for introspection
|
125
128
|
# and testing purposes.
|
126
129
|
#
|
127
|
-
# @return [Hash<
|
128
|
-
# for this filter class, keyed by
|
130
|
+
# @return [Hash<String, Filter>] hash of {Filter} instances configured
|
131
|
+
# for this filter class, keyed by filter key name for efficient access
|
129
132
|
#
|
130
133
|
# @example Accessing configured filters for introspection
|
131
134
|
# class MyFilters < Kiroshi::Filters
|
@@ -135,17 +138,17 @@ module Kiroshi
|
|
135
138
|
# end
|
136
139
|
#
|
137
140
|
# MyFilters.filter_configs.length # => 3
|
138
|
-
# MyFilters.filter_configs.keys # => [
|
139
|
-
# MyFilters.filter_configs[
|
140
|
-
# MyFilters.filter_configs[
|
141
|
-
# MyFilters.filter_configs[
|
142
|
-
# MyFilters.filter_configs[
|
141
|
+
# MyFilters.filter_configs.keys # => ["name", "status", "category"]
|
142
|
+
# MyFilters.filter_configs["name"].attribute # => :name
|
143
|
+
# MyFilters.filter_configs["name"].match # => :like
|
144
|
+
# MyFilters.filter_configs["status"].match # => :exact
|
145
|
+
# MyFilters.filter_configs["category"].table_name # => :categories
|
143
146
|
#
|
144
147
|
# @example Using in tests to verify filter configuration
|
145
148
|
# RSpec.describe ProductFilters do
|
146
149
|
# it 'configures the expected filters' do
|
147
|
-
# expect(described_class.filter_configs).to have_key(
|
148
|
-
# expect(described_class.filter_configs[
|
150
|
+
# expect(described_class.filter_configs).to have_key("name")
|
151
|
+
# expect(described_class.filter_configs["name"].match).to eq(:like)
|
149
152
|
# end
|
150
153
|
# end
|
151
154
|
#
|
data/lib/kiroshi/filters.rb
CHANGED
@@ -40,15 +40,17 @@ module Kiroshi
|
|
40
40
|
|
41
41
|
extend ClassMethods
|
42
42
|
|
43
|
-
# @method self.filter_by(
|
43
|
+
# @method self.filter_by(filter_key, **options)
|
44
44
|
# @api public
|
45
|
-
# @param
|
45
|
+
# @param filter_key [Symbol] the filter key name to identify this filter
|
46
46
|
# @param options [Hash] additional options passed to {Filter#initialize}
|
47
47
|
# @option options [Symbol] :match (:exact) the matching type
|
48
48
|
# - +:exact+ for exact matching (default)
|
49
49
|
# - +:like+ for partial matching using SQL LIKE with wildcards
|
50
|
-
# @option options [String, Symbol, nil] :table (nil) the table name to qualify the
|
50
|
+
# @option options [String, Symbol, nil] :table (nil) the table name to qualify the column
|
51
51
|
# when dealing with joined tables that have conflicting column names
|
52
|
+
# @option options [Symbol, nil] :column (nil) the column name to use in database queries,
|
53
|
+
# defaults to filter_key if not specified
|
52
54
|
#
|
53
55
|
# @return [Filter] the new filter instance that was created and registered
|
54
56
|
#
|
@@ -91,6 +93,13 @@ module Kiroshi
|
|
91
93
|
# filter_by :category_name, table: :categories # Category name via join
|
92
94
|
# end
|
93
95
|
#
|
96
|
+
# @example Using custom column names
|
97
|
+
# class UserFilters < Kiroshi::Filters
|
98
|
+
# filter_by :full_name, column: :name, match: :like # Filter key 'full_name' maps to 'name' column
|
99
|
+
# filter_by :user_email, column: :email # Filter key 'user_email' maps to 'email' column
|
100
|
+
# filter_by :account_status, column: :status # Filter key 'account_status' maps to 'status' column
|
101
|
+
# end
|
102
|
+
#
|
94
103
|
# @note When using table qualification, ensure that the specified table
|
95
104
|
# is properly joined in the scope being filtered. The filter will not
|
96
105
|
# automatically add joins - it only qualifies the column name.
|
@@ -98,12 +107,12 @@ module Kiroshi
|
|
98
107
|
# @see Filter#initialize for detailed information about filter options
|
99
108
|
# @see Filters#apply for how these filters are used during query execution
|
100
109
|
#
|
101
|
-
# @since 0.
|
110
|
+
# @since 0.3.0
|
102
111
|
|
103
112
|
# Creates a new Filters instance
|
104
113
|
#
|
105
114
|
# @param filters [Hash] a hash containing the filter values to be applied.
|
106
|
-
# Keys should correspond to
|
115
|
+
# Keys should correspond to filter keys defined with {.filter_by}.
|
107
116
|
# Values will be used for filtering. Nil or blank values are ignored.
|
108
117
|
#
|
109
118
|
# @example Creating filters with values
|
@@ -121,7 +130,7 @@ module Kiroshi
|
|
121
130
|
#
|
122
131
|
# @since 0.1.0
|
123
132
|
def initialize(filters = {})
|
124
|
-
@filters = filters
|
133
|
+
@filters = filters
|
125
134
|
end
|
126
135
|
|
127
136
|
# Applies all configured filters to the given scope
|
@@ -158,8 +167,8 @@ module Kiroshi
|
|
158
167
|
#
|
159
168
|
# @since 0.2.0
|
160
169
|
def apply(scope)
|
161
|
-
filters.compact.each do |
|
162
|
-
filter = self.class.filter_for(
|
170
|
+
filters.compact.each do |filter_key, value|
|
171
|
+
filter = self.class.filter_for(filter_key)
|
163
172
|
next unless filter
|
164
173
|
|
165
174
|
scope = filter.apply(scope: scope, value: value)
|
@@ -170,14 +179,17 @@ module Kiroshi
|
|
170
179
|
|
171
180
|
private
|
172
181
|
|
173
|
-
|
174
|
-
|
175
|
-
# @!method filters
|
176
|
-
# @api private
|
177
|
-
# @private
|
182
|
+
# Returns the hash of filter values to be applied
|
178
183
|
#
|
179
|
-
#
|
184
|
+
# Uses lazy initialization to ensure @filters is never nil,
|
185
|
+
# defaulting to an empty hash when no filters were provided.
|
180
186
|
#
|
181
|
-
#
|
187
|
+
# @return [Hash] the hash of filter values to be applied
|
188
|
+
#
|
189
|
+
# @api private
|
190
|
+
# @since 0.3.0
|
191
|
+
def filters
|
192
|
+
@filters ||= {}
|
193
|
+
end
|
182
194
|
end
|
183
195
|
end
|
data/lib/kiroshi/version.rb
CHANGED
data/lib/kiroshi.rb
CHANGED
@@ -270,5 +270,45 @@ RSpec.describe Kiroshi::FilterQuery::Exact, type: :model do
|
|
270
270
|
end
|
271
271
|
end
|
272
272
|
end
|
273
|
+
|
274
|
+
context 'when Filter#column is different from filter_key' do
|
275
|
+
let(:filter) { Kiroshi::Filter.new(:user_name, match: :exact, column: :full_name) }
|
276
|
+
let(:filter_value) { 'John Doe' }
|
277
|
+
|
278
|
+
let!(:matching_document) { create(:document, full_name: 'John Doe') }
|
279
|
+
let!(:non_matching_document) { create(:document, full_name: 'Jane Smith') }
|
280
|
+
|
281
|
+
let(:expected_sql) do
|
282
|
+
<<~SQL.squish
|
283
|
+
SELECT "documents".* FROM "documents" WHERE "documents"."full_name" = 'John Doe'
|
284
|
+
SQL
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'uses the column name instead of filter_key in SQL' do
|
288
|
+
expect(query.apply.to_sql).to eq(expected_sql)
|
289
|
+
end
|
290
|
+
|
291
|
+
it 'returns records that match the column value' do
|
292
|
+
expect(query.apply).to include(matching_document)
|
293
|
+
end
|
294
|
+
|
295
|
+
it 'does not return records that do not match the column value' do
|
296
|
+
expect(query.apply).not_to include(non_matching_document)
|
297
|
+
end
|
298
|
+
|
299
|
+
context 'with table qualification' do
|
300
|
+
let(:filter) { Kiroshi::Filter.new(:user_name, match: :exact, table: :documents, column: :full_name) }
|
301
|
+
|
302
|
+
let(:expected_sql) do
|
303
|
+
<<~SQL.squish
|
304
|
+
SELECT "documents".* FROM "documents" WHERE "documents"."full_name" = 'John Doe'
|
305
|
+
SQL
|
306
|
+
end
|
307
|
+
|
308
|
+
it 'generates SQL with proper table and column qualification' do
|
309
|
+
expect(query.apply.to_sql).to eq(expected_sql)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
273
313
|
end
|
274
314
|
end
|
@@ -17,7 +17,7 @@ RSpec.describe Kiroshi::FilterQuery::Like, type: :model do
|
|
17
17
|
|
18
18
|
let(:expected_sql) do
|
19
19
|
<<~SQL.squish
|
20
|
-
SELECT "documents".* FROM "documents" WHERE (documents.name LIKE '%test%')
|
20
|
+
SELECT "documents".* FROM "documents" WHERE ("documents"."name" LIKE '%test%')
|
21
21
|
SQL
|
22
22
|
end
|
23
23
|
|
@@ -47,7 +47,7 @@ RSpec.describe Kiroshi::FilterQuery::Like, type: :model do
|
|
47
47
|
|
48
48
|
let(:expected_sql) do
|
49
49
|
<<~SQL.squish
|
50
|
-
SELECT "documents".* FROM "documents" WHERE (documents.status LIKE '%pub%')
|
50
|
+
SELECT "documents".* FROM "documents" WHERE ("documents"."status" LIKE '%pub%')
|
51
51
|
SQL
|
52
52
|
end
|
53
53
|
|
@@ -78,7 +78,7 @@ RSpec.describe Kiroshi::FilterQuery::Like, type: :model do
|
|
78
78
|
|
79
79
|
let(:expected_sql) do
|
80
80
|
<<~SQL.squish
|
81
|
-
SELECT "documents".* FROM "documents" WHERE (documents.version LIKE '%1.2%')
|
81
|
+
SELECT "documents".* FROM "documents" WHERE ("documents"."version" LIKE '%1.2%')
|
82
82
|
SQL
|
83
83
|
end
|
84
84
|
|
@@ -104,7 +104,7 @@ RSpec.describe Kiroshi::FilterQuery::Like, type: :model do
|
|
104
104
|
|
105
105
|
let(:expected_sql) do
|
106
106
|
<<~SQL.squish
|
107
|
-
SELECT "documents".* FROM "documents" WHERE (documents.name LIKE '%nonexistent%')
|
107
|
+
SELECT "documents".* FROM "documents" WHERE ("documents"."name" LIKE '%nonexistent%')
|
108
108
|
SQL
|
109
109
|
end
|
110
110
|
|
@@ -218,7 +218,7 @@ RSpec.describe Kiroshi::FilterQuery::Like, type: :model do
|
|
218
218
|
|
219
219
|
it 'generates SQL with tags table qualification' do
|
220
220
|
result_sql = query.apply.to_sql
|
221
|
-
expect(result_sql).to include('tags.name LIKE')
|
221
|
+
expect(result_sql).to include('"tags"."name" LIKE')
|
222
222
|
end
|
223
223
|
|
224
224
|
it 'generates SQL with correct LIKE pattern for tag name' do
|
@@ -245,7 +245,7 @@ RSpec.describe Kiroshi::FilterQuery::Like, type: :model do
|
|
245
245
|
|
246
246
|
it 'generates SQL with documents table qualification' do
|
247
247
|
result_sql = query.apply.to_sql
|
248
|
-
expect(result_sql).to include('documents.name LIKE')
|
248
|
+
expect(result_sql).to include('"documents"."name" LIKE')
|
249
249
|
end
|
250
250
|
|
251
251
|
it 'generates SQL with correct LIKE pattern for document name' do
|
@@ -263,7 +263,52 @@ RSpec.describe Kiroshi::FilterQuery::Like, type: :model do
|
|
263
263
|
|
264
264
|
it 'generates SQL with string table qualification' do
|
265
265
|
result_sql = query.apply.to_sql
|
266
|
-
expect(result_sql).to include('tags.name LIKE')
|
266
|
+
expect(result_sql).to include('"tags"."name" LIKE')
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
context 'when Filter#column is different from filter_key' do
|
272
|
+
let(:filter) { Kiroshi::Filter.new(:user_name, match: :like, column: :full_name) }
|
273
|
+
let(:filter_value) { 'John' }
|
274
|
+
|
275
|
+
let!(:matching_document) { create(:document, full_name: 'John Doe') }
|
276
|
+
let!(:another_match) { create(:document, full_name: 'Johnny Smith') }
|
277
|
+
let!(:non_matching_document) { create(:document, full_name: 'Jane Wilson') }
|
278
|
+
|
279
|
+
let(:expected_sql) do
|
280
|
+
<<~SQL.squish
|
281
|
+
SELECT "documents".* FROM "documents" WHERE ("documents"."full_name" LIKE '%John%')
|
282
|
+
SQL
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'uses the column name instead of filter_key in SQL' do
|
286
|
+
expect(query.apply.to_sql).to eq(expected_sql)
|
287
|
+
end
|
288
|
+
|
289
|
+
it 'returns records that partially match the column value' do
|
290
|
+
expect(query.apply).to include(matching_document)
|
291
|
+
end
|
292
|
+
|
293
|
+
it 'returns multiple records that contain the column value' do
|
294
|
+
expect(query.apply).to include(another_match)
|
295
|
+
end
|
296
|
+
|
297
|
+
it 'does not return records that do not contain the column value' do
|
298
|
+
expect(query.apply).not_to include(non_matching_document)
|
299
|
+
end
|
300
|
+
|
301
|
+
context 'with table qualification' do
|
302
|
+
let(:filter) { Kiroshi::Filter.new(:user_name, match: :like, table: :documents, column: :full_name) }
|
303
|
+
|
304
|
+
let(:expected_sql) do
|
305
|
+
<<~SQL.squish
|
306
|
+
SELECT "documents".* FROM "documents" WHERE ("documents"."full_name" LIKE '%John%')
|
307
|
+
SQL
|
308
|
+
end
|
309
|
+
|
310
|
+
it 'generates SQL with proper table and column qualification' do
|
311
|
+
expect(query.apply.to_sql).to eq(expected_sql)
|
267
312
|
end
|
268
313
|
end
|
269
314
|
end
|
@@ -39,7 +39,7 @@ RSpec.describe Kiroshi::FilterRunner, type: :model do
|
|
39
39
|
end
|
40
40
|
|
41
41
|
it 'generates correct SQL with table name prefix' do
|
42
|
-
expected_sql = "SELECT \"documents\".* FROM \"documents\" WHERE (documents
|
42
|
+
expected_sql = "SELECT \"documents\".* FROM \"documents\" WHERE (\"documents\".\"name\" LIKE '%test%')"
|
43
43
|
expect(runner.apply.to_sql).to eq(expected_sql)
|
44
44
|
end
|
45
45
|
end
|
@@ -90,5 +90,46 @@ RSpec.describe Kiroshi::FilterRunner, type: :model do
|
|
90
90
|
expect(runner.apply).not_to include(non_matching_document)
|
91
91
|
end
|
92
92
|
end
|
93
|
+
|
94
|
+
context 'when Filter#column is different from filter_key' do
|
95
|
+
let(:filter) { Kiroshi::Filter.new(:user_name, match: :exact, column: :full_name) }
|
96
|
+
let(:filter_value) { 'John Doe' }
|
97
|
+
|
98
|
+
let!(:matching_document) { create(:document, full_name: 'John Doe') }
|
99
|
+
let!(:non_matching_document) { create(:document, full_name: 'Jane Smith') }
|
100
|
+
|
101
|
+
it 'filters using the column name instead of filter_key' do
|
102
|
+
expect(runner.apply).to include(matching_document)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'does not return non-matching records' do
|
106
|
+
expect(runner.apply).not_to include(non_matching_document)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'generates correct SQL using the column name' do
|
110
|
+
expected_sql = "SELECT \"documents\".* FROM \"documents\" WHERE \"documents\".\"full_name\" = 'John Doe'"
|
111
|
+
expect(runner.apply.to_sql).to eq(expected_sql)
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'with LIKE match' do
|
115
|
+
let(:filter) { Kiroshi::Filter.new(:user_name, match: :like, column: :full_name) }
|
116
|
+
let(:filter_value) { 'John' }
|
117
|
+
|
118
|
+
let!(:partial_match) { create(:document, full_name: 'Johnny Smith') }
|
119
|
+
|
120
|
+
it 'performs LIKE filtering using the column name' do
|
121
|
+
expect(runner.apply).to include(matching_document)
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'includes partial matches using the column name' do
|
125
|
+
expect(runner.apply).to include(partial_match)
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'generates correct LIKE SQL using the column name' do
|
129
|
+
expected_sql = "SELECT \"documents\".* FROM \"documents\" WHERE (\"documents\".\"full_name\" LIKE '%John%')"
|
130
|
+
expect(runner.apply.to_sql).to eq(expected_sql)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
93
134
|
end
|
94
135
|
end
|
@@ -52,7 +52,51 @@ RSpec.describe Kiroshi::Filters::ClassMethods, type: :model do
|
|
52
52
|
it do
|
53
53
|
expect { filters_class.filter_by :name, match: :like, table: :documents }
|
54
54
|
.to change { filter_instance.apply(scope) }
|
55
|
-
.from(scope).to(scope.where('documents.name LIKE ?', '%test%'))
|
55
|
+
.from(scope).to(scope.where('"documents"."name" LIKE ?', '%test%'))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'when column is different from filter_key' do
|
60
|
+
let(:filters) { { user_name: 'John Doe' } }
|
61
|
+
|
62
|
+
context 'with exact match' do
|
63
|
+
it do
|
64
|
+
expect { filters_class.filter_by :user_name, column: :full_name }
|
65
|
+
.to change { filter_instance.apply(scope) }
|
66
|
+
.from(scope).to(scope.where(full_name: 'John Doe'))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'with like match' do
|
71
|
+
let(:filters) { { user_name: 'John' } }
|
72
|
+
|
73
|
+
it do
|
74
|
+
expect { filters_class.filter_by :user_name, match: :like, column: :full_name }
|
75
|
+
.to change { filter_instance.apply(scope) }
|
76
|
+
.from(scope).to(scope.where('"documents"."full_name" LIKE ?', '%John%'))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'with table qualification and different column' do
|
81
|
+
let(:scope) { Document.joins(:tags) }
|
82
|
+
let(:filters) { { tag_identifier: 'ruby' } }
|
83
|
+
|
84
|
+
it do
|
85
|
+
expect { filters_class.filter_by :tag_identifier, table: :tags, column: :name }
|
86
|
+
.to change { filter_instance.apply(scope) }
|
87
|
+
.from(scope).to(scope.where(tags: { name: 'ruby' }))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'with like match, table qualification and different column' do
|
92
|
+
let(:scope) { Document.joins(:tags) }
|
93
|
+
let(:filters) { { tag_identifier: 'rub' } }
|
94
|
+
|
95
|
+
it do
|
96
|
+
expect { filters_class.filter_by :tag_identifier, match: :like, table: :tags, column: :name }
|
97
|
+
.to change { filter_instance.apply(scope) }
|
98
|
+
.from(scope).to(scope.where('"tags"."name" LIKE ?', '%rub%'))
|
99
|
+
end
|
56
100
|
end
|
57
101
|
end
|
58
102
|
end
|
@@ -91,6 +91,142 @@ RSpec.describe Kiroshi::Filters, type: :model do
|
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
94
|
+
context 'when filters have string keys' do
|
95
|
+
before do
|
96
|
+
filters_class.filter_by :name, match: :like
|
97
|
+
filters_class.filter_by :status
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'with single string key filter' do
|
101
|
+
let(:filters) { { 'name' => 'test' } }
|
102
|
+
|
103
|
+
it 'returns documents matching the string key filter' do
|
104
|
+
expect(filter_instance.apply(scope)).to include(document)
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'does not return documents not matching the string key filter' do
|
108
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'generates SQL with LIKE operation for string key' do
|
112
|
+
expect(filter_instance.apply(scope).to_sql).to include('LIKE')
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'with multiple string key filters' do
|
117
|
+
let(:filters) { { 'name' => 'test', 'status' => 'finished' } }
|
118
|
+
|
119
|
+
it 'returns documents matching all string key filters' do
|
120
|
+
expect(filter_instance.apply(scope)).to include(document)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'does not return documents not matching all string key filters' do
|
124
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'with mixed string and symbol keys' do
|
129
|
+
let(:filters) { { 'name' => 'test', status: 'finished' } }
|
130
|
+
|
131
|
+
it 'returns documents matching both string and symbol key filters' do
|
132
|
+
expect(filter_instance.apply(scope)).to include(document)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'does not return documents not matching all mixed key filters' do
|
136
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'treats string and symbol keys equivalently' do
|
140
|
+
string_result = filters_class.new({ 'name' => 'test', 'status' => 'finished' }).apply(scope)
|
141
|
+
symbol_result = filters_class.new({ name: 'test', status: 'finished' }).apply(scope)
|
142
|
+
|
143
|
+
expect(string_result.to_sql).to eq(symbol_result.to_sql)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context 'when filters is an instance of ActionController::Parameters' do
|
149
|
+
before do
|
150
|
+
filters_class.filter_by :name, match: :like
|
151
|
+
filters_class.filter_by :status
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'with permitted parameters' do
|
155
|
+
let(:filters) do
|
156
|
+
ActionController::Parameters.new(
|
157
|
+
name: 'test',
|
158
|
+
status: 'finished',
|
159
|
+
unauthorized_param: 'ignored'
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'returns documents matching the permitted parameters' do
|
164
|
+
expect(filter_instance.apply(scope)).to include(document)
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'does not return documents not matching the permitted parameters' do
|
168
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'generates SQL with LIKE operation for ActionController::Parameters' do
|
172
|
+
expect(filter_instance.apply(scope).to_sql).to include('LIKE')
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'generates SQL with exact match for status parameter' do
|
176
|
+
expect(filter_instance.apply(scope).to_sql).to include("'finished'")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
context 'with unpermitted parameters' do
|
181
|
+
let(:filters) do
|
182
|
+
ActionController::Parameters.new(
|
183
|
+
name: 'test',
|
184
|
+
status: 'finished'
|
185
|
+
)
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'works with unpermitted parameters' do
|
189
|
+
expect(filter_instance.apply(scope)).to include(document)
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'does not return documents not matching the parameters' do
|
193
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
context 'with string keys in ActionController::Parameters' do
|
198
|
+
let(:filters) do
|
199
|
+
ActionController::Parameters.new(
|
200
|
+
'name' => 'test',
|
201
|
+
'status' => 'finished'
|
202
|
+
)
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'returns documents matching the string key parameters' do
|
206
|
+
expect(filter_instance.apply(scope)).to include(document)
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'does not return documents not matching the string key parameters' do
|
210
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'treats ActionController::Parameters with string keys same as regular hash' do
|
214
|
+
ac_params_result = filter_instance.apply(scope)
|
215
|
+
hash_result = filters_class.new({ 'name' => 'test', 'status' => 'finished' }).apply(scope)
|
216
|
+
|
217
|
+
expect(ac_params_result.to_sql).to eq(hash_result.to_sql)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
context 'with empty ActionController::Parameters' do
|
222
|
+
let(:filters) { ActionController::Parameters.new({}) }
|
223
|
+
|
224
|
+
it 'returns the original scope unchanged' do
|
225
|
+
expect(filter_instance.apply(scope)).to eq(scope)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
94
230
|
context 'when scope has joined tables with clashing fields' do
|
95
231
|
let(:scope) { Document.joins(:tags) }
|
96
232
|
let(:filters) { { name: 'test_name' } }
|
@@ -143,7 +279,7 @@ RSpec.describe Kiroshi::Filters, type: :model do
|
|
143
279
|
|
144
280
|
it 'generates SQL with table-qualified LIKE operation' do
|
145
281
|
result = filter_instance.apply(scope)
|
146
|
-
expect(result.to_sql).to include('documents.name LIKE')
|
282
|
+
expect(result.to_sql).to include('"documents"."name" LIKE')
|
147
283
|
end
|
148
284
|
|
149
285
|
it 'generates SQL with correct LIKE pattern' do
|
@@ -153,6 +289,80 @@ RSpec.describe Kiroshi::Filters, type: :model do
|
|
153
289
|
end
|
154
290
|
end
|
155
291
|
|
292
|
+
context 'when specifying a different column' do
|
293
|
+
let(:scope) { Document.joins(:tags) }
|
294
|
+
let(:filters) { { tag_name: 'ruby' } }
|
295
|
+
|
296
|
+
let!(:ruby_tag) { Tag.find_or_create_by(name: 'ruby') }
|
297
|
+
let!(:js_tag) { Tag.find_or_create_by(name: 'javascript') }
|
298
|
+
|
299
|
+
before do
|
300
|
+
filters_class.filter_by :tag_name, table: :tags, column: :name
|
301
|
+
|
302
|
+
document.tags << [ruby_tag]
|
303
|
+
other_document.tags << [js_tag]
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'filters by the specified column name instead of filter key' do
|
307
|
+
expect(filter_instance.apply(scope)).to include(document)
|
308
|
+
end
|
309
|
+
|
310
|
+
it 'does not return documents not matching the column filter' do
|
311
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
312
|
+
end
|
313
|
+
|
314
|
+
it 'generates SQL that filters by tags.name using the column parameter' do
|
315
|
+
expect(filter_instance.apply(scope).to_sql).to include('"tags"."name"')
|
316
|
+
end
|
317
|
+
|
318
|
+
it 'generates SQL that includes the filter value' do
|
319
|
+
expect(filter_instance.apply(scope).to_sql).to include("'ruby'")
|
320
|
+
end
|
321
|
+
|
322
|
+
it 'does not use the filter key name in the SQL' do
|
323
|
+
# The filter key is :tag_name but column is :name, so SQL should use 'name' not 'tag_name'
|
324
|
+
expect(filter_instance.apply(scope).to_sql).not_to include('tag_name')
|
325
|
+
end
|
326
|
+
|
327
|
+
context 'with LIKE matching' do
|
328
|
+
let(:filters) { { tag_name: 'rub' } }
|
329
|
+
|
330
|
+
before do
|
331
|
+
filters_class.filter_by :tag_name, table: :tags, column: :name, match: :like
|
332
|
+
end
|
333
|
+
|
334
|
+
it 'applies LIKE matching to the specified column' do
|
335
|
+
expect(filter_instance.apply(scope)).to include(document)
|
336
|
+
end
|
337
|
+
|
338
|
+
it 'generates SQL with LIKE operation on the specified column' do
|
339
|
+
expect(filter_instance.apply(scope).to_sql).to include('"tags"."name" LIKE')
|
340
|
+
end
|
341
|
+
|
342
|
+
it 'generates SQL with correct LIKE pattern' do
|
343
|
+
expect(filter_instance.apply(scope).to_sql).to include("'%rub%'")
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
context 'with different filter key and column names' do
|
348
|
+
let(:filters) { { user_full_name: 'test' } }
|
349
|
+
|
350
|
+
before do
|
351
|
+
filters_class.filter_by :user_full_name, column: :name, match: :like
|
352
|
+
end
|
353
|
+
|
354
|
+
it 'uses the column name in database queries' do
|
355
|
+
result = filter_instance.apply(Document.all)
|
356
|
+
expect(result.to_sql).to include('"documents"."name"')
|
357
|
+
end
|
358
|
+
|
359
|
+
it 'does not use the filter key in SQL' do
|
360
|
+
result = filter_instance.apply(Document.all)
|
361
|
+
expect(result.to_sql).not_to include('user_full_name')
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
156
366
|
context 'when filter was defined in the superclass' do
|
157
367
|
subject(:filters_class) { Class.new(parent_class) }
|
158
368
|
|
data/spec/spec_helper.rb
CHANGED
data/spec/support/db/schema.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kiroshi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Darthjee
|
@@ -46,7 +46,9 @@ extensions: []
|
|
46
46
|
extra_rdoc_files: []
|
47
47
|
files:
|
48
48
|
- ".circleci/config.yml"
|
49
|
+
- ".codacy.yml"
|
49
50
|
- ".gitignore"
|
51
|
+
- ".markdownlint.json"
|
50
52
|
- ".rspec"
|
51
53
|
- ".rubocop.yml"
|
52
54
|
- ".rubocop_todo.yml"
|