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 +4 -4
- data/.rubocop_todo.yml +4 -8
- data/README.md +56 -41
- data/lib/kiroshi/filter.rb +57 -41
- data/lib/kiroshi/filter_query/exact.rb +38 -0
- data/lib/kiroshi/filter_query/like.rb +42 -0
- data/lib/kiroshi/filter_query.rb +127 -0
- data/lib/kiroshi/filter_runner.rb +164 -0
- data/lib/kiroshi/filters/class_methods.rb +169 -0
- data/lib/kiroshi/filters.rb +68 -62
- data/lib/kiroshi/version.rb +1 -1
- data/lib/kiroshi.rb +138 -3
- data/spec/lib/kiroshi/filter_query/exact_spec.rb +274 -0
- data/spec/lib/kiroshi/filter_query/like_spec.rb +271 -0
- data/spec/lib/kiroshi/filter_query_spec.rb +39 -0
- data/spec/lib/kiroshi/filter_runner_spec.rb +94 -0
- data/spec/lib/kiroshi/filter_spec.rb +9 -12
- data/spec/lib/kiroshi/filters/class_methods_spec.rb +59 -0
- data/spec/lib/kiroshi/filters_spec.rb +165 -6
- data/spec/support/db/schema.rb +14 -0
- data/spec/support/factories/tag.rb +7 -0
- data/spec/support/models/document.rb +2 -0
- data/spec/support/models/tag.rb +7 -0
- metadata +14 -2
@@ -0,0 +1,164 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kiroshi
|
4
|
+
# @api private
|
5
|
+
# @author darthjee
|
6
|
+
#
|
7
|
+
# A filter runner that applies filtering logic to ActiveRecord scopes
|
8
|
+
#
|
9
|
+
# This class handles the actual application of filter logic to database queries,
|
10
|
+
# supporting both exact matches and partial matches using SQL LIKE operations.
|
11
|
+
# It separates the filter configuration from the filter execution logic.
|
12
|
+
#
|
13
|
+
# @example Creating and running a filter
|
14
|
+
# filter = Kiroshi::Filter.new(:name, match: :like)
|
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')
|
21
|
+
# result = runner.apply
|
22
|
+
#
|
23
|
+
# @since 0.1.0
|
24
|
+
class FilterRunner
|
25
|
+
# Creates a new FilterRunner instance
|
26
|
+
#
|
27
|
+
# @param filter [Kiroshi::Filter] the filter configuration
|
28
|
+
# @param scope [ActiveRecord::Relation] the scope to filter
|
29
|
+
# @param value [Object, nil] the specific value to use for filtering, defaults to nil
|
30
|
+
#
|
31
|
+
# @since 0.2.0
|
32
|
+
def initialize(filter:, scope:, value: nil)
|
33
|
+
@filter = filter
|
34
|
+
@scope = scope
|
35
|
+
@value = value
|
36
|
+
end
|
37
|
+
|
38
|
+
# Applies the filter logic to the scope
|
39
|
+
#
|
40
|
+
# This method contains the actual filtering logic, checking the filter's
|
41
|
+
# match type and applying the appropriate WHERE clause to the scope.
|
42
|
+
#
|
43
|
+
# @return [ActiveRecord::Relation] the filtered scope
|
44
|
+
#
|
45
|
+
# @example Applying exact match filter
|
46
|
+
# runner = FilterRunner.new(filter: filter, scope: scope, value: 'John')
|
47
|
+
# runner.apply
|
48
|
+
#
|
49
|
+
# @example Applying LIKE filter
|
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')
|
55
|
+
# runner.apply
|
56
|
+
#
|
57
|
+
# @example With no value (returns unchanged scope)
|
58
|
+
# runner = FilterRunner.new(filter: filter, scope: scope, value: nil)
|
59
|
+
# runner.apply
|
60
|
+
# # Returns the original scope unchanged
|
61
|
+
#
|
62
|
+
# @since 0.1.1
|
63
|
+
def apply
|
64
|
+
return scope unless value.present?
|
65
|
+
|
66
|
+
query_strategy = FilterQuery.for(filter.match).new(self)
|
67
|
+
query_strategy.apply
|
68
|
+
end
|
69
|
+
|
70
|
+
attr_reader :scope, :value
|
71
|
+
|
72
|
+
# @!method scope
|
73
|
+
# @api private
|
74
|
+
#
|
75
|
+
# Returns the current scope being filtered
|
76
|
+
#
|
77
|
+
# @return [ActiveRecord::Relation] the scope
|
78
|
+
#
|
79
|
+
# @since 0.1.1
|
80
|
+
|
81
|
+
# @!method value
|
82
|
+
# @api private
|
83
|
+
#
|
84
|
+
# Returns the filter value for the current filter
|
85
|
+
#
|
86
|
+
# @return [Object] the filter value or nil if not present
|
87
|
+
#
|
88
|
+
# @since 0.2.0
|
89
|
+
|
90
|
+
# Returns the table name to use for the filter
|
91
|
+
#
|
92
|
+
# This method prioritizes the filter's table_name over the scope's table_name.
|
93
|
+
# If the filter has a specific table_name configured, it uses that;
|
94
|
+
# otherwise, it falls back to the scope's table_name.
|
95
|
+
#
|
96
|
+
# @return [String] the table name to use for filtering
|
97
|
+
#
|
98
|
+
# @example With filter table_name specified
|
99
|
+
# filter = Kiroshi::Filter.new(:name, table: 'tags')
|
100
|
+
# runner = FilterRunner.new(filter: filter, scope: Document.joins(:tags), value: 'ruby')
|
101
|
+
# runner.table_name # => 'tags'
|
102
|
+
#
|
103
|
+
# @example Without filter table_name (fallback to scope)
|
104
|
+
# filter = Kiroshi::Filter.new(:name)
|
105
|
+
# runner = FilterRunner.new(filter: filter, scope: Document.all, value: 'test')
|
106
|
+
# runner.table_name # => 'documents'
|
107
|
+
#
|
108
|
+
# @since 0.1.1
|
109
|
+
def table_name
|
110
|
+
filter_table_name || scope_table_name
|
111
|
+
end
|
112
|
+
|
113
|
+
# @!method scope
|
114
|
+
# @api private
|
115
|
+
#
|
116
|
+
# Returns the current scope being filtered
|
117
|
+
#
|
118
|
+
# @return [ActiveRecord::Relation] the scope
|
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
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
attr_reader :filter
|
130
|
+
|
131
|
+
# @!method filter
|
132
|
+
# @api private
|
133
|
+
# @private
|
134
|
+
#
|
135
|
+
# Returns the filter configuration
|
136
|
+
#
|
137
|
+
# @return [Kiroshi::Filter] the filter configuration
|
138
|
+
|
139
|
+
delegate :attribute, to: :filter
|
140
|
+
delegate :table_name, to: :scope, prefix: true
|
141
|
+
delegate :table_name, to: :filter, prefix: true
|
142
|
+
|
143
|
+
# @!method attribute
|
144
|
+
# @api private
|
145
|
+
#
|
146
|
+
# Returns the attribute name to filter by
|
147
|
+
#
|
148
|
+
# @return [Symbol] the attribute name to filter by
|
149
|
+
|
150
|
+
# @!method scope_table_name
|
151
|
+
# @api private
|
152
|
+
#
|
153
|
+
# Returns the table name from the scope
|
154
|
+
#
|
155
|
+
# @return [String] the table name from the scope
|
156
|
+
|
157
|
+
# @!method filter_table_name
|
158
|
+
# @api private
|
159
|
+
#
|
160
|
+
# Returns the table name from the filter configuration
|
161
|
+
#
|
162
|
+
# @return [String, nil] the table name from the filter or nil if not specified
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,169 @@
|
|
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(attribute, **options)
|
42
|
+
# @param attribute [Symbol] the attribute name to filter by
|
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 attribute
|
48
|
+
# when dealing with joined tables that have conflicting column names
|
49
|
+
#
|
50
|
+
# @return (see Filters.filter_by)
|
51
|
+
# @example (see Filters.filter_by)
|
52
|
+
# @note (see Filters.filter_by)
|
53
|
+
# @see (see Filters.filter_by)
|
54
|
+
# @since (see Filters.filter_by)
|
55
|
+
def filter_by(attribute, **)
|
56
|
+
Filter.new(attribute, **).tap do |filter|
|
57
|
+
filter_configs[attribute] = filter
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @api private
|
62
|
+
# Returns the filter configuration for a specific attribute
|
63
|
+
#
|
64
|
+
# This method provides a convenient way to retrieve a specific filter
|
65
|
+
# by its attribute name. It's a shorthand for accessing the filter_configs
|
66
|
+
# hash directly and is used internally by the filtering system.
|
67
|
+
#
|
68
|
+
# @param attribute [Symbol] the attribute name to look up
|
69
|
+
#
|
70
|
+
# @return [Filter, nil] the filter instance for the given attribute,
|
71
|
+
# or nil if no filter is configured for that attribute
|
72
|
+
#
|
73
|
+
# @example Retrieving a specific filter
|
74
|
+
# class MyFilters < Kiroshi::Filters
|
75
|
+
# filter_by :name, match: :like
|
76
|
+
# filter_by :status
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# MyFilters.filter_for(:name) # => #<Kiroshi::Filter:0x... @attribute=:name @match=:like>
|
80
|
+
# MyFilters.filter_for(:status) # => #<Kiroshi::Filter:0x... @attribute=:status @match=:exact>
|
81
|
+
# MyFilters.filter_for(:unknown) # => nil
|
82
|
+
#
|
83
|
+
# @see .filter_configs for accessing the complete filter registry
|
84
|
+
# @see Filters#apply for how this method is used during filtering
|
85
|
+
#
|
86
|
+
# @since 0.2.0
|
87
|
+
def filter_for(attribute)
|
88
|
+
filter_configs[attribute] || inherited_filter_for(attribute)
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# @api private
|
94
|
+
# @private
|
95
|
+
#
|
96
|
+
# Searches for a filter in the inheritance chain
|
97
|
+
#
|
98
|
+
# This method looks up the inheritance chain to find a filter configuration
|
99
|
+
# for the given attribute. It only searches in superclasses that inherit
|
100
|
+
# from Kiroshi::Filters, stopping when it reaches a non-Filters class.
|
101
|
+
#
|
102
|
+
# @param attribute [Symbol] the attribute name to look up
|
103
|
+
# @return [Filter, nil] the filter instance from a parent class, or nil if not found
|
104
|
+
#
|
105
|
+
# @since 0.2.0
|
106
|
+
def inherited_filter_for(attribute)
|
107
|
+
return nil unless superclass < Kiroshi::Filters
|
108
|
+
|
109
|
+
superclass.filter_for(attribute)
|
110
|
+
end
|
111
|
+
|
112
|
+
# @api private
|
113
|
+
# @private
|
114
|
+
#
|
115
|
+
# Returns the hash of configured filters for this filter class
|
116
|
+
#
|
117
|
+
# This method provides access to the internal registry of filters
|
118
|
+
# that have been configured using {.filter_by}. The returned hash
|
119
|
+
# contains {Filter} instances keyed by their attribute names, allowing
|
120
|
+
# for efficient O(1) lookup during filter application.
|
121
|
+
#
|
122
|
+
# This method is primarily used internally by {Filters#apply} to
|
123
|
+
# iterate through and apply all configured filters to a scope.
|
124
|
+
# While marked as private API, it may be useful for introspection
|
125
|
+
# and testing purposes.
|
126
|
+
#
|
127
|
+
# @return [Hash<Symbol, Filter>] hash of {Filter} instances configured
|
128
|
+
# for this filter class, keyed by attribute name for efficient access
|
129
|
+
#
|
130
|
+
# @example Accessing configured filters for introspection
|
131
|
+
# class MyFilters < Kiroshi::Filters
|
132
|
+
# filter_by :name, match: :like
|
133
|
+
# filter_by :status
|
134
|
+
# filter_by :category, table: :categories
|
135
|
+
# end
|
136
|
+
#
|
137
|
+
# MyFilters.filter_configs.length # => 3
|
138
|
+
# MyFilters.filter_configs.keys # => [:name, :status, :category]
|
139
|
+
# MyFilters.filter_configs[:name].attribute # => :name
|
140
|
+
# MyFilters.filter_configs[:name].match # => :like
|
141
|
+
# MyFilters.filter_configs[:status].match # => :exact
|
142
|
+
# MyFilters.filter_configs[:category].table_name # => :categories
|
143
|
+
#
|
144
|
+
# @example Using in tests to verify filter configuration
|
145
|
+
# RSpec.describe ProductFilters do
|
146
|
+
# it 'configures the expected filters' do
|
147
|
+
# expect(described_class.filter_configs).to have_key(:name)
|
148
|
+
# expect(described_class.filter_configs[:name].match).to eq(:like)
|
149
|
+
# end
|
150
|
+
# end
|
151
|
+
#
|
152
|
+
# @note This method returns a reference to the actual internal hash.
|
153
|
+
# Modifying the returned hash directly will affect the filter class
|
154
|
+
# configuration. Use {.filter_by} for proper filter registration.
|
155
|
+
#
|
156
|
+
# @note The hash is lazily initialized on first access and persists
|
157
|
+
# for the lifetime of the class. Each filter class maintains its
|
158
|
+
# own separate filter_configs hash.
|
159
|
+
#
|
160
|
+
# @see .filter_by for adding filters to this configuration
|
161
|
+
# @see Filters#apply for how these configurations are used
|
162
|
+
#
|
163
|
+
# @since 0.2.0
|
164
|
+
def filter_configs
|
165
|
+
@filter_configs ||= {}
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
data/lib/kiroshi/filters.rb
CHANGED
@@ -36,66 +36,69 @@ module Kiroshi
|
|
36
36
|
#
|
37
37
|
# @since 0.1.0
|
38
38
|
class Filters
|
39
|
-
|
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
|
-
# @param attribute [Symbol] the attribute name to filter by
|
47
|
-
# @param options [Hash] additional options passed to {Filter#initialize}
|
48
|
-
# @option options [Symbol] :match (:exact) the matching type
|
49
|
-
# - +:exact+ for exact matching (default)
|
50
|
-
# - +:like+ for partial matching using SQL LIKE
|
51
|
-
#
|
52
|
-
# @return [Filter] the new filter instance
|
53
|
-
#
|
54
|
-
# @example Defining exact match filters
|
55
|
-
# class ProductFilters < Kiroshi::Filters
|
56
|
-
# filter_by :category
|
57
|
-
# filter_by :brand
|
58
|
-
# end
|
59
|
-
#
|
60
|
-
# @example Defining partial match filters
|
61
|
-
# class SearchFilters < Kiroshi::Filters
|
62
|
-
# filter_by :title, match: :like
|
63
|
-
# filter_by :description, match: :like
|
64
|
-
# end
|
65
|
-
#
|
66
|
-
# @example Mixed filter types
|
67
|
-
# class OrderFilters < Kiroshi::Filters
|
68
|
-
# filter_by :customer_name, match: :like
|
69
|
-
# filter_by :status, match: :exact
|
70
|
-
# filter_by :payment_method
|
71
|
-
# end
|
72
|
-
#
|
73
|
-
# @since 0.1.0
|
74
|
-
def filter_by(attribute, **)
|
75
|
-
Filter.new(attribute, **).tap do |filter|
|
76
|
-
filter_configs << filter
|
77
|
-
end
|
78
|
-
end
|
39
|
+
autoload :ClassMethods, 'kiroshi/filters/class_methods'
|
79
40
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
41
|
+
extend ClassMethods
|
42
|
+
|
43
|
+
# @method self.filter_by(attribute, **options)
|
44
|
+
# @api public
|
45
|
+
# @param attribute [Symbol] the attribute name to filter by
|
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 attribute
|
51
|
+
# when dealing with joined tables that have conflicting column names
|
52
|
+
#
|
53
|
+
# @return [Filter] the new filter instance that was created and registered
|
54
|
+
#
|
55
|
+
# @example Defining exact match filters
|
56
|
+
# class ProductFilters < Kiroshi::Filters
|
57
|
+
# filter_by :category # Exact match on category
|
58
|
+
# filter_by :brand # Exact match on brand
|
59
|
+
# filter_by :active # Exact match on active status
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# @example Defining partial match filters
|
63
|
+
# class SearchFilters < Kiroshi::Filters
|
64
|
+
# filter_by :title, match: :like # Partial match on title
|
65
|
+
# filter_by :description, match: :like # Partial match on description
|
66
|
+
# filter_by :author_name, match: :like # Partial match on author name
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# @example Mixed filter types with different matching strategies
|
70
|
+
# class OrderFilters < Kiroshi::Filters
|
71
|
+
# filter_by :customer_name, match: :like # Partial match for customer search
|
72
|
+
# filter_by :status, match: :exact # Exact match for order status
|
73
|
+
# filter_by :payment_method # Exact match (default) for payment
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# @example Filters with table qualification for joined queries
|
77
|
+
# class DocumentTagFilters < Kiroshi::Filters
|
78
|
+
# filter_by :name, table: :documents # Filter by document name
|
79
|
+
# filter_by :tag_name, table: :tags # Filter by tag name
|
80
|
+
# filter_by :category, table: :categories # Filter by category name
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# @example Complex real-world filter class
|
84
|
+
# class ProductSearchFilters < Kiroshi::Filters
|
85
|
+
# filter_by :name, match: :like # Product name search
|
86
|
+
# filter_by :category_id # Exact category match
|
87
|
+
# filter_by :brand, match: :like # Brand name search
|
88
|
+
# filter_by :price_min # Minimum price
|
89
|
+
# filter_by :price_max # Maximum price
|
90
|
+
# filter_by :in_stock # Availability filter
|
91
|
+
# filter_by :category_name, table: :categories # Category name via join
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# @note When using table qualification, ensure that the specified table
|
95
|
+
# is properly joined in the scope being filtered. The filter will not
|
96
|
+
# automatically add joins - it only qualifies the column name.
|
97
|
+
#
|
98
|
+
# @see Filter#initialize for detailed information about filter options
|
99
|
+
# @see Filters#apply for how these filters are used during query execution
|
100
|
+
#
|
101
|
+
# @since 0.1.0
|
99
102
|
|
100
103
|
# Creates a new Filters instance
|
101
104
|
#
|
@@ -153,10 +156,13 @@ module Kiroshi
|
|
153
156
|
# filtered_articles = filters.apply(Article.all)
|
154
157
|
# # Generates: WHERE title LIKE '%Ruby%'
|
155
158
|
#
|
156
|
-
# @since 0.
|
159
|
+
# @since 0.2.0
|
157
160
|
def apply(scope)
|
158
|
-
|
159
|
-
|
161
|
+
filters.compact.each do |attribute, value|
|
162
|
+
filter = self.class.filter_for(attribute)
|
163
|
+
next unless filter
|
164
|
+
|
165
|
+
scope = filter.apply(scope: scope, value: value)
|
160
166
|
end
|
161
167
|
|
162
168
|
scope
|
data/lib/kiroshi/version.rb
CHANGED
data/lib/kiroshi.rb
CHANGED
@@ -2,8 +2,143 @@
|
|
2
2
|
|
3
3
|
# @api public
|
4
4
|
# @author darthjee
|
5
|
+
#
|
6
|
+
# Kiroshi - Flexible ActiveRecord Query Filtering
|
7
|
+
#
|
8
|
+
# Kiroshi provides a clean and extensible way to filter ActiveRecord queries
|
9
|
+
# using a declarative DSL. It supports multiple matching strategies and can
|
10
|
+
# be easily integrated into Rails controllers and other components.
|
11
|
+
#
|
12
|
+
# The gem is designed around the main concept:
|
13
|
+
# - {Filters}: A base class for creating reusable filter sets
|
14
|
+
#
|
15
|
+
# Individual filters are handled internally and don't require direct interaction.
|
16
|
+
#
|
17
|
+
# @example Basic filter class definition
|
18
|
+
# class DocumentFilters < Kiroshi::Filters
|
19
|
+
# filter_by :name, match: :like
|
20
|
+
# filter_by :status
|
21
|
+
# filter_by :category
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # Usage
|
25
|
+
# filters = DocumentFilters.new(name: 'report', status: 'published')
|
26
|
+
# filtered_documents = filters.apply(Document.all)
|
27
|
+
# # Generates: WHERE name LIKE '%report%' AND status = 'published'
|
28
|
+
#
|
29
|
+
# @example Controller integration
|
30
|
+
# # URL: /articles?filter[title]=ruby&filter[author]=john&filter[category]=tech
|
31
|
+
# class ArticlesController < ApplicationController
|
32
|
+
# def index
|
33
|
+
# @articles = article_filters.apply(Article.published)
|
34
|
+
# render json: @articles
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# private
|
38
|
+
#
|
39
|
+
# def article_filters
|
40
|
+
# ArticleFilters.new(filter_params)
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# def filter_params
|
44
|
+
# params[:filter]&.permit(:title, :author, :category, :tag)
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# class ArticleFilters < Kiroshi::Filters
|
49
|
+
# filter_by :title, match: :like
|
50
|
+
# filter_by :author, match: :like
|
51
|
+
# filter_by :category
|
52
|
+
# filter_by :tag
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# @example Advanced filtering scenarios
|
56
|
+
# class UserFilters < Kiroshi::Filters
|
57
|
+
# filter_by :email, match: :like
|
58
|
+
# filter_by :role
|
59
|
+
# filter_by :active, match: :exact
|
60
|
+
# filter_by :department
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# # Apply multiple filters
|
64
|
+
# filters = UserFilters.new(
|
65
|
+
# email: 'admin',
|
66
|
+
# role: 'moderator',
|
67
|
+
# active: true
|
68
|
+
# )
|
69
|
+
# filtered_users = filters.apply(User.includes(:department))
|
70
|
+
# # Generates: WHERE email LIKE '%admin%' AND role = 'moderator' AND active = true
|
71
|
+
#
|
72
|
+
# @example Empty value handling
|
73
|
+
# filters = DocumentFilters.new(name: '', status: 'published')
|
74
|
+
# result = filters.apply(Document.all)
|
75
|
+
# # Only status filter is applied, name is ignored due to empty value
|
76
|
+
#
|
77
|
+
# @example Chaining with existing scopes
|
78
|
+
# # URL: /orders?filter[status]=completed&filter[customer_name]=john
|
79
|
+
# class OrderFilters < Kiroshi::Filters
|
80
|
+
# filter_by :customer_name, match: :like
|
81
|
+
# filter_by :status
|
82
|
+
# filter_by :payment_method
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# # Apply to pre-filtered scope
|
86
|
+
# recent_orders = Order.where('created_at > ?', 1.month.ago)
|
87
|
+
# filters = OrderFilters.new(status: 'completed', customer_name: 'john')
|
88
|
+
# filtered_orders = filters.apply(recent_orders)
|
89
|
+
#
|
90
|
+
# @example Complex controller with pagination
|
91
|
+
# # URL: /products?filter[name]=laptop&filter[category]=electronics&filter[in_stock]=true&page=2
|
92
|
+
# class ProductsController < ApplicationController
|
93
|
+
# def index
|
94
|
+
# @products = filtered_products.page(params[:page])
|
95
|
+
# render json: {
|
96
|
+
# products: @products,
|
97
|
+
# total: filtered_products.count,
|
98
|
+
# filters_applied: applied_filter_count
|
99
|
+
# }
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# private
|
103
|
+
#
|
104
|
+
# def filtered_products
|
105
|
+
# @filtered_products ||= product_filters.apply(base_scope)
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# def base_scope
|
109
|
+
# Product.includes(:category, :brand).available
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# def product_filters
|
113
|
+
# ProductFilters.new(filter_params)
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# def filter_params
|
117
|
+
# params[:filter]&.permit(:name, :category, :brand, :price_range, :in_stock)
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
# def applied_filter_count
|
121
|
+
# filter_params.compact.count
|
122
|
+
# end
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# class ProductFilters < Kiroshi::Filters
|
126
|
+
# filter_by :name, match: :like
|
127
|
+
# filter_by :category
|
128
|
+
# filter_by :brand
|
129
|
+
# filter_by :in_stock, match: :exact
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# @see Filters Base class for creating filter sets
|
133
|
+
# @see https://github.com/darthjee/kiroshi GitHub repository
|
134
|
+
# @see https://www.rubydoc.info/gems/kiroshi YARD documentation
|
135
|
+
#
|
136
|
+
# @since 0.1.0
|
5
137
|
module Kiroshi
|
6
|
-
autoload :VERSION,
|
7
|
-
|
8
|
-
autoload :
|
138
|
+
autoload :VERSION, 'kiroshi/version'
|
139
|
+
|
140
|
+
autoload :Filters, 'kiroshi/filters'
|
141
|
+
autoload :Filter, 'kiroshi/filter'
|
142
|
+
autoload :FilterRunner, 'kiroshi/filter_runner'
|
143
|
+
autoload :FilterQuery, 'kiroshi/filter_query'
|
9
144
|
end
|