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.
@@ -12,7 +12,12 @@ module Kiroshi
12
12
  #
13
13
  # @example Creating and running a filter
14
14
  # filter = Kiroshi::Filter.new(:name, match: :like)
15
- # runner = Kiroshi::FilterRunner.new(filter: filter, scope: User.all, filters: { name: 'John' })
15
+ # runner = Kiroshi::FilterRunner.new(filter: filter, scope: User.all, value: 'John')
16
+ # result = runner.apply
17
+ #
18
+ # @example Creating and running a filter with specific value
19
+ # filter = Kiroshi::Filter.new(:status)
20
+ # runner = Kiroshi::FilterRunner.new(filter: filter, scope: User.all, value: 'active')
16
21
  # result = runner.apply
17
22
  #
18
23
  # @since 0.1.0
@@ -21,13 +26,13 @@ module Kiroshi
21
26
  #
22
27
  # @param filter [Kiroshi::Filter] the filter configuration
23
28
  # @param scope [ActiveRecord::Relation] the scope to filter
24
- # @param filters [Hash] a hash containing filter values
29
+ # @param value [Object, nil] the specific value to use for filtering, defaults to nil
25
30
  #
26
- # @since 0.1.0
27
- def initialize(filter:, scope:, filters:)
31
+ # @since 0.2.0
32
+ def initialize(filter:, scope:, value: nil)
28
33
  @filter = filter
29
34
  @scope = scope
30
- @filters = filters
35
+ @value = value
31
36
  end
32
37
 
33
38
  # Applies the filter logic to the scope
@@ -38,41 +43,49 @@ module Kiroshi
38
43
  # @return [ActiveRecord::Relation] the filtered scope
39
44
  #
40
45
  # @example Applying exact match filter
41
- # runner = FilterRunner.new(filter: filter, scope: scope, filters: { name: 'John' })
46
+ # runner = FilterRunner.new(filter: filter, scope: scope, value: 'John')
42
47
  # runner.apply
43
48
  #
44
49
  # @example Applying LIKE filter
45
- # runner = FilterRunner.new(filter: filter, scope: scope, filters: { title: 'Ruby' })
50
+ # runner = FilterRunner.new(filter: filter, scope: scope, value: 'Ruby')
51
+ # runner.apply
52
+ #
53
+ # @example With specific value provided
54
+ # runner = FilterRunner.new(filter: filter, scope: scope, value: 'specific_value')
46
55
  # runner.apply
47
56
  #
48
- # @example With no matching value
49
- # runner = FilterRunner.new(filter: filter, scope: scope, filters: { name: nil })
57
+ # @example With no value (returns unchanged scope)
58
+ # runner = FilterRunner.new(filter: filter, scope: scope, value: nil)
50
59
  # runner.apply
51
60
  # # Returns the original scope unchanged
52
61
  #
53
62
  # @since 0.1.1
54
63
  def apply
55
- return scope unless filter_value.present?
64
+ return scope unless value.present?
56
65
 
57
66
  query_strategy = FilterQuery.for(filter.match).new(self)
58
67
  query_strategy.apply
59
68
  end
60
69
 
61
- # Returns the filter value for the current filter's attribute
70
+ attr_reader :scope, :value
71
+
72
+ # @!method scope
73
+ # @api private
62
74
  #
63
- # @return [Object, nil] the filter value or nil if not present
75
+ # Returns the current scope being filtered
64
76
  #
65
- # @since 0.1.1
66
- def filter_value
67
- filters[filter.attribute]
68
- end
77
+ # @return [ActiveRecord::Relation] the scope
78
+ #
79
+ # @since 0.1.1
69
80
 
70
- # Returns the current scope being filtered
81
+ # @!method value
82
+ # @api private
71
83
  #
72
- # @return [ActiveRecord::Relation] the scope
84
+ # Returns the filter value for the current filter
73
85
  #
74
- # @since 0.1.1
75
- attr_reader :scope
86
+ # @return [Object] the filter value or nil if not present
87
+ #
88
+ # @since 0.2.0
76
89
 
77
90
  # Returns the table name to use for the filter
