search_rails 1.1.2 → 2.0.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.
data/lib/search.rb ADDED
@@ -0,0 +1,667 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Search
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ # def self.search_fields
8
+ # {
9
+ # id: { node: -> { arel_table[:id] }, type: :integer },
10
+ # name: { node: -> { arel_table[:name] }, type: :string },
11
+ # input: { node: -> { arel_table[:input] }, type: :string },
12
+ # updated_at: { node: -> { arel_table[:updated_at] }, type: :datetime },
13
+ # created_at: { node: -> { arel_table[:created_at] }, type: :datetime },
14
+ # }
15
+ # end
16
+ def search_fields
17
+ {}
18
+ end
19
+
20
+ def _search_extract_node(node, keys = [])
21
+ keys << node[:key]
22
+
23
+ if node[:value].is_a?(Hash)
24
+ _search_extract_node(node[:value], keys)
25
+ else
26
+ { **node, key: keys.join(":") }
27
+ end
28
+ end
29
+
30
+ def _search_cast(node:, type: "text")
31
+ Arel::Nodes::NamedFunction.new("cast", [node.as(type)])
32
+ end
33
+
34
+ def _search_cast_boolean(value)
35
+ if value.is_a?(Range)
36
+ first = _search_cast_boolean(value.first)
37
+ last = _search_cast_boolean(value.last)
38
+
39
+ value.exclude_end? ? first...last : first..last
40
+ else
41
+ ActiveModel::Type::Boolean.new.cast(value)
42
+ end
43
+ end
44
+
45
+ def _search_cast_integer(value)
46
+ if value.is_a?(Range)
47
+ first = _search_cast_integer(value.first)
48
+ last = _search_cast_integer(value.last)
49
+
50
+ value.exclude_end? ? first...last : first..last
51
+ else
52
+ ActiveModel::Type::Integer.new.cast(value)
53
+ end
54
+ end
55
+
56
+ def _search_cast_string(value)
57
+ if value.is_a?(Range)
58
+ first = _search_cast_string(value.first)
59
+ last = _search_cast_string(value.last)
60
+
61
+ value.exclude_end? ? first...last : first..last
62
+ else
63
+ ActiveModel::Type::String.new.cast(value)
64
+ end
65
+ end
66
+
67
+ def _search_cast_datetime(value)
68
+ if value.is_a?(Range)
69
+ first = _search_cast_datetime(value.first)
70
+ last = _search_cast_datetime(value.last)
71
+ first = first.first if first.is_a?(Range)
72
+ last = last.last if last.is_a?(Range)
73
+ value.exclude_end? ? first...last : first..last
74
+ else
75
+ Chronic.time_class = Time.zone
76
+ Chronic.parse(_search_cast_string(value), guess: false)
77
+ end
78
+ end
79
+ end
80
+
81
+ included do
82
+ # q:
83
+ # ""
84
+ # "dorian"
85
+ # "given_name:dor"
86
+ # "age>30 given_name:dor family_name:mar programmer"
87
+ # params:
88
+ # {
89
+ # verified: true,
90
+ # primary: false,
91
+ # id: 1,
92
+ # given_name: "Dorian",
93
+ # }
94
+ # fields:
95
+ # [:id, :name, :input]
96
+ scope :search,
97
+ ->(q: "", fields: self.search_fields&.keys) do
98
+ raise ArgumentError unless self.search_fields.is_an?(Hash)
99
+ raise ArgumentError unless fields.is_an?(Array)
100
+
101
+ fields = fields.map(&:to_s)
102
+ q = q.to_s
103
+
104
+ where(
105
+ id:
106
+ _search_joins(
107
+ scope:
108
+ _search_parsed(
109
+ parsed: ::Query.evaluate(q),
110
+ scope: all,
111
+ fields: fields
112
+ ),
113
+ fields: fields
114
+ )
115
+ )
116
+ end
117
+
118
+ scope :_search_joins,
119
+ ->(scope:, fields:) do
120
+ fields = fields.map(&:to_s)
121
+
122
+ relations =
123
+ fields.map { |field| self.search_fields.fetch(field.to_sym)[:relation] }.compact
124
+
125
+ relations.reduce(scope) do |scope_with_relations, relation|
126
+ relation.call(scope_with_relations)
127
+ end
128
+ end
129
+
130
+ scope :_search_parsed,
131
+ ->(parsed:, scope:, fields:) do
132
+ if parsed.is_a?(String)
133
+ scope._search_fields(q: parsed, fields: fields)
134
+ elsif parsed.is_a?(Hash)
135
+ if parsed.key?(:left)
136
+ if parsed[:operator] == "or"
137
+ scope.where(
138
+ id:
139
+ _search_parsed(
140
+ parsed: parsed[:left],
141
+ scope: scope,
142
+ fields: fields
143
+ )
144
+ ).or(
145
+ scope.where(
146
+ id:
147
+ _search_parsed(
148
+ parsed: parsed[:right],
149
+ scope: scope,
150
+ fields: fields
151
+ )
152
+ )
153
+ )
154
+ elsif parsed[:operator] == "and"
155
+ scope.where(
156
+ id:
157
+ _search_parsed(
158
+ parsed: parsed[:left],
159
+ scope: scope,
160
+ fields: fields
161
+ )
162
+ ).where(
163
+ id:
164
+ _search_parsed(
165
+ parsed: parsed[:right],
166
+ scope: scope,
167
+ fields: fields
168
+ )
169
+ )
170
+ end
171
+ elsif parsed.key?(:right)
172
+ scope.where.not(
173
+ id:
174
+ _search_parsed(
175
+ parsed: parsed[:right],
176
+ scope: scope,
177
+ fields: fields
178
+ )
179
+ )
180
+ elsif parsed.key?(:key)
181
+ parsed = _search_extract_node(parsed)
182
+ key = parsed[:key].to_s.presence_in(fields)
183
+ operator = parsed[:operator]
184
+ value = parsed[:value]
185
+
186
+ if key.blank?
187
+ scope.none
188
+ else
189
+ scope._search_field(key: key, operator: operator, value: value)
190
+ end
191
+ else
192
+ raise ArgumentError
193
+ end
194
+ else
195
+ raise ArgumentError
196
+ end
197
+ end
198
+
199
+ # q:
200
+ # "dorian"
201
+ # "1"
202
+ # fields: [:id, :given_name, :family_name]
203
+ scope :_search_fields,
204
+ ->(q: "", fields: search_fields&.keys) do
205
+ raise ArgumentError unless search_fields.is_an?(Hash)
206
+ raise ArgumentError unless fields.is_an?(Array)
207
+
208
+ fields = fields.map(&:to_s)
209
+ q = q.to_s
210
+
211
+ where(
212
+ fields
213
+ .map do |field|
214
+ field = self.search_fields.fetch(field.to_sym)
215
+ node = field[:node].call
216
+ casted_field = _search_cast(node: node, type: :text)
217
+ casted_field.matches("%#{q}%", nil, false)
218
+ end
219
+ .reduce(&:or)
220
+ )
221
+
222
+ end
223
+
224
+ # key: input, name, id, created_at, updated_at, verified, admin, ...
225
+ # operator: :, =, >, ~, <, >=, ...
226
+ # value: "pomodoro", 123, true, false
227
+ scope :_search_field,
228
+ ->(key:, operator: ":", value:, fields: search_fields&.keys) do
229
+ raise ArgumentError unless search_fields.is_an?(Hash)
230
+ raise ArgumentError unless fields.is_an?(Array)
231
+
232
+ fields = fields.map(&:to_s)
233
+ key = key.to_s.presence_in(fields)
234
+
235
+ raise ArgumentError if key.blank?
236
+ raise ArgumentError if operator.blank?
237
+
238
+ field = self.search_fields.fetch(key.to_sym)
239
+
240
+ case operator
241
+ when ":"
242
+ _search_colon(field: field, value: value)
243
+ when "^"
244
+ _search_starts(field: field, value: value)
245
+ when "$"
246
+ _search_ends(field: field, value: value)
247
+ when ">="
248
+ _search_greater_or_equal(field: field, value: value)
249
+ when "<="
250
+ _search_lesser_or_equal(field: field, value: value)
251
+ when ">"
252
+ _search_greater(field: field, value: value)
253
+ when "<"
254
+ _search_lesser(field: field, value: value)
255
+ when "~"
256
+ _search_matches(field: field, value: value)
257
+ when "="
258
+ _search_equal(field: field, value: value)
259
+ when "!:"
260
+ where.not(id: _search_colon(field: field, value: value))
261
+ when "!!"
262
+ where.not(id: _search_colon(field: field, value: value))
263
+ when "!^"
264
+ where.not(id: _search_starts(field: field, value: value))
265
+ when "!$"
266
+ where.not(id: _search_ends(field: field, value: value))
267
+ when "!>="
268
+ where.not(
269
+ id: _search_greater_or_equal(field: field, value: value)
270
+ )
271
+ when "!<="
272
+ where.not(id: _search_lesser_or_equal(field: field, value: value))
273
+ when "!>"
274
+ where.not(id: _search_greater(field: field, value: value))
275
+ when "!<"
276
+ where.not(id: _search_lesser(field: field, value: value))
277
+ when "!="
278
+ where.not(id: _search_equal(field: field, value: value))
279
+ else
280
+ raise ArgumentError
281
+ end
282
+ end
283
+
284
+ # id:1, name:dorian, verified:true, created_at:today
285
+ scope :_search_colon,
286
+ ->(field:, value:) do
287
+ node = field[:node].call
288
+
289
+ case field[:type]
290
+ when :integer
291
+ _search_integer_eq(node: node, value: value)
292
+ when :string
293
+ _search_string_matches(node: node, value: value)
294
+ when :datetime
295
+ _search_datetime_eq(node: node, value: value)
296
+ when :boolean
297
+ _search_boolean_eq(node: node, value: value)
298
+ else
299
+ raise ArgumentError
300
+ end
301
+ end
302
+
303
+ scope :_search_matches,
304
+ ->(field:, value:) do
305
+ node = field[:node].call
306
+
307
+ case field[:type]
308
+ when :integer
309
+ _search_integer_eq(node: node, value: value)
310
+ when :string
311
+ _search_string_matches(node: node, value: value)
312
+ when :datetime
313
+ _search_datetime_eq(node: node, value: value)
314
+ when :boolean
315
+ _search_boolean_eq(node: node, value: value)
316
+ else
317
+ raise ArgumentError
318
+ end
319
+ end
320
+
321
+ scope :_search_ends,
322
+ ->(field:, value:) do
323
+ node = field[:node].call
324
+
325
+ case field[:type]
326
+ when :integer
327
+ _search_integer_eq(node: node, value: value)
328
+ when :string
329
+ _search_string_ends(node: node, value: value)
330
+ when :datetime
331
+ _search_datetime_eq(node: node, value: value)
332
+ when :boolean
333
+ _search_boolean_eq(node: node, value: value)
334
+ else
335
+ raise ArgumentError
336
+ end
337
+ end
338
+
339
+ scope :_search_starts,
340
+ ->(field:, value:) do
341
+ node = field[:node].call
342
+
343
+ case field[:type]
344
+ when :integer
345
+ _search_integer_eq(node: node, value: value)
346
+ when :string
347
+ _search_string_starts(node: node, value: value)
348
+ when :datetime
349
+ _search_datetime_eq(node: node, value: value)
350
+ when :boolean
351
+ _search_boolean_eq(node: node, value: value)
352
+ else
353
+ raise ArgumentError
354
+ end
355
+ end
356
+
357
+ scope :_search_equal,
358
+ ->(field:, value:) do
359
+ node = field[:node].call
360
+
361
+ case field[:type]
362
+ when :integer
363
+ _search_integer_eq(node: node, value: value)
364
+ when :string
365
+ _search_string_eq(node: node, value: value)
366
+ when :datetime
367
+ _search_datetime_eq(node: node, value: value)
368
+ when :boolean
369
+ _search_boolean_eq(node: node, value: value)
370
+ else
371
+ raise ArgumentError
372
+ end
373
+ end
374
+
375
+ scope :_search_lesser,
376
+ ->(field:, value:) do
377
+ node = field[:node].call
378
+
379
+ case field[:type]
380
+ when :integer
381
+ _search_integer_lt(node: node, value: value)
382
+ when :string
383
+ _search_string_lt(node: node, value: value)
384
+ when :datetime
385
+ _search_datetime_lt(node: node, value: value)
386
+ when :boolean
387
+ none
388
+ else
389
+ raise ArgumentError
390
+ end
391
+ end
392
+
393
+ scope :_search_lesser_or_equal,
394
+ ->(field:, value:) do
395
+ node = field[:node].call
396
+
397
+ case field[:type]
398
+ when :integer
399
+ _search_integer_lteq(node: node, value: value)
400
+ when :string
401
+ _search_string_lteq(node: node, value: value)
402
+ when :datetime
403
+ _search_datetime_lteq(node: node, value: value)
404
+ when :boolean
405
+ _search_boolean_eq(node: node, value: value)
406
+ else
407
+ raise ArgumentError
408
+ end
409
+ end
410
+
411
+ scope :_search_greater,
412
+ ->(field:, value:) do
413
+ node = field[:node].call
414
+
415
+ case field[:type]
416
+ when :integer
417
+ _search_integer_gt(node: node, value: value)
418
+ when :string
419
+ _search_string_gt(node: node, value: value)
420
+ when :datetime
421
+ _search_datetime_gt(node: node, value: value)
422
+ when :boolean
423
+ none
424
+ else
425
+ raise ArgumentError
426
+ end
427
+ end
428
+
429
+ scope :_search_greater_or_equal,
430
+ ->(field:, value:) do
431
+ node = field[:node].call
432
+
433
+ case field[:type]
434
+ when :integer
435
+ _search_integer_gteq(node: node, value: value)
436
+ when :string
437
+ _search_string_gteq(node: node, value: value)
438
+ when :datetime
439
+ _search_datetime_gteq(node: node, value: value)
440
+ when :boolean
441
+ _search_boolean_eq(node: node, value: value)
442
+ else
443
+ raise ArgumentError
444
+ end
445
+ end
446
+
447
+ scope :_search_integer_eq,
448
+ ->(node:, value:) do
449
+ node = _search_cast(node: node, type: :bigint)
450
+ value = _search_cast_integer(value)
451
+
452
+ if value.is_a?(Range)
453
+ if value.exclude_end?
454
+ where(node.gteq(value.first).and(node.lt(value.last)))
455
+ else
456
+ where(node.gteq(value.first).and(node.lteq(value.last)))
457
+ end
458
+ else
459
+ where(node.eq(value))
460
+ end
461
+ end
462
+
463
+ scope :_search_datetime_eq,
464
+ ->(node:, value:) do
465
+ node = _search_cast(node: node, type: :timestamp)
466
+ value = _search_cast_datetime(value)
467
+
468
+ if value.is_a?(Range)
469
+ if value.exclude_end?
470
+ where(node.gteq(value.first).and(node.lt(value.last)))
471
+ else
472
+ where(node.gteq(value.first).and(node.lteq(value.last)))
473
+ end
474
+ else
475
+ where(node.eq(value))
476
+ end
477
+ end
478
+
479
+ scope :_search_string_eq,
480
+ ->(node:, value:) do
481
+ node = _search_cast(node: node, type: :text)
482
+ value = _search_cast_string(value)
483
+
484
+ if value.is_a?(Range)
485
+ if value.exclude_end?
486
+ where(node.gteq(value.first).and(node.lt(value.last)))
487
+ else
488
+ where(node.gteq(value.first).and(node.lteq(value.last)))
489
+ end
490
+ else
491
+ where(node.eq(value))
492
+ end
493
+ end
494
+
495
+ scope :_search_boolean_eq,
496
+ ->(node:, value:) do
497
+ node = _search_cast(node: node, type: :boolean)
498
+ value = _search_cast_boolean(value)
499
+
500
+ if value.is_a?(Range)
501
+ if value.exclude_end?
502
+ where(node.eq(value.first).and(node.not_eq(value.last)))
503
+ else
504
+ where(node.eq(value.first)).or(where(node.eq(value.last)))
505
+ end
506
+ else
507
+ where(node.eq(value))
508
+ end
509
+ end
510
+
511
+ scope :_search_integer_lt,
512
+ ->(node:, value:) do
513
+ node = _search_cast(node: node, type: :bigint)
514
+ value = _search_cast_integer(value)
515
+ value = value.first if value.is_a?(Range)
516
+
517
+ where(node.lt(value))
518
+ end
519
+
520
+ scope :_search_datetime_lt,
521
+ ->(node:, value:) do
522
+ node = _search_cast(node: node, type: :timestamp)
523
+ value = _search_cast_datetime(value)
524
+ value = value.first if value.is_a?(Range)
525
+
526
+ where(node.lt(value))
527
+ end
528
+
529
+ scope :_search_string_lt,
530
+ ->(node:, value:) do
531
+ node = _search_cast(node: node, type: :text)
532
+ value = _search_cast_string(value)
533
+ value = value.first if value.is_a?(Range)
534
+
535
+ where(node.lt(value))
536
+ end
537
+
538
+ scope :_search_integer_lteq,
539
+ ->(node:, value:) do
540
+ node = _search_cast(node: node, type: :bigint)
541
+ value = _search_cast_integer(value)
542
+ value = value.first if value.is_a?(Range)
543
+
544
+ where(node.lteq(value))
545
+ end
546
+
547
+ scope :_search_datetime_lteq,
548
+ ->(node:, value:) do
549
+ node = _search_cast(node: node, type: :timestamp)
550
+ value = _search_cast_datetime(value)
551
+ value = value.first if value.is_a?(Range)
552
+
553
+ where(node.lteq(value))
554
+ end
555
+
556
+ scope :_search_string_lteq,
557
+ ->(node:, value:) do
558
+ node = _search_cast(node: node, type: :text)
559
+ value = _search_cast_string(value)
560
+ value = value.first if value.is_a?(Range)
561
+
562
+ where(node.lteq(value))
563
+ end
564
+
565
+ scope :_search_integer_gt,
566
+ ->(node:, value:) do
567
+ node = _search_cast(node: node, type: :bigint)
568
+ value = _search_cast_integer(value)
569
+ value = value.last if value.is_a?(Range)
570
+
571
+ where(node.gt(value))
572
+ end
573
+
574
+ scope :_search_datetime_gt,
575
+ ->(node:, value:) do
576
+ node = _search_cast(node: node, type: :timestamp)
577
+ value = _search_cast_datetime(value)
578
+ value = value.last if value.is_a?(Range)
579
+
580
+ where(node.gt(value))
581
+ end
582
+
583
+ scope :_search_string_gt,
584
+ ->(node:, value:) do
585
+ node = _search_cast(node: node, type: :text)
586
+ value = _search_cast_string(value)
587
+ value = value.last if value.is_a?(Range)
588
+
589
+ where(node.gt(value))
590
+ end
591
+
592
+ scope :_search_integer_gteq,
593
+ ->(node:, value:) do
594
+ node = _search_cast(node: node, type: :bigint)
595
+ value = _search_cast_integer(value)
596
+ value = value.last if value.is_a?(Range)
597
+
598
+ where(node.gteq(value))
599
+ end
600
+
601
+ scope :_search_datetime_gteq,
602
+ ->(node:, value:) do
603
+ node = _search_cast(node: node, type: :timestamp)
604
+ value = _search_cast_datetime(value)
605
+ value = value.last if value.is_a?(Range)
606
+
607
+ where(node.gteq(value))
608
+ end
609
+
610
+ scope :_search_string_gteq,
611
+ ->(node:, value:) do
612
+ node = _search_cast(node: node, type: :text)
613
+ value = _search_cast_string(value)
614
+ value = value.last if value.is_a?(Range)
615
+
616
+ where(node.gteq(value))
617
+ end
618
+
619
+ scope :_search_string_matches,
620
+ ->(node:, value:) do
621
+ node = _search_cast(node: node, type: :text)
622
+ value = _search_cast_string(value)
623
+
624
+ if value.is_a?(Range)
625
+ if value.exclude_end?
626
+ where(node.gteq(value.first).and(node.lt(value.last)))
627
+ else
628
+ where(node.gteq(value.first).and(node.lteq(value.last)))
629
+ end
630
+ else
631
+ where(node.matches("%#{value}%", nil, false))
632
+ end
633
+ end
634
+
635
+ scope :_search_string_ends,
636
+ ->(node:, value:) do
637
+ node = _search_cast(node: node, type: :text)
638
+ value = _search_cast_string(value)
639
+
640
+ if value.is_a?(Range)
641
+ if value.exclude_end?
642
+ where(node.gteq(value.first).and(node.lt(value.last)))
643
+ else
644
+ where(node.gteq(value.first).and(node.lteq(value.last)))
645
+ end
646
+ else
647
+ where(node.matches("%#{value}", nil, false))
648
+ end
649
+ end
650
+
651
+ scope :_search_string_starts,
652
+ ->(node:, value:) do
653
+ node = _search_cast(node: node, type: :text)
654
+ value = _search_cast_string(value)
655
+
656
+ if value.is_a?(Range)
657
+ if value.exclude_end?
658
+ where(node.gteq(value.first).and(node.lt(value.last)))
659
+ else
660
+ where(node.gteq(value.first).and(node.lteq(value.last)))
661
+ end
662
+ else
663
+ where(node.matches("#{value}%", nil, false))
664
+ end
665
+ end
666
+ end
667
+ end