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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3bbe023d89c78eca0371b05303b85c44da60ce794a1c39844b2ab719350cc665
4
- data.tar.gz: aa64d92280278a488c7bb2c5385fc7fd5f28a04a5b7a22aa1cbd1681311bf316
3
+ metadata.gz: ff66735688ded970094d5a88a9bcd67ad4f9cac7679fc55ceb75534d9b16d9fc
4
+ data.tar.gz: d6da2da4ec294c0d134ceff028e19b30fa5e13ddab2fae2ad0582a5fdfc559aa
5
5
  SHA512:
6
- metadata.gz: 588fb1ce51bb238a8536b08d123a6cd23b8393896eea4f66df436b8fb641e016f517e411a6936ea6277b76c9d34564d44e82b547517b27ec61762634dbaa1ca3
7
- data.tar.gz: 7f90feb28c09bc3858110427fe2266ec16fcc699238689ceea3432ed56a6e839ad1d33d49818e7563998817b3d41fc839f175d5e64bbdb0d838f4375e0d5698a
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-17 15:11:59 UTC using RuboCop version 1.79.2.
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: 2
17
- # Configuration parameters: AllowedConstants.
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.1.0](https://www.rubydoc.info/gems/kiroshi/0.1.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.1.0](https://github.com/darthjee/kiroshi/tree/0.1.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.1.0...master)
19
+ [Next release](https://github.com/darthjee/kiroshi/compare/0.2.0...master)
20
20
 
21
21
  ## Installation
22
22
 
@@ -100,6 +100,7 @@ products = filters.apply(Product.all)
100
100
  ##### Controller Integration
101
101
 
102
102
  ```ruby
103
+ # URL: /documents?filter[name]=report&filter[status]=published&filter[author]=john
103
104
  class DocumentsController < ApplicationController
104
105
  def index
105
106
  @documents = document_filters.apply(Document.all)
@@ -113,7 +114,7 @@ class DocumentsController < ApplicationController
113
114
  end
114
115
 
115
116
  def filter_params
116
- params.permit(:name, :status, :category, :author)
117
+ params[:filter]&.permit(:name, :status, :category, :author)
117
118
  end
118
119
  end
119
120
 
@@ -128,6 +129,7 @@ end
128
129
  ##### Nested Resource Filtering
129
130
 
130
131
  ```ruby
132
+ # URL: /users/123/articles?filter[title]=ruby&filter[published]=true&filter[tag]=tutorial
131
133
  class ArticleFilters < Kiroshi::Filters
132
134
  filter_by :title, match: :like
133
135
  filter_by :published
@@ -141,54 +143,67 @@ def articles
141
143
  end
142
144
 
143
145
  def article_filters
144
- ArticleFilters.new(params.permit(:title, :published, :tag))
146
+ ArticleFilters.new(params[:filter]&.permit(:title, :published, :tag))
145
147
  end
146
148
  ```
147
149
 
148
- ### Kiroshi::Filter
150
+ ##### Joined Tables and Table Qualification
149
151
 
150
- [Filter](https://www.rubydoc.info/gems/kiroshi/Kiroshi/Filter)
151
- is the individual filter class that applies filtering logic to ActiveRecord scopes.
152
- It's automatically used by `Kiroshi::Filters`, but can also be used standalone.
153
-
154
- #### Standalone Usage
152
+ When working with joined tables that have columns with the same name, you can specify which table to filter on using the `table` parameter:
155
153
 
156
154
  ```ruby
157
- # Create individual filters
158
- name_filter = Kiroshi::Filter.new(:name, match: :like)
159
- status_filter = Kiroshi::Filter.new(:status, match: :exact)
160
-
161
- # Apply filters manually
162
- scope = Document.all
163
- scope = name_filter.apply(scope, { name: 'report' })
164
- scope = status_filter.apply(scope, { status: 'published' })
165
- ```
155
+ class DocumentFilters < Kiroshi::Filters
156
+ filter_by :name, match: :like # Filters by documents.name (default table)
157
+ filter_by :tag_name, match: :like, table: :tags # Filters by tags.name
158
+ filter_by :status # Filters by documents.status
159
+ filter_by :category, table: :documents # Explicitly filter by documents.category
160
+ end
166
161
 
167
- #### Filter Options
162
+ # Example with joined scope
163
+ scope = Document.joins(:tags)
164
+ filters = DocumentFilters.new(tag_name: 'ruby', status: 'published')
165
+ filtered_documents = filters.apply(scope)
166
+ # Generates: WHERE tags.name LIKE '%ruby%' AND documents.status = 'published'
167
+ ```
168
168
 
169
- - `match: :exact` - Performs exact matching (default)
170
- - `match: :like` - Performs partial matching using SQL LIKE
169
+ ###### Table Qualification Examples
171
170
 
172
171
  ```ruby
173
- # Exact match filter
174
- exact_filter = Kiroshi::Filter.new(:status)
175
- exact_filter.apply(Document.all, { status: 'published' })
176
- # Generates: WHERE status = 'published'
177
-
178
- # LIKE match filter
179
- like_filter = Kiroshi::Filter.new(:title, match: :like)
180
- like_filter.apply(Document.all, { title: 'Ruby' })
181
- # Generates: WHERE title LIKE '%Ruby%'
172
+ # Filter documents by tag name and document status
173
+ class DocumentTagFilters < Kiroshi::Filters
174
+ filter_by :tag_name, match: :like, table: :tags # Search in tags.name
175
+ filter_by :status, table: :documents # Search in documents.status
176
+ filter_by :title, match: :like # Search in documents.title (default table)
177
+ end
178
+
179
+ scope = Document.joins(:tags)
180
+ filters = DocumentTagFilters.new(tag_name: 'programming', status: 'published', title: 'Ruby')
181
+ result = filters.apply(scope)
182
+ # Generates: WHERE tags.name LIKE '%programming%' AND documents.status = 'published' AND documents.title LIKE '%Ruby%'
183
+
184
+ # Filter by both document and tag attributes with different field names
185
+ class AdvancedDocumentFilters < Kiroshi::Filters
186
+ filter_by :title, match: :like, table: :documents
187
+ filter_by :tag_name, match: :like, table: :tags
188
+ filter_by :category, table: :documents
189
+ filter_by :tag_color, table: :tags
190
+ end
191
+
192
+ scope = Document.joins(:tags)
193
+ filters = AdvancedDocumentFilters.new(
194
+ title: 'Ruby',
195
+ tag_name: 'tutorial',
196
+ category: 'programming',
197
+ tag_color: 'blue'
198
+ )
199
+ result = filters.apply(scope)
200
+ # Generates: WHERE documents.title LIKE '%Ruby%' AND tags.name LIKE '%tutorial%' AND documents.category = 'programming' AND tags.color = 'blue'
182
201
  ```
183
202
 
184
- #### Empty Value Handling
203
+ The `table` parameter accepts both symbols and strings, and helps resolve column name ambiguity in complex joined queries.
185
204
 
186
- Filters automatically ignore empty or nil values:
205
+ ## API Reference
187
206
 
188
- ```ruby
189
- filter = Kiroshi::Filter.new(:name)
190
- filter.apply(Document.all, { name: nil }) # Returns original scope
191
- filter.apply(Document.all, { name: '' }) # Returns original scope
192
- filter.apply(Document.all, {}) # Returns original scope
193
- filter.apply(Document.all, { name: 'value' }) # Applies filter
194
- ```
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.
208
+
209
+ For detailed API documentation, see the [YARD documentation](https://www.rubydoc.info/gems/kiroshi/0.2.0).
@@ -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,18 +11,46 @@ 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, { name: 'John' })
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, { title: 'Ruby' })
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
26
+ attr_reader :attribute, :match, :table_name
27
+
28
+ # @!method attribute
29
+ # @api private
30
+ #
31
+ # Returns the attribute name to filter by
32
+ #
33
+ # @return [Symbol] the attribute name to filter by
34
+
35
+ # @!method match
36
+ # @api private
37
+ #
38
+ # Returns the matching type (+:exact+ or +:like+)
39
+ #
40
+ # @return [Symbol] the matching type (+:exact+ or +:like+)
41
+
42
+ # @!method table_name
43
+ # @api private
44
+ #
45
+ # Returns the table name to qualify the attribute
46
+ #
47
+ # @return [String, String, nil] the table name or nil if not specified
48
+
21
49
  # Creates a new Filter instance
22
50
  #
23
51
  # @param attribute [Symbol] the attribute name to filter by
24
52
  # @param match [Symbol] the matching type, defaults to :exact
53
+ # @param table [String, Symbol, nil] the table name to qualify the attribute, defaults to nil
25
54
  # @option match [Symbol] :exact performs exact matching (default)
26
55
  # @option match [Symbol] :like performs partial matching using SQL LIKE
27
56
  #
@@ -31,69 +60,56 @@ module Kiroshi
31
60
  # @example Creating a partial match filter
32
61
  # filter = Kiroshi::Filter.new(:name, match: :like)
33
62
  #
63
+ # @example Creating a filter with table qualification
64
+ # filter = Kiroshi::Filter.new(:name, table: 'documents')
65
+ #
34
66
  # @since 0.1.0
35
- def initialize(attribute, match: :exact)
67
+ def initialize(attribute, match: :exact, table: nil)
36
68
  @attribute = attribute
37
69
  @match = match
70
+ @table_name = table
38
71
  end
39
72
 
40
73
  # Applies the filter to the given scope
41
74
  #
42
- # This method examines the filters hash for a value corresponding to the
43
- # filter's attribute and applies the appropriate WHERE clause to the scope.
44
- # If no value is present or the value is blank, the original scope is returned unchanged.
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.
45
78
  #
46
79
  # @param scope [ActiveRecord::Relation] the ActiveRecord scope to filter
47
- # @param filters [Hash] a hash containing filter values
80
+ # @param value [Object, nil] the value to use for filtering, defaults to nil
48
81
  #
49
82
  # @return [ActiveRecord::Relation] the filtered scope
50
83
  #
51
84
  # @example Applying an exact filter
52
85
  # filter = Kiroshi::Filter.new(:status)
53
- # filter.apply(Document.all, { status: 'published' })
86
+ # filter.apply(scope: Document.all, value: 'published')
54
87
  # # Generates: WHERE status = 'published'
55
88
  #
56
89
  # @example Applying a LIKE filter
57
90
  # filter = Kiroshi::Filter.new(:title, match: :like)
58
- # filter.apply(Article.all, { title: 'Ruby' })
91
+ # filter.apply(scope: Article.all, value: 'Ruby')
59
92
  # # Generates: WHERE title LIKE '%Ruby%'
60
93
  #
94
+ # @example Applying a filter with table qualification
95
+ # filter = Kiroshi::Filter.new(:name, table: 'documents')
96
+ # filter.apply(scope: Document.joins(:tags), value: 'report')
97
+ # # Generates: WHERE documents.name = 'report'
98
+ #
99
+ # @example Applying a filter with table qualification for tags
100
+ # filter = Kiroshi::Filter.new(:name, table: 'tags')
101
+ # filter.apply(scope: Document.joins(:tags), value: 'ruby')
102
+ # # Generates: WHERE tags.name = 'ruby'
103
+ #
61
104
  # @example With empty filter value
62
105
  # filter = Kiroshi::Filter.new(:name)
63
- # filter.apply(User.all, { name: nil })
106
+ # filter.apply(scope: User.all, value: nil)
64
107
  # # Returns the original scope unchanged
65
108
  #
66
- # @since 0.1.0
67
- def apply(scope, filters)
68
- filter_value = filters[attribute]
69
- return scope unless filter_value.present?
70
-
71
- case match
72
- when :like
73
- scope.where("#{attribute} LIKE ?", "%#{filter_value}%")
74
- else # :exact (default)
75
- scope.where(attribute => filter_value)
76
- end
109
+ # @since 0.2.0
110
+ def apply(scope:, value: nil)
111
+ runner = FilterRunner.new(filter: self, scope: scope, value: value)
112
+ runner.apply
77
113
  end
78
-
79
- private
80
-
81
- attr_reader :attribute, :match
82
-
83
- # @!method attribute
84
- # @api private
85
- # @private
86
- #
87
- # Returns the attribute name to filter by
88
- #
89
- # @return [Symbol] the attribute name to filter by
90
-
91
- # @!method match
92
- # @api private
93
- # @private
94
- #
95
- # Returns the matching type (+:exact+ or +:like+)
96
- #
97
- # @return [Symbol] the matching type (+:exact+ or +:like+)
98
114
  end
99
115
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kiroshi
4
+ class FilterQuery
5
+ # @api private
6
+ # @author darthjee
7
+ #
8
+ # Query strategy for exact matching
9
+ #
10
+ # This class implements the exact match query strategy, generating
11
+ # WHERE clauses with exact equality comparisons.
12
+ #
13
+ # @example Applying exact match query
14
+ # query = Kiroshi::FilterQuery::Exact.new(filter_runner)
15
+ # query.apply
16
+ # # Generates: WHERE attribute = 'value'
17
+ #
18
+ # @since 0.1.1
19
+ class Exact < FilterQuery
20
+ # Applies exact match filtering to the scope
21
+ #
22
+ # This method generates a WHERE clause with exact equality matching
23
+ # for the filter's attribute and value.
24
+ #
25
+ # @return [ActiveRecord::Relation] the filtered scope with exact match
26
+ #
27
+ # @example Applying exact match
28
+ # query = Exact.new(filter_runner)
29
+ # query.apply
30
+ # # Generates: WHERE status = 'published'
31
+ #
32
+ # @since 0.1.1
33
+ def apply
34
+ scope.where(table_name => { attribute => value })
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kiroshi
4
+ class FilterQuery
5
+ # @api private
6
+ # @author darthjee
7
+ #
8
+ # Query strategy for LIKE matching
9
+ #
10
+ # This class implements the LIKE match query strategy, generating
11
+ # WHERE clauses with SQL LIKE operations for partial matching.
12
+ #
13
+ # @example Applying LIKE match query
14
+ # query = Kiroshi::FilterQuery::Like.new(filter_runner)
15
+ # query.apply
16
+ # # Generates: WHERE table_name.attribute LIKE '%value%'
17
+ #
18
+ # @since 0.1.1
19
+ class Like < FilterQuery
20
+ # Applies LIKE match filtering to the scope
21
+ #
22
+ # This method generates a WHERE clause with SQL LIKE operation
23
+ # for partial matching, including table name prefix to avoid
24
+ # column ambiguity in complex queries.
25
+ #
26
+ # @return [ActiveRecord::Relation] the filtered scope with LIKE match
27
+ #
28
+ # @example Applying LIKE match
29
+ # query = Like.new(filter_runner)
30
+ # query.apply
31
+ # # Generates: WHERE documents.name LIKE '%ruby%'
32
+ #
33
+ # @since 0.1.1
34
+ def apply
35
+ scope.where(
36
+ "#{table_name}.#{attribute} LIKE ?",
37
+ "%#{value}%"
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kiroshi
4
+ # @api private
5
+ # @author darthjee
6
+ #
7
+ # Factory class for creating filter query strategies
8
+ #
9
+ # This class implements the Strategy pattern for handling different types of
10
+ # database queries based on the filter match type. It provides a factory method
11
+ # to create the appropriate query strategy class.
12
+ #
13
+ # @example Getting an exact match query strategy
14
+ # query = Kiroshi::FilterQuery.for(:exact).new(filter_runner)
15
+ # query.apply
16
+ #
17
+ # @example Getting a LIKE match query strategy
18
+ # query = Kiroshi::FilterQuery.for(:like).new(filter_runner)
19
+ # query.apply
20
+ #
21
+ # @since 0.1.1
22
+ class FilterQuery
23
+ autoload :Exact, 'kiroshi/filter_query/exact'
24
+ autoload :Like, 'kiroshi/filter_query/like'
25
+
26
+ class << self
27
+ # Factory method to create the appropriate query strategy
28
+ #
29
+ # This method returns the correct query strategy class based on the
30
+ # match type provided. It serves as the main entry point for creating
31
+ # query strategies.
32
+ #
33
+ # @param match [Symbol] the type of matching to perform
34
+ # - :exact for exact matching
35
+ # - :like for partial matching using SQL LIKE
36
+ #
37
+ # @return [Class] the appropriate FilterQuery subclass
38
+ #
39
+ # @example Creating an exact match query
40
+ # query_class = Kiroshi::FilterQuery.for(:exact)
41
+ # # Returns Kiroshi::FilterQuery::Exact
42
+ #
43
+ # @example Creating a LIKE match query
44
+ # query_class = Kiroshi::FilterQuery.for(:like)
45
+ # # Returns Kiroshi::FilterQuery::Like
46
+ #
47
+ # @raise [ArgumentError] when an unsupported match type is provided
48
+ #
49
+ # @since 0.1.1
50
+ def for(match)
51
+ case match
52
+ when :exact
53
+ Exact
54
+ when :like
55
+ Like
56
+ else
57
+ raise ArgumentError, "Unsupported match type: #{match}"
58
+ end
59
+ end
60
+ end
61
+
62
+ # Creates a new FilterQuery instance
63
+ #
64
+ # @param filter_runner [Kiroshi::FilterRunner] the filter runner instance
65
+ #
66
+ # @since 0.1.1
67
+ def initialize(filter_runner)
68
+ @filter_runner = filter_runner
69
+ end
70
+
71
+ # Base implementation for applying a filter query
72
+ #
73
+ # This method should be overridden by subclasses to provide specific
74
+ # query logic for each match type.
75
+ #
76
+ # @return [ActiveRecord::Relation] the filtered scope
77
+ #
78
+ # @raise [NotImplementedError] when called on the base class
79
+ #
80
+ # @since 0.1.1
81
+ def apply
82
+ raise NotImplementedError, 'Subclasses must implement #apply method'
83
+ end
84
+
85
+ private
86
+
87
+ attr_reader :filter_runner
88
+
89
+ # @!method filter_runner
90
+ # @api private
91
+ # @private
92
+ #
93
+ # Returns the filter runner instance
94
+ #
95
+ # @return [Kiroshi::FilterRunner] the filter runner instance
96
+
97
+ delegate :scope, :attribute, :table_name, :value, to: :filter_runner
98
+
99
+ # @!method scope
100
+ # @api private
101
+ #
102
+ # Returns the current scope being filtered
103
+ #
104
+ # @return [ActiveRecord::Relation] the scope
105
+
106
+ # @!method attribute
107
+ # @api private
108
+ #
109
+ # Returns the attribute name to filter by
110
+ #
111
+ # @return [Symbol] the attribute name
112
+
113
+ # @!method table_name
114
+ # @api private
115
+ #
116
+ # Returns the table name for the filter
117
+ #
118
+ # @return [String] the table name
119
+
120
+ # @!method value
121
+ # @api private
122
+ #
123
+ # Returns the filter value
124
+ #
125
+ # @return [Object] the filter value
126
+ end
127
+ end