bharat_filter_engine 0.1.2 → 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/Gemfile +12 -9
- data/README.md +1 -1
- data/Rakefile +5 -3
- data/lib/bharat_filter_engine/association_resolver.rb +18 -37
- data/lib/bharat_filter_engine/column_helper.rb +28 -0
- data/lib/bharat_filter_engine/dynamic_filter_values.rb +17 -18
- data/lib/bharat_filter_engine/engine.rb +249 -91
- data/lib/bharat_filter_engine/errors.rb +1 -1
- data/lib/bharat_filter_engine/join_tracker.rb +37 -0
- data/lib/bharat_filter_engine/rule_normalizer.rb +88 -30
- data/lib/bharat_filter_engine/search_builder.rb +48 -24
- data/lib/bharat_filter_engine/version.rb +1 -1
- data/lib/bharat_filter_engine.rb +14 -12
- data/spec/association_resolver_spec.rb +60 -41
- data/spec/dynamic_filter_values_spec.rb +161 -29
- data/spec/engine_spec.rb +1144 -538
- data/spec/join_tracker_spec.rb +70 -0
- data/spec/rule_normalizer_spec.rb +181 -32
- data/spec/same_table_join_spec.rb +199 -0
- data/spec/search_builder_spec.rb +222 -42
- data/spec/spec_helper.rb +11 -12
- metadata +14 -6
data/spec/search_builder_spec.rb
CHANGED
|
@@ -1,79 +1,78 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require 'spec_helper'
|
|
4
4
|
|
|
5
|
+
RSpec.describe BharatFilterEngine::SearchBuilder do
|
|
5
6
|
let!(:client) do
|
|
6
7
|
Client.create!(
|
|
7
|
-
name:
|
|
8
|
-
email:
|
|
8
|
+
name: 'John Doe',
|
|
9
|
+
email: 'john@example.com'
|
|
9
10
|
)
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
let!(:lead) do
|
|
13
14
|
Lead.create!(
|
|
14
|
-
source:
|
|
15
|
+
source: 'google',
|
|
15
16
|
client: client
|
|
16
17
|
)
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
let!(:sale) do
|
|
20
21
|
Sale.create!(
|
|
21
|
-
stage:
|
|
22
|
+
stage: 'qualified',
|
|
22
23
|
lead: lead
|
|
23
24
|
)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
it
|
|
27
|
-
|
|
27
|
+
it 'supports simple search' do
|
|
28
28
|
result =
|
|
29
29
|
described_class.new(
|
|
30
30
|
scope: Sale.all,
|
|
31
|
-
allowed_columns: [
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
allowed_columns: %i[
|
|
32
|
+
stage
|
|
33
|
+
lead__source
|
|
34
34
|
]
|
|
35
|
-
).apply(
|
|
35
|
+
).apply('google')
|
|
36
36
|
|
|
37
37
|
expect(result).to contain_exactly(sale)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
it
|
|
40
|
+
it 'supports case insensitive search' do
|
|
41
41
|
result =
|
|
42
42
|
described_class.new(
|
|
43
43
|
scope: Sale.all,
|
|
44
44
|
allowed_columns: [:stage]
|
|
45
|
-
).apply(
|
|
45
|
+
).apply('QUAL')
|
|
46
46
|
|
|
47
47
|
expect(result).to contain_exactly(sale)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
it
|
|
51
|
-
|
|
50
|
+
it 'supports nested field search' do
|
|
52
51
|
result =
|
|
53
52
|
described_class.new(
|
|
54
53
|
scope: Sale.all,
|
|
55
54
|
allowed_columns: [
|
|
56
|
-
:
|
|
55
|
+
:lead__client__name
|
|
57
56
|
]
|
|
58
57
|
).apply(
|
|
59
|
-
|
|
58
|
+
'lead__client__name=John'
|
|
60
59
|
)
|
|
61
60
|
|
|
62
61
|
expect(result).to contain_exactly(sale)
|
|
63
62
|
end
|
|
64
63
|
|
|
65
|
-
it
|
|
64
|
+
it 'supports multi-level association search' do
|
|
66
65
|
organization = Organization.create!(
|
|
67
|
-
name:
|
|
66
|
+
name: 'Acme'
|
|
68
67
|
)
|
|
69
68
|
|
|
70
69
|
client_two = Client.create!(
|
|
71
|
-
name:
|
|
70
|
+
name: 'Jane',
|
|
72
71
|
organization: organization
|
|
73
72
|
)
|
|
74
73
|
|
|
75
74
|
lead_two = Lead.create!(
|
|
76
|
-
source:
|
|
75
|
+
source: 'referral',
|
|
77
76
|
client: client_two
|
|
78
77
|
)
|
|
79
78
|
|
|
@@ -85,56 +84,76 @@ RSpec.describe BharatFilterEngine::SearchBuilder do
|
|
|
85
84
|
described_class.new(
|
|
86
85
|
scope: Sale.all,
|
|
87
86
|
allowed_columns: [
|
|
88
|
-
:
|
|
87
|
+
:lead__client__organization__name
|
|
89
88
|
]
|
|
90
89
|
).apply(
|
|
91
|
-
|
|
90
|
+
'lead__client__organization__name=Acme'
|
|
92
91
|
)
|
|
93
92
|
|
|
94
93
|
expect(result).to contain_exactly(sale_two)
|
|
95
94
|
end
|
|
96
95
|
|
|
97
|
-
it
|
|
96
|
+
it 'returns each record once when searching through a has_many association' do
|
|
97
|
+
matching_client = Client.create!(name: 'John1')
|
|
98
|
+
Lead.create!(source: 'google', client: matching_client)
|
|
99
|
+
Lead.create!(source: 'google', client: matching_client)
|
|
100
|
+
Client.create!(name: 'John2').tap do |other_client|
|
|
101
|
+
Lead.create!(source: 'meta', client: other_client)
|
|
102
|
+
end
|
|
98
103
|
|
|
104
|
+
result =
|
|
105
|
+
described_class.new(
|
|
106
|
+
scope: Client.all,
|
|
107
|
+
allowed_columns: [:leads__source]
|
|
108
|
+
).apply(
|
|
109
|
+
'leads__source=google'
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
expect(result).to contain_exactly(client, matching_client)
|
|
113
|
+
expect(result.count).to eq(2)
|
|
114
|
+
expect(result.order(:id).limit(1).pluck(:id)).to eq([client.id])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'supports OR search' do
|
|
99
118
|
result =
|
|
100
119
|
described_class.new(
|
|
101
120
|
scope: Sale.all,
|
|
102
|
-
allowed_columns: [
|
|
103
|
-
|
|
104
|
-
|
|
121
|
+
allowed_columns: %i[
|
|
122
|
+
stage
|
|
123
|
+
lead__source
|
|
105
124
|
]
|
|
106
125
|
).apply(
|
|
107
|
-
|
|
126
|
+
'stage=qualified|lead__source=google'
|
|
108
127
|
)
|
|
109
128
|
|
|
110
129
|
expect(result).to contain_exactly(sale)
|
|
111
130
|
end
|
|
112
131
|
|
|
113
|
-
it
|
|
132
|
+
it 'supports combined AND and OR conditions' do
|
|
114
133
|
sale_one = Sale.create!(
|
|
115
|
-
stage:
|
|
116
|
-
approval_status:
|
|
134
|
+
stage: 'qualified',
|
|
135
|
+
approval_status: 'approved'
|
|
117
136
|
)
|
|
118
137
|
|
|
119
138
|
sale_two = Sale.create!(
|
|
120
|
-
stage:
|
|
121
|
-
approval_status:
|
|
139
|
+
stage: 'qualified',
|
|
140
|
+
approval_status: 'pending'
|
|
122
141
|
)
|
|
123
142
|
|
|
124
143
|
Sale.create!(
|
|
125
|
-
stage:
|
|
126
|
-
approval_status:
|
|
144
|
+
stage: 'new',
|
|
145
|
+
approval_status: 'approved'
|
|
127
146
|
)
|
|
128
147
|
|
|
129
148
|
result =
|
|
130
149
|
described_class.new(
|
|
131
150
|
scope: Sale.all,
|
|
132
|
-
allowed_columns: [
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
allowed_columns: %i[
|
|
152
|
+
stage
|
|
153
|
+
approval_status
|
|
135
154
|
]
|
|
136
155
|
).apply(
|
|
137
|
-
|
|
156
|
+
'stage=qualified&approval_status=approved|approval_status=pending'
|
|
138
157
|
)
|
|
139
158
|
|
|
140
159
|
expect(result).to contain_exactly(
|
|
@@ -143,15 +162,176 @@ RSpec.describe BharatFilterEngine::SearchBuilder do
|
|
|
143
162
|
)
|
|
144
163
|
end
|
|
145
164
|
|
|
146
|
-
it
|
|
165
|
+
it 'returns no records when the searched field is not configured' do
|
|
166
|
+
result =
|
|
167
|
+
described_class.new(
|
|
168
|
+
scope: Sale.all,
|
|
169
|
+
allowed_columns: [:stage]
|
|
170
|
+
).apply(
|
|
171
|
+
'unknown=value'
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
expect(result).to be_empty
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it 'returns no records when the searched model column is not allowed' do
|
|
178
|
+
result =
|
|
179
|
+
described_class.new(
|
|
180
|
+
scope: Sale.all,
|
|
181
|
+
allowed_columns: [:stage]
|
|
182
|
+
).apply(
|
|
183
|
+
'approval_status=approved'
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
expect(result).to be_empty
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it 'returns no records when the searched association column is not allowed' do
|
|
190
|
+
result =
|
|
191
|
+
described_class.new(
|
|
192
|
+
scope: Sale.all,
|
|
193
|
+
allowed_columns: [:stage]
|
|
194
|
+
).apply(
|
|
195
|
+
'lead__client__email=john@example.com'
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
expect(result).to be_empty
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'supports searching the id column as text' do
|
|
202
|
+
result =
|
|
203
|
+
described_class.new(
|
|
204
|
+
scope: Sale.all,
|
|
205
|
+
allowed_columns: [:id]
|
|
206
|
+
).apply(
|
|
207
|
+
"id=#{sale.id}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
expect(result).to contain_exactly(sale)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it "handles a value that itself contains an '=' sign" do
|
|
214
|
+
sale_with_url = Sale.create!(
|
|
215
|
+
stage: 'https://example.com?x=1'
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
result =
|
|
219
|
+
described_class.new(
|
|
220
|
+
scope: Sale.all,
|
|
221
|
+
allowed_columns: [:stage]
|
|
222
|
+
).apply(
|
|
223
|
+
'stage=https://example.com?x=1'
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
expect(result).to contain_exactly(sale_with_url)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it 'skips a whitespace-only expression value' do
|
|
147
230
|
result =
|
|
148
231
|
described_class.new(
|
|
149
232
|
scope: Sale.all,
|
|
150
233
|
allowed_columns: [:stage]
|
|
151
234
|
).apply(
|
|
152
|
-
|
|
235
|
+
'stage= '
|
|
153
236
|
)
|
|
154
237
|
|
|
238
|
+
# every expression in the (only) AND group is blank/skipped, so
|
|
239
|
+
# the group can never match anything
|
|
155
240
|
expect(result).to be_empty
|
|
156
241
|
end
|
|
242
|
+
|
|
243
|
+
it 'returns the scope unchanged when allowed_columns is empty' do
|
|
244
|
+
result =
|
|
245
|
+
described_class.new(
|
|
246
|
+
scope: Sale.all,
|
|
247
|
+
allowed_columns: []
|
|
248
|
+
).apply('anything')
|
|
249
|
+
|
|
250
|
+
expect(result).to contain_exactly(sale)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it 'raises when none of the allowed_columns can be resolved' do
|
|
254
|
+
expect do
|
|
255
|
+
described_class.new(
|
|
256
|
+
scope: Sale.all,
|
|
257
|
+
allowed_columns: %i[
|
|
258
|
+
totally_not_a_column
|
|
259
|
+
also__not__real
|
|
260
|
+
]
|
|
261
|
+
)
|
|
262
|
+
end.to raise_error(BharatFilterEngine::InvalidConfigurationError)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it 'does not raise when only some allowed_columns are invalid' do
|
|
266
|
+
expect do
|
|
267
|
+
described_class.new(
|
|
268
|
+
scope: Sale.all,
|
|
269
|
+
allowed_columns: %i[
|
|
270
|
+
stage
|
|
271
|
+
totally_not_a_column
|
|
272
|
+
]
|
|
273
|
+
)
|
|
274
|
+
end.not_to raise_error
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it 'reuses a single AssociationResolver across a whole search' do
|
|
278
|
+
allow(BharatFilterEngine::AssociationResolver)
|
|
279
|
+
.to receive(:new)
|
|
280
|
+
.and_call_original
|
|
281
|
+
|
|
282
|
+
described_class.new(
|
|
283
|
+
scope: Sale.all,
|
|
284
|
+
allowed_columns: %i[
|
|
285
|
+
lead__source
|
|
286
|
+
lead__client__name
|
|
287
|
+
]
|
|
288
|
+
).apply(
|
|
289
|
+
'lead__source=google&lead__client__name=John'
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
expect(BharatFilterEngine::AssociationResolver)
|
|
293
|
+
.to have_received(:new)
|
|
294
|
+
.once
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
describe 'LIKE wildcard escaping' do
|
|
298
|
+
it 'treats a literal underscore as a literal character, not a wildcard' do
|
|
299
|
+
match = Sale.create!(stage: 'sale_2026')
|
|
300
|
+
Sale.create!(stage: 'saleX2026')
|
|
301
|
+
|
|
302
|
+
result =
|
|
303
|
+
described_class.new(
|
|
304
|
+
scope: Sale.all,
|
|
305
|
+
allowed_columns: [:stage]
|
|
306
|
+
).apply('sale_2026')
|
|
307
|
+
|
|
308
|
+
expect(result).to contain_exactly(match)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it 'treats a literal percent sign as a literal character, not a wildcard' do
|
|
312
|
+
match = Sale.create!(stage: '50%off')
|
|
313
|
+
Sale.create!(stage: '50xoff')
|
|
314
|
+
|
|
315
|
+
result =
|
|
316
|
+
described_class.new(
|
|
317
|
+
scope: Sale.all,
|
|
318
|
+
allowed_columns: [:stage]
|
|
319
|
+
).apply('50%off')
|
|
320
|
+
|
|
321
|
+
expect(result).to contain_exactly(match)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it 'escapes wildcards in field-based search too' do
|
|
325
|
+
match = Sale.create!(stage: 'sale_2026')
|
|
326
|
+
Sale.create!(stage: 'saleX2026')
|
|
327
|
+
|
|
328
|
+
result =
|
|
329
|
+
described_class.new(
|
|
330
|
+
scope: Sale.all,
|
|
331
|
+
allowed_columns: [:stage]
|
|
332
|
+
).apply('stage=sale_2026')
|
|
333
|
+
|
|
334
|
+
expect(result).to contain_exactly(match)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
157
337
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
require 'active_record'
|
|
6
|
+
require 'database_cleaner/active_record'
|
|
7
|
+
require 'bharat_filter_engine'
|
|
8
|
+
require 'sqlite3'
|
|
8
9
|
|
|
9
10
|
ActiveRecord::Base.establish_connection(
|
|
10
|
-
adapter:
|
|
11
|
-
database:
|
|
11
|
+
adapter: 'sqlite3',
|
|
12
|
+
database: ':memory:'
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
ActiveRecord::Schema.define do
|
|
@@ -26,6 +27,7 @@ ActiveRecord::Schema.define do
|
|
|
26
27
|
t.string :source
|
|
27
28
|
t.string :status
|
|
28
29
|
t.references :client
|
|
30
|
+
t.integer :referrer_id
|
|
29
31
|
t.datetime :created_at
|
|
30
32
|
end
|
|
31
33
|
|
|
@@ -41,29 +43,26 @@ ActiveRecord::Schema.define do
|
|
|
41
43
|
end
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
|
|
45
46
|
class Organization < ActiveRecord::Base
|
|
46
47
|
has_many :clients
|
|
47
48
|
end
|
|
48
49
|
|
|
49
|
-
|
|
50
50
|
class Client < ActiveRecord::Base
|
|
51
51
|
belongs_to :organization
|
|
52
52
|
has_many :leads
|
|
53
|
+
has_many :referred_leads, class_name: 'Lead', foreign_key: :referrer_id
|
|
53
54
|
end
|
|
54
55
|
|
|
55
|
-
|
|
56
56
|
class Lead < ActiveRecord::Base
|
|
57
57
|
belongs_to :client
|
|
58
|
+
belongs_to :referrer, class_name: 'Client', optional: true
|
|
58
59
|
has_many :sales
|
|
59
60
|
end
|
|
60
61
|
|
|
61
|
-
|
|
62
62
|
class Sale < ActiveRecord::Base
|
|
63
63
|
belongs_to :lead
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
|
|
67
66
|
RSpec.configure do |config|
|
|
68
67
|
config.before(:suite) do
|
|
69
68
|
DatabaseCleaner.clean_with(:truncation)
|
metadata
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bharat_filter_engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jayesh Rathore
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09
|
|
11
|
+
date: 2026-11-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: activemodel
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
@@ -25,7 +25,7 @@ dependencies:
|
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '6.1'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
28
|
+
name: activerecord
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
@@ -53,7 +53,9 @@ dependencies:
|
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '6.1'
|
|
55
55
|
description: Configurable Rails ActiveRecord filtering engine supporting string, array,
|
|
56
|
-
boolean, numeric, date-range,
|
|
56
|
+
boolean, numeric (with gte/lte/between ranges), date-range, presence, negate/exclude,
|
|
57
|
+
nested association, and AND/OR search filters, plus dynamic filter-value lookups
|
|
58
|
+
for dropdowns/autocomplete.
|
|
57
59
|
email:
|
|
58
60
|
- jayeshrathore7024@gmail.com
|
|
59
61
|
executables: []
|
|
@@ -67,25 +69,31 @@ files:
|
|
|
67
69
|
- Rakefile
|
|
68
70
|
- lib/bharat_filter_engine.rb
|
|
69
71
|
- lib/bharat_filter_engine/association_resolver.rb
|
|
72
|
+
- lib/bharat_filter_engine/column_helper.rb
|
|
70
73
|
- lib/bharat_filter_engine/dynamic_filter_values.rb
|
|
71
74
|
- lib/bharat_filter_engine/engine.rb
|
|
72
75
|
- lib/bharat_filter_engine/errors.rb
|
|
76
|
+
- lib/bharat_filter_engine/join_tracker.rb
|
|
73
77
|
- lib/bharat_filter_engine/rule_normalizer.rb
|
|
74
78
|
- lib/bharat_filter_engine/search_builder.rb
|
|
75
79
|
- lib/bharat_filter_engine/version.rb
|
|
76
80
|
- spec/association_resolver_spec.rb
|
|
77
81
|
- spec/dynamic_filter_values_spec.rb
|
|
78
82
|
- spec/engine_spec.rb
|
|
83
|
+
- spec/join_tracker_spec.rb
|
|
79
84
|
- spec/rule_normalizer_spec.rb
|
|
85
|
+
- spec/same_table_join_spec.rb
|
|
80
86
|
- spec/search_builder_spec.rb
|
|
81
87
|
- spec/spec_helper.rb
|
|
82
88
|
homepage: https://github.com/jayesh70240/bharat-filter-engine
|
|
83
89
|
licenses:
|
|
84
90
|
- MIT
|
|
85
91
|
metadata:
|
|
92
|
+
homepage_uri: https://github.com/jayesh70240/bharat-filter-engine
|
|
86
93
|
source_code_uri: https://github.com/jayesh70240/bharat-filter-engine
|
|
87
94
|
changelog_uri: https://github.com/jayesh70240/bharat-filter-engine/blob/main/CHANGELOG.md
|
|
88
95
|
bug_tracker_uri: https://github.com/jayesh70240/bharat-filter-engine/issues
|
|
96
|
+
rubygems_mfa_required: 'true'
|
|
89
97
|
post_install_message:
|
|
90
98
|
rdoc_options: []
|
|
91
99
|
require_paths:
|
|
@@ -101,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
101
109
|
- !ruby/object:Gem::Version
|
|
102
110
|
version: '0'
|
|
103
111
|
requirements: []
|
|
104
|
-
rubygems_version: 3.5.
|
|
112
|
+
rubygems_version: 3.5.22
|
|
105
113
|
signing_key:
|
|
106
114
|
specification_version: 4
|
|
107
115
|
summary: A generic filtering engine for Rails ActiveRecord
|