kiroshi 0.1.0 → 0.1.1
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/README.md +94 -5
- data/lib/kiroshi/filter.rb +41 -30
- data/lib/kiroshi/filter_query/exact.rb +38 -0
- data/lib/kiroshi/filter_query/like.rb +42 -0
- data/lib/kiroshi/filter_query.rb +131 -0
- data/lib/kiroshi/filter_runner.rb +152 -0
- data/lib/kiroshi/filters.rb +12 -5
- data/lib/kiroshi/version.rb +1 -1
- data/lib/kiroshi.rb +157 -3
- data/spec/lib/kiroshi/filter_query/exact_spec.rb +280 -0
- data/spec/lib/kiroshi/filter_query/like_spec.rb +275 -0
- data/spec/lib/kiroshi/filter_query_spec.rb +39 -0
- data/spec/lib/kiroshi/filter_runner_spec.rb +110 -0
- data/spec/lib/kiroshi/filters_spec.rb +63 -0
- data/spec/support/db/schema.rb +14 -0
- data/spec/support/factories/tag.rb +7 -0
- data/spec/support/models/document.rb +2 -0
- data/spec/support/models/tag.rb +7 -0
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3df16fe842c4fcd160633af8a5e4d91fa19cbb857dc135a31160c1f389a5f6c1
|
4
|
+
data.tar.gz: b3f98740895ae5c611d616c36472d77ce412a1086d9d6a9ffe4532a8b3fa2235
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 328641f23218cf3a3a3841af7027f42981dced635c03e47642e5f7bd3a9194d94e13e59a87f51f71adc51344642a682148606daff113ebf80f638e0cb0f5be19
|
7
|
+
data.tar.gz: e3cb7920836e0ad2d86d05875c74a57759c5b28d9309124539c61c902adaaaa3a9bf1d0f094e9b19500d23f4034563ac9de234b9fd6f36a21084054bfed09c13
|
data/README.md
CHANGED
@@ -7,16 +7,16 @@
|
|
7
7
|
|
8
8
|
## Yard Documentation
|
9
9
|
|
10
|
-
[https://www.rubydoc.info/gems/kiroshi/0.1.
|
10
|
+
[https://www.rubydoc.info/gems/kiroshi/0.1.1](https://www.rubydoc.info/gems/kiroshi/0.1.1)
|
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.1.
|
17
|
+
Current Release: [0.1.1](https://github.com/darthjee/kiroshi/tree/0.1.1)
|
18
18
|
|
19
|
-
[Next release](https://github.com/darthjee/kiroshi/compare/0.1.
|
19
|
+
[Next release](https://github.com/darthjee/kiroshi/compare/0.1.1...master)
|
20
20
|
|
21
21
|
## Installation
|
22
22
|
|
@@ -100,6 +100,7 @@ products = filters.apply(Product.all)
|
|
100
100
|
##### Controller Integration
|
101
101
|
|
102
102
|
```ruby
|
103
|
+
# URL: /documents?filter[name]=report&filter[status]=published&filter[author]=john
|
103
104
|
class DocumentsController < ApplicationController
|
104
105
|
def index
|
105
106
|
@documents = document_filters.apply(Document.all)
|
@@ -113,7 +114,7 @@ class DocumentsController < ApplicationController
|
|
113
114
|
end
|
114
115
|
|
115
116
|
def filter_params
|
116
|
-
params
|
117
|
+
params[:filter]&.permit(:name, :status, :category, :author)
|
117
118
|
end
|
118
119
|
end
|
119
120
|
|
@@ -128,6 +129,7 @@ end
|
|
128
129
|
##### Nested Resource Filtering
|
129
130
|
|
130
131
|
```ruby
|
132
|
+
# URL: /users/123/articles?filter[title]=ruby&filter[published]=true&filter[tag]=tutorial
|
131
133
|
class ArticleFilters < Kiroshi::Filters
|
132
134
|
filter_by :title, match: :like
|
133
135
|
filter_by :published
|
@@ -141,10 +143,65 @@ def articles
|
|
141
143
|
end
|
142
144
|
|
143
145
|
def article_filters
|
144
|
-
ArticleFilters.new(params
|
146
|
+
ArticleFilters.new(params[:filter]&.permit(:title, :published, :tag))
|
145
147
|
end
|
146
148
|
```
|
147
149
|
|
150
|
+
##### Joined Tables and Table Qualification
|
151
|
+
|
152
|
+
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
|
+
|
154
|
+
```ruby
|
155
|
+
class DocumentFilters < Kiroshi::Filters
|
156
|
+
filter_by :name, match: :like # Filters by documents.name (default table)
|
157
|
+
filter_by :tag_name, match: :like, table: :tags # Filters by tags.name
|
158
|
+
filter_by :status # Filters by documents.status
|
159
|
+
filter_by :category, table: :documents # Explicitly filter by documents.category
|
160
|
+
end
|
161
|
+
|
162
|
+
# Example with joined scope
|
163
|
+
scope = Document.joins(:tags)
|
164
|
+
filters = DocumentFilters.new(tag_name: 'ruby', status: 'published')
|
165
|
+
filtered_documents = filters.apply(scope)
|
166
|
+
# Generates: WHERE tags.name LIKE '%ruby%' AND documents.status = 'published'
|
167
|
+
```
|
168
|
+
|
169
|
+
###### Table Qualification Examples
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
# Filter documents by tag name and document status
|
173
|
+
class DocumentTagFilters < Kiroshi::Filters
|
174
|
+
filter_by :tag_name, match: :like, table: :tags # Search in tags.name
|
175
|
+
filter_by :status, table: :documents # Search in documents.status
|
176
|
+
filter_by :title, match: :like # Search in documents.title (default table)
|
177
|
+
end
|
178
|
+
|
179
|
+
scope = Document.joins(:tags)
|
180
|
+
filters = DocumentTagFilters.new(tag_name: 'programming', status: 'published', title: 'Ruby')
|
181
|
+
result = filters.apply(scope)
|
182
|
+
# Generates: WHERE tags.name LIKE '%programming%' AND documents.status = 'published' AND documents.title LIKE '%Ruby%'
|
183
|
+
|
184
|
+
# Filter by both document and tag attributes with different field names
|
185
|
+
class AdvancedDocumentFilters < Kiroshi::Filters
|
186
|
+
filter_by :title, match: :like, table: :documents
|
187
|
+
filter_by :tag_name, match: :like, table: :tags
|
188
|
+
filter_by :category, table: :documents
|
189
|
+
filter_by :tag_color, table: :tags
|
190
|
+
end
|
191
|
+
|
192
|
+
scope = Document.joins(:tags)
|
193
|
+
filters = AdvancedDocumentFilters.new(
|
194
|
+
title: 'Ruby',
|
195
|
+
tag_name: 'tutorial',
|
196
|
+
category: 'programming',
|
197
|
+
tag_color: 'blue'
|
198
|
+
)
|
199
|
+
result = filters.apply(scope)
|
200
|
+
# Generates: WHERE documents.title LIKE '%Ruby%' AND tags.name LIKE '%tutorial%' AND documents.category = 'programming' AND tags.color = 'blue'
|
201
|
+
```
|
202
|
+
|
203
|
+
The `table` parameter accepts both symbols and strings, and helps resolve column name ambiguity in complex joined queries.
|
204
|
+
|
148
205
|
### Kiroshi::Filter
|
149
206
|
|
150
207
|
[Filter](https://www.rubydoc.info/gems/kiroshi/Kiroshi/Filter)
|
@@ -168,6 +225,7 @@ scope = status_filter.apply(scope, { status: 'published' })
|
|
168
225
|
|
169
226
|
- `match: :exact` - Performs exact matching (default)
|
170
227
|
- `match: :like` - Performs partial matching using SQL LIKE
|
228
|
+
- `table: :table_name` - Specifies which table to filter on (useful for joined queries)
|
171
229
|
|
172
230
|
```ruby
|
173
231
|
# Exact match filter
|
@@ -179,6 +237,16 @@ exact_filter.apply(Document.all, { status: 'published' })
|
|
179
237
|
like_filter = Kiroshi::Filter.new(:title, match: :like)
|
180
238
|
like_filter.apply(Document.all, { title: 'Ruby' })
|
181
239
|
# Generates: WHERE title LIKE '%Ruby%'
|
240
|
+
|
241
|
+
# Table-qualified filter for joined queries
|
242
|
+
tag_filter = Kiroshi::Filter.new(:name, match: :like, table: :tags)
|
243
|
+
tag_filter.apply(Document.joins(:tags), { name: 'programming' })
|
244
|
+
# Generates: WHERE tags.name LIKE '%programming%'
|
245
|
+
|
246
|
+
# Document-specific filter in joined query
|
247
|
+
doc_filter = Kiroshi::Filter.new(:title, match: :exact, table: :documents)
|
248
|
+
doc_filter.apply(Document.joins(:tags), { title: 'Ruby Guide' })
|
249
|
+
# Generates: WHERE documents.title = 'Ruby Guide'
|
182
250
|
```
|
183
251
|
|
184
252
|
#### Empty Value Handling
|
@@ -192,3 +260,24 @@ filter.apply(Document.all, { name: '' }) # Returns original scope
|
|
192
260
|
filter.apply(Document.all, {}) # Returns original scope
|
193
261
|
filter.apply(Document.all, { name: 'value' }) # Applies filter
|
194
262
|
```
|
263
|
+
|
264
|
+
#### Handling Column Name Ambiguity
|
265
|
+
|
266
|
+
When working with joined tables that have columns with the same name, use the `table` parameter to specify which table's column to filter:
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
# Without table specification - may cause ambiguity
|
270
|
+
scope = Document.joins(:tags) # Both documents and tags have 'name' column
|
271
|
+
|
272
|
+
# Specify which table to filter on
|
273
|
+
name_filter = Kiroshi::Filter.new(:name, match: :like, table: :tags)
|
274
|
+
result = name_filter.apply(scope, { name: 'ruby' })
|
275
|
+
# Generates: WHERE tags.name LIKE '%ruby%'
|
276
|
+
|
277
|
+
# Or filter by document name specifically
|
278
|
+
doc_name_filter = Kiroshi::Filter.new(:name, match: :like, table: :documents)
|
279
|
+
result = doc_name_filter.apply(scope, { name: 'guide' })
|
280
|
+
# Generates: WHERE documents.name LIKE '%guide%'
|
281
|
+
```
|
282
|
+
|
283
|
+
**Priority**: When using `Kiroshi::Filters`, if a filter specifies a `table`, it takes priority over the scope's default table name.
|
data/lib/kiroshi/filter.rb
CHANGED
@@ -18,10 +18,34 @@ module Kiroshi
|
|
18
18
|
#
|
19
19
|
# @since 0.1.0
|
20
20
|
class Filter
|
21
|
+
attr_reader :attribute, :match, :table_name
|
22
|
+
|
23
|
+
# @!method attribute
|
24
|
+
# @api private
|
25
|
+
#
|
26
|
+
# Returns the attribute name to filter by
|
27
|
+
#
|
28
|
+
# @return [Symbol] the attribute name to filter by
|
29
|
+
|
30
|
+
# @!method match
|
31
|
+
# @api private
|
32
|
+
#
|
33
|
+
# Returns the matching type (+:exact+ or +:like+)
|
34
|
+
#
|
35
|
+
# @return [Symbol] the matching type (+:exact+ or +:like+)
|
36
|
+
|
37
|
+
# @!method table_name
|
38
|
+
# @api private
|
39
|
+
#
|
40
|
+
# Returns the table name to qualify the attribute
|
41
|
+
#
|
42
|
+
# @return [String, String, nil] the table name or nil if not specified
|
43
|
+
|
21
44
|
# Creates a new Filter instance
|
22
45
|
#
|
23
46
|
# @param attribute [Symbol] the attribute name to filter by
|
24
47
|
# @param match [Symbol] the matching type, defaults to :exact
|
48
|
+
# @param table [String, Symbol, nil] the table name to qualify the attribute, defaults to nil
|
25
49
|
# @option match [Symbol] :exact performs exact matching (default)
|
26
50
|
# @option match [Symbol] :like performs partial matching using SQL LIKE
|
27
51
|
#
|
@@ -31,10 +55,14 @@ module Kiroshi
|
|
31
55
|
# @example Creating a partial match filter
|
32
56
|
# filter = Kiroshi::Filter.new(:name, match: :like)
|
33
57
|
#
|
58
|
+
# @example Creating a filter with table qualification
|
59
|
+
# filter = Kiroshi::Filter.new(:name, table: 'documents')
|
60
|
+
#
|
34
61
|
# @since 0.1.0
|
35
|
-
def initialize(attribute, match: :exact)
|
62
|
+
def initialize(attribute, match: :exact, table: nil)
|
36
63
|
@attribute = attribute
|
37
64
|
@match = match
|
65
|
+
@table_name = table
|
38
66
|
end
|
39
67
|
|
40
68
|
# Applies the filter to the given scope
|
@@ -58,6 +86,16 @@ module Kiroshi
|
|
58
86
|
# filter.apply(Article.all, { title: 'Ruby' })
|
59
87
|
# # Generates: WHERE title LIKE '%Ruby%'
|
60
88
|
#
|
89
|
+
# @example Applying a filter with table qualification
|
90
|
+
# filter = Kiroshi::Filter.new(:name, table: 'documents')
|
91
|
+
# filter.apply(Document.joins(:tags), { name: 'report' })
|
92
|
+
# # Generates: WHERE documents.name = 'report'
|
93
|
+
#
|
94
|
+
# @example Applying a filter with table qualification for tags
|
95
|
+
# filter = Kiroshi::Filter.new(:name, table: 'tags')
|
96
|
+
# filter.apply(Document.joins(:tags), { name: 'ruby' })
|
97
|
+
# # Generates: WHERE tags.name = 'ruby'
|
98
|
+
#
|
61
99
|
# @example With empty filter value
|
62
100
|
# filter = Kiroshi::Filter.new(:name)
|
63
101
|
# filter.apply(User.all, { name: nil })
|
@@ -65,35 +103,8 @@ module Kiroshi
|
|
65
103
|
#
|
66
104
|
# @since 0.1.0
|
67
105
|
def apply(scope, filters)
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
case match
|
72
|
-
when :like
|
73
|
-
scope.where("#{attribute} LIKE ?", "%#{filter_value}%")
|
74
|
-
else # :exact (default)
|
75
|
-
scope.where(attribute => filter_value)
|
76
|
-
end
|
106
|
+
runner = FilterRunner.new(filter: self, scope: scope, filters: filters)
|
107
|
+
runner.apply
|
77
108
|
end
|
78
|
-
|
79
|
-
private
|
80
|
-
|
81
|
-
attr_reader :attribute, :match
|
82
|
-
|
83
|
-
# @!method attribute
|
84
|
-
# @api private
|
85
|
-
# @private
|
86
|
-
#
|
87
|
-
# Returns the attribute name to filter by
|
88
|
-
#
|
89
|
-
# @return [Symbol] the attribute name to filter by
|
90
|
-
|
91
|
-
# @!method match
|
92
|
-
# @api private
|
93
|
-
# @private
|
94
|
-
#
|
95
|
-
# Returns the matching type (+:exact+ or +:like+)
|
96
|
-
#
|
97
|
-
# @return [Symbol] the matching type (+:exact+ or +:like+)
|
98
109
|
end
|
99
110
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kiroshi
|
4
|
+
class FilterQuery
|
5
|
+
# @api private
|
6
|
+
# @author darthjee
|
7
|
+
#
|
8
|
+
# Query strategy for exact matching
|
9
|
+
#
|
10
|
+
# This class implements the exact match query strategy, generating
|
11
|
+
# WHERE clauses with exact equality comparisons.
|
12
|
+
#
|
13
|
+
# @example Applying exact match query
|
14
|
+
# query = Kiroshi::FilterQuery::Exact.new(filter_runner)
|
15
|
+
# query.apply
|
16
|
+
# # Generates: WHERE attribute = 'value'
|
17
|
+
#
|
18
|
+
# @since 0.1.1
|
19
|
+
class Exact < FilterQuery
|
20
|
+
# Applies exact match filtering to the scope
|
21
|
+
#
|
22
|
+
# This method generates a WHERE clause with exact equality matching
|
23
|
+
# for the filter's attribute and value.
|
24
|
+
#
|
25
|
+
# @return [ActiveRecord::Relation] the filtered scope with exact match
|
26
|
+
#
|
27
|
+
# @example Applying exact match
|
28
|
+
# query = Exact.new(filter_runner)
|
29
|
+
# query.apply
|
30
|
+
# # Generates: WHERE status = 'published'
|
31
|
+
#
|
32
|
+
# @since 0.1.1
|
33
|
+
def apply
|
34
|
+
scope.where(table_name => { attribute => filter_value })
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kiroshi
|
4
|
+
class FilterQuery
|
5
|
+
# @api private
|
6
|
+
# @author darthjee
|
7
|
+
#
|
8
|
+
# Query strategy for LIKE matching
|
9
|
+
#
|
10
|
+
# This class implements the LIKE match query strategy, generating
|
11
|
+
# WHERE clauses with SQL LIKE operations for partial matching.
|
12
|
+
#
|
13
|
+
# @example Applying LIKE match query
|
14
|
+
# query = Kiroshi::FilterQuery::Like.new(filter_runner)
|
15
|
+
# query.apply
|
16
|
+
# # Generates: WHERE table_name.attribute LIKE '%value%'
|
17
|
+
#
|
18
|
+
# @since 0.1.1
|
19
|
+
class Like < FilterQuery
|
20
|
+
# Applies LIKE match filtering to the scope
|
21
|
+
#
|
22
|
+
# This method generates a WHERE clause with SQL LIKE operation
|
23
|
+
# for partial matching, including table name prefix to avoid
|
24
|
+
# column ambiguity in complex queries.
|
25
|
+
#
|
26
|
+
# @return [ActiveRecord::Relation] the filtered scope with LIKE match
|
27
|
+
#
|
28
|
+
# @example Applying LIKE match
|
29
|
+
# query = Like.new(filter_runner)
|
30
|
+
# query.apply
|
31
|
+
# # Generates: WHERE documents.name LIKE '%ruby%'
|
32
|
+
#
|
33
|
+
# @since 0.1.1
|
34
|
+
def apply
|
35
|
+
scope.where(
|
36
|
+
"#{table_name}.#{attribute} LIKE ?",
|
37
|
+
"%#{filter_value}%"
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kiroshi
|
4
|
+
# @api private
|
5
|
+
# @author darthjee
|
6
|
+
#
|
7
|
+
# Factory class for creating filter query strategies
|
8
|
+
#
|
9
|
+
# This class implements the Strategy pattern for handling different types of
|
10
|
+
# database queries based on the filter match type. It provides a factory method
|
11
|
+
# to create the appropriate query strategy class.
|
12
|
+
#
|
13
|
+
# @example Getting an exact match query strategy
|
14
|
+
# query = Kiroshi::FilterQuery.for(:exact).new(filter_runner)
|
15
|
+
# query.apply
|
16
|
+
#
|
17
|
+
# @example Getting a LIKE match query strategy
|
18
|
+
# query = Kiroshi::FilterQuery.for(:like).new(filter_runner)
|
19
|
+
# query.apply
|
20
|
+
#
|
21
|
+
# @since 0.1.1
|
22
|
+
class FilterQuery
|
23
|
+
autoload :Exact, 'kiroshi/filter_query/exact'
|
24
|
+
autoload :Like, 'kiroshi/filter_query/like'
|
25
|
+
|
26
|
+
class << self
|
27
|
+
# Factory method to create the appropriate query strategy
|
28
|
+
#
|
29
|
+
# This method returns the correct query strategy class based on the
|
30
|
+
# match type provided. It serves as the main entry point for creating
|
31
|
+
# query strategies.
|
32
|
+
#
|
33
|
+
# @param match [Symbol] the type of matching to perform
|
34
|
+
# - :exact for exact matching
|
35
|
+
# - :like for partial matching using SQL LIKE
|
36
|
+
#
|
37
|
+
# @return [Class] the appropriate FilterQuery subclass
|
38
|
+
#
|
39
|
+
# @example Creating an exact match query
|
40
|
+
# query_class = Kiroshi::FilterQuery.for(:exact)
|
41
|
+
# # Returns Kiroshi::FilterQuery::Exact
|
42
|
+
#
|
43
|
+
# @example Creating a LIKE match query
|
44
|
+
# query_class = Kiroshi::FilterQuery.for(:like)
|
45
|
+
# # Returns Kiroshi::FilterQuery::Like
|
46
|
+
#
|
47
|
+
# @raise [ArgumentError] when an unsupported match type is provided
|
48
|
+
#
|
49
|
+
# @since 0.1.1
|
50
|
+
def for(match)
|
51
|
+
case match
|
52
|
+
when :exact
|
53
|
+
Exact
|
54
|
+
when :like
|
55
|
+
Like
|
56
|
+
else
|
57
|
+
raise ArgumentError, "Unsupported match type: #{match}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Creates a new FilterQuery instance
|
63
|
+
#
|
64
|
+
# @param filter_runner [Kiroshi::FilterRunner] the filter runner instance
|
65
|
+
#
|
66
|
+
# @since 0.1.1
|
67
|
+
def initialize(filter_runner)
|
68
|
+
@filter_runner = filter_runner
|
69
|
+
end
|
70
|
+
|
71
|
+
# Base implementation for applying a filter query
|
72
|
+
#
|
73
|
+
# This method should be overridden by subclasses to provide specific
|
74
|
+
# query logic for each match type.
|
75
|
+
#
|
76
|
+
# @return [ActiveRecord::Relation] the filtered scope
|
77
|
+
#
|
78
|
+
# @raise [NotImplementedError] when called on the base class
|
79
|
+
#
|
80
|
+
# @since 0.1.1
|
81
|
+
def apply
|
82
|
+
raise NotImplementedError, 'Subclasses must implement #apply method'
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
attr_reader :filter_runner
|
88
|
+
|
89
|
+
# @!method filter_runner
|
90
|
+
# @api private
|
91
|
+
# @private
|
92
|
+
#
|
93
|
+
# Returns the filter runner instance
|
94
|
+
#
|
95
|
+
# @return [Kiroshi::FilterRunner] the filter runner instance
|
96
|
+
|
97
|
+
delegate :scope, :attribute, :table_name, :filter_value, to: :filter_runner
|
98
|
+
|
99
|
+
# @!method scope
|
100
|
+
# @api private
|
101
|
+
# @private
|
102
|
+
#
|
103
|
+
# Returns the ActiveRecord scope being filtered
|
104
|
+
#
|
105
|
+
# @return [ActiveRecord::Relation] the scope being filtered
|
106
|
+
|
107
|
+
# @!method attribute
|
108
|
+
# @api private
|
109
|
+
# @private
|
110
|
+
#
|
111
|
+
# Returns the attribute name to filter by
|
112
|
+
#
|
113
|
+
# @return [Symbol] the attribute name to filter by
|
114
|
+
|
115
|
+
# @!method table_name
|
116
|
+
# @api private
|
117
|
+
# @private
|
118
|
+
#
|
119
|
+
# Returns the table name from the scope
|
120
|
+
#
|
121
|
+
# @return [String] the table name
|
122
|
+
|
123
|
+
# @!method filter_value
|
124
|
+
# @api private
|
125
|
+
# @private
|
126
|
+
#
|
127
|
+
# Returns the filter value for the current filter's attribute
|
128
|
+
#
|
129
|
+
# @return [Object, nil] the filter value or nil if not present
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kiroshi
|
4
|
+
# @api private
|
5
|
+
# @author darthjee
|
6
|
+
#
|
7
|
+
# A filter runner that applies filtering logic to ActiveRecord scopes
|
8
|
+
#
|
9
|
+
# This class handles the actual application of filter logic to database queries,
|
10
|
+
# supporting both exact matches and partial matches using SQL LIKE operations.
|
11
|
+
# It separates the filter configuration from the filter execution logic.
|
12
|
+
#
|
13
|
+
# @example Creating and running a filter
|
14
|
+
# filter = Kiroshi::Filter.new(:name, match: :like)
|
15
|
+
# runner = Kiroshi::FilterRunner.new(filter: filter, scope: User.all, filters: { name: 'John' })
|
16
|
+
# result = runner.apply
|
17
|
+
#
|
18
|
+
# @since 0.1.0
|
19
|
+
class FilterRunner
|
20
|
+
# Creates a new FilterRunner instance
|
21
|
+
#
|
22
|
+
# @param filter [Kiroshi::Filter] the filter configuration
|
23
|
+
# @param scope [ActiveRecord::Relation] the scope to filter
|
24
|
+
# @param filters [Hash] a hash containing filter values
|
25
|
+
#
|
26
|
+
# @since 0.1.0
|
27
|
+
def initialize(filter:, scope:, filters:)
|
28
|
+
@filter = filter
|
29
|
+
@scope = scope
|
30
|
+
@filters = filters
|
31
|
+
end
|
32
|
+
|
33
|
+
# Applies the filter logic to the scope
|
34
|
+
#
|
35
|
+
# This method contains the actual filtering logic, checking the filter's
|
36
|
+
# match type and applying the appropriate WHERE clause to the scope.
|
37
|
+
#
|
38
|
+
# @return [ActiveRecord::Relation] the filtered scope
|
39
|
+
#
|
40
|
+
# @example Applying exact match filter
|
41
|
+
# runner = FilterRunner.new(filter: filter, scope: scope, filters: { name: 'John' })
|
42
|
+
# runner.apply
|
43
|
+
#
|
44
|
+
# @example Applying LIKE filter
|
45
|
+
# runner = FilterRunner.new(filter: filter, scope: scope, filters: { title: 'Ruby' })
|
46
|
+
# runner.apply
|
47
|
+
#
|
48
|
+
# @example With no matching value
|
49
|
+
# runner = FilterRunner.new(filter: filter, scope: scope, filters: { name: nil })
|
50
|
+
# runner.apply
|
51
|
+
# # Returns the original scope unchanged
|
52
|
+
#
|
53
|
+
# @since 0.1.1
|
54
|
+
def apply
|
55
|
+
return scope unless filter_value.present?
|
56
|
+
|
57
|
+
query_strategy = FilterQuery.for(filter.match).new(self)
|
58
|
+
query_strategy.apply
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the filter value for the current filter's attribute
|
62
|
+
#
|
63
|
+
# @return [Object, nil] the filter value or nil if not present
|
64
|
+
#
|
65
|
+
# @since 0.1.1
|
66
|
+
def filter_value
|
67
|
+
filters[filter.attribute]
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the current scope being filtered
|
71
|
+
#
|
72
|
+
# @return [ActiveRecord::Relation] the scope
|
73
|
+
#
|
74
|
+
# @since 0.1.1
|
75
|
+
attr_reader :scope
|
76
|
+
|
77
|
+
# Returns the table name to use for the filter
|
78
|
+
#
|
79
|
+
# This method prioritizes the filter's table_name over the scope's table_name.
|
80
|
+
# If the filter has a specific table_name configured, it uses that;
|
81
|
+
# otherwise, it falls back to the scope's table_name.
|
82
|
+
#
|
83
|
+
# @return [String] the table name to use for filtering
|
84
|
+
#
|
85
|
+
# @example With filter table_name specified
|
86
|
+
# filter = Kiroshi::Filter.new(:name, table: 'tags')
|
87
|
+
# runner = FilterRunner.new(filter: filter, scope: Document.joins(:tags), filters: {})
|
88
|
+
# runner.table_name # => 'tags'
|
89
|
+
#
|
90
|
+
# @example Without filter table_name (fallback to scope)
|
91
|
+
# filter = Kiroshi::Filter.new(:name)
|
92
|
+
# runner = FilterRunner.new(filter: filter, scope: Document.all, filters: {})
|
93
|
+
# runner.table_name # => 'documents'
|
94
|
+
#
|
95
|
+
# @since 0.1.1
|
96
|
+
def table_name
|
97
|
+
filter_table_name || scope_table_name
|
98
|
+
end
|
99
|
+
|
100
|
+
# @!method scope
|
101
|
+
# @api private
|
102
|
+
#
|
103
|
+
# Returns the current scope being filtered
|
104
|
+
#
|
105
|
+
# @return [ActiveRecord::Relation] the scope
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
attr_reader :filter, :filters
|
110
|
+
|
111
|
+
# @!method filter
|
112
|
+
# @api private
|
113
|
+
# @private
|
114
|
+
#
|
115
|
+
# Returns the filter configuration
|
116
|
+
#
|
117
|
+
# @return [Kiroshi::Filter] the filter configuration
|
118
|
+
|
119
|
+
# @!method filters
|
120
|
+
# @api private
|
121
|
+
# @private
|
122
|
+
#
|
123
|
+
# Returns the hash of filter values
|
124
|
+
#
|
125
|
+
# @return [Hash] the hash of filter values
|
126
|
+
|
127
|
+
delegate :attribute, to: :filter
|
128
|
+
delegate :table_name, to: :scope, prefix: true
|
129
|
+
delegate :table_name, to: :filter, prefix: true
|
130
|
+
|
131
|
+
# @!method attribute
|
132
|
+
# @api private
|
133
|
+
#
|
134
|
+
# Returns the attribute name to filter by
|
135
|
+
#
|
136
|
+
# @return [Symbol] the attribute name to filter by
|
137
|
+
|
138
|
+
# @!method scope_table_name
|
139
|
+
# @api private
|
140
|
+
#
|
141
|
+
# Returns the table name from the scope
|
142
|
+
#
|
143
|
+
# @return [String] the table name from the scope
|
144
|
+
|
145
|
+
# @!method filter_table_name
|
146
|
+
# @api private
|
147
|
+
#
|
148
|
+
# Returns the table name from the filter configuration
|
149
|
+
#
|
150
|
+
# @return [String, nil] the table name from the filter or nil if not specified
|
151
|
+
end
|
152
|
+
end
|
data/lib/kiroshi/filters.rb
CHANGED
@@ -43,11 +43,13 @@ module Kiroshi
|
|
43
43
|
# be applied when {#apply} is called. Each call creates a new {Filter}
|
44
44
|
# instance with the specified configuration.
|
45
45
|
#
|
46
|
-
# @
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
46
|
+
# @overload filter_by(attribute, **options)
|
47
|
+
# @param attribute [Symbol] the attribute name to filter by
|
48
|
+
# @param options [Hash] additional options passed to {Filter#initialize}
|
49
|
+
# @option options [Symbol] :match (:exact) the matching type
|
50
|
+
# - +:exact+ for exact matching (default)
|
51
|
+
# - +:like+ for partial matching using SQL LIKE
|
52
|
+
# @option options [String, Symbol, nil] :table (nil) the table name to qualify the attribute
|
51
53
|
#
|
52
54
|
# @return [Filter] the new filter instance
|
53
55
|
#
|
@@ -70,6 +72,11 @@ module Kiroshi
|
|
70
72
|
# filter_by :payment_method
|
71
73
|
# end
|
72
74
|
#
|
75
|
+
# @example Filter with table qualification
|
76
|
+
# class DocumentTagFilters < Kiroshi::Filters
|
77
|
+
# filter_by :name, table: :tags
|
78
|
+
# end
|
79
|
+
#
|
73
80
|
# @since 0.1.0
|
74
81
|
def filter_by(attribute, **)
|
75
82
|
Filter.new(attribute, **).tap do |filter|
|
data/lib/kiroshi/version.rb
CHANGED