kiroshi 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,16 +3,16 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  RSpec.describe Kiroshi::Filters, type: :model do
6
- describe '#apply' do
7
- subject(:filter_instance) { filters_class.new(filters) }
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
- let(:scope) { Document.all }
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
@@ -91,8 +91,144 @@ RSpec.describe Kiroshi::Filters, type: :model do
91
91
  end
92
92
  end
93
93
 
94
+ context 'when filters have string keys' do
95
+ before do
96
+ filters_class.filter_by :name, match: :like
97
+ filters_class.filter_by :status
98
+ end
99
+
100
+ context 'with single string key filter' do
101
+ let(:filters) { { 'name' => 'test' } }
102
+
103
+ it 'returns documents matching the string key filter' do
104
+ expect(filter_instance.apply(scope)).to include(document)
105
+ end
106
+
107
+ it 'does not return documents not matching the string key filter' do
108
+ expect(filter_instance.apply(scope)).not_to include(other_document)
109
+ end
110
+
111
+ it 'generates SQL with LIKE operation for string key' do
112
+ expect(filter_instance.apply(scope).to_sql).to include('LIKE')
113
+ end
114
+ end
115
+
116
+ context 'with multiple string key filters' do
117
+ let(:filters) { { 'name' => 'test', 'status' => 'finished' } }
118
+
119
+ it 'returns documents matching all string key filters' do
120
+ expect(filter_instance.apply(scope)).to include(document)
121
+ end
122
+
123
+ it 'does not return documents not matching all string key filters' do
124
+ expect(filter_instance.apply(scope)).not_to include(other_document)
125
+ end
126
+ end
127
+
128
+ context 'with mixed string and symbol keys' do
129
+ let(:filters) { { 'name' => 'test', status: 'finished' } }
130
+
131
+ it 'returns documents matching both string and symbol key filters' do
132
+ expect(filter_instance.apply(scope)).to include(document)
133
+ end
134
+
135
+ it 'does not return documents not matching all mixed key filters' do
136
+ expect(filter_instance.apply(scope)).not_to include(other_document)
137
+ end
138
+
139
+ it 'treats string and symbol keys equivalently' do
140
+ string_result = filters_class.new({ 'name' => 'test', 'status' => 'finished' }).apply(scope)
141
+ symbol_result = filters_class.new({ name: 'test', status: 'finished' }).apply(scope)
142
+
143
+ expect(string_result.to_sql).to eq(symbol_result.to_sql)
144
+ end
145
+ end
146
+ end
147
+
148
+ context 'when filters is an instance of ActionController::Parameters' do
149
+ before do
150
+ filters_class.filter_by :name, match: :like
151
+ filters_class.filter_by :status
152
+ end
153
+
154
+ context 'with permitted parameters' do
155
+ let(:filters) do
156
+ ActionController::Parameters.new(
157
+ name: 'test',
158
+ status: 'finished',
159
+ unauthorized_param: 'ignored'
160
+ )
161
+ end
162
+
163
+ it 'returns documents matching the permitted parameters' do
164
+ expect(filter_instance.apply(scope)).to include(document)
165
+ end
166
+
167
+ it 'does not return documents not matching the permitted parameters' do
168
+ expect(filter_instance.apply(scope)).not_to include(other_document)
169
+ end
170
+
171
+ it 'generates SQL with LIKE operation for ActionController::Parameters' do
172
+ expect(filter_instance.apply(scope).to_sql).to include('LIKE')
173
+ end
174
+
175
+ it 'generates SQL with exact match for status parameter' do
176
+ expect(filter_instance.apply(scope).to_sql).to include("'finished'")
177
+ end
178
+ end
179
+
180
+ context 'with unpermitted parameters' do
181
+ let(:filters) do
182
+ ActionController::Parameters.new(
183
+ name: 'test',
184
+ status: 'finished'
185
+ )
186
+ end
187
+
188
+ it 'works with unpermitted parameters' do
189
+ expect(filter_instance.apply(scope)).to include(document)
190
+ end
191
+
192
+ it 'does not return documents not matching the parameters' do
193
+ expect(filter_instance.apply(scope)).not_to include(other_document)
194
+ end
195
+ end
196
+
197
+ context 'with string keys in ActionController::Parameters' do
198
+ let(:filters) do
199
+ ActionController::Parameters.new(
200
+ 'name' => 'test',
201
+ 'status' => 'finished'
202
+ )
203
+ end
204
+
205
+ it 'returns documents matching the string key parameters' do
206
+ expect(filter_instance.apply(scope)).to include(document)
207
+ end
208
+
209
+ it 'does not return documents not matching the string key parameters' do
210
+ expect(filter_instance.apply(scope)).not_to include(other_document)
211
+ end
212
+
213
+ it 'treats ActionController::Parameters with string keys same as regular hash' do
214
+ ac_params_result = filter_instance.apply(scope)
215
+ hash_result = filters_class.new({ 'name' => 'test', 'status' => 'finished' }).apply(scope)
216
+
217
+ expect(ac_params_result.to_sql).to eq(hash_result.to_sql)
218
+ end
219
+ end
220
+
221
+ context 'with empty ActionController::Parameters' do
222
+ let(:filters) { ActionController::Parameters.new({}) }
223
+
224
+ it 'returns the original scope unchanged' do
225
+ expect(filter_instance.apply(scope)).to eq(scope)
226
+ end
227
+ end
228
+ end
229
+
94
230
  context 'when scope has joined tables with clashing fields' do