78
91
  #
@@ -84,12 +97,12 @@ module Kiroshi
84
97
  #
85
98
  # @example With filter table_name specified
86
99
  # filter = Kiroshi::Filter.new(:name, table: 'tags')
87
- # runner = FilterRunner.new(filter: filter, scope: Document.joins(:tags), filters: {})
100
+ # runner = FilterRunner.new(filter: filter, scope: Document.joins(:tags), value: 'ruby')
88
101
  # runner.table_name # => 'tags'
89
102
  #
90
103
  # @example Without filter table_name (fallback to scope)
91
104
  # filter = Kiroshi::Filter.new(:name)
92
- # runner = FilterRunner.new(filter: filter, scope: Document.all, filters: {})
105
+ # runner = FilterRunner.new(filter: filter, scope: Document.all, value: 'test')
93
106
  # runner.table_name # => 'documents'
94
107
  #
95
108
  # @since 0.1.1
@@ -104,9 +117,16 @@ module Kiroshi
104
117
  #
105
118
  # @return [ActiveRecord::Relation] the scope
106
119
 
120
+ # @!method value
121
+ # @api private
122
+ #
123
+ # Returns the filter value for the current filter
124
+ #
125
+ # @return [Object, nil] the filter value or nil if not present
126
+
107
127
  private
108
128
 
109
- attr_reader :filter, :filters
129
+ attr_reader :filter
110
130
 
111
131
  # @!method filter
112
132
  # @api private
@@ -116,24 +136,16 @@ module Kiroshi
116
136
  #
117
137
  # @return [Kiroshi::Filter] the filter configuration
118
138
 
119
- # @!method filters
120
- # @api private
121
- # @private
122
- #
123
- # Returns the hash of filter values
124
- #
125
- # @return [Hash] the hash of filter values
126
-
127
- delegate :attribute, to: :filter
139
+ delegate :column, to: :filter
128
140
  delegate :table_name, to: :scope, prefix: true
129
141
  delegate :table_name, to: :filter, prefix: true
130
142
 
131
- # @!method attribute
143
+ # @!method column
132
144
  # @api private
133
145
  #
134
- # Returns the attribute name to filter by
146
+ # Returns the column name to use in database queries
135
147
  #
136
- # @return [Symbol] the attribute name to filter by
148
+ # @return [Symbol] the column name to use in database queries
137
149
 
138
150
  # @!method scope_table_name
