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.
@@ -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
@@ -10,6 +10,8 @@ SimpleCov.start 'gem'
10
10
 
11
11
  require 'kiroshi'
12
12
  require 'pry-nav'
13
+ require 'active_support/all'
14
+ require 'factory_bot'
13
15
 
14
16
  require 'active_record'
15
17
  ActiveRecord::Base.establish_connection(
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :document, class: '::Document' do
5
+ sequence(:name) { |n| "Name-#{n}" }
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :tag, class: '::Tag' do
5
+ sequence(:name) { |n| "tag-#{n}" }
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.configure do |config|
4
+ config.include FactoryBot::Syntax::Methods
5
+ end
6
+
7
+ FactoryBot.find_definitions
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Document < ActiveRecord::Base
4
+ validates :name, presence: true
5
+
6
+ has_and_belongs_to_many :tags
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tag < ActiveRecord::Base
4
+ validates :name, presence: true, uniqueness: true
5
+
6
+ has_and_belongs_to_many :documents
7
+ 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.0.1
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-15 00:00:00.000000000 Z
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: []