command_search 0.8.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15ffe763615f2dbf119a8960fb91405376f2d2e30b3f43f594c6b3c7730ce96c
4
- data.tar.gz: afe8f45ba3199a987b5ee2abbb7429fc13d89658d8dd0c48af74c19d3f4634ac
3
+ metadata.gz: b4560d75eabd25ff56808df06ab6b72d97813fbd75909739d15bfa2e13055557
4
+ data.tar.gz: 8c48236dd477f98450076128b7737121fabb16398c123e3c567f049bd68cadbf
5
5
  SHA512:
6
- metadata.gz: 3b10c60d4c3cca7daea2f1d14b3fc87afbdaf79922f1fc2f6fb4b33b97af8983c61ce0140e5ec1234749bd1b738fe6f34b9f6932db7f7f5ec7b95492ad863e3b
7
- data.tar.gz: ad4acd1ceeafa32f0982a9c076724b3b933177653e997f2d8a5c9635f61d3f17980e47285c32b375a1ec0a28765dd5eaf00f01c5a160d8cd7e9cc1f1c3cc04f2
6
+ metadata.gz: b0abdf98f95abba34b3a27d8f805f5183b1089e7291dad6b7f3a67e6cdb8a2413dad6813cf06e7118b733b44132428d79c89545739184022f3fc6d6222e60f8d
7
+ data.tar.gz: 836f1b245c14396850259c7e326e4ed1451bfcda6df07a594162ed4bbd1725abdf4a4a46c90c75c23eeb3b3a0ed242e7caa02cded714437acc1cc9ce65a63781
@@ -1,10 +1,11 @@
1
1
  load(__dir__ + '/command_search/aliaser.rb')
2
2
  load(__dir__ + '/command_search/lexer.rb')
3
3
  load(__dir__ + '/command_search/parser.rb')
4
- load(__dir__ + '/command_search/command_dealiaser.rb')
4
+ load(__dir__ + '/command_search/normalizer.rb')
5
5
  load(__dir__ + '/command_search/optimizer.rb')
6
- load(__dir__ + '/command_search/mongoer.rb')
7
- load(__dir__ + '/command_search/memory.rb')
6
+
7
+ load(__dir__ + '/command_search/backends/memory.rb')
8
+ load(__dir__ + '/command_search/backends/mongoer.rb')
8
9
 
9
10
  class Boolean; end
10
11
 
@@ -17,19 +18,17 @@ module CommandSearch
17
18
  command_fields = options[:command_fields] || {}
18
19
 
19
20
  aliased_query = Aliaser.alias(query, aliases)
20
- tokens = Lexer.lex(aliased_query)
21
- parsed = Parser.parse!(tokens)
22
- dealiased = CommandDealiaser.dealias(parsed, command_fields)
23
- cleaned = CommandDealiaser.decompose_unaliasable(dealiased, command_fields)
24
- opted = Optimizer.optimize(cleaned)
21
+ ast = Lexer.lex(aliased_query)
22
+ Parser.parse!(ast)
23
+ Optimizer.optimize!(ast)
24
+ command_fields = Normalizer.normalize!(ast, command_fields)
25
25
 
26
26
  if source.respond_to?(:mongo_client) && source.queryable
27
- fields = [:__CommandSearch_mongo_fields_dummy_key__] if fields.empty?
28
- mongo_query = Mongoer.build_query(opted, fields, command_fields)
27
+ fields = [:__CommandSearch_dummy_key__] if fields.empty?
28
+ mongo_query = Mongoer.build_query(ast, fields, command_fields)
29
29
  return source.where(mongo_query)
30
30
  end
31
31
 
32
- selector = Memory.build_query(opted, fields, command_fields)
33
- source.select(&selector)
32
+ source.select { |x| Memory.check(x, ast, fields, command_fields) }
34
33
  end
35
34
  end
