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,152 @@
|
|
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, filters: { name: 'John' })
|
16
|
+
# result = runner.apply
|
17
|
+
#
|
18
|
+
# @since 0.1.0
|
19
|
+
class FilterRunner
|
20
|
+
# Creates a new FilterRunner instance
|
21
|
+
#
|
22
|
+
# @param filter [Kiroshi::Filter] the filter configuration
|
23
|
+
# @param scope [ActiveRecord::Relation] the scope to filter
|
24
|
+
# @param filters [Hash] a hash containing filter values
|
25
|
+
#
|
26
|
+
# @since 0.1.0
|
27
|
+
def initialize(filter:, scope:, filters:)
|
28
|
+
@filter = filter
|
29
|
+
@scope = scope
|
30
|
+
@filters = filters
|
31
|
+
end
|
32
|
+
|
33
|
+
# Applies the filter logic to the scope
|
34
|
+
#
|
35
|
+
# This method contains the actual filtering logic, checking the filter's
|
36
|
+
# match type and applying the appropriate WHERE clause to the scope.
|
37
|
+
#
|
38
|
+
# @return [ActiveRecord::Relation] the filtered scope
|
39
|
+
#
|
40
|
+
# @example Applying exact match filter
|
41
|
+
# runner = FilterRunner.new(filter: filter, scope: scope, filters: { name: 'John' })
|
42
|
+
# runner.apply
|
43
|
+
#
|
44
|
+
# @example Applying LIKE filter
|
45
|
+
# runner = FilterRunner.new(filter: filter, scope: scope, filters: { title: 'Ruby' })
|
46
|
+
# runner.apply
|
47
|
+
#
|
48
|
+
# @example With no matching value
|
49
|
+
# runner = FilterRunner.new(filter: filter, scope: scope, filters: { name: nil })
|
50
|
+
# runner.apply
|
51
|
+
# # Returns the original scope unchanged
|
52
|
+
#
|
53
|
+
# @since 0.1.1
|
54
|
+
def apply
|
55
|
+
return scope unless filter_value.present?
|
56
|
+
|
57
|
+
query_strategy = FilterQuery.for(filter.match).new(self)
|
58
|
+
query_strategy.apply
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the filter value for the current filter's attribute
|
62
|
+
#
|
63
|
+
# @return [Object, nil] the filter value or nil if not present
|
64
|
+
#
|
65
|
+
# @since 0.1.1
|
66
|
+
def filter_value
|
67
|
+
filters[filter.attribute]
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the current scope being filtered
|
71
|
+
#
|
72
|
+
# @return [ActiveRecord::Relation] the scope
|
73
|
+
#
|
74
|
+
# @since 0.1.1
|
75
|
+
attr_reader :scope
|
76
|
+
|
77
|
+
# Returns the table name to use for the filter
|
78
|
+
#
|
79
|
+
# This method prioritizes the filter's table_name over the scope's table_name.
|
80
|
+
# If the filter has a specific table_name configured, it uses that;
|
81
|
+
# otherwise, it falls back to the scope's table_name.
|
82
|
+
#
|
83
|
+
# @return [String] the table name to use for filtering
|
84
|
+
#
|
85
|
+
# @example With filter table_name specified
|
86
|
+
# filter = Kiroshi::Filter.new(:name, table: 'tags')
|
87
|
+
# runner = FilterRunner.new(filter: filter, scope: Document.joins(:tags), filters: {})
|
88
|
+
# runner.table_name # => 'tags'
|
89
|
+
#
|
90
|
+
# @example Without filter table_name (fallback to scope)
|
91
|
+
# filter = Kiroshi::Filter.new(:name)
|
92
|
+
# runner = FilterRunner.new(filter: filter, scope: Document.all, filters: {})
|
93
|
+
# runner.table_name # => 'documents'
|
94
|
+
#
|
95
|
+
# @since 0.1.1
|
96
|
+
def table_name
|
97
|
+
filter_table_name || scope_table_name
|
98
|
+
end
|
99
|
+
|
100
|
+
# @!method scope
|
101
|
+
# @api private
|
102
|
+
#
|
103
|
+
# Returns the current scope being filtered
|
104
|
+
#
|
105
|
+
# @return [ActiveRecord::Relation] the scope
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
attr_reader :filter, :filters
|
110
|
+
|
111
|
+
# @!method filter
|
112
|
+
# @api private
|
113
|
+
# @private
|
114
|
+
#
|
115
|
+
# Returns the filter configuration
|
116
|
+
#
|
117
|
+
# @return [Kiroshi::Filter] the filter configuration
|
118
|
+
|
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
|
128
|
+
delegate :table_name, to: :scope, prefix: true
|
129
|
+
delegate :table_name, to: :filter, prefix: true
|
130
|
+
|
131
|
+
# @!method attribute
|
132
|
+
# @api private
|
133
|
+
#
|
134
|
+
# Returns the attribute name to filter by
|
135
|
+
#
|
136
|
+
# @return [Symbol] the attribute name to filter by
|
137
|
+
|
138
|
+
# @!method scope_table_name
|
139
|
+
# @api private
|
140
|
+
#
|
141
|
+
# Returns the table name from the scope
|
142
|
+
#
|
143
|
+
# @return [String] the table name from the scope
|
144
|
+
|
145
|
+
# @!method filter_table_name
|
146
|
+
# @api private
|
147
|
+
#
|
148
|
+
# Returns the table name from the filter configuration
|
149
|
+
#
|
150
|
+
# @return [String, nil] the table name from the filter or nil if not specified
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kiroshi
|
4
|
+
# @api public
|
5
|
+
# Base class for implementing filter sets on ActiveRecord scopes
|
6
|
+
#
|
7
|
+
# This class provides a foundation for creating reusable filter collections
|
8
|
+
# that can be applied to ActiveRecord queries. It uses a class-level DSL
|
9
|
+
# to define filters and an instance-level interface to apply them.
|
10
|
+
#
|
11
|
+
# The class is designed to be inherited by specific filter implementations
|
12
|
+
# that define their own set of filters using the {.filter_by} method.
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
# @author darthjee
|
16
|
+
#
|
17
|
+
# @example Basic usage with inheritance
|
18
|
+
# class DocumentFilters < Kiroshi::Filters
|
19
|
+
# filter_by :name, match: :like
|
20
|
+
# filter_by :status
|
21
|
+
# filter_by :created_at, match: :exact
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# filters = DocumentFilters.new(name: 'report', status: 'published')
|
25
|
+
# filtered_documents = filters.apply(Document.all)
|
26
|
+
#
|
27
|
+
# @example Multiple filter types
|
28
|
+
# class UserFilters < Kiroshi::Filters
|
29
|
+
# filter_by :email, match: :like
|
30
|
+
# filter_by :role
|
31
|
+
# filter_by :active, match: :exact
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# filters = UserFilters.new(email: 'admin', role: 'moderator')
|
35
|
+
# filtered_users = filters.apply(User.all)
|
36
|
+
#
|
37
|
+
# @since 0.1.0
|
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
|
86
|
+
|
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
|
106
|
+
|
107
|
+
# Creates a new Filters instance
|
108
|
+
#
|
109
|
+
# @param filters [Hash] a hash containing the filter values to be applied.
|
110
|
+
# Keys should correspond to attributes defined with {.filter_by}.
|
111
|
+
# Values will be used for filtering. Nil or blank values are ignored.
|
112
|
+
#
|
113
|
+
# @example Creating filters with values
|
114
|
+
# filters = DocumentFilters.new(
|
115
|
+
# name: 'annual report',
|
116
|
+
# status: 'published',
|
117
|
+
# category: 'finance'
|
118
|
+
# )
|
119
|
+
#
|
120
|
+
# @example Creating filters with partial values
|
121
|
+
# filters = UserFilters.new(email: 'admin') # Only email filter will be applied
|
122
|
+
#
|
123
|
+
# @example Creating empty filters
|
124
|
+
# filters = ProductFilters.new({}) # No filters will be applied
|
125
|
+
#
|
126
|
+
# @since 0.1.0
|
127
|
+
def initialize(filters = {})
|
128
|
+
@filters = filters || {}
|
129
|
+
end
|
130
|
+
|
131
|
+
# Applies all configured filters to the given scope
|
132
|
+
#
|
133
|
+
# This method iterates through all filters defined via {.filter_by}
|
134
|
+
# and applies each one sequentially to the scope. Filters with no
|
135
|
+
# corresponding value in the filters hash or with blank values are
|
136
|
+
# automatically skipped.
|
137
|
+
#
|
138
|
+
# @param scope [ActiveRecord::Relation] the ActiveRecord scope to filter
|
139
|
+
#
|
140
|
+
# @return [ActiveRecord::Relation] the filtered scope with all
|
141
|
+
# applicable filters applied
|
142
|
+
#
|
143
|
+
# @example Applying filters to a scope
|
144
|
+
# class ArticleFilters < Kiroshi::Filters
|
145
|
+
# filter_by :title, match: :like
|
146
|
+
# filter_by :published, match: :exact
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# filters = ArticleFilters.new(title: 'Ruby', published: true)
|
150
|
+
# filtered_articles = filters.apply(Article.all)
|
151
|
+
# # Generates: WHERE title LIKE '%Ruby%' AND published = true
|
152
|
+
#
|
153
|
+
# @example With empty filters
|
154
|
+
# filters = ArticleFilters.new({})
|
155
|
+
# filtered_articles = filters.apply(Article.all)
|
156
|
+
# # Returns the original scope unchanged
|
157
|
+
#
|
158
|
+
# @example With partial filters
|
159
|
+
# filters = ArticleFilters.new(title: 'Ruby') # published filter ignored
|
160
|
+
# filtered_articles = filters.apply(Article.all)
|
161
|
+
# # Generates: WHERE title LIKE '%Ruby%'
|
162
|
+
#
|
163
|
+
# @since 0.1.0
|
164
|
+
def apply(scope)
|
165
|
+
self.class.filter_configs.each do |filter|
|
166
|
+
scope = filter.apply(scope, filters)
|
167
|
+
end
|
168
|
+
|
169
|
+
scope
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
attr_reader :filters
|
175
|
+
|
176
|
+
# @!method filters
|
177
|
+
# @api private
|
178
|
+
# @private
|
179
|
+
#
|
180
|
+
# Returns the hash of filter values to be applied
|
181
|
+
#
|
182
|
+
# @return [Hash] the hash of filter values to be applied
|
183
|
+
end
|
184
|
+
end
|
data/lib/kiroshi/version.rb
CHANGED
data/lib/kiroshi.rb
CHANGED
@@ -2,6 +2,162 @@
|
|
2
2
|
|
3
3
|
# @api public
|
4
4
|
# @author darthjee
|
5
|
-
|
6
|
-
|
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 two main concepts:
|
13
|
+
# - {Filters}: A base class for creating reusable filter sets
|
14
|
+
# - {Filter}: Individual filters that can be applied to scopes
|
15
|
+
#
|
16
|
+
# @example Basic filter class definition
|
17
|
+
# class DocumentFilters < Kiroshi::Filters
|
18
|
+
# filter_by :name, match: :like
|
19
|
+
# filter_by :status
|
20
|
+
# filter_by :category
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # Usage
|
24
|
+
# filters = DocumentFilters.new(name: 'report', status: 'published')
|
25
|
+
# filtered_documents = filters.apply(Document.all)
|
26
|
+
# # Generates: WHERE name LIKE '%report%' AND status = 'published'
|
27
|
+
#
|
28
|
+
# @example Controller integration
|
29
|
+
# # URL: /articles?filter[title]=ruby&filter[author]=john&filter[category]=tech
|
30
|
+
# class ArticlesController < ApplicationController
|
31
|
+
# def index
|
32
|
+
# @articles = article_filters.apply(Article.published)
|
33
|
+
# render json: @articles
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# private
|
37
|
+
#
|
38
|
+
# def article_filters
|
39
|
+
# ArticleFilters.new(filter_params)
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# def filter_params
|
43
|
+
# params[:filter]&.permit(:title, :author, :category, :tag)
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# class ArticleFilters < Kiroshi::Filters
|
48
|
+
# filter_by :title, match: :like
|
49
|
+
# filter_by :author, match: :like
|
50
|
+
# filter_by :category
|
51
|
+
# filter_by :tag
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# @example Advanced filtering scenarios
|
55
|
+
# class UserFilters < Kiroshi::Filters
|
56
|
+
# filter_by :email, match: :like
|
57
|
+
# filter_by :role
|
58
|
+
# filter_by :active, match: :exact
|
59
|
+
# filter_by :department
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# # Apply multiple filters
|
63
|
+
# filters = UserFilters.new(
|
64
|
+
# email: 'admin',
|
65
|
+
# role: 'moderator',
|
66
|
+
# active: true
|
67
|
+
# )
|
68
|
+
# filtered_users = filters.apply(User.includes(:department))
|
69
|
+
# # Generates: WHERE email LIKE '%admin%' AND role = 'moderator' AND active = true
|
70
|
+
#
|
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
|
+
# @example Empty value handling
|
91
|
+
# filters = DocumentFilters.new(name: '', status: 'published')
|
92
|
+
# result = filters.apply(Document.all)
|
93
|
+
# # Only status filter is applied, name is ignored due to empty value
|
94
|
+
#
|
95
|
+
# @example Chaining with existing scopes
|
96
|
+
# # URL: /orders?filter[status]=completed&filter[customer_name]=john
|
97
|
+
# class OrderFilters < Kiroshi::Filters
|
98
|
+
# filter_by :customer_name, match: :like
|
99
|
+
# filter_by :status
|
100
|
+
# filter_by :payment_method
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# # Apply to pre-filtered scope
|
104
|
+
# recent_orders = Order.where('created_at > ?', 1.month.ago)
|
105
|
+
# filters = OrderFilters.new(status: 'completed', customer_name: 'john')
|
106
|
+
# filtered_orders = filters.apply(recent_orders)
|
107
|
+
#
|
108
|
+
# @example Complex controller with pagination
|
109
|
+
# # URL: /products?filter[name]=laptop&filter[category]=electronics&filter[in_stock]=true&page=2
|
110
|
+
# class ProductsController < ApplicationController
|
111
|
+
# def index
|
112
|
+
# @products = filtered_products.page(params[:page])
|
113
|
+
# render json: {
|
114
|
+
# products: @products,
|
115
|
+
# total: filtered_products.count,
|
116
|
+
# filters_applied: applied_filter_count
|
117
|
+
# }
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
# private
|
121
|
+
#
|
122
|
+
# def filtered_products
|
123
|
+
# @filtered_products ||= product_filters.apply(base_scope)
|
124
|
+
# end
|
125
|
+
#
|
126
|
+
# def base_scope
|
127
|
+
# Product.includes(:category, :brand).available
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# def product_filters
|
131
|
+
# ProductFilters.new(filter_params)
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
# def filter_params
|
135
|
+
# params[:filter]&.permit(:name, :category, :brand, :price_range, :in_stock)
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# def applied_filter_count
|
139
|
+
# filter_params.compact.count
|
140
|
+
# end
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# class ProductFilters < Kiroshi::Filters
|
144
|
+
# filter_by :name, match: :like
|
145
|
+
# filter_by :category
|
146
|
+
# filter_by :brand
|
147
|
+
# filter_by :in_stock, match: :exact
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# @see Filters Base class for creating filter sets
|
151
|
+
# @see Filter Individual filter implementation
|
152
|
+
# @see https://github.com/darthjee/kiroshi GitHub repository
|
153
|
+
# @see https://www.rubydoc.info/gems/kiroshi YARD documentation
|
154
|
+
#
|
155
|
+
# @since 0.1.0
|
156
|
+
module Kiroshi
|
157
|
+
autoload :VERSION, 'kiroshi/version'
|
158
|
+
|
159
|
+
autoload :Filters, 'kiroshi/filters'
|
160
|
+
autoload :Filter, 'kiroshi/filter'
|
161
|
+
autoload :FilterRunner, 'kiroshi/filter_runner'
|
162
|
+
autoload :FilterQuery, 'kiroshi/filter_query'
|
7
163
|
end
|