kiroshi 0.1.1 → 0.2.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/.rubocop_todo.yml +4 -8
- data/README.md +6 -80
- data/lib/kiroshi/filter.rb +19 -14
- data/lib/kiroshi/filter_query/exact.rb +1 -1
- data/lib/kiroshi/filter_query/like.rb +1 -1
- data/lib/kiroshi/filter_query.rb +8 -12
- data/lib/kiroshi/filter_runner.rb +43 -31
- data/lib/kiroshi/filters/class_methods.rb +169 -0
- data/lib/kiroshi/filters.rb +68 -69
- data/lib/kiroshi/version.rb +1 -1
- data/lib/kiroshi.rb +3 -22
- data/spec/lib/kiroshi/filter_query/exact_spec.rb +1 -7
- data/spec/lib/kiroshi/filter_query/like_spec.rb +1 -5
- data/spec/lib/kiroshi/filter_runner_spec.rb +12 -28
- data/spec/lib/kiroshi/filter_spec.rb +9 -12
- data/spec/lib/kiroshi/filters/class_methods_spec.rb +59 -0
- data/spec/lib/kiroshi/filters_spec.rb +104 -8
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff66735688ded970094d5a88a9bcd67ad4f9cac7679fc55ceb75534d9b16d9fc
|
4
|
+
data.tar.gz: d6da2da4ec294c0d134ceff028e19b30fa5e13ddab2fae2ad0582a5fdfc559aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 15f142b43b0fcd7fc12f8cd2c1246031aabec104deaf1270030eca5afa56d5776d5c3fec301422c12baec0c531d5366f39832b4afe6ec3d1c83f40ce3e94aea5
|
7
|
+
data.tar.gz: af94b97459b7a157c6ea30f36c36b937e9a75c25b7081106cf6b6d104b3514687a7da4842e9ac847b7669954bab65f707e8a609d55e3666393f6da09cad6d9c4
|
data/.rubocop_todo.yml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on 2025-08-
|
3
|
+
# on 2025-08-18 22:27:27 UTC using RuboCop version 1.79.2.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
@@ -13,11 +13,7 @@ RSpec/NoExpectationExample:
|
|
13
13
|
Exclude:
|
14
14
|
- 'spec/lib/kiroshi_spec.rb'
|
15
15
|
|
16
|
-
# Offense count:
|
17
|
-
|
18
|
-
Style/Documentation:
|
16
|
+
# Offense count: 1
|
17
|
+
RSpec/PendingWithoutReason:
|
19
18
|
Exclude:
|
20
|
-
- 'spec
|
21
|
-
- 'test/**/*'
|
22
|
-
- 'lib/kiroshi/filter.rb'
|
23
|
-
- 'lib/kiroshi/filters.rb'
|
19
|
+
- 'spec/lib/kiroshi/filters_spec.rb'
|
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.2.0](https://www.rubydoc.info/gems/kiroshi/0.2.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.2.0](https://github.com/darthjee/kiroshi/tree/0.2.0)
|
18
18
|
|
19
|
-
[Next release](https://github.com/darthjee/kiroshi/compare/0.
|
19
|
+
[Next release](https://github.com/darthjee/kiroshi/compare/0.2.0...master)
|
20
20
|
|
21
21
|
## Installation
|
22
22
|
|
@@ -202,82 +202,8 @@ result = filters.apply(scope)
|
|
202
202
|
|
203
203
|
The `table` parameter accepts both symbols and strings, and helps resolve column name ambiguity in complex joined queries.
|
204
204
|
|
205
|
-
|
205
|
+
## API Reference
|
206
206
|
|
207
|
-
|
208
|
-
is the individual filter class that applies filtering logic to ActiveRecord scopes.
|
209
|
-
It's automatically used by `Kiroshi::Filters`, but can also be used standalone.
|
207
|
+
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.
|
210
208
|
|
211
|
-
|
212
|
-
|
213
|
-
```ruby
|
214
|
-
# Create individual filters
|
215
|
-
name_filter = Kiroshi::Filter.new(:name, match: :like)
|
216
|
-
status_filter = Kiroshi::Filter.new(:status, match: :exact)
|
217
|
-
|
218
|
-
# Apply filters manually
|
219
|
-
scope = Document.all
|
220
|
-
scope = name_filter.apply(scope, { name: 'report' })
|
221
|
-
scope = status_filter.apply(scope, { status: 'published' })
|
222
|
-
```
|
223
|
-
|
224
|
-
#### Filter Options
|
225
|
-
|
226
|
-
- `match: :exact` - Performs exact matching (default)
|
227
|
-
- `match: :like` - Performs partial matching using SQL LIKE
|
228
|
-
- `table: :table_name` - Specifies which table to filter on (useful for joined queries)
|
229
|
-
|
230
|
-
```ruby
|
231
|
-
# Exact match filter
|
232
|
-
exact_filter = Kiroshi::Filter.new(:status)
|
233
|
-
exact_filter.apply(Document.all, { status: 'published' })
|
234
|
-
# Generates: WHERE status = 'published'
|
235
|
-
|
236
|
-
# LIKE match filter
|
237
|
-
like_filter = Kiroshi::Filter.new(:title, match: :like)
|
238
|
-
like_filter.apply(Document.all, { title: 'Ruby' })
|
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'
|
250
|
-
```
|
251
|
-
|
252
|
-
#### Empty Value Handling
|
253
|
-
|
254
|
-
Filters automatically ignore empty or nil values:
|
255
|
-
|
256
|
-
```ruby
|
257
|
-
filter = Kiroshi::Filter.new(:name)
|
258
|
-
filter.apply(Document.all, { name: nil }) # Returns original scope
|
259
|
-
filter.apply(Document.all, { name: '' }) # Returns original scope
|
260
|
-
filter.apply(Document.all, {}) # Returns original scope
|
261
|
-
filter.apply(Document.all, { name: 'value' }) # Applies filter
|
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.
|
209
|
+
For detailed API documentation, see the [YARD documentation](https://www.rubydoc.info/gems/kiroshi/0.2.0).
|
data/lib/kiroshi/filter.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Kiroshi
|
4
|
+
# @api private
|
4
5
|
# @author darthjee
|
5
6
|
#
|
6
7
|
# A filter class that applies filtering logic to ActiveRecord scopes
|
@@ -10,11 +11,15 @@ module Kiroshi
|
|
10
11
|
#
|
11
12
|
# @example Creating and applying an exact filter
|
12
13
|
# filter = Kiroshi::Filter.new(:name)
|
13
|
-
# filtered_scope = filter.apply(Document.all,
|
14
|
+
# filtered_scope = filter.apply(scope: Document.all, value: 'John')
|
14
15
|
#
|
15
16
|
# @example Creating and applying a LIKE filter
|
16
17
|
# filter = Kiroshi::Filter.new(:title, match: :like)
|
17
|
-
# filtered_scope = filter.apply(Article.all,
|
18
|
+
# filtered_scope = filter.apply(scope: Article.all, value: 'Ruby')
|
19
|
+
#
|
20
|
+
# @example Creating and applying a filter with specific value
|
21
|
+
# filter = Kiroshi::Filter.new(:status)
|
22
|
+
# filtered_scope = filter.apply(scope: Document.all, value: 'published')
|
18
23
|
#
|
19
24
|
# @since 0.1.0
|
20
25
|
class Filter
|
@@ -67,43 +72,43 @@ module Kiroshi
|
|
67
72
|
|
68
73
|
# Applies the filter to the given scope
|
69
74
|
#
|
70
|
-
# This method
|
71
|
-
#
|
72
|
-
#
|
75
|
+
# This method applies the appropriate WHERE clause to the scope using the
|
76
|
+
# provided value. If no value is present or the value is blank, the original
|
77
|
+
# scope is returned unchanged.
|
73
78
|
#
|
74
79
|
# @param scope [ActiveRecord::Relation] the ActiveRecord scope to filter
|
75
|
-
# @param
|
80
|
+
# @param value [Object, nil] the value to use for filtering, defaults to nil
|
76
81
|
#
|
77
82
|
# @return [ActiveRecord::Relation] the filtered scope
|
78
83
|
#
|
79
84
|
# @example Applying an exact filter
|
80
85
|
# filter = Kiroshi::Filter.new(:status)
|
81
|
-
# filter.apply(Document.all,
|
86
|
+
# filter.apply(scope: Document.all, value: 'published')
|
82
87
|
# # Generates: WHERE status = 'published'
|
83
88
|
#
|
84
89
|
# @example Applying a LIKE filter
|
85
90
|
# filter = Kiroshi::Filter.new(:title, match: :like)
|
86
|
-
# filter.apply(Article.all,
|
91
|
+
# filter.apply(scope: Article.all, value: 'Ruby')
|
87
92
|
# # Generates: WHERE title LIKE '%Ruby%'
|
88
93
|
#
|
89
94
|
# @example Applying a filter with table qualification
|
90
95
|
# filter = Kiroshi::Filter.new(:name, table: 'documents')
|
91
|
-
# filter.apply(Document.joins(:tags),
|
96
|
+
# filter.apply(scope: Document.joins(:tags), value: 'report')
|
92
97
|
# # Generates: WHERE documents.name = 'report'
|
93
98
|
#
|
94
99
|
# @example Applying a filter with table qualification for tags
|
95
100
|
# filter = Kiroshi::Filter.new(:name, table: 'tags')
|
96
|
-
# filter.apply(Document.joins(:tags),
|
101
|
+
# filter.apply(scope: Document.joins(:tags), value: 'ruby')
|
97
102
|
# # Generates: WHERE tags.name = 'ruby'
|
98
103
|
#
|
99
104
|
# @example With empty filter value
|
100
105
|
# filter = Kiroshi::Filter.new(:name)
|
101
|
-
# filter.apply(User.all,
|
106
|
+
# filter.apply(scope: User.all, value: nil)
|
102
107
|
# # Returns the original scope unchanged
|
103
108
|
#
|
104
|
-
# @since 0.
|
105
|
-
def apply(scope
|
106
|
-
runner = FilterRunner.new(filter: self, scope: scope,
|
109
|
+
# @since 0.2.0
|
110
|
+
def apply(scope:, value: nil)
|
111
|
+
runner = FilterRunner.new(filter: self, scope: scope, value: value)
|
107
112
|
runner.apply
|
108
113
|
end
|
109
114
|
end
|
data/lib/kiroshi/filter_query.rb
CHANGED
@@ -94,38 +94,34 @@ module Kiroshi
|
|
94
94
|
#
|
95
95
|
# @return [Kiroshi::FilterRunner] the filter runner instance
|
96
96
|
|
97
|
-
delegate :scope, :attribute, :table_name, :
|
97
|
+
delegate :scope, :attribute, :table_name, :value, to: :filter_runner
|
98
98
|
|
99
99
|
# @!method scope
|
100
100
|
# @api private
|
101
|
-
# @private
|
102
101
|
#
|
103
|
-
# Returns the
|
102
|
+
# Returns the current scope being filtered
|
104
103
|
#
|
105
|
-
# @return [ActiveRecord::Relation] the scope
|
104
|
+
# @return [ActiveRecord::Relation] the scope
|
106
105
|
|
107
106
|
# @!method attribute
|
108
107
|
# @api private
|
109
|
-
# @private
|
110
108
|
#
|
111
109
|
# Returns the attribute name to filter by
|
112
110
|
#
|
113
|
-
# @return [Symbol] the attribute name
|
111
|
+
# @return [Symbol] the attribute name
|
114
112
|
|
115
113
|
# @!method table_name
|
116
114
|
# @api private
|
117
|
-
# @private
|
118
115
|
#
|
119
|
-
# Returns the table name
|
116
|
+
# Returns the table name for the filter
|
120
117
|
#
|
121
118
|
# @return [String] the table name
|
122
119
|
|
123
|
-
# @!method
|
120
|
+
# @!method value
|
124
121
|
# @api private
|
125
|
-
# @private
|
126
122
|
#
|
127
|
-
# Returns the filter value
|
123
|
+
# Returns the filter value
|
128
124
|
#
|
129
|
-
# @return [Object
|
125
|
+
# @return [Object] the filter value
|
130
126
|
end
|
131
127
|
end
|
@@ -12,7 +12,12 @@ module Kiroshi
|
|
12
12
|
#
|
13
13
|
# @example Creating and running a filter
|
14
14
|
# filter = Kiroshi::Filter.new(:name, match: :like)
|
15
|
-
# runner = Kiroshi::FilterRunner.new(filter: filter, scope: User.all,
|
15
|
+
# runner = Kiroshi::FilterRunner.new(filter: filter, scope: User.all, value: 'John')
|
16
|
+
# result = runner.apply
|
17
|
+
#
|
18
|
+
# @example Creating and running a filter with specific value
|
19
|
+
# filter = Kiroshi::Filter.new(:status)
|
20
|
+
# runner = Kiroshi::FilterRunner.new(filter: filter, scope: User.all, value: 'active')
|
16
21
|
# result = runner.apply
|
17
22
|
#
|
18
23
|
# @since 0.1.0
|
@@ -21,13 +26,13 @@ module Kiroshi
|
|
21
26
|
#
|
22
27
|
# @param filter [Kiroshi::Filter] the filter configuration
|
23
28
|
# @param scope [ActiveRecord::Relation] the scope to filter
|
24
|
-
# @param
|
29
|
+
# @param value [Object, nil] the specific value to use for filtering, defaults to nil
|
25
30
|
#
|
26
|
-
# @since 0.
|
27
|
-
def initialize(filter:, scope:,
|
31
|
+
# @since 0.2.0
|
32
|
+
def initialize(filter:, scope:, value: nil)
|
28
33
|
@filter = filter
|
29
34
|
@scope = scope
|
30
|
-
@
|
35
|
+
@value = value
|
31
36
|
end
|
32
37
|
|
33
38
|
# Applies the filter logic to the scope
|
@@ -38,41 +43,49 @@ module Kiroshi
|
|
38
43
|
# @return [ActiveRecord::Relation] the filtered scope
|
39
44
|
#
|
40
45
|
# @example Applying exact match filter
|
41
|
-
# runner = FilterRunner.new(filter: filter, scope: scope,
|
46
|
+
# runner = FilterRunner.new(filter: filter, scope: scope, value: 'John')
|
42
47
|
# runner.apply
|
43
48
|
#
|
44
49
|
# @example Applying LIKE filter
|
45
|
-
# runner = FilterRunner.new(filter: filter, scope: scope,
|
50
|
+
# runner = FilterRunner.new(filter: filter, scope: scope, value: 'Ruby')
|
51
|
+
# runner.apply
|
52
|
+
#
|
53
|
+
# @example With specific value provided
|
54
|
+
# runner = FilterRunner.new(filter: filter, scope: scope, value: 'specific_value')
|
46
55
|
# runner.apply
|
47
56
|
#
|
48
|
-
# @example With no
|
49
|
-
# runner = FilterRunner.new(filter: filter, scope: scope,
|
57
|
+
# @example With no value (returns unchanged scope)
|
58
|
+
# runner = FilterRunner.new(filter: filter, scope: scope, value: nil)
|
50
59
|
# runner.apply
|
51
60
|
# # Returns the original scope unchanged
|
52
61
|
#
|
53
62
|
# @since 0.1.1
|
54
63
|
def apply
|
55
|
-
return scope unless
|
64
|
+
return scope unless value.present?
|
56
65
|
|
57
66
|
query_strategy = FilterQuery.for(filter.match).new(self)
|
58
67
|
query_strategy.apply
|
59
68
|
end
|
60
69
|
|
61
|
-
|
70
|
+
attr_reader :scope, :value
|
71
|
+
|
72
|
+
# @!method scope
|
73
|
+
# @api private
|
62
74
|
#
|
63
|
-
#
|
75
|
+
# Returns the current scope being filtered
|
64
76
|
#
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
end
|
77
|
+
# @return [ActiveRecord::Relation] the scope
|
78
|
+
#
|
79
|
+
# @since 0.1.1
|
69
80
|
|
70
|
-
#
|
81
|
+
# @!method value
|
82
|
+
# @api private
|
71
83
|
#
|
72
|
-
#
|
84
|
+
# Returns the filter value for the current filter
|
73
85
|
#
|
74
|
-
#
|
75
|
-
|
86
|
+
# @return [Object] the filter value or nil if not present
|
87
|
+
#
|
88
|
+
# @since 0.2.0
|
76
89
|
|
77
90
|
# Returns the table name to use for the filter
|
78
91
|
#
|
@@ -84,12 +97,12 @@ module Kiroshi
|
|
84
97
|
#
|
85
98
|
# @example With filter table_name specified
|
86
99
|
# filter = Kiroshi::Filter.new(:name, table: 'tags')
|
87
|
-
# runner = FilterRunner.new(filter: filter, scope: Document.joins(:tags),
|
100
|
+
# runner = FilterRunner.new(filter: filter, scope: Document.joins(:tags), value: 'ruby')
|
88
101
|
# runner.table_name # => 'tags'
|
89
102
|
#
|
90
103
|
# @example Without filter table_name (fallback to scope)
|
91
104
|
# filter = Kiroshi::Filter.new(:name)
|
92
|
-
# runner = FilterRunner.new(filter: filter, scope: Document.all,
|
105
|
+
# runner = FilterRunner.new(filter: filter, scope: Document.all, value: 'test')
|
93
106
|
# runner.table_name # => 'documents'
|
94
107
|
#
|
95
108
|
# @since 0.1.1
|
@@ -104,9 +117,16 @@ module Kiroshi
|
|
104
117
|
#
|
105
118
|
# @return [ActiveRecord::Relation] the scope
|
106
119
|
|
120
|
+
# @!method value
|
121
|
+
# @api private
|
122
|
+
#
|
123
|
+
# Returns the filter value for the current filter
|
124
|
+
#
|
125
|
+
# @return [Object, nil] the filter value or nil if not present
|
126
|
+
|
107
127
|
private
|
108
128
|
|
109
|
-
attr_reader :filter
|
129
|
+
attr_reader :filter
|
110
130
|
|
111
131
|
# @!method filter
|
112
132
|
# @api private
|
@@ -116,14 +136,6 @@ module Kiroshi
|
|
116
136
|
#
|
117
137
|
# @return [Kiroshi::Filter] the filter configuration
|
118
138
|
|
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
139
|
delegate :attribute, to: :filter
|
128
140
|
delegate :table_name, to: :scope, prefix: true
|
129
141
|
delegate :table_name, to: :filter, prefix: true
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kiroshi
|
4
|
+
class Filters
|
5
|
+
# @api public
|
6
|
+
# Class-level methods for configuring filters in Kiroshi::Filters
|
7
|
+
#
|
8
|
+
# This module provides the DSL methods that allow filter classes to
|
9
|
+
# define their filtering behavior using class-level method calls.
|
10
|
+
# These methods are automatically available when extending Kiroshi::Filters.
|
11
|
+
#
|
12
|
+
# The primary interface is the {.filter_by} method, which registers
|
13
|
+
# filters that will be applied when {Filters#apply} is called on
|
14
|
+
# instances of the filter class.
|
15
|
+
#
|
16
|
+
# @example Basic filter configuration
|
17
|
+
# class DocumentFilters < Kiroshi::Filters
|
18
|
+
# filter_by :name, match: :like
|
19
|
+
# filter_by :status
|
20
|
+
# filter_by :category, table: :documents
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @example Accessing filter configurations
|
24
|
+
# DocumentFilters.filter_configs.keys # => [:name, :status, :category]
|
25
|
+
# DocumentFilters.filter_configs[:name].match # => :like
|
26
|
+
#
|
27
|
+
# @since 0.2.0
|
28
|
+
# @author darthjee
|
29
|
+
module ClassMethods
|
30
|
+
# Defines a filter for the current filter class
|
31
|
+
#
|
32
|
+
# This method is used at the class level to configure filters that will
|
33
|
+
# be applied when {Filters#apply} is called. Each call creates a new {Filter}
|
34
|
+
# instance with the specified configuration and stores it in the class's
|
35
|
+
# filter registry for later use during filtering operations.
|
36
|
+
#
|
37
|
+
# The method supports various matching strategies and table qualification
|
38
|
+
# options to handle complex database queries with joins and ambiguous
|
39
|
+
# column names.
|
40
|
+
#
|
41
|
+
# @overload filter_by(attribute, **options)
|
42
|
+
# @param attribute [Symbol] the attribute name to filter by
|
43
|
+
# @param options [Hash] additional options passed to {Filter#initialize}
|
44
|
+
# @option options [Symbol] :match (:exact) the matching type
|
45
|
+
# - +:exact+ for exact matching (default)
|
46
|
+
# - +:like+ for partial matching using SQL LIKE with wildcards
|
47
|
+
# @option options [String, Symbol, nil] :table (nil) the table name to qualify the attribute
|
48
|
+
# when dealing with joined tables that have conflicting column names
|
49
|
+
#
|
50
|
+
# @return (see Filters.filter_by)
|
51
|
+
# @example (see Filters.filter_by)
|
52
|
+
# @note (see Filters.filter_by)
|
53
|
+
# @see (see Filters.filter_by)
|
54
|
+
# @since (see Filters.filter_by)
|
55
|
+
def filter_by(attribute, **)
|
56
|
+
Filter.new(attribute, **).tap do |filter|
|
57
|
+
filter_configs[attribute] = filter
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @api private
|
62
|
+
# Returns the filter configuration for a specific attribute
|
63
|
+
#
|
64
|
+
# This method provides a convenient way to retrieve a specific filter
|
65
|
+
# by its attribute name. It's a shorthand for accessing the filter_configs
|
66
|
+
# hash directly and is used internally by the filtering system.
|
67
|
+
#
|
68
|
+
# @param attribute [Symbol] the attribute name to look up
|
69
|
+
#
|
70
|
+
# @return [Filter, nil] the filter instance for the given attribute,
|
71
|
+
# or nil if no filter is configured for that attribute
|
72
|
+
#
|
73
|
+
# @example Retrieving a specific filter
|
74
|
+
# class MyFilters < Kiroshi::Filters
|
75
|
+
# filter_by :name, match: :like
|
76
|
+
# filter_by :status
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# MyFilters.filter_for(:name) # => #<Kiroshi::Filter:0x... @attribute=:name @match=:like>
|
80
|
+
# MyFilters.filter_for(:status) # => #<Kiroshi::Filter:0x... @attribute=:status @match=:exact>
|
81
|
+
# MyFilters.filter_for(:unknown) # => nil
|
82
|
+
#
|
83
|
+
# @see .filter_configs for accessing the complete filter registry
|
84
|
+
# @see Filters#apply for how this method is used during filtering
|
85
|
+
#
|
86
|
+
# @since 0.2.0
|
87
|
+
def filter_for(attribute)
|
88
|
+
filter_configs[attribute] || inherited_filter_for(attribute)
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# @api private
|
94
|
+
# @private
|
95
|
+
#
|
96
|
+
# Searches for a filter in the inheritance chain
|
97
|
+
#
|
98
|
+
# This method looks up the inheritance chain to find a filter configuration
|
99
|
+
# for the given attribute. It only searches in superclasses that inherit
|
100
|
+
# from Kiroshi::Filters, stopping when it reaches a non-Filters class.
|
101
|
+
#
|
102
|
+
# @param attribute [Symbol] the attribute name to look up
|
103
|
+
# @return [Filter, nil] the filter instance from a parent class, or nil if not found
|
104
|
+
#
|
105
|
+
# @since 0.2.0
|
106
|
+
def inherited_filter_for(attribute)
|
107
|
+
return nil unless superclass < Kiroshi::Filters
|
108
|
+
|
109
|
+
superclass.filter_for(attribute)
|
110
|
+
end
|
111
|
+
|
112
|
+
# @api private
|
113
|
+
# @private
|
114
|
+
#
|
115
|
+
# Returns the hash of configured filters for this filter class
|
116
|
+
#
|
117
|
+
# This method provides access to the internal registry of filters
|
118
|
+
# that have been configured using {.filter_by}. The returned hash
|
119
|
+
# contains {Filter} instances keyed by their attribute names, allowing
|
120
|
+
# for efficient O(1) lookup during filter application.
|
121
|
+
#
|
122
|
+
# This method is primarily used internally by {Filters#apply} to
|
123
|
+
# iterate through and apply all configured filters to a scope.
|
124
|
+
# While marked as private API, it may be useful for introspection
|
125
|
+
# and testing purposes.
|
126
|
+
#
|
127
|
+
# @return [Hash<Symbol, Filter>] hash of {Filter} instances configured
|
128
|
+
# for this filter class, keyed by attribute name for efficient access
|
129
|
+
#
|
130
|
+
# @example Accessing configured filters for introspection
|
131
|
+
# class MyFilters < Kiroshi::Filters
|
132
|
+
# filter_by :name, match: :like
|
133
|
+
# filter_by :status
|
134
|
+
# filter_by :category, table: :categories
|
135
|
+
# end
|
136
|
+
#
|
137
|
+
# MyFilters.filter_configs.length # => 3
|
138
|
+
# MyFilters.filter_configs.keys # => [:name, :status, :category]
|
139
|
+
# MyFilters.filter_configs[:name].attribute # => :name
|
140
|
+
# MyFilters.filter_configs[:name].match # => :like
|
141
|
+
# MyFilters.filter_configs[:status].match # => :exact
|
142
|
+
# MyFilters.filter_configs[:category].table_name # => :categories
|
143
|
+
#
|
144
|
+
# @example Using in tests to verify filter configuration
|
145
|
+
# RSpec.describe ProductFilters do
|
146
|
+
# it 'configures the expected filters' do
|
147
|
+
# expect(described_class.filter_configs).to have_key(:name)
|
148
|
+
# expect(described_class.filter_configs[:name].match).to eq(:like)
|
149
|
+
# end
|
150
|
+
# end
|
151
|
+
#
|
152
|
+
# @note This method returns a reference to the actual internal hash.
|
153
|
+
# Modifying the returned hash directly will affect the filter class
|
154
|
+
# configuration. Use {.filter_by} for proper filter registration.
|
155
|
+
#
|
156
|
+
# @note The hash is lazily initialized on first access and persists
|
157
|
+
# for the lifetime of the class. Each filter class maintains its
|
158
|
+
# own separate filter_configs hash.
|
159
|
+
#
|
160
|
+
# @see .filter_by for adding filters to this configuration
|
161
|
+
# @see Filters#apply for how these configurations are used
|
162
|
+
#
|
163
|
+
# @since 0.2.0
|
164
|
+
def filter_configs
|
165
|
+
@filter_configs ||= {}
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
data/lib/kiroshi/filters.rb
CHANGED
@@ -36,73 +36,69 @@ module Kiroshi
|
|
36
36
|
#
|
37
37
|
# @since 0.1.0
|
38
38
|
class Filters
|
39
|
-
|
40
|
-
# Defines a filter for the current filter class
|
41
|
-
#
|
42
|
-
# This method is used at the class level to configure filters that will
|
43
|
-
# be applied when {#apply} is called. Each call creates a new {Filter}
|
44
|
-
# instance with the specified configuration.
|
45
|
-
#
|
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
|
53
|
-
#
|
54
|
-
# @return [Filter] the new filter instance
|
55
|
-
#
|
56
|
-
# @example Defining exact match filters
|
57
|
-
# class ProductFilters < Kiroshi::Filters
|
58
|
-
# filter_by :category
|
59
|
-
# filter_by :brand
|
60
|
-
# end
|
61
|
-
#
|
62
|
-
# @example Defining partial match filters
|
63
|
-
# class SearchFilters < Kiroshi::Filters
|
64
|
-
# filter_by :title, match: :like
|
65
|
-
# filter_by :description, match: :like
|
66
|
-
# end
|
67
|
-
#
|
68
|
-
# @example Mixed filter types
|
69
|
-
# class OrderFilters < Kiroshi::Filters
|
70
|
-
# filter_by :customer_name, match: :like
|
71
|
-
# filter_by :status, match: :exact
|
72
|
-
# filter_by :payment_method
|
73
|
-
# end
|
74
|
-
#
|
75
|
-
# @example Filter with table qualification
|
76
|
-
# class DocumentTagFilters < Kiroshi::Filters
|
77
|
-
# filter_by :name, table: :tags
|
78
|
-
# end
|
79
|
-
#
|
80
|
-
# @since 0.1.0
|
81
|
-
def filter_by(attribute, **)
|
82
|
-
Filter.new(attribute, **).tap do |filter|
|
83
|
-
filter_configs << filter
|
84
|
-
end
|
85
|
-
end
|
39
|
+
autoload :ClassMethods, 'kiroshi/filters/class_methods'
|
86
40
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
41
|
+
extend ClassMethods
|
42
|
+
|
43
|
+
# @method self.filter_by(attribute, **options)
|
44
|
+
# @api public
|
45
|
+
# @param attribute [Symbol] the attribute name to filter by
|
46
|
+
# @param options [Hash] additional options passed to {Filter#initialize}
|
47
|
+
# @option options [Symbol] :match (:exact) the matching type
|
48
|
+
# - +:exact+ for exact matching (default)
|
49
|
+
# - +:like+ for partial matching using SQL LIKE with wildcards
|
50
|
+
# @option options [String, Symbol, nil] :table (nil) the table name to qualify the attribute
|
51
|
+
# when dealing with joined tables that have conflicting column names
|
52
|
+
#
|
53
|
+
# @return [Filter] the new filter instance that was created and registered
|
54
|
+
#
|
55
|
+
# @example Defining exact match filters
|
56
|
+
# class ProductFilters < Kiroshi::Filters
|
57
|
+
# filter_by :category # Exact match on category
|
58
|
+
# filter_by :brand # Exact match on brand
|
59
|
+
# filter_by :active # Exact match on active status
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# @example Defining partial match filters
|
63
|
+
# class SearchFilters < Kiroshi::Filters
|
64
|
+
# filter_by :title, match: :like # Partial match on title
|
65
|
+
# filter_by :description, match: :like # Partial match on description
|
66
|
+
# filter_by :author_name, match: :like # Partial match on author name
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# @example Mixed filter types with different matching strategies
|
70
|
+
# class OrderFilters < Kiroshi::Filters
|
71
|
+
# filter_by :customer_name, match: :like # Partial match for customer search
|
72
|
+
# filter_by :status, match: :exact # Exact match for order status
|
73
|
+
# filter_by :payment_method # Exact match (default) for payment
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# @example Filters with table qualification for joined queries
|
77
|
+
# class DocumentTagFilters < Kiroshi::Filters
|
78
|
+
# filter_by :name, table: :documents # Filter by document name
|
79
|
+
# filter_by :tag_name, table: :tags # Filter by tag name
|
80
|
+
# filter_by :category, table: :categories # Filter by category name
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# @example Complex real-world filter class
|
84
|
+
# class ProductSearchFilters < Kiroshi::Filters
|
85
|
+
# filter_by :name, match: :like # Product name search
|
86
|
+
# filter_by :category_id # Exact category match
|
87
|
+
# filter_by :brand, match: :like # Brand name search
|
88
|
+
# filter_by :price_min # Minimum price
|
89
|
+
# filter_by :price_max # Maximum price
|
90
|
+
# filter_by :in_stock # Availability filter
|
91
|
+
# filter_by :category_name, table: :categories # Category name via join
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# @note When using table qualification, ensure that the specified table
|
95
|
+
# is properly joined in the scope being filtered. The filter will not
|
96
|
+
# automatically add joins - it only qualifies the column name.
|
97
|
+
#
|
98
|
+
# @see Filter#initialize for detailed information about filter options
|
99
|
+
# @see Filters#apply for how these filters are used during query execution
|
100
|
+
#
|
101
|
+
# @since 0.1.0
|
106
102
|
|
107
103
|
# Creates a new Filters instance
|
108
104
|
#
|
@@ -160,10 +156,13 @@ module Kiroshi
|
|
160
156
|
# filtered_articles = filters.apply(Article.all)
|
161
157
|
# # Generates: WHERE title LIKE '%Ruby%'
|
162
158
|
#
|
163
|
-
# @since 0.
|
159
|
+
# @since 0.2.0
|
164
160
|
def apply(scope)
|
165
|
-
|
166
|
-
|
161
|
+
filters.compact.each do |attribute, value|
|
162
|
+
filter = self.class.filter_for(attribute)
|
163
|
+
next unless filter
|
164
|
+
|
165
|
+
scope = filter.apply(scope: scope, value: value)
|
167
166
|
end
|
168
167
|
|
169
168
|
scope
|
data/lib/kiroshi/version.rb
CHANGED
data/lib/kiroshi.rb
CHANGED
@@ -9,9 +9,10 @@
|
|
9
9
|
# using a declarative DSL. It supports multiple matching strategies and can
|
10
10
|
# be easily integrated into Rails controllers and other components.
|
11
11
|
#
|
12
|
-
# The gem is designed around
|
12
|
+
# The gem is designed around the main concept:
|
13
13
|
# - {Filters}: A base class for creating reusable filter sets
|
14
|
-
#
|
14
|
+
#
|
15
|
+
# Individual filters are handled internally and don't require direct interaction.
|
15
16
|
#
|
16
17
|
# @example Basic filter class definition
|
17
18
|
# class DocumentFilters < Kiroshi::Filters
|
@@ -68,25 +69,6 @@
|
|
68
69
|
# filtered_users = filters.apply(User.includes(:department))
|
69
70
|
# # Generates: WHERE email LIKE '%admin%' AND role = 'moderator' AND active = true
|
70
71
|
#
|
71
|
-
# @example Individual filter usage
|
72
|
-
# # Create standalone filters
|
73
|
-
# name_filter = Kiroshi::Filter.new(:name, match: :like)
|
74
|
-
# status_filter = Kiroshi::Filter.new(:status)
|
75
|
-
#
|
76
|
-
# # Apply filters step by step
|
77
|
-
# scope = Document.all
|
78
|
-
# scope = name_filter.apply(scope, { name: 'annual' })
|
79
|
-
# scope = status_filter.apply(scope, { status: 'published' })
|
80
|
-
#
|
81
|
-
# @example Filter matching types
|
82
|
-
# # Exact matching (default)
|
83
|
-
# Kiroshi::Filter.new(:status)
|
84
|
-
# # Generates: WHERE status = 'value'
|
85
|
-
#
|
86
|
-
# # Partial matching with LIKE
|
87
|
-
# Kiroshi::Filter.new(:title, match: :like)
|
88
|
-
# # Generates: WHERE title LIKE '%value%'
|
89
|
-
#
|
90
72
|
# @example Empty value handling
|
91
73
|
# filters = DocumentFilters.new(name: '', status: 'published')
|
92
74
|
# result = filters.apply(Document.all)
|
@@ -148,7 +130,6 @@
|
|
148
130
|
# end
|
149
131
|
#
|
150
132
|
# @see Filters Base class for creating filter sets
|
151
|
-
# @see Filter Individual filter implementation
|
152
133
|
# @see https://github.com/darthjee/kiroshi GitHub repository
|
153
134
|
# @see https://www.rubydoc.info/gems/kiroshi YARD documentation
|
154
135
|
#
|
@@ -6,11 +6,10 @@ RSpec.describe Kiroshi::FilterQuery::Exact, type: :model do
|
|
6
6
|
describe '#apply' do
|
7
7
|
subject(:query) { described_class.new(filter_runner) }
|
8
8
|
|
9
|
-
let(:filter_runner) { Kiroshi::FilterRunner.new(filter: filter, scope: scope,
|
9
|
+
let(:filter_runner) { Kiroshi::FilterRunner.new(filter: filter, scope: scope, value: filter_value) }
|
10
10
|
let(:filter) { Kiroshi::Filter.new(:name, match: :exact) }
|
11
11
|
let(:scope) { Document.all }
|
12
12
|
let(:filter_value) { 'test_document' }
|
13
|
-
let(:filters) { { name: filter_value } }
|
14
13
|
|
15
14
|
let!(:matching_document) { create(:document, name: 'test_document') }
|
16
15
|
let!(:non_matching_document) { create(:document, name: 'other_document') }
|
@@ -36,7 +35,6 @@ RSpec.describe Kiroshi::FilterQuery::Exact, type: :model do
|
|
36
35
|
context 'when filtering by status attribute' do
|
37
36
|
let(:filter) { Kiroshi::Filter.new(:status, match: :exact) }
|
38
37
|
let(:filter_value) { 'published' }
|
39
|
-
let(:filters) { { status: filter_value } }
|
40
38
|
|
41
39
|
let!(:published_document) { create(:document, status: 'published') }
|
42
40
|
let!(:draft_document) { create(:document, status: 'draft') }
|
@@ -63,7 +61,6 @@ RSpec.describe Kiroshi::FilterQuery::Exact, type: :model do
|
|
63
61
|
context 'when filtering with numeric values' do
|
64
62
|
let(:filter) { Kiroshi::Filter.new(:priority, match: :exact) }
|
65
63
|
let(:filter_value) { 1 }
|
66
|
-
let(:filters) { { priority: filter_value } }
|
67
64
|
|
68
65
|
let!(:high_priority_document) { create(:document, priority: 1) }
|
69
66
|
let!(:medium_priority_document) { create(:document, priority: 2) }
|
@@ -90,7 +87,6 @@ RSpec.describe Kiroshi::FilterQuery::Exact, type: :model do
|
|
90
87
|
context 'when filtering with boolean values' do
|
91
88
|
let(:filter) { Kiroshi::Filter.new(:active, match: :exact) }
|
92
89
|
let(:filter_value) { true }
|
93
|
-
let(:filters) { { active: filter_value } }
|
94
90
|
|
95
91
|
let!(:active_document) { create(:document, active: true) }
|
96
92
|
let!(:inactive_document) { create(:document, active: false) }
|
@@ -154,7 +150,6 @@ RSpec.describe Kiroshi::FilterQuery::Exact, type: :model do
|
|
154
150
|
context 'when filter has table configured' do
|
155
151
|
let(:scope) { Document.joins(:tags) }
|
156
152
|
let(:filter_value) { 'ruby' }
|
157
|
-
let(:filters) { { name: filter_value } }
|
158
153
|
|
159
154
|
let!(:first_tag) { Tag.find_or_create_by(name: 'ruby') }
|
160
155
|
let!(:second_tag) { Tag.find_or_create_by(name: 'javascript') }
|
@@ -252,7 +247,6 @@ RSpec.describe Kiroshi::FilterQuery::Exact, type: :model do
|
|
252
247
|
context 'when filtering by different attributes with table qualification' do
|
253
248
|
let(:filter) { Kiroshi::Filter.new(:id, match: :exact, table: :tags) }
|
254
249
|
let(:filter_value) { first_tag.id }
|
255
|
-
let(:filters) { { id: filter_value } }
|
256
250
|
|
257
251
|
let(:expected_sql) do
|
258
252
|
<<~SQL.squish
|
@@ -6,11 +6,10 @@ RSpec.describe Kiroshi::FilterQuery::Like, type: :model do
|
|
6
6
|
describe '#apply' do
|
7
7
|
subject(:query) { described_class.new(filter_runner) }
|
8
8
|
|
9
|
-
let(:filter_runner) { Kiroshi::FilterRunner.new(filter: filter, scope: scope,
|
9
|
+
let(:filter_runner) { Kiroshi::FilterRunner.new(filter: filter, scope: scope, value: filter_value) }
|
10
10
|
let(:filter) { Kiroshi::Filter.new(:name, match: :like) }
|
11
11
|
let(:scope) { Document.all }
|
12
12
|
let(:filter_value) { 'test' }
|
13
|
-
let(:filters) { { name: filter_value } }
|
14
13
|
|
15
14
|
let!(:matching_document) { create(:document, name: 'test_document') }
|
16
15
|
let!(:another_match) { create(:document, name: 'my_test_file') }
|
@@ -41,7 +40,6 @@ RSpec.describe Kiroshi::FilterQuery::Like, type: :model do
|
|
41
40
|
context 'when filtering by status attribute' do
|
42
41
|
let(:filter) { Kiroshi::Filter.new(:status, match: :like) }
|
43
42
|
let(:filter_value) { 'pub' }
|
44
|
-
let(:filters) { { status: filter_value } }
|
45
43
|
|
46
44
|
let!(:published_document) { create(:document, status: 'published') }
|
47
45
|
let!(:republished_document) { create(:document, status: 'republished') }
|
@@ -73,7 +71,6 @@ RSpec.describe Kiroshi::FilterQuery::Like, type: :model do
|
|
73
71
|
context 'when filtering with numeric values as strings' do
|
74
72
|
let(:filter) { Kiroshi::Filter.new(:version, match: :like) }
|
75
73
|
let(:filter_value) { '1.2' }
|
76
|
-
let(:filters) { { version: filter_value } }
|
77
74
|
|
78
75
|
let!(:version_match) { create(:document, version: '1.2.3') }
|
79
76
|
let!(:another_version) { create(:document, version: '2.1.2') }
|
@@ -190,7 +187,6 @@ RSpec.describe Kiroshi::FilterQuery::Like, type: :model do
|
|
190
187
|
context 'when filter has table configured' do
|
191
188
|
let(:scope) { Document.joins(:tags) }
|
192
189
|
let(:filter_value) { 'ruby' }
|
193
|
-
let(:filters) { { name: filter_value } }
|
194
190
|
|
195
191
|
let!(:first_tag) { Tag.find_or_create_by(name: 'ruby') }
|
196
192
|
let!(:second_tag) { Tag.find_or_create_by(name: 'ruby_on_rails') }
|
@@ -4,12 +4,12 @@ require 'spec_helper'
|
|
4
4
|
|
5
5
|
RSpec.describe Kiroshi::FilterRunner, type: :model do
|
6
6
|
describe '#apply' do
|
7
|
-
subject(:runner) { described_class.new(filter: filter, scope: scope,
|
7
|
+
subject(:runner) { described_class.new(filter: filter, scope: scope, value: filter_value) }
|
8
8
|
|
9
9
|
let(:scope) { Document.all }
|
10
10
|
let(:filter_value) { 'test_value' }
|
11
|
-
let(:
|
12
|
-
let!(:matching_document) { create(:document, name:
|
11
|
+
let(:document_name) { filter_value }
|
12
|
+
let!(:matching_document) { create(:document, name: document_name) }
|
13
13
|
let!(:non_matching_document) { create(:document, name: 'other_value') }
|
14
14
|
|
15
15
|
context 'when filter match is :exact' do
|
@@ -57,8 +57,9 @@ RSpec.describe Kiroshi::FilterRunner, type: :model do
|
|
57
57
|
end
|
58
58
|
|
59
59
|
context 'when filter value is not present' do
|
60
|
-
let(:
|
61
|
-
let(:
|
60
|
+
let(:document_name) { 'Some name' }
|
61
|
+
let(:filter) { Kiroshi::Filter.new(:name) }
|
62
|
+
let(:filter_value) { nil }
|
62
63
|
|
63
64
|
it 'returns the original scope unchanged' do
|
64
65
|
expect(runner.apply).to eq(scope)
|
@@ -66,35 +67,18 @@ RSpec.describe Kiroshi::FilterRunner, type: :model do
|
|
66
67
|
end
|
67
68
|
|
68
69
|
context 'when filter value is empty string' do
|
69
|
-
let(:
|
70
|
-
let(:
|
71
|
-
|
72
|
-
it 'returns the original scope unchanged' do
|
73
|
-
expect(runner.apply).to eq(scope)
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
context 'when filter attribute is not in filters hash' do
|
78
|
-
let(:filter) { Kiroshi::Filter.new(:status) }
|
79
|
-
let(:filters) { { name: 'test_value' } }
|
80
|
-
|
81
|
-
it 'returns the original scope unchanged' do
|
82
|
-
expect(runner.apply).to eq(scope)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
context 'when filters hash is empty' do
|
87
|
-
let(:filter) { Kiroshi::Filter.new(:name) }
|
88
|
-
let(:filters) { {} }
|
70
|
+
let(:document_name) { 'Some name' }
|
71
|
+
let(:filter) { Kiroshi::Filter.new(:name) }
|
72
|
+
let(:filter_value) { '' }
|
89
73
|
|
90
74
|
it 'returns the original scope unchanged' do
|
91
75
|
expect(runner.apply).to eq(scope)
|
92
76
|
end
|
93
77
|
end
|
94
78
|
|
95
|
-
context 'with
|
96
|
-
let(:filter)
|
97
|
-
let(:
|
79
|
+
context 'with status filter' do
|
80
|
+
let(:filter) { Kiroshi::Filter.new(:status, match: :exact) }
|
81
|
+
let(:filter_value) { 'finished' }
|
98
82
|
let!(:matching_document) { create(:document, name: 'test_name', status: 'finished') }
|
99
83
|
let!(:non_matching_document) { create(:document, name: 'other_name', status: 'processing') }
|
100
84
|
|
@@ -6,19 +6,18 @@ RSpec.describe Kiroshi::Filter, type: :model do
|
|
6
6
|
describe '#apply' do
|
7
7
|
let(:scope) { Document.all }
|
8
8
|
let(:filter_value) { 'test_value' }
|
9
|
-
let(:filters) { { name: filter_value } }
|
10
9
|
let!(:matching_document) { create(:document, name: filter_value) }
|
11
10
|
let!(:non_matching_document) { create(:document, name: 'other_value') }
|
12
11
|
|
13
12
|
context 'when match is :exact' do
|
14
13
|
subject(:filter) { described_class.new(:name, match: :exact) }
|
15
14
|
|
16
|
-
it 'returns
|
17
|
-
expect(filter.apply(scope,
|
15
|
+
it 'returns documents matching the filter' do
|
16
|
+
expect(filter.apply(scope: scope, value: filter_value)).to include(matching_document)
|
18
17
|
end
|
19
18
|
|
20
|
-
it 'does not return
|
21
|
-
expect(filter.apply(scope,
|
19
|
+
it 'does not return documents not matching the filter' do
|
20
|
+
expect(filter.apply(scope: scope, value: filter_value)).not_to include(non_matching_document)
|
22
21
|
end
|
23
22
|
end
|
24
23
|
|
@@ -30,11 +29,11 @@ RSpec.describe Kiroshi::Filter, type: :model do
|
|
30
29
|
let!(:non_matching_document) { create(:document, name: 'other_value') }
|
31
30
|
|
32
31
|
it 'returns partial matches' do
|
33
|
-
expect(filter.apply(scope,
|
32
|
+
expect(filter.apply(scope: scope, value: filter_value)).to include(matching_document)
|
34
33
|
end
|
35
34
|
|
36
35
|
it 'does not return non-matching records' do
|
37
|
-
expect(filter.apply(scope,
|
36
|
+
expect(filter.apply(scope: scope, value: filter_value)).not_to include(non_matching_document)
|
38
37
|
end
|
39
38
|
end
|
40
39
|
|
@@ -42,21 +41,19 @@ RSpec.describe Kiroshi::Filter, type: :model do
|
|
42
41
|
subject(:filter) { described_class.new(:name) }
|
43
42
|
|
44
43
|
it 'defaults to exact match returning only exact matches' do
|
45
|
-
expect(filter.apply(scope,
|
44
|
+
expect(filter.apply(scope: scope, value: filter_value)).to include(matching_document)
|
46
45
|
end
|
47
46
|
|
48
47
|
it 'defaults to exact match returning not returning when filtering by a non-matching value' do
|
49
|
-
expect(filter.apply(scope,
|
48
|
+
expect(filter.apply(scope: scope, value: filter_value)).not_to include(non_matching_document)
|
50
49
|
end
|
51
50
|
end
|
52
51
|
|
53
52
|
context 'when filter value is not present' do
|
54
53
|
subject(:filter) { described_class.new(:name) }
|
55
54
|
|
56
|
-
let(:filters) { { name: nil } }
|
57
|
-
|
58
55
|
it 'returns the original scope unchanged' do
|
59
|
-
expect(filter.apply(scope,
|
56
|
+
expect(filter.apply(scope: scope, value: nil)).to eq(scope)
|
60
57
|
end
|
61
58
|
end
|
62
59
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe Kiroshi::Filters::ClassMethods, type: :model do
|
6
|
+
subject(:filters_class) { Class.new(Kiroshi::Filters) }
|
7
|
+
|
8
|
+
let(:filter_instance) { filters_class.new(filters) }
|
9
|
+
let(:scope) { Document.all }
|
10
|
+
let(:filters) { {} }
|
11
|
+
|
12
|
+
describe '.filter_by' do
|
13
|
+
let(:scope) { Document.all }
|
14
|
+
let(:filters) { { name: name } }
|
15
|
+
let(:name) { 'test_name' }
|
16
|
+
|
17
|
+
context 'when adding a new filter' do
|
18
|
+
it do
|
19
|
+
expect { filters_class.filter_by :name }
|
20
|
+
.to change { filter_instance.apply(scope) }
|
21
|
+
.from(scope).to(scope.where(name: name))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when adding a filter with table qualification' do
|
26
|
+
let(:scope) { Document.joins(:tags) }
|
27
|
+
|
28
|
+
it do
|
29
|
+
expect { filters_class.filter_by :name, table: :documents }
|
30
|
+
.to change { filter_instance.apply(scope) }
|
31
|
+
.from(scope).to(scope.where(documents: { name: name }))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when adding a filter with different table' do
|
36
|
+
let(:scope) { Document.joins(:tags) }
|
37
|
+
let(:filters) { { name: 'ruby' } }
|
38
|
+
let(:name) { 'ruby' }
|
39
|
+
|
40
|
+
it do
|
41
|
+
expect { filters_class.filter_by :name, table: :tags }
|
42
|
+
.to change { filter_instance.apply(scope) }
|
43
|
+
.from(scope).to(scope.where(tags: { name: name }))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when adding a like filter with table qualification' do
|
48
|
+
let(:scope) { Document.joins(:tags) }
|
49
|
+
let(:filters) { { name: 'test' } }
|
50
|
+
let(:name) { 'test' }
|
51
|
+
|
52
|
+
it do
|
53
|
+
expect { filters_class.filter_by :name, match: :like, table: :documents }
|
54
|
+
.to change { filter_instance.apply(scope) }
|
55
|
+
.from(scope).to(scope.where('documents.name LIKE ?', '%test%'))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -3,16 +3,16 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
RSpec.describe Kiroshi::Filters, type: :model do
|
6
|
-
|
7
|
-
|
6
|
+
subject(:filters_class) { Class.new(described_class) }
|
7
|
+
|
8
|
+
let(:filter_instance) { filters_class.new(filters) }
|
9
|
+
let(:scope) { Document.all }
|
10
|
+
let(:filters) { {} }
|
8
11
|
|
9
|
-
|
10
|
-
let(:filters) { {} }
|
12
|
+
describe '#apply' do
|
11
13
|
let!(:document) { create(:document, name: 'test_name', status: 'finished') }
|
12
14
|
let!(:other_document) { create(:document, name: 'other_name', status: 'processing') }
|
13
15
|
|
14
|
-
let(:filters_class) { Class.new(described_class) }
|
15
|
-
|
16
16
|
context 'when no filters are configured' do
|
17
17
|
context 'when no filters are provided' do
|
18
18
|
it 'returns the original scope unchanged' do
|
@@ -92,7 +92,7 @@ RSpec.describe Kiroshi::Filters, type: :model do
|
|
92
92
|
end
|
93
93
|
|
94
94
|
context 'when scope has joined tables with clashing fields' do
|
95
|
-
let(:scope)
|
95
|
+
let(:scope) { Document.joins(:tags) }
|
96
96
|
let(:filters) { { name: 'test_name' } }
|
97
97
|
|
98
98
|
let!(:first_tag) { Tag.find_or_create_by(name: 'ruby') }
|
@@ -128,7 +128,6 @@ RSpec.describe Kiroshi::Filters, type: :model do
|
|
128
128
|
let(:filters) { { name: 'test' } }
|
129
129
|
|
130
130
|
before do
|
131
|
-
filters_class.instance_variable_set(:@filter_configs, [])
|
132
131
|
filters_class.filter_by :name, match: :like
|
133
132
|
end
|
134
133
|
|
@@ -153,5 +152,102 @@ RSpec.describe Kiroshi::Filters, type: :model do
|
|
153
152
|
end
|
154
153
|
end
|
155
154
|
end
|
155
|
+
|
156
|
+
context 'when filter was defined in the superclass' do
|
157
|
+
subject(:filters_class) { Class.new(parent_class) }
|
158
|
+
|
159
|
+
let(:parent_class) { Class.new(described_class) }
|
160
|
+
let(:filters) { { name: 'test_name' } }
|
161
|
+
|
162
|
+
before do
|
163
|
+
parent_class.filter_by :name
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'applies the filter defined in the parent class' do
|
167
|
+
expect(filter_instance.apply(scope)).to include(document)
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'does not return documents not matching the inherited filter' do
|
171
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'generates SQL that includes the filter value from parent class' do
|
175
|
+
result = filter_instance.apply(scope)
|
176
|
+
expect(result.to_sql).to include("'test_name'")
|
177
|
+
end
|
178
|
+
|
179
|
+
context 'when child class adds its own filter' do
|
180
|
+
let(:filters) { { name: 'test_name', status: 'finished' } }
|
181
|
+
|
182
|
+
before do
|
183
|
+
filters_class.filter_by :status
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'applies both parent and child filters' do
|
187
|
+
expect(filter_instance.apply(scope)).to include(document)
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'does not return documents not matching all filters' do
|
191
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context 'when child class overrides parent filter' do
|
196
|
+
let(:filters) { { name: 'test' } }
|
197
|
+
|
198
|
+
before do
|
199
|
+
filters_class.filter_by :name, match: :like
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'uses the child class filter configuration' do
|
203
|
+
expect(filter_instance.apply(scope)).to include(document)
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'does not use the parent class filter configuration' do
|
207
|
+
expect(filter_instance.apply(scope).to_sql)
|
208
|
+
.to include('LIKE')
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'generates SQL that includes LIKE operation with the filter value' do
|
212
|
+
expect(filter_instance.apply(scope).to_sql)
|
213
|
+
.to include("'%test%'")
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
context 'when child class overrides parent filter with table qualification' do
|
218
|
+
let(:scope) { Document.joins(:tags) }
|
219
|
+
let(:filters) { { name: 'ruby' } }
|
220
|
+
|
221
|
+
let!(:ruby_tag) { Tag.find_or_create_by(name: 'ruby') }
|
222
|
+
let!(:js_tag) { Tag.find_or_create_by(name: 'javascript') }
|
223
|
+
|
224
|
+
before do
|
225
|
+
filters_class.filter_by :name, table: :tags
|
226
|
+
|
227
|
+
document.tags << [ruby_tag]
|
228
|
+
other_document.tags << [js_tag]
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'uses the child class table qualification (tags.name)' do
|
232
|
+
expect(filter_instance.apply(scope)).to include(document)
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'does not return documents with different tag names' do
|
236
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'generates SQL that filters by tags.name, not documents.name' do
|
240
|
+
expect(filter_instance.apply(scope).to_sql).to include('"tags"."name"')
|
241
|
+
end
|
242
|
+
|
243
|
+
it 'generates SQL that does not include documents.name' do
|
244
|
+
expect(filter_instance.apply(scope).to_sql).not_to include('"documents"."name"')
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'generates SQL that includes the tag filter value' do
|
248
|
+
expect(filter_instance.apply(scope).to_sql).to include("'ruby'")
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
156
252
|
end
|
157
253
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kiroshi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Darthjee
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -70,6 +70,7 @@ files:
|
|
70
70
|
- lib/kiroshi/filter_query/like.rb
|
71
71
|
- lib/kiroshi/filter_runner.rb
|
72
72
|
- lib/kiroshi/filters.rb
|
73
|
+
- lib/kiroshi/filters/class_methods.rb
|
73
74
|
- lib/kiroshi/version.rb
|
74
75
|
- spec/integration/readme/.keep
|
75
76
|
- spec/integration/yard/.keep
|
@@ -78,6 +79,7 @@ files:
|
|
78
79
|
- spec/lib/kiroshi/filter_query_spec.rb
|
79
80
|
- spec/lib/kiroshi/filter_runner_spec.rb
|
80
81
|
- spec/lib/kiroshi/filter_spec.rb
|
82
|
+
- spec/lib/kiroshi/filters/class_methods_spec.rb
|
81
83
|
- spec/lib/kiroshi/filters_spec.rb
|
82
84
|
- spec/lib/kiroshi_spec.rb
|
83
85
|
- spec/spec_helper.rb
|