139
151
  # @api private
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kiroshi
4
+ class Filters
5
+ # @api public
6
+ # Class-level methods for configuring filters in Kiroshi::Filters
7
+ #
8
+ # This module provides the DSL methods that allow filter classes to
9
+ # define their filtering behavior using class-level method calls.
10
+ # These methods are automatically available when extending Kiroshi::Filters.
11
+ #
12
+ # The primary interface is the {.filter_by} method, which registers
13
+ # filters that will be applied when {Filters#apply} is called on
14
+ # instances of the filter class.
15
+ #
16
+ # @example Basic filter configuration
17
+ # class DocumentFilters < Kiroshi::Filters
18
+ # filter_by :name, match: :like
19
+ # filter_by :status
20
+ # filter_by :category, table: :documents
21
+ # end
22
+ #
23
+ # @example Accessing filter configurations
24
+ # DocumentFilters.filter_configs.keys # => ["name", "status", "category"]
25
+ # DocumentFilters.filter_configs["name"].match # => :like
26
+ #
27
+ # @since 0.2.0
28
+ # @author darthjee
29
+ module ClassMethods
30
+ # Defines a filter for the current filter class
31
+ #
32
+ # This method is used at the class level to configure filters that will
33
+ # be applied when {Filters#apply} is called. Each call creates a new {Filter}
34
+ # instance with the specified configuration and stores it in the class's
35
+ # filter registry for later use during filtering operations.
36
+ #
37
+ # The method supports various matching strategies and table qualification
38
+ # options to handle complex database queries with joins and ambiguous
39
+ # column names.
40
+ #
41
+ # @overload filter_by(filter_key, **options)
42
+ # @param filter_key [Symbol] the filter key name to identify this filter
43
+ # @param options [Hash] additional options passed to {Filter#initialize}
44
+ # @option options [Symbol] :match (:exact) the matching type
45
+ # - +:exact+ for exact matching (default)
46
+ # - +:like+ for partial matching using SQL LIKE with wildcards
47
+ # @option options [String, Symbol, nil] :table (nil) the table name to qualify the column
48
+ # when dealing with joined tables that have conflicting column names
49
+ # @option options [Symbol, nil] :column (nil) the column name to use in database queries,
50
+ # defaults to filter_key if not specified
51
+ #
52
+ # @return (see Filters.filter_by)
53
+ # @example (see Filters.filter_by)
54
+ # @note (see Filters.filter_by)
55
+ # @see (see Filters.filter_by)
56
+ # @since (see Filters.filter_by)
57
+ def filter_by(filter_key, **options)
58
+ Filter.new(filter_key, **options).tap do |filter|
59
+ filter_configs[filter_key.to_s] = filter
60
+ end
61
+ end
62
+
63
+ # @api private
64
+ # Returns the filter configuration for a specific filter key
65
+ #
66
+ # This method provides a convenient way to retrieve a specific filter
67
+ # by its filter key name. It's a shorthand for accessing the filter_configs
68
+ # hash directly and is used internally by the filtering system.
69
+ #
70
+ # @param filter_key [Symbol, String] the filter key name to look up
71
+ #
72
+ # @return [Filter, nil] the filter instance for the given filter key,
73
+ # or nil if no filter is configured for that filter key
74
+ #
75
+ # @example Retrieving a specific filter
76
+ # class MyFilters < Kiroshi::Filters
77
+ # filter_by :name, match: :like
78
+ # filter_by :status
79
+ # end
80
+ #
81
+ # MyFilters.filter_for(:name) # => #<Kiroshi::Filter:0x... @filter_key=:name @match=:like>
82
+ # MyFilters.filter_for(:status) # => #<Kiroshi::Filter:0x... @filter_key=:status @match=:exact>
83
+ # MyFilters.filter_for(:unknown) # => nil
84
+ #
85
+ # @see .filter_configs for accessing the complete filter registry
86
+ # @see Filters#apply for how this method is used during filtering
87
+ #
88
+ # @since 0.3.0
89
+ def filter_for(filter_key)
90
+ filter_key_string = filter_key.to_s
91
+ filter_configs[filter_key_string] || inherited_filter_for(filter_key_string)
92
+ end
93
+
94
+ private
95
+
96
+ # @api private
97
+ # @private
98
+ #
99
+ # Searches for a filter in the inheritance chain
100
+ #
101
+ # This method looks up the inheritance chain to find a filter configuration
102
+ # for the given filter key. It only searches in superclasses that inherit
103
+ # from Kiroshi::Filters, stopping when it reaches a non-Filters class.
104
+ #
105
+ # @param filter_key_string [String] the filter key name to look up
106
+ # @return [Filter, nil] the filter instance from a parent class, or nil if not found
107
+ #
108
+ # @since 0.3.0
109
+ def inherited_filter_for(filter_key_string)
110
+ return nil unless superclass < Kiroshi::Filters
111
+
112
+ superclass.filter_for(filter_key_string)
113
+ end
114
+
115
+ # @api private
116
+ # @private
117
+ #
118
+ # Returns the hash of configured filters for this filter class
119
+ #
120
+ # This method provides access to the internal registry of filters
121
+ # that have been configured using {.filter_by}. The returned hash
122
+ # contains {Filter} instances keyed by their filter key names, allowing
123
+ # for efficient O(1) lookup during filter application.
124
+ #
125
+ # This method is primarily used internally by {Filters#apply} to
126
+ # iterate through and apply all configured filters to a scope.
127
+ # While marked as private API, it may be useful for introspection
128
+ # and testing purposes.
129
+ #
130
+ # @return [Hash<String, Filter>] hash of {Filter} instances configured
131
+ # for this filter class, keyed by filter key name for efficient access
132
+ #
133
+ # @example Accessing configured filters for introspection
134
+ # class MyFilters < Kiroshi::Filters
135
+ # filter_by :name, match: :like
136
+ # filter_by :status
137
+ # filter_by :category, table: :categories
138
+ # end
139
+ #
140
+ # MyFilters.filter_configs.length # => 3
141
+ # MyFilters.filter_configs.keys # => ["name", "status", "category"]
142
+ # MyFilters.filter_configs["name"].attribute # => :name
143
+ # MyFilters.filter_configs["name"].match # => :like
144
+ # MyFilters.filter_configs["status"].match # => :exact
145
+ # MyFilters.filter_configs["category"].table_name # => :categories
146
+ #
147
+ # @example Using in tests to verify filter configuration
148
+ # RSpec.describe ProductFilters do
149
+ # it 'configures the expected filters' do
150
+ # expect(described_class.filter_configs).to have_key("name")
151
+ # expect(described_class.filter_configs["name"].match).to eq(:like)
152
+ # end
153
+ # end
154
+ #
155
+ # @note This method returns a reference to the actual internal hash.
156
+ # Modifying the returned hash directly will affect the filter class
157
+ # configuration. Use {.filter_by} for proper filter registration.
158
+ #
159
+ # @note The hash is lazily initialized on first access and persists
160
+ # for the lifetime of the class. Each filter class maintains its
161
+ # own separate filter_configs hash.
162
+ #
163
+ # @see .filter_by for adding filters to this configuration
164
+ # @see Filters#apply for how these configurations are used
165
+ #
166
+ # @since 0.2.0
167
+ def filter_configs
168
+ @filter_configs ||= {}
169
+ end
170
+ end
171
+ end
172
+ end
@@ -36,78 +36,83 @@ module Kiroshi
36
36
  #
