kiroshi 0.1.0 → 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 +56 -41
- data/lib/kiroshi/filter.rb +57 -41
- data/lib/kiroshi/filter_query/exact.rb +38 -0
- data/lib/kiroshi/filter_query/like.rb +42 -0
- data/lib/kiroshi/filter_query.rb +127 -0
- data/lib/kiroshi/filter_runner.rb +164 -0
- data/lib/kiroshi/filters/class_methods.rb +169 -0
- data/lib/kiroshi/filters.rb +68 -62
- data/lib/kiroshi/version.rb +1 -1
- data/lib/kiroshi.rb +138 -3
- data/spec/lib/kiroshi/filter_query/exact_spec.rb +274 -0
- data/spec/lib/kiroshi/filter_query/like_spec.rb +271 -0
- data/spec/lib/kiroshi/filter_query_spec.rb +39 -0
- data/spec/lib/kiroshi/filter_runner_spec.rb +94 -0
- 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 +165 -6
- 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 +14 -2
@@ -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
|
@@ -90,5 +90,164 @@ RSpec.describe Kiroshi::Filters, type: :model do
|
|
90
90
|
expect(filter_instance.apply(scope)).to eq(scope)
|
91
91
|
end
|
92
92
|
end
|
93
|
+
|
94
|
+
context 'when scope has joined tables with clashing fields' do
|
95
|
+
let(:scope) { Document.joins(:tags) }
|
96
|
+
let(:filters) { { name: 'test_name' } }
|
97
|
+
|
98
|
+
let!(:first_tag) { Tag.find_or_create_by(name: 'ruby') }
|
99
|
+
let!(:second_tag) { Tag.find_or_create_by(name: 'programming') }
|
100
|
+
|
101
|
+
before do
|
102
|
+
filters_class.filter_by :name
|
103
|
+
document.tags << [first_tag, second_tag]
|
104
|
+
other_document.tags << [first_tag]
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'filters by document name, not tag name' do
|
108
|
+
result = filter_instance.apply(scope)
|
109
|
+
expect(result).to include(document)
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'does not return documents that do not match document name' do
|
113
|
+
result = filter_instance.apply(scope)
|
114
|
+
expect(result).not_to include(other_document)
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'generates SQL that includes documents table qualification for name field' do
|
118
|
+
result = filter_instance.apply(scope)
|
119
|
+
expect(result.to_sql).to include('"documents"."name"')
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'generates SQL that includes the filter value' do
|
123
|
+
result = filter_instance.apply(scope)
|
124
|
+
expect(result.to_sql).to include("'test_name'")
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'when using like filter' do
|
128
|
+
let(:filters) { { name: 'test' } }
|
129
|
+
|
130
|
+
before do
|
131
|
+
filters_class.filter_by :name, match: :like
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'filters by document name with LIKE operation' do
|
135
|
+
result = filter_instance.apply(scope)
|
136
|
+
expect(result).to include(document)
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'does not return documents that do not match document name pattern' do
|
140
|
+
result = filter_instance.apply(scope)
|
141
|
+
expect(result).not_to include(other_document)
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'generates SQL with table-qualified LIKE operation' do
|
145
|
+
result = filter_instance.apply(scope)
|
146
|
+
expect(result.to_sql).to include('documents.name LIKE')
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'generates SQL with correct LIKE pattern' do
|
150
|
+
result = filter_instance.apply(scope)
|
151
|
+
expect(result.to_sql).to include("'%test%'")
|
152
|
+
end
|
153
|
+
end
|
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
|
93
252
|
end
|
94
253
|
end
|
data/spec/support/db/schema.rb
CHANGED
@@ -6,5 +6,19 @@ ActiveRecord::Schema.define do
|
|
6
6
|
create_table :documents, force: true do |t|
|
7
7
|
t.string :name
|
8
8
|
t.string :status
|
9
|
+
t.boolean :active
|
10
|
+
t.integer :priority
|
11
|
+
t.string :version
|
12
|
+
end
|
13
|
+
|
14
|
+
create_table :tags, force: true do |t|
|
15
|
+
t.string :name, null: false
|
16
|
+
t.index :name, unique: true
|
17
|
+
end
|
18
|
+
|
19
|
+
create_table :documents_tags, force: true do |t|
|
20
|
+
t.references :document, null: false, foreign_key: true
|
21
|
+
t.references :tag, null: false, foreign_key: true
|
22
|
+
t.index %i[document_id tag_id], unique: true
|
9
23
|
end
|
10
24
|
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
|
@@ -65,19 +65,31 @@ files:
|
|
65
65
|
- kiroshi.jpg
|
66
66
|
- lib/kiroshi.rb
|
67
67
|
- lib/kiroshi/filter.rb
|
68
|
+
- lib/kiroshi/filter_query.rb
|
69
|
+
- lib/kiroshi/filter_query/exact.rb
|
70
|
+
- lib/kiroshi/filter_query/like.rb
|
71
|
+
- lib/kiroshi/filter_runner.rb
|
68
72
|
- lib/kiroshi/filters.rb
|
73
|
+
- lib/kiroshi/filters/class_methods.rb
|
69
74
|
- lib/kiroshi/version.rb
|
70
75
|
- spec/integration/readme/.keep
|
71
76
|
- spec/integration/yard/.keep
|
77
|
+
- spec/lib/kiroshi/filter_query/exact_spec.rb
|
78
|
+
- spec/lib/kiroshi/filter_query/like_spec.rb
|
79
|
+
- spec/lib/kiroshi/filter_query_spec.rb
|
80
|
+
- spec/lib/kiroshi/filter_runner_spec.rb
|
72
81
|
- spec/lib/kiroshi/filter_spec.rb
|
82
|
+
- spec/lib/kiroshi/filters/class_methods_spec.rb
|
73
83
|
- spec/lib/kiroshi/filters_spec.rb
|
74
84
|
- spec/lib/kiroshi_spec.rb
|
75
85
|
- spec/spec_helper.rb
|
76
86
|
- spec/support/db/schema.rb
|
77
87
|
- spec/support/factories/document.rb
|
88
|
+
- spec/support/factories/tag.rb
|
78
89
|
- spec/support/factory_bot.rb
|
79
90
|
- spec/support/models/.keep
|
80
91
|
- spec/support/models/document.rb
|
92
|
+
- spec/support/models/tag.rb
|
81
93
|
- spec/support/shared_examples/.keep
|
82
94
|
homepage: https://github.com/darthjee/kiroshi
|
83
95
|
licenses: []
|