95
- let(:scope) { Document.joins(:tags) }
231
+ let(:scope) { Document.joins(:tags) }
96
232
  let(:filters) { { name: 'test_name' } }
97
233
 
98
234
  let!(:first_tag) { Tag.find_or_create_by(name: 'ruby') }
@@ -128,7 +264,6 @@ RSpec.describe Kiroshi::Filters, type: :model do
128
264
  let(:filters) { { name: 'test' } }
129
265
 
130
266
  before do
131
- filters_class.instance_variable_set(:@filter_configs, [])
132
267
  filters_class.filter_by :name, match: :like
133
268
  end
134
269
 
@@ -144,7 +279,7 @@ RSpec.describe Kiroshi::Filters, type: :model do
144
279
 
145
280
  it 'generates SQL with table-qualified LIKE operation' do
146
281
  result = filter_instance.apply(scope)
147
- expect(result.to_sql).to include('documents.name LIKE')
282
+ expect(result.to_sql).to include('"documents"."name" LIKE')
148
283
  end
149
284
 
150
285
  it 'generates SQL with correct LIKE pattern' do
@@ -153,5 +288,176 @@ RSpec.describe Kiroshi::Filters, type: :model do
153
288
  end
154
289
  end
155
290
  end
291
+
292
+ context 'when specifying a different column' do
293
+ let(:scope) { Document.joins(:tags) }
294
+ let(:filters) { { tag_name: 'ruby' } }
295
+
296
+ let!(:ruby_tag) { Tag.find_or_create_by(name: 'ruby') }
297
+ let!(:js_tag) { Tag.find_or_create_by(name: 'javascript') }
298
+
299
+ before do
300
+ filters_class.filter_by :tag_name, table: :tags, column: :name
301
+
302
+ document.tags << [ruby_tag]
303
+ other_document.tags << [js_tag]
304
+ end
305
+
306
+ it 'filters by the specified column name instead of filter key' do
307
+ expect(filter_instance.apply(scope)).to include(document)
308
+ end
309
+
310
+ it 'does not return documents not matching the column filter' do
311
+ expect(filter_instance.apply(scope)).not_to include(other_document)
312
+ end
313
+
314
+ it 'generates SQL that filters by tags.name using the column parameter' do
315
+ expect(filter_instance.apply(scope).to_sql).to include('"tags"."name"')
316
+ end
317
+
318
+ it 'generates SQL that includes the filter value' do
319
+ expect(filter_instance.apply(scope).to_sql).to include("'ruby'")
320
+ end
321
+
322
+ it 'does not use the filter key name in the SQL' do
323
+ # The filter key is :tag_name but column is :name, so SQL should use 'name' not 'tag_name'
324
+ expect(filter_instance.apply(scope).to_sql).not_to include('tag_name')
325
+ end
326
+
327
+ context 'with LIKE matching' do
328
+ let(:filters) { { tag_name: 'rub' } }
329
+
330
+ before do
331
+ filters_class.filter_by :tag_name, table: :tags, column: :name, match: :like
332
+ end
333
+
334
+ it 'applies LIKE matching to the specified column' do
335
+ expect(filter_instance.apply(scope)).to include(document)
336
+ end
337
+
338
+ it 'generates SQL with LIKE operation on the specified column' do
339
+ expect(filter_instance.apply(scope).to_sql).to include('"tags"."name" LIKE')
340
+ end
341
+
342
+ it 'generates SQL with correct LIKE pattern' do
343
+ expect(filter_instance.apply(scope).to_sql).to include("'%rub%'")
344
+ end
345
+ end
346
+
347
+ context 'with different filter key and column names' do
348
+ let(:filters) { { user_full_name: 'test' } }
349
+
350
+ before do
351
+ filters_class.filter_by :user_full_name, column: :name, match: :like
352
+ end
353
+
354
+ it 'uses the column name in database queries' do
355
+ result = filter_instance.apply(Document.all)
356
+ expect(result.to_sql).to include('"documents"."name"')
357
+ end
358
+
359
+ it 'does not use the filter key in SQL' do
360
+ result = filter_instance.apply(Document.all)
361
+ expect(result.to_sql).not_to include('user_full_name')
362
+ end
363
+ end
364
+ end
365
+
366
+ context 'when filter was defined in the superclass' do
367
+ subject(:filters_class) { Class.new(parent_class) }
368
+
369
+ let(:parent_class) { Class.new(described_class) }
370
+ let(:filters) { { name: 'test_name' } }
371
+
372
+ before do
373
+ parent_class.filter_by :name
374
+ end
375
+
376
+ it 'applies the filter defined in the parent class' do
377
+ expect(filter_instance.apply(scope)).to include(document)
378
+ end
379
+
380
+ it 'does not return documents not matching the inherited filter' do
381
+ expect(filter_instance.apply(scope)).not_to include(other_document)
382
+ end
383
+
384
+ it 'generates SQL that includes the filter value from parent class' do
385
+ result = filter_instance.apply(scope)
386
+ expect(result.to_sql).to include("'test_name'")
387
+ end
388
+
389
+ context 'when child class adds its own filter' do
390
+ let(:filters) { { name: 'test_name', status: 'finished' } }
391
+
392
+ before do
393
+ filters_class.filter_by :status
394
+ end
395
+
396
+ it 'applies both parent and child filters' do
397
+ expect(filter_instance.apply(scope)).to include(document)
398
+ end
399
+
400
+ it 'does not return documents not matching all filters' do
401
+ expect(filter_instance.apply(scope)).not_to include(other_document)
402
+ end
403
+ end
404
+
405
+ context 'when child class overrides parent filter' do
406
+ let(:filters) { { name: 'test' } }
407
+
408
+ before do
409
+ filters_class.filter_by :name, match: :like
410
+ end
411
+
412
+ it 'uses the child class filter configuration' do
413
+ expect(filter_instance.apply(scope)).to include(document)
414
+ end
415
+
416
+ it 'does not use the parent class filter configuration' do
417
+ expect(filter_instance.apply(scope).to_sql)
418
+ .to include('LIKE')
419
+ end
420
+
421
+ it 'generates SQL that includes LIKE operation with the filter value' do
422
+ expect(filter_instance.apply(scope).to_sql)
423
+ .to include("'%test%'")
424
+ end
425
+ end
426
+
427
+ context 'when child class overrides parent filter with table qualification' do
428
+ let(:scope) { Document.joins(:tags) }
429
+ let(:filters) { { name: 'ruby' } }
430
+
431
+ let!(:ruby_tag) { Tag.find_or_create_by(name: 'ruby') }
432
+ let!(:js_tag) { Tag.find_or_create_by(name: 'javascript') }
433
+
434
+ before do
435
+ filters_class.filter_by :name, table: :tags
436
+
437
+ document.tags << [ruby_tag]
438
+ other_document.tags << [js_tag]
439
+ end
440
+
441
+ it 'uses the child class table qualification (tags.name)' do
442
+ expect(filter_instance.apply(scope)).to include(document)
443
+ end
444
+
445
+ it 'does not return documents with different tag names' do
446
+ expect(filter_instance.apply(scope)).not_to include(other_document)
447
+ end
448
+
449
+ it 'generates SQL that filters by tags.name, not documents.name' do
450
+ expect(filter_instance.apply(scope).to_sql).to include('"tags"."name"')
451
+ end
452
+
453
+ it 'generates SQL that does not include documents.name' do
454
+ expect(filter_instance.apply(scope).to_sql).not_to include('"documents"."name"')
455
+ end
456
+
457
+ it 'generates SQL that includes the tag filter value' do
458
+ expect(filter_instance.apply(scope).to_sql).to include("'ruby'")
459
+ end
460
+ end
461
+ end
156
462
  end