37
37
  # @since 0.1.0
38
38
  class Filters
39
- class << self
40
- # Defines a filter for the current filter class
41
- #
42
- # This method is used at the class level to configure filters that will
43
- # be applied when {#apply} is called. Each call creates a new {Filter}
44
- # instance with the specified configuration.
45
- #
46
- # @overload filter_by(attribute, **options)
47
- # @param attribute [Symbol] the attribute name to filter by
48
- # @param options [Hash] additional options passed to {Filter#initialize}
49
- # @option options [Symbol] :match (:exact) the matching type
50
- # - +:exact+ for exact matching (default)
51
- # - +:like+ for partial matching using SQL LIKE
52
- # @option options [String, Symbol, nil] :table (nil) the table name to qualify the attribute
53
- #
54
- # @return [Filter] the new filter instance
55
- #
56
- # @example Defining exact match filters
57
- # class ProductFilters < Kiroshi::Filters
58
- # filter_by :category
59
- # filter_by :brand
60
- # end
61
- #
62
- # @example Defining partial match filters
63
- # class SearchFilters < Kiroshi::Filters
64
- # filter_by :title, match: :like
65
- # filter_by :description, match: :like
66
- # end
67
- #
68
- # @example Mixed filter types
69
- # class OrderFilters < Kiroshi::Filters
70
- # filter_by :customer_name, match: :like
71
- # filter_by :status, match: :exact
72
- # filter_by :payment_method
73
- # end
74
- #
75
- # @example Filter with table qualification
76
- # class DocumentTagFilters < Kiroshi::Filters
77
- # filter_by :name, table: :tags
78
- # end
79
- #
80
- # @since 0.1.0
81
- def filter_by(attribute, **)
82
- Filter.new(attribute, **).tap do |filter|
83
- filter_configs << filter
84
- end
85
- end
39
+ autoload :ClassMethods, 'kiroshi/filters/class_methods'
86
40
 
