kiroshi 0.0.1 → 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/.rubocop_todo.yml +10 -8
- data/Gemfile +1 -0
- data/README.md +261 -7
- data/config/yardstick.yml +1 -1
- data/kiroshi.jpg +0 -0
- data/lib/kiroshi/filter.rb +110 -0
- 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 +184 -0
- data/lib/kiroshi/version.rb +2 -2
- data/lib/kiroshi.rb +158 -2
- 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/filter_spec.rb +63 -0
- data/spec/lib/kiroshi/filters_spec.rb +157 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/support/db/schema.rb +19 -0
- data/spec/support/factories/document.rb +7 -0
- data/spec/support/factories/tag.rb +7 -0
- data/spec/support/factory_bot.rb +7 -0
- data/spec/support/models/document.rb +7 -0
- data/spec/support/models/tag.rb +7 -0
- metadata +20 -2
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe Kiroshi::FilterRunner, type: :model do
|
6
|
+
describe '#apply' do
|
7
|
+
subject(:runner) { described_class.new(filter: filter, scope: scope, filters: filters) }
|
8
|
+
|
9
|
+
let(:scope) { Document.all }
|
10
|
+
let(:filter_value) { 'test_value' }
|
11
|
+
let(:filters) { { name: filter_value } }
|
12
|
+
let!(:matching_document) { create(:document, name: filter_value) }
|
13
|
+
let!(:non_matching_document) { create(:document, name: 'other_value') }
|
14
|
+
|
15
|
+
context 'when filter match is :exact' do
|
16
|
+
let(:filter) { Kiroshi::Filter.new(:name, match: :exact) }
|
17
|
+
|
18
|
+
it 'returns exact matches' do
|
19
|
+
expect(runner.apply).to include(matching_document)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'does not return non-matching records' do
|
23
|
+
expect(runner.apply).not_to include(non_matching_document)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'when filter match is :like' do
|
28
|
+
let(:filter) { Kiroshi::Filter.new(:name, match: :like) }
|
29
|
+
let(:filter_value) { 'test' }
|
30
|
+
let!(:matching_document) { create(:document, name: 'test_document') }
|
31
|
+
let!(:non_matching_document) { create(:document, name: 'other_value') }
|
32
|
+
|
33
|
+
it 'returns partial matches' do
|
34
|
+
expect(runner.apply).to include(matching_document)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'does not return non-matching records' do
|
38
|
+
expect(runner.apply).not_to include(non_matching_document)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'generates correct SQL with table name prefix' do
|
42
|
+
expected_sql = "SELECT \"documents\".* FROM \"documents\" WHERE (documents.name LIKE '%test%')"
|
43
|
+
expect(runner.apply.to_sql).to eq(expected_sql)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when filter match is not specified (default)' do
|
48
|
+
let(:filter) { Kiroshi::Filter.new(:name) }
|
49
|
+
|
50
|
+
it 'defaults to exact match returning only exact matches' do
|
51
|
+
expect(runner.apply).to include(matching_document)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'defaults to exact match not returning non-matching records' do
|
55
|
+
expect(runner.apply).not_to include(non_matching_document)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'when filter value is not present' do
|
60
|
+
let(:filter) { Kiroshi::Filter.new(:name) }
|
61
|
+
let(:filters) { { name: nil } }
|
62
|
+
|
63
|
+
it 'returns the original scope unchanged' do
|
64
|
+
expect(runner.apply).to eq(scope)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when filter value is empty string' do
|
69
|
+
let(:filter) { Kiroshi::Filter.new(:name) }
|
70
|
+
let(:filters) { { name: '' } }
|
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) { {} }
|
89
|
+
|
90
|
+
it 'returns the original scope unchanged' do
|
91
|
+
expect(runner.apply).to eq(scope)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'with multiple attributes' do
|
96
|
+
let(:filter) { Kiroshi::Filter.new(:status, match: :exact) }
|
97
|
+
let(:filters) { { name: 'test_name', status: 'finished' } }
|
98
|
+
let!(:matching_document) { create(:document, name: 'test_name', status: 'finished') }
|
99
|
+
let!(:non_matching_document) { create(:document, name: 'other_name', status: 'processing') }
|
100
|
+
|
101
|
+
it 'filters by the configured attribute only returning the matched' do
|
102
|
+
expect(runner.apply).to include(matching_document)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'does not return non-matching records' do
|
106
|
+
expect(runner.apply).not_to include(non_matching_document)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe Kiroshi::Filter, type: :model do
|
6
|
+
describe '#apply' do
|
7
|
+
let(:scope) { Document.all }
|
8
|
+
let(:filter_value) { 'test_value' }
|
9
|
+
let(:filters) { { name: filter_value } }
|
10
|
+
let!(:matching_document) { create(:document, name: filter_value) }
|
11
|
+
let!(:non_matching_document) { create(:document, name: 'other_value') }
|
12
|
+
|
13
|
+
context 'when match is :exact' do
|
14
|
+
subject(:filter) { described_class.new(:name, match: :exact) }
|
15
|
+
|
16
|
+
it 'returns exact matches' do
|
17
|
+
expect(filter.apply(scope, filters)).to include(matching_document)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'does not return non-matching records' do
|
21
|
+
expect(filter.apply(scope, filters)).not_to include(non_matching_document)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when match is :like' do
|
26
|
+
subject(:filter) { described_class.new(:name, match: :like) }
|
27
|
+
|
28
|
+
let(:filter_value) { 'test' }
|
29
|
+
let!(:matching_document) { create(:document, name: 'test_document') }
|
30
|
+
let!(:non_matching_document) { create(:document, name: 'other_value') }
|
31
|
+
|
32
|
+
it 'returns partial matches' do
|
33
|
+
expect(filter.apply(scope, filters)).to include(matching_document)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'does not return non-matching records' do
|
37
|
+
expect(filter.apply(scope, filters)).not_to include(non_matching_document)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'when match is not specified (default)' do
|
42
|
+
subject(:filter) { described_class.new(:name) }
|
43
|
+
|
44
|
+
it 'defaults to exact match returning only exact matches' do
|
45
|
+
expect(filter.apply(scope, filters)).to include(matching_document)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'defaults to exact match returning not returning when filtering by a non-matching value' do
|
49
|
+
expect(filter.apply(scope, filters)).not_to include(non_matching_document)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when filter value is not present' do
|
54
|
+
subject(:filter) { described_class.new(:name) }
|
55
|
+
|
56
|
+
let(:filters) { { name: nil } }
|
57
|
+
|
58
|
+
it 'returns the original scope unchanged' do
|
59
|
+
expect(filter.apply(scope, filters)).to eq(scope)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe Kiroshi::Filters, type: :model do
|
6
|
+
describe '#apply' do
|
7
|
+
subject(:filter_instance) { filters_class.new(filters) }
|
8
|
+
|
9
|
+
let(:scope) { Document.all }
|
10
|
+
let(:filters) { {} }
|
11
|
+
let!(:document) { create(:document, name: 'test_name', status: 'finished') }
|
12
|
+
let!(:other_document) { create(:document, name: 'other_name', status: 'processing') }
|
13
|
+
|
14
|
+
let(:filters_class) { Class.new(described_class) }
|
15
|
+
|
16
|
+
context 'when no filters are configured' do
|
17
|
+
context 'when no filters are provided' do
|
18
|
+
it 'returns the original scope unchanged' do
|
19
|
+
expect(filter_instance.apply(scope)).to eq(scope)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when filters are provided' do
|
24
|
+
let(:filters) { { name: 'test_name' } }
|
25
|
+
|
26
|
+
it 'returns the original scope unchanged' do
|
27
|
+
expect(filter_instance.apply(scope)).to eq(scope)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'when one exact filter is configured' do
|
33
|
+
let(:filters) { { name: 'test_name' } }
|
34
|
+
|
35
|
+
before do
|
36
|
+
filters_class.filter_by :name
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'returns documents matching the exact filter' do
|
40
|
+
expect(filter_instance.apply(scope)).to include(document)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'does not return documents not matching the exact filter' do
|
44
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'when one like filter is configured' do
|
49
|
+
let(:filters) { { name: 'test' } }
|
50
|
+
|
51
|
+
before do
|
52
|
+
filters_class.filter_by :name, match: :like
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'returns documents matching the like filter' do
|
56
|
+
expect(filter_instance.apply(scope)).to include(document)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'does not return documents not matching the like filter' do
|
60
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'when multiple filters are configured' do
|
65
|
+
let(:filters) { { name: 'test', status: 'finished' } }
|
66
|
+
|
67
|
+
before do
|
68
|
+
filters_class.filter_by :name, match: :like
|
69
|
+
filters_class.filter_by :status
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'returns documents matching all filters' do
|
73
|
+
expect(filter_instance.apply(scope)).to include(document)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'does not return documents not matching all filters' do
|
77
|
+
expect(filter_instance.apply(scope)).not_to include(other_document)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'when filters hash is empty' do
|
82
|
+
before do
|
83
|
+
filters_class.filter_by :name
|
84
|
+
filters_class.filter_by :status
|
85
|
+
end
|
86
|
+
|
87
|
+
let(:filters) { {} }
|
88
|
+
|
89
|
+
it 'returns the original scope unchanged' do
|
90
|
+
expect(filter_instance.apply(scope)).to eq(scope)
|
91
|
+
end
|
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.instance_variable_set(:@filter_configs, [])
|
132
|
+
filters_class.filter_by :name, match: :like
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'filters by document name with LIKE operation' do
|
136
|
+
result = filter_instance.apply(scope)
|
137
|
+
expect(result).to include(document)
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'does not return documents that do not match document name pattern' do
|
141
|
+
result = filter_instance.apply(scope)
|
142
|
+
expect(result).not_to include(other_document)
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'generates SQL with table-qualified LIKE operation' do
|
146
|
+
result = filter_instance.apply(scope)
|
147
|
+
expect(result.to_sql).to include('documents.name LIKE')
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'generates SQL with correct LIKE pattern' do
|
151
|
+
result = filter_instance.apply(scope)
|
152
|
+
expect(result.to_sql).to include("'%test%'")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/support/db/schema.rb
CHANGED
@@ -2,4 +2,23 @@
|
|
2
2
|
|
3
3
|
ActiveRecord::Schema.define do
|
4
4
|
self.verbose = false
|
5
|
+
|
6
|
+
create_table :documents, force: true do |t|
|
7
|
+
t.string :name
|
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
|
23
|
+
end
|
5
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.1.1
|
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-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -62,14 +62,32 @@ files:
|
|
62
62
|
- config/yardstick.yml
|
63
63
|
- docker-compose.yml
|
64
64
|
- kiroshi.gemspec
|
65
|
+
- kiroshi.jpg
|
65
66
|
- lib/kiroshi.rb
|
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
|
72
|
+
- lib/kiroshi/filters.rb
|
66
73
|
- lib/kiroshi/version.rb
|
67
74
|
- spec/integration/readme/.keep
|
68
75
|
- spec/integration/yard/.keep
|
76
|
+
- spec/lib/kiroshi/filter_query/exact_spec.rb
|
77
|
+
- spec/lib/kiroshi/filter_query/like_spec.rb
|
78
|
+
- spec/lib/kiroshi/filter_query_spec.rb
|
79
|
+
- spec/lib/kiroshi/filter_runner_spec.rb
|
80
|
+
- spec/lib/kiroshi/filter_spec.rb
|
81
|
+
- spec/lib/kiroshi/filters_spec.rb
|
69
82
|
- spec/lib/kiroshi_spec.rb
|
70
83
|
- spec/spec_helper.rb
|
71
84
|
- spec/support/db/schema.rb
|
85
|
+
- spec/support/factories/document.rb
|
86
|
+
- spec/support/factories/tag.rb
|
87
|
+
- spec/support/factory_bot.rb
|
72
88
|
- spec/support/models/.keep
|
89
|
+
- spec/support/models/document.rb
|
90
|
+
- spec/support/models/tag.rb
|
73
91
|
- spec/support/shared_examples/.keep
|
74
92
|
homepage: https://github.com/darthjee/kiroshi
|
75
93
|
licenses: []
|