157
463
  end
data/spec/spec_helper.rb CHANGED
@@ -11,6 +11,7 @@ SimpleCov.start 'gem'
11
11
  require 'kiroshi'
12
12
  require 'pry-nav'
13
13
  require 'active_support/all'
14
+ require 'action_controller/metal/strong_parameters'
14
15
  require 'factory_bot'
15
16
 
16
17
  require 'active_record'
@@ -5,6 +5,7 @@ ActiveRecord::Schema.define do
5
5
 
6
6
  create_table :documents, force: true do |t|
7
7
  t.string :name
8
+ t.string :full_name
8
9
  t.string :status
9
10
  t.boolean :active
10
11
  t.integer :priority
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.1.1
4
+ version: 0.3.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-18 00:00:00.000000000 Z
11
+ date: 2025-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -46,7 +46,9 @@ extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
48
  - ".circleci/config.yml"
49
+ - ".codacy.yml"
49
50
  - ".gitignore"
51
+ - ".markdownlint.json"
50
52
  - ".rspec"
51
53
  - ".rubocop.yml"
52
54
  - ".rubocop_todo.yml"
@@ -70,6 +72,7 @@ files:
70
72
  - lib/kiroshi/filter_query/like.rb
71
73
  - lib/kiroshi/filter_runner.rb
72
74
  - lib/kiroshi/filters.rb
75
+ - lib/kiroshi/filters/class_methods.rb
73
76
  - lib/kiroshi/version.rb
74
77
  - spec/integration/readme/.keep
75
78
  - spec/integration/yard/.keep
@@ -78,6 +81,7 @@ files:
78
81
  - spec/lib/kiroshi/filter_query_spec.rb
79
82
  - spec/lib/kiroshi/filter_runner_spec.rb
80
83
  - spec/lib/kiroshi/filter_spec.rb
84
+ - spec/lib/kiroshi/filters/class_methods_spec.rb
81
85
  - spec/lib/kiroshi/filters_spec.rb
82
86
  - spec/lib/kiroshi_spec.rb
83
87
  - spec/spec_helper.rb