87
- # Returns the list of configured filters for this class
88
- #
89
- # @return [Array<Filter>] array of {Filter} instances configured
90
- # for this filter class
91
- #
92
- # @example Accessing configured filters
93
- # class MyFilters < Kiroshi::Filters
94
- # filter_by :name
95
- # filter_by :status, match: :like
96
- # end
97
- #
98
- # MyFilters.filter_configs.length # => 2
99
- # MyFilters.filter_configs.first.attribute # => :name
100
- #
101
- # @since 0.1.0
102
- def filter_configs
103
- @filter_configs ||= []
104
- end
105
- end
41
+ extend ClassMethods
42
+
43
+ # @method self.filter_by(filter_key, **options)
44
+ # @api public
45
+ # @param filter_key [Symbol] the filter key name to identify this filter
46
+ # @param options [Hash] additional options passed to {Filter#initialize}
47
+ # @option options [Symbol] :match (:exact) the matching type
48
+ # - +:exact+ for exact matching (default)
49
+ # - +:like+ for partial matching using SQL LIKE with wildcards
50
+ # @option options [String, Symbol, nil] :table (nil) the table name to qualify the column
51
+ # when dealing with joined tables that have conflicting column names
52
+ # @option options [Symbol, nil] :column (nil) the column name to use in database queries,
53
+ # defaults to filter_key if not specified
54
+ #
55
+ # @return [Filter] the new filter instance that was created and registered
56
+ #
57
+ # @example Defining exact match filters
58
+ # class ProductFilters < Kiroshi::Filters
59
+ # filter_by :category # Exact match on category
60
+ # filter_by :brand # Exact match on brand
61
+ # filter_by :active # Exact match on active status
62
+ # end
63
+ #
64
+ # @example Defining partial match filters
65
+ # class SearchFilters < Kiroshi::Filters
66
+ # filter_by :title, match: :like # Partial match on title
67
+ # filter_by :description, match: :like # Partial match on description
68
+ # filter_by :author_name, match: :like # Partial match on author name
69
+ # end
70
+ #
71
+ # @example Mixed filter types with different matching strategies
72
+ # class OrderFilters < Kiroshi::Filters
73
+ # filter_by :customer_name, match: :like # Partial match for customer search
74
+ # filter_by :status, match: :exact # Exact match for order status
75
+ # filter_by :payment_method # Exact match (default) for payment
76
+ # end
77
+ #
78
+ # @example Filters with table qualification for joined queries
79
+ # class DocumentTagFilters < Kiroshi::Filters
80
+ # filter_by :name, table: :documents # Filter by document name
81
+ # filter_by :tag_name, table: :tags # Filter by tag name
82
+ # filter_by :category, table: :categories # Filter by category name
83
+ # end
84
+ #
85
+ # @example Complex real-world filter class
86
+ # class ProductSearchFilters < Kiroshi::Filters
87
+ # filter_by :name, match: :like # Product name search
88
+ # filter_by :category_id # Exact category match
89
+ # filter_by :brand, match: :like # Brand name search
90
+ # filter_by :price_min # Minimum price
91
+ # filter_by :price_max # Maximum price
92
+ # filter_by :in_stock # Availability filter
93
+ # filter_by :category_name, table: :categories # Category name via join
94
+ # end
95
+ #
96
+ # @example Using custom column names
97
+ # class UserFilters < Kiroshi::Filters
98
+ # filter_by :full_name, column: :name, match: :like # Filter key 'full_name' maps to 'name' column
99
+ # filter_by :user_email, column: :email # Filter key 'user_email' maps to 'email' column
100
+ # filter_by :account_status, column: :status # Filter key 'account_status' maps to 'status' column
101
+ # end
102
+ #
103
+ # @note When using table qualification, ensure that the specified table
104
+ # is properly joined in the scope being filtered. The filter will not
105
+ # automatically add joins - it only qualifies the column name.
106
+ #
107
+ # @see Filter#initialize for detailed information about filter options
108
+ # @see Filters#apply for how these filters are used during query execution
109
+ #
110
+ # @since 0.3.0
106
111
 
107
112
  # Creates a new Filters instance
108
113
  #
109
114
  # @param filters [Hash] a hash containing the filter values to be applied.
110
- # Keys should correspond to attributes defined with {.filter_by}.
115
+ # Keys should correspond to filter keys defined with {.filter_by}.
111
116
  # Values will be used for filtering. Nil or blank values are ignored.
112
117
  #
113
118
  # @example Creating filters with values