@@ -0,0 +1,70 @@
1
+ module CommandSearch
2
+ module Memory
3
+ module_function
4
+
5
+ def command_check(item, val)
6
+ cmd = val[0][:value]
7
+ cmd_search = val[1][:value]
8
+ item_val = item[cmd.to_sym] || item[cmd]
9
+ val_type = val[1][:type]
10
+ val_type = Boolean if val_type == :existence && cmd_search == true
11
+ if val_type == Boolean
12
+ !!item_val == cmd_search
13
+ elsif val_type == :existence
14
+ item_val == nil
15
+ elsif !item_val
16
+ return false
17
+ elsif val_type == Time
18
+ item_time = item_val.to_time
19
+ cmd_search.first <= item_time && item_time < cmd_search.last
20
+ elsif cmd_search.is_a?(Regexp)
21
+ item_val[cmd_search]
22
+ elsif cmd_search == ''
23
+ item_val == cmd_search
24
+ else
25
+ item_val.to_s[/#{Regexp.escape(cmd_search)}/i]
26
+ end
27
+ end
28
+
29
+ def compare_check(item, node, cmd_types)
30
+ cmd = node[:value].first
31
+ cmd_val = cmd[:value]
32
+ cmd_type = cmd_types[cmd[:value].to_sym]
33
+ item_val = item[cmd_val.to_sym] || item[cmd_val.to_s]
34
+ search = node[:value].last
35
+ val = search[:value]
36
+ if val.is_a?(Time)
37
+ item_val = item_val.to_time if item_val
38
+ elsif search[:type] == :str && cmd_types[val.to_sym]
39
+ val = item[val.to_sym] || item[val.to_s]
40
+ end
41
+ args = [item_val, val]
42
+ return unless args.all?
43
+ fn = node[:nest_op].to_sym.to_proc
44
+ fn.call(*args.map(&:to_f))
45
+ end
46
+
47
+ def check(item, ast, fields, cmd_types)
48
+ ast.all? do |node|
49
+ val = node[:value]
50
+ case node[:nest_type]
51
+ when nil
52
+ fields.any? do |x|
53
+ item_val = item[x.to_sym] || item[x.to_s]
54
+ item_val.to_s[val] if item_val
55
+ end
56
+ when :colon
57
+ command_check(item, val)
58
+ when :compare
59
+ compare_check(item, node, cmd_types)
60
+ when :minus
61
+ !val.all? { |v| check(item, [v], fields, cmd_types) }
62
+ when :pipe
63
+ val.any? { |v| check(item, [v], fields, cmd_types) }
64
+ when :paren
65
+ val.all? { |v| check(item, [v], fields, cmd_types) }
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,88 @@
1
+ module CommandSearch
2
+ module Mongoer
3
+ module_function
4
+
5
+ def build_search(node, fields, cmd_fields)
6
+ val = node[:value]
7
+ forms = fields.map do |field|
8
+ type = cmd_fields[field.to_sym]
9
+ if type == Numeric
10
+ { field => node[:number_value] }
11
+ else
12
+ { field => val }
13
+ end
14
+ end
15
+ return forms if forms.count < 2
16
+ { '$or' => forms }
17
+ end
18
+
19
+ def build_command(node)
20
+ (field_node, search_node) = node[:value]
21
+ key = field_node[:value]
22
+ val = search_node[:value]
23
+ search_type = search_node[:type]
24
+ search_type = Boolean if search_type == :existence && val == true
25
+ if search_type == Boolean
26
+ # These queries can return true for empty arrays.
27
+ val = [
28
+ { key => { '$exists' => true } },
29
+ { key => { '$ne' => !val } }
30
+ ]
31
+ key = '$and'
32
+ elsif search_type == :existence
33
+ val = { '$exists' => false }
34
+ elsif search_type == Time
35
+ return [{ CommandSearchNilTime: true }, { CommandSearchNilTime: false }] unless val
36
+ return [
37
+ { key => { '$gte' => val[0] } },
38
+ { key => { '$lt' => val[1] } }
39
+ ]
40
+ end
41
+ { key => val }
42
+ end
43
+
44
+ def build_compare(node, cmd_fields)
45
+ op_map = { '<' => '$lt', '>' => '$gt', '<=' => '$lte', '>=' => '$gte' }
46
+ op = op_map[node[:nest_op]]
47
+ key = node[:value][0][:value]
48
+ val = node[:value][1][:value]
49
+ if val.class == String && cmd_fields[val.to_sym]
50
+ val = '$' + val
51
+ key = '$' + key
52
+ val = [key, val]
53
+ key = '$expr'
54
+ end
55
+ { key => { op => val } }
56
+ end
57
+
58
+ def build_searches!(ast, fields, cmd_fields)
59
+ mongo_types = { paren: '$and', pipe: '$or', minus: '$nor' }
60
+ ast.map! do |node|
61
+ type = node[:nest_type]
62
+ if type == :colon
63
+ build_command(node)
64
+ elsif type == :compare
65
+ build_compare(node, cmd_fields)
66
+ elsif key = mongo_types[type]
67
+ build_searches!(node[:value], fields, cmd_fields)
68
+ val = node[:value]
69
+ if key == '$nor' && val.count > 1
70
+ next { key => [{ '$and' => val }] }
71
+ end
72
+ val.map! { |x| x['$or'] || x }.flatten! unless key == '$and'
73
+ { key => val }
74
+ else
75
+ build_search(node, fields, cmd_fields)
76
+ end
77
+ end
78
+ ast.flatten!
79
+ end
80
+
81
+ def build_query(ast, fields, cmd_fields)
82
+ build_searches!(ast, fields, cmd_fields)
83
+ return {} if ast == []
84
+ return ast.first if ast.count == 1
85
+ { '$and' => ast }
86
+ end
87
+ end
88
+ end
@@ -9,7 +9,7 @@ module CommandSearch
9
9
  match = nil
10
10
  case input[i..-1]
11
11
  when /\A\s+/
12
- type = :space
12
+ next i += Regexp.last_match[0].length
13
13
  when /\A"(.*?)"/
14
14
  match = Regexp.last_match[1]
15
15
  type = :quoted_str
@@ -0,0 +1,121 @@
1
+ require('chronic')
2
+
3
+ module CommandSearch
4
+ module Normalizer
5
+ module_function
6
+
7
+ def cast_bool(type, node)
8
+ if type == Boolean
9
+ node[:type] = Boolean
10
+ node[:value] = !!node[:value][0][/t/i]
11
+ return
12
+ end
13
+ return unless type.is_a?(Array) && type.include?(:allow_existence_boolean)
14
+ return unless node[:type] == :str && node[:value][/\Atrue\Z|\Afalse\Z/i]
15
+ node[:type] = :existence
16
+ node[:value] = !!node[:value][0][/t/i]
17
+ end
18
+
19
+ def cast_time(node)
20
+ search_node = node[:value][1]
21
+ search_node[:type] = Time
22
+ str = search_node[:value]
23
+ if str == str.to_i.to_s
24
+ search_node[:value] = [Time.new(str), Time.new(str.to_i + 1)]
25
+ else
26
+ time_str = str.tr('._-', ' ')
27
+ times = Chronic.parse(time_str, { guess: nil })
28
+ times ||= Chronic.parse(str, { guess: nil })
29
+ if times
30
+ search_node[:value] = [times.first, times.last]
31
+ else
32
+ search_node[:value] = nil
33
+ return
34
+ end
35
+ end
36
+ return unless node[:nest_type] == :compare
37
+ op = node[:nest_op]
38
+ if op == '<' || op == '>='
39
+ search_node[:value] = search_node[:value].first
40
+ else
41
+ search_node[:value] = search_node[:value].last
42
+ search_node[:value] -= 1
43
+ end
44
+ end
45
+
46
+ def cast_regex(node)
47
+ type = node[:type]
48
+ return unless type == :str || type == :quoted_str || type == :number
49
+ raw = node[:value]
50
+ str = Regexp.escape(raw)
51
+ return node[:value] = /#{str}/i unless type == :quoted_str
52
+ return node[:value] = '' if raw == ''
53
+ return node[:value] = /\b#{str}\b/ unless raw[/(^\W)|(\W$)/]
54
+ border_a = '(^|\s|[^:+\w])'
55
+ border_b = '($|\s|[^:+\w])'
56
+ node[:value] = Regexp.new(border_a + str + border_b)
57
+ end
58
+
59
+ def flip_operator!(node, cmd_fields)
60
+ val = node[:value]
61
+ return if cmd_fields[val[0][:value].to_sym]
62
+ return unless cmd_fields[val[1][:value].to_sym]
63
+ flip_ops = { '<' => '>', '>' => '<', '<=' => '>=', '>=' => '<=' }
64
+ node[:nest_op] = flip_ops[node[:nest_op]]
65
+ node[:value].reverse!
66
+ end
67
+
68
+ def dealias_key(key, cmd_fields)
69
+ key = cmd_fields[key.to_sym] while cmd_fields[key.to_sym].is_a?(Symbol)
70
+ key.to_s
71
+ end
72
+
73
+ def dealias!(ast, cmd_fields)
74
+ ast.map! do |node|
75
+ nest = node[:nest_type]
76
+ unless nest
77
+ node[:number_value] = node[:value] if node[:type] == :number
78
+ cast_regex(node)
79
+ next node
80
+ end
81
+ unless nest == :colon || nest == :compare
82
+ dealias!(node[:value], cmd_fields)
83
+ next node
84
+ end
85
+ flip_operator!(node, cmd_fields) if nest == :compare
86
+ (key_node, search_node) = node[:value]
87
+ new_key = dealias_key(key_node[:value], cmd_fields)
88
+ type = cmd_fields[new_key.to_sym]
89
+ node[:value][0][:value] = new_key
90
+ if type
91
+ cast_bool(type, search_node)
92
+ type = (type - [:allow_existence_boolean]).first if type.is_a?(Array)
93
+ cast_time(node) if [Time, Date, DateTime].include?(type)
94
+ cast_regex(search_node) if type == String
95
+ next node
96
+ end
97
+ str_values = "#{new_key}#{node[:nest_op]}#{search_node[:value]}"
98
+ node = { type: :str, value: str_values }
99
+ cast_regex(node)
100
+ node
101
+ end
102
+ end
103
+
104
+ def normalize!(ast, cmd_fields)
105
+ dealias!(ast, cmd_fields)
106
+ clean = {}
107
+ cmd_fields.each do |k, v|
108
+ next if v.is_a?(Symbol)
109
+ if v.is_a?(Array)
110
+ clean[k] = (v - [:allow_existence_boolean]).first
111
+ next
112
+ end
113
+ v = Numeric if v == Integer
114
+ v = Time if v == Date
115
+ v = Time if v == DateTime
116
+ next clean[k] = v
117
+ end
118
+ clean
119
+ end
120
+ end
121
+ end
@@ -2,60 +2,33 @@ module CommandSearch
2
2
  module Optimizer
3
3
  module_function
4
4
 
5
- def ands_and_ors!(ast)
5
+ def denest!(ast, parent_type = :paren)
6
6
  ast.map! do |node|
7
- next node unless node[:nest_type] == :paren || node[:nest_type] == :pipe
8
- ands_and_ors!(node[:value])
9
- next node[:value].first if node[:value].length < 2
10
- next node unless node[:nest_type] == :pipe
11
- node[:value].map! do |kid|
12
- next kid[:value] if kid[:nest_type] == :pipe
13
- kid
14
- end
15
- node[:value].flatten!
16
- node[:value].uniq!
17
- node
18
- end
19
- end
20
-
21
- def negate_negate!(ast)
22
- ast.map! do |node|
23
- next node unless node[:nest_type]
24
- negate_negate!(node[:value])
25
- next [] if node[:value] == []
7
+ next [] if node[:type] == :quoted_str && node[:value] == '' && [:paren, :pipe, :minus].include?(parent_type)
26
8
  type = node[:nest_type]
27
- child_type = node[:value].first[:nest_type]
28
- next node unless type == :minus && child_type == :minus
29
- node[:value].first[:value]
30
- end
31
- ast.flatten!
32
- end
33
-
34
- def denest_parens!(ast, parent_type = :root)
35
- ast.map! do |node|
36
- next node unless node[:nest_type]
37
- denest_parens!(node[:value], node[:nest_type])
38
- # valid_self && (valid_parent || valid_child)
39
- if node[:nest_type] == :paren && (parent_type != :pipe || node[:value].count < 2)
40
- next node[:value]
9
+ next node unless type
10
+ next node unless type == :paren || type == :pipe || type == :minus
11
+ denest!(node[:value], type)
12
+ next [] if node[:value] == []
13
+ if type == :minus
14
+ only_child = node[:value].count == 1
15
+ child = node[:value].first
16
+ next child[:value] if only_child && child[:nest_type] == :minus
17
+ next node
41
18
  end
19
+ next node[:value] if node[:value].count == 1
20
+ next node[:value] if type == parent_type
21
+ next node[:value] if type == :paren && parent_type == :minus
22
+ next node if type == :paren
23
+ denest!(node[:value], type) # type == :pipe, parent_type == :paren
24
+ node[:value].uniq!
42
25
  node
43
26
  end
44
27
  ast.flatten!
45
28
  end
46
29
 
47
- def remove_empty_strings!(ast)
48
- ast.reject! do |node|
49
- remove_empty_strings!(node[:value]) if [:paren, :pipe, :minus].include?(node[:nest_type])
50
- node[:type] == :quoted_str && node[:value] == ''
51
- end
52
- end
53
-
54
- def optimize(ast)
55
- denest_parens!(ast)
56
- remove_empty_strings!(ast)
57
- negate_negate!(ast)
58
- ands_and_ors!(ast)
30
+ def optimize!(ast)
31
+ denest!(ast)
59
32
  ast.uniq!
60
33
  ast
61
34
  end
@@ -47,7 +47,7 @@ module CommandSearch
47
47
  left = input[i][:type]
48
48
  right = input[i + 2][:type]
49
49
  if types.include?(left) && types.include?(right)
50
- input.insert(i + 1, input[i + 1])
50
+ input.insert(i + 1, input[i + 1].clone())
51
51
  end
52
52
  i += 1
53
53
  end
@@ -83,7 +83,6 @@ module CommandSearch
83
83
  input[i - 1..i] = merge_strs(input[i - 1..i], [1, 0]) if i > 0
84
84
  end
85
85
 
86
- input.select! { |x| x[:type] != :space }
87
86
  input[-1][:type] = :str if input[-1] && input[-1][:type] == :minus
88
87
  end
89
88
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: command_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - zumbalogy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-14 00:00:00.000000000 Z
11
+ date: 2019-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chronic
@@ -32,10 +32,10 @@ extra_rdoc_files: []
32
32
  files:
33
33
  - lib/command_search.rb
34
34
  - lib/command_search/aliaser.rb
35
- - lib/command_search/command_dealiaser.rb
35
+ - lib/command_search/backends/memory.rb
36
+ - lib/command_search/backends/mongoer.rb
36
37
  - lib/command_search/lexer.rb
37
- - lib/command_search/memory.rb
38
- - lib/command_search/mongoer.rb
38
+ - lib/command_search/normalizer.rb
39
39
  - lib/command_search/optimizer.rb
40
40
  - lib/command_search/parser.rb
41
41
  homepage: https://github.com/zumbalogy/command_search
@@ -1,48 +0,0 @@
1
- module CommandSearch
2
- module CommandDealiaser
3
- module_function
4
-
5
- def dealias_key(key, aliases)
6
- key = aliases[key.to_sym] while aliases[key.to_sym].is_a?(Symbol)
7
- key.to_s
8
- end
9
-
10
- def dealias_values((key_node, search_node), aliases)
11
- new_key = dealias_key(key_node[:value], aliases)
12
- key_node[:value] = new_key
13
- [key_node, search_node]
14
- end
15
-
16
- def unnest_unaliased(node, aliases)
17
- type = node[:nest_type]
18
- if type == :colon
19
- val = node[:value][0][:value].to_sym
20
- return node if aliases[val]
21
- elsif type == :compare
22
- return node if node[:value].any? { |child| aliases[child[:value].to_sym] }
23
- end
24
- values = node[:value].map { |x| x[:value] }
25
- str_values = values.join(node[:nest_op])
26
- { type: :str, value: str_values }
27
- end
28
-
29
- def dealias(ast, aliases)
30
- ast.map! do |x|
31
- next x unless x[:nest_type]
32
- dealias(x[:value], aliases)
33
- next x unless [:colon, :compare].include?(x[:nest_type])
34
- x[:value] = dealias_values(x[:value], aliases)
35
- x
36
- end
37
- end
38
-
39
- def decompose_unaliasable(ast, aliases)
40
- ast.map! do |x|
41
- next x unless x[:nest_type]
42
- decompose_unaliasable(x[:value], aliases)
43
- next x unless [:colon, :compare].include?(x[:nest_type])
44
- unnest_unaliased(x, aliases)
45
- end
46
- end
47
- end
48
- end
@@ -1,125 +0,0 @@
1
- require('chronic')
2
-
3
- module CommandSearch
4
- module Memory
5
- module_function
6
-
7
- def command_check(item, val, command_types)
8
- cmd = val[0][:value].to_sym
9
- cmd_search = val[1][:value]
10
- raw_cmd_type = [command_types[cmd]].flatten
11
- allow_existence_boolean = raw_cmd_type.include?(:allow_existence_boolean)
12
- cmd_type = (raw_cmd_type - [:allow_existence_boolean]).first
13
- if cmd_type == Boolean
14
- if cmd_search[/true/i]
15
- item[cmd]
16
- else
17
- item[cmd] == false
18
- end
19
- elsif allow_existence_boolean && (cmd_search[/true/i] || cmd_search[/false/i])
20
- if cmd_search[/true/i]
21
- item[cmd]
22
- else
23
- item[cmd] == nil
24
- end
25
- elsif !item.key?(cmd)
26
- return false
27
- elsif [Date, Time, DateTime].include?(cmd_type)
28
- item_time = item[cmd].to_time
29
- if cmd_search == cmd_search.to_i.to_s
30
- Time.new(cmd_search) <= item_time && item_time < Time.new(cmd_search.to_i + 1)
31
- else
32
- time_str = cmd_search.gsub(/[\._-]/, ' ')
33
- input_times = Chronic.parse(time_str, { guess: nil }) || Chronic.parse(cmd_search, { guess: nil })
34
- input_times.first <= item_time && item_time < input_times.last
35
- end
36
- elsif val[1][:type] == :quoted_str
37
- regex = /\b#{Regexp.escape(cmd_search)}\b/
38
- regex = /\A\Z/ if cmd_search == ''
39
- if cmd_search[/(^\W)|(\W$)/]
40
- head_border = '(?<=^|[^:+\w])'
41
- tail_border = '(?=$|[^:+\w])'
42
- regex = Regexp.new(head_border + Regexp.escape(cmd_search) + tail_border)
43
- end
44
- item[cmd][regex]
45
- else
46
- item[cmd].to_s[/#{Regexp.escape(cmd_search)}/i]
47
- end
48
- end
49
-
50
- def compare_check(item, node, command_types)
51
- children = node[:value]
52
- cmd = children.find { |c| command_types[c[:value].to_sym] }
53
- raw_cmd_type = [command_types[cmd[:value].to_sym]].flatten
54
- cmd_type = (raw_cmd_type - [:allow_existence_boolean]).first
55
-
56
- args = children.map do |child|
57
- child_val = child[:value]
58
- item_val = item[child_val.to_s] || item[child_val.to_sym]
59
- item_val ||= child_val unless child == cmd
60
- next unless item_val
61
- next item_val.to_time if child == cmd && [Date, Time, DateTime].include?(cmd_type)
62
- next item_val if child == cmd
63
- if [Date, Time, DateTime].include?(cmd_type)
64
- date_start_map = {
65
- '<' => :start,
66
- '>' => :end,
67
- '<=' => :end,
68
- '>=' => :start
69
- }
70
- date_pick = date_start_map[node[:nest_op]]
71
- time_str = item_val.gsub(/[\._-]/, ' ')
72
- next Time.new(time_str) if time_str == time_str.to_i.to_s
73
-
74
- date = Chronic.parse(time_str, { guess: nil }) || Chronic.parse(item_val, { guess: nil })
75
- if date_pick == :start
76
- date.first
77
- else
78
- date.last
79
- end
80
- else
81
- item_val
82
- end
83
- end
84
- return unless args.all?
85
- fn = node[:nest_op].to_sym.to_proc
86
- fn.call(*args.map(&:to_f))
87
- end
88
-
89
- def check(item, ast, fields, command_types)
90
- field_vals = fields.map { |x| item[x] || item[x.to_s] || item[x.to_sym] }.compact
91
- ast_array = ast.is_a?(Array) ? ast : [ast]
92
- ast_array.all? do |node|
93
- val = node[:value]
94
- case node[:nest_type]
95
- when nil
96
- if node[:type] == :quoted_str
97
- regex = /\b#{Regexp.escape(val)}\b/
98
- if val[/(^\W)|(\W$)/]
99
- head_border = '(?<=^|[^:+\w])'
100
- tail_border = '(?=$|[^:+\w])'
101
- regex = Regexp.new(head_border + Regexp.escape(val) + tail_border)
102
- end
103
- field_vals.any? { |x| x.to_s[regex] }
104
- else
105
- field_vals.any? { |x| x.to_s[/#{Regexp.escape(val)}/i] }
106
- end
107
- when :colon
108
- command_check(item, val, command_types)
109
- when :compare
110
- compare_check(item, node, command_types)
111
- when :pipe
112
- val.any? { |v| check(item, v, fields, command_types) }
113
- when :minus
114
- !val.all? { |v| check(item, v, fields, command_types) }
115
- when :paren
116
- val.all? { |v| check(item, v, fields, command_types) }
117
- end
118
- end
119
- end
120
-
121
- def build_query(ast, fields, command_types = {})
122
- proc { |x| check(x, ast, fields, command_types) }
123
- end
124
- end
125
- end
@@ -1,229 +0,0 @@
1
- require('chronic')
2
-
3
- module CommandSearch
4
- module Mongoer
5
- module_function
6
-
7
- def numeric_field?(field, command_types)
8
- type = command_types[field.to_sym]
9
- if type.is_a?(Array)
10
- type = (type - [:allow_existence_boolean]).first
11
- end
12
- [Numeric, Integer].include?(type)
13
- end
14
-
15
- def build_str_regex(raw, type)
16
- str = Regexp.escape(raw)
17
- return /#{str}/i unless type == :quoted_str
18
- return '' if raw == ''
19
- return /\b#{str}\b/ unless raw[/(^\W)|(\W$)/]
20
- border_a = '(^|\s|[^:+\w])'
21
- border_b = '($|\s|[^:+\w])'
22
- Regexp.new(border_a + str + border_b)
23
- end
24
-
25
- def build_search(ast_node, fields, command_types)
26
- str = ast_node[:value] || ''
27
- fields = [fields] unless fields.is_a?(Array)
28
- regex = build_str_regex(str, ast_node[:type])
29
-
30
- forms = fields.map do |field|
31
- if numeric_field?(field, command_types)
32
- { field => str }
33
- else
34
- { field => regex }
35
- end
36
- end
37
- return forms if forms.count < 2
38
- { '$or' => forms }
39
- end
40
-
41
- def is_bool_str?(str, search_type)
42
- search_type != :quoted_str && str[/\Atrue\Z|\Afalse\Z/i]
43
- end
44
-
45
- def make_boolean(str)
46
- str[0] == 't'
47
- end
48
-
49
- def build_time_command(key, val)
50
- time_str = val.tr('_.-', ' ')
51
- if time_str == time_str.to_i.to_s
52
- date_a = Time.new(time_str)
53
- date_b = Time.new(time_str.to_i + 1).yesterday
54
- else
55
- date = Chronic.parse(time_str, guess: nil) || Chronic.parse(val, guess: nil)
56
- return [{ CommandSeachDummyDate: true }, { CommandSeachDummyDate: false }] unless date
57
- date_a = date.begin
58
- date_b = date.end
59
- end
60
- [
61
- { key => { '$gte' => date_a } },
62
- { key => { '$lte' => date_b } }
63
- ]
64
- end
65
-
66
- def build_command(ast_node, command_types)
67
- (field_node, search_node) = ast_node[:value]
68
- key = field_node[:value]
69
- raw_type = command_types[key.to_sym]
70
- type = raw_type
71
-
72
- raw_val = search_node[:value]
73
- search_type = search_node[:type]
74
-
75
- if raw_type.is_a?(Array)
76
- type = (raw_type - [:allow_existence_boolean]).first
77
- is_bool = raw_type.include?(:allow_existence_boolean) && is_bool_str?(raw_val, search_type)
78
- else
79
- type = raw_type
80
- is_bool = false
81
- end
82
-
83
- if type == Boolean
84
- bool = make_boolean(raw_val)
85
- val = [
86
- { key => { '$exists' => true } },
87
- { key => { '$ne' => !bool } }
88
- ]
89
- key = '$and'
90
- elsif is_bool
91
- # These queries return true for empty arrays.
92
- bool = make_boolean(raw_val)
93
- if bool
94
- val = [
95
- { key => { '$exists' => true } },
96
- { key => { '$ne' => false } }
97
- ]
98
- key = '$and'
99
- else
100
- val = { '$exists' => false }
101
- end
102
- elsif type == String
103
- val = build_str_regex(raw_val, search_type)
104
- elsif [Numeric, Integer].include?(type)
105
- val = raw_val
106
- elsif [Date, Time, DateTime].include?(type)
107
- return build_time_command(key, raw_val)
108
- end
109
- { key => val }
110
- end
111
-
112
- def build_compare(ast_node, command_types)
113
- flip_ops = {
114
- '<' => '>',
115
- '>' => '<',
116
- '<=' => '>=',
117
- '>=' => '<='
118
- }
119
- mongo_op_map = {
120
- '<' => '$lt',
121
- '>' => '$gt',
122
- '<=' => '$lte',
123
- '>=' => '$gte'
124
- }
125
-
126
- keys = command_types.keys
127
- (first_node, last_node) = ast_node[:value]
128
- key = first_node[:value]
129
- val = last_node[:value]
130
- op = ast_node[:nest_op]
131
-
132
- if keys.include?(val.to_sym)
133
- (key, val) = [val, key]
134
- op = flip_ops[op]
135
- end
136
-
137
- mongo_op = mongo_op_map[op]
138
- raw_type = command_types[key.to_sym]
139
-
140
- if raw_type.is_a?(Array)
141
- type = (raw_type - [:allow_existence_boolean]).first
142
- else
143
- type = raw_type
144
- end
145
-
146
- if command_types[val.to_sym]
147
- val = '$' + val
148
- key = '$' + key
149
- val = [key, val]
150
- key = '$expr'
151
- elsif [Date, Time, DateTime].include?(type)
152
- # foo < day | day.start
153
- # foo <= day | day.end
154
- # foo > day | day.end
155
- # foo >= day | day.start
156
- date_start_map = {
157
- '<' => :start,
158
- '>' => :end,
159
- '<=' => :end,
160
- '>=' => :start
161
- }
162
- date_pick = date_start_map[op]
163
- time_str = val.tr('_.-', ' ')
164
-
165
- if time_str == time_str.to_i.to_s
166
- date = [Time.new(time_str), Time.new(time_str.to_i + 1).yesterday]
167
- else
168
- date = Chronic.parse(time_str, guess: nil) || Chronic.parse(val, guess: nil)
169
- end
170
-
171
- date = date || []
172
-
173
- if date_pick == :start
174
- val = date.first
175
- elsif date_pick == :end
176
- val = date.last
177
- end
178
- end
179
- { key => { mongo_op => val } }
180
- end
181
-
182
- def build_searches!(ast, fields, command_types)
183
- ast.map! do |x|
184
- type = x[:nest_type]
185
- if type == :colon
186
- build_command(x, command_types)
187
- elsif type == :compare
188
- build_compare(x, command_types)
189
- elsif [:paren, :pipe, :minus].include?(type)
190
- build_searches!(x[:value], fields, command_types)
191
- x
192
- else
193
- build_search(x, fields, command_types)
194
- end
195
- end
196
- ast.flatten!
197
- end
198
-
199
- def build_tree!(ast)
200
- mongo_types = { paren: '$and', pipe: '$or', minus: '$nor' }
201
- ast.each do |node|
202
- next node unless node[:nest_type]
203
- build_tree!(node[:value])
204
- key = mongo_types[node[:nest_type]]
205
- if key == '$nor' && node[:value].count > 1
206
- node[key] = [{ '$and' => node[:value] }]
207
- else
208
- node[key] = node[:value]
209
- end
210
- node['$or'].map! { |x| x['$or'] || x }.flatten! if node['$or']
211
- node['$nor'].map! { |x| x['$or'] || x }.flatten! if node['$nor']
212
- node.delete(:nest_type)
213
- node.delete(:nest_op)
214
- node.delete(:value)
215
- node.delete(:type)
216
- end
217
- end
218
-
219
- def build_query(ast, fields, command_types)
220
- out = ast
221
- build_searches!(out, fields, command_types)
222
- build_tree!(out)
223
- out = {} if out == []
224
- out = out.first if out.count == 1
225
- out = { '$and' => out } if out.count > 1
226
- out
227
- end
228
- end
229
- end