command_search 0.8.2 → 0.9.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 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