@@ -125,7 +130,7 @@ module Kiroshi
125
130
  #
126
131
  # @since 0.1.0
127
132
  def initialize(filters = {})
128
- @filters = filters || {}
133
+ @filters = filters
129
134
  end
130
135
 
131
136
  # Applies all configured filters to the given scope
@@ -160,10 +165,13 @@ module Kiroshi
160
165
  # filtered_articles = filters.apply(Article.all)
161
166
  # # Generates: WHERE title LIKE '%Ruby%'
162
167
  #
163
- # @since 0.1.0
168
+ # @since 0.2.0
164
169
  def apply(scope)
165
- self.class.filter_configs.each do |filter|
166
- scope = filter.apply(scope, filters)
170
+ filters.compact.each do |filter_key, value|
171
+ filter = self.class.filter_for(filter_key)
172
+ next unless filter
173
+
174
+ scope = filter.apply(scope: scope, value: value)
167
175
  end
168
176
 
169
177
  scope
@@ -171,14 +179,17 @@ module Kiroshi
171
179
 
172
180
  private
173
181
 
174
- attr_reader :filters
175
-
176
- # @!method filters
177
- # @api private
178
- # @private
182
+ # Returns the hash of filter values to be applied
179
183
  #
180
- # Returns the hash of filter values to be applied
184
+ # Uses lazy initialization to ensure @filters is never nil,
185
+ # defaulting to an empty hash when no filters were provided.
181
186
  #
182
- # @return [Hash] the hash of filter values to be applied
187
+ # @return [Hash] the hash of filter values to be applied
188
+ #
189
+ # @api private
190
+ # @since 0.3.0
191
+ def filters
192
+ @filters ||= {}
193
+ end
183
194
  end
184
195
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kiroshi
4
- VERSION = '0.1.1'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/kiroshi.rb CHANGED
@@ -9,9 +9,10 @@
9
9
  # using a declarative DSL. It supports multiple matching strategies and can
10
10
  # be easily integrated into Rails controllers and other components.
11
11
  #
12
- # The gem is designed around two main concepts:
12
+ # The gem is designed around the main concept:
13
13
  # - {Filters}: A base class for creating reusable filter sets
14
- # - {Filter}: Individual filters that can be applied to scopes
14
+ #
15
+ # Individual filters are handled internally and don't require direct interaction.
15
16
  #
16
17
  # @example Basic filter class definition
17
18
  # class DocumentFilters < Kiroshi::Filters
@@ -68,25 +69,6 @@
68
69
  # filtered_users = filters.apply(User.includes(:department))
69
70
  # # Generates: WHERE email LIKE '%admin%' AND role = 'moderator' AND active = true
70
71
  #
71
- # @example Individual filter usage
72
- # # Create standalone filters
73
- # name_filter = Kiroshi::Filter.new(:name, match: :like)
74
- # status_filter = Kiroshi::Filter.new(:status)
75
- #
76
- # # Apply filters step by step
77
- # scope = Document.all
78
- # scope = name_filter.apply(scope, { name: 'annual' })
79
- # scope = status_filter.apply(scope, { status: 'published' })
80
- #
81
- # @example Filter matching types
82
- # # Exact matching (default)
83
- # Kiroshi::Filter.new(:status)
84
- # # Generates: WHERE status = 'value'
85
- #
86
- # # Partial matching with LIKE
87
- # Kiroshi::Filter.new(:title, match: :like)
88
- # # Generates: WHERE title LIKE '%value%'
89
- #
90
72
  # @example Empty value handling
91
73
  # filters = DocumentFilters.new(name: '', status: 'published')
92
74
  # result = filters.apply(Document.all)
@@ -148,9 +130,7 @@
148
130
  # end
149
131
  #
150
132
  # @see Filters Base class for creating filter sets
151
- # @see Filter Individual filter implementation
152
133
  # @see https://github.com/darthjee/kiroshi GitHub repository
153
- # @see https://www.rubydoc.info/gems/kiroshi YARD documentation
154
134
  #
155
135
  # @since 0.1.0
156
136
  module Kiroshi