command_search 0.2.0 → 0.3.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: 1ab34234037969a83820730a8a209e1fe0ca29ed9a2b34f1f1c3f0a72b570b5c
4
- data.tar.gz: f3c4af7b842ae0a59057a2d700bb9c39b739c96fe18248377deecdc38c70aede
3
+ metadata.gz: 93226578c66261e1923eeb6f56647b3eed274b3fe0dad3e8f016780c34e46831
4
+ data.tar.gz: 5d73e80d4b7f8fbca8febe5dc548332d48cd25f46b7611fae6c075268fdda5b0
5
5
  SHA512:
6
- metadata.gz: d1e7ffc036d37d54217a78184a2c18705ad85529cad90e75851c6512d10a0214d22d083da7cf504eed6257bc28576c885a56dd383bc4e37cb39919ac1e8e665d
7
- data.tar.gz: d4ce49e9747f21e06ced4884b4a72f03c6c335ab9d20e91f7e6ebd985a3a9cbca11dc0cacf1422d0ced8f4d322b9b48f95327099769030fa820eade6da4f5d7c
6
+ metadata.gz: a25f5ac8e3b4ba433df88abc38551162532ba7e04e47744e44a0ba7dd107a95471490d0506e98746f1344a259f94a8b2ff229f3468a5078ad4ef97158901b948
7
+ data.tar.gz: b14a8926f623e7b1ca05e51e5c0fbf1a754012a4e84284166e1fbc29b8894bf47b2fd58841d397575c1859dab46f722a21e0317afff482ec2da40302faddcf19
@@ -18,7 +18,7 @@ module CommandSearch
18
18
 
19
19
  aliased_query = Aliaser.alias(query, aliases)
20
20
  tokens = Lexer.lex(aliased_query)
21
- parsed = Parser.parse(tokens)
21
+ parsed = Parser.parse!(tokens)
22
22
  dealiased = CommandDealiaser.dealias(parsed, command_fields)
23
23
  cleaned = CommandDealiaser.decompose_unaliasable(dealiased, command_fields)
24
24
  opted = Optimizer.optimize(cleaned)
@@ -8,32 +8,34 @@ module CommandSearch
8
8
  Regexp.new(head_border + Regexp.escape(str) + tail_border, 'i')
9
9
  end
10
10
 
11
- def opens_quote?(str)
12
- while str[/".*"/] || str[/'.*'/]
13
- mark = str[/["']/]
14
- str.sub(/#{mark}.*#{mark}/, '')
15
- end
16
- str[/"/] || str[/\B'/]
11
+ def quotes?(head, tail)
12
+ return true if head.count("'").odd? && tail.count("'").odd?
13
+ return true if head.count('"').odd? && tail.count('"').odd?
14
+ false
17
15
  end
18
16
 
19
17
  def alias_item(query, alias_key, alias_value)
20
18
  if alias_key.is_a?(Regexp)
21
19
  pattern = alias_key
20
+ elsif alias_key.is_a?(String)
21
+ pattern = build_regex(alias_key)
22
22
  else
23
- pattern = build_regex(alias_key.to_s)
23
+ return query
24
24
  end
25
25
  current_match = query[pattern]
26
26
  return query unless current_match
27
27
  offset = Regexp.last_match.offset(0)
28
28
  head = query[0...offset.first]
29
29
  tail = alias_item(query[offset.last..-1], alias_key, alias_value)
30
- if opens_quote?(head)
30
+ if quotes?(head, tail)
31
31
  replacement = current_match
32
32
  else
33
33
  if alias_value.is_a?(String)
34
34
  replacement = alias_value
35
35
  elsif alias_value.is_a?(Proc)
36
36
  replacement = alias_value.call(current_match).to_s
37
+ else
38
+ return query
37
39
  end
38
40
  end
39
41
  head + replacement + tail
@@ -23,9 +23,9 @@ module CommandSearch
23
23
  end
24
24
 
25
25
  def dealias(ast, aliases)
26
- ast.flat_map do |x|
26
+ ast.map! do |x|
27
27
  next x unless x[:nest_type]
28
- x[:value] = dealias(x[:value], aliases)
28
+ dealias(x[:value], aliases)
29
29
  next x unless [:colon, :compare].include?(x[:nest_type])
30
30
  x[:value] = dealias_values(x[:value], aliases)
31
31
  x
@@ -33,9 +33,9 @@ module CommandSearch
33
33
  end
34
34
 
35
35
  def decompose_unaliasable(ast, aliases)
36
- ast.flat_map do |x|
36
+ ast.map! do |x|
37
37
  next x unless x[:nest_type]
38
- x[:value] = decompose_unaliasable(x[:value], aliases)
38
+ decompose_unaliasable(x[:value], aliases)
39
39
  next x unless [:colon, :compare].include?(x[:nest_type])
40
40
  unnest_unaliased(x, aliases)
41
41
  end
@@ -8,29 +8,29 @@ module CommandSearch
8
8
  while i < input.length
9
9
  match = nil
10
10
  case input[i..-1]
11
- when /^\s+/
11
+ when /\A\s+/
12
12
  type = :space
13
- when /^"(.*?)"/
13
+ when /\A"(.*?)"/
14
14
  match = Regexp.last_match[1]
15
15
  type = :quoted_str
16
- when /^'(.*?)'/
16
+ when /\A'(.*?)'/
17
17
  match = Regexp.last_match[1]
18
18
  type = :quoted_str
19
- when /^\-?\d+(\.\d+)?(?=$|[\s"':|<>()])/
19
+ when /\A\-?\d+(\.\d+)?(?=$|[\s"':|<>()])/
20
20
  type = :number
21
- when /^-/
21
+ when /\A-/
22
22
  type = :minus
23
- when /^[^\s:"|<>()]+/
23
+ when /\A[^\s:"|<>()]+/
24
24
  type = :str
25
- when /^\|+/
25
+ when /\A\|+/
26
26
  type = :pipe
27
- when /^[()]/
27
+ when /\A[()]/
28
28
  type = :paren
29
- when /^:/
29
+ when /\A:/
30
30
  type = :colon
31
- when /^[<>]=?/
31
+ when /\A[<>]=?/
32
32
  type = :compare
33
- when /^./
33
+ when /\A./
34
34
  type = :str
35
35
  end
36
36
  match = match || Regexp.last_match[0]
@@ -10,7 +10,6 @@ module CommandSearch
10
10
  raw_cmd_type = [command_types[cmd]].flatten
11
11
  allow_existence_boolean = raw_cmd_type.include?(:allow_existence_boolean)
12
12
  cmd_type = (raw_cmd_type - [:allow_existence_boolean]).first
13
- return unless cmd_type
14
13
  if cmd_type == Boolean
15
14
  if cmd_search[/true/i]
16
15
  item[cmd]
@@ -25,8 +24,6 @@ module CommandSearch
25
24
  end
26
25
  elsif !item.key?(cmd)
27
26
  return false
28
- elsif val[1][:type] == :str
29
- item[cmd][/#{Regexp.escape(cmd_search)}/i]
30
27
  elsif val[1][:type] == :quoted_str
31
28
  regex = /\b#{Regexp.escape(cmd_search)}\b/
32
29
  if cmd_search[/(^\W)|(\W$)/]
@@ -5,7 +5,7 @@ module CommandSearch
5
5
  module_function
6
6
 
7
7
  def build_search(ast_node, fields)
8
- str = ast_node[:value]
8
+ str = ast_node[:value] || ''
9
9
  fields = [fields] unless fields.is_a?(Array)
10
10
  if ast_node[:type] == :quoted_str
11
11
  regex = /\b#{Regexp.escape(str)}\b/
@@ -15,9 +15,7 @@ module CommandSearch
15
15
  regex = Regexp.new(head_border + Regexp.escape(str) + tail_border)
16
16
  end
17
17
  else
18
- # TODO: This OR check is only needed due to mutable objects between subclasses of command_search,
19
- # and is only needed for outside use or benchmarking.
20
- regex = /#{Regexp.escape(str || '')}/i
18
+ regex = /#{Regexp.escape(str)}/i
21
19
  end
22
20
  if ast_node[:negate]
23
21
  forms = fields.map { |f| { f => { '$not' => regex } } }
@@ -33,17 +31,16 @@ module CommandSearch
33
31
  end
34
32
 
35
33
  def is_bool_str?(str)
36
- return true if str[/^true$|^false$/i]
34
+ return true if str[/\Atrue\Z|\Afalse\Z/i]
37
35
  false
38
36
  end
39
37
 
40
38
  def make_boolean(str)
41
- return true if str[/^true$/i]
39
+ return true if str[/\Atrue\Z/i]
42
40
  false
43
41
  end
44
42
 
45
43
  def build_command(ast_node, command_types)
46
- # aliasing will is done before ast gets to mongoer.rb
47
44
  (field_node, search_node) = ast_node[:value]
48
45
  key = field_node[:value]
49
46
  raw_type = command_types[key.to_sym]
@@ -60,8 +57,7 @@ module CommandSearch
60
57
  type = raw_type
61
58
  end
62
59
 
63
- if defined?(Boolean) && type == Boolean
64
- # val = make_boolean(raw_val)
60
+ if type == Boolean
65
61
  bool = make_boolean(raw_val)
66
62
  bool = !bool if field_node[:negate]
67
63
  val = [
@@ -100,7 +96,7 @@ module CommandSearch
100
96
  elsif [Numeric, Integer].include?(type)
101
97
  if raw_val == raw_val.to_i.to_s
102
98
  val = raw_val.to_i
103
- elsif raw_val.to_f != 0 || raw_val[/^[\.0]*0$/]
99
+ elsif raw_val.to_f != 0 || raw_val[/\A[\.0]*0\Z/]
104
100
  val = raw_val.to_f
105
101
  else
106
102
  val = raw_val
@@ -205,40 +201,41 @@ module CommandSearch
205
201
  { key => { mongo_op => val } }
206
202
  end
207
203
 
208
- def build_searches(ast, fields, command_types)
209
- ast.flat_map do |x|
204
+ def build_searches!(ast, fields, command_types)
205
+ ast.map! do |x|
210
206
  type = x[:nest_type]
211
207
  if type == :colon
212
208
  build_command(x, command_types)
213
209
  elsif type == :compare
214
210
  build_compare(x, command_types)
215
211
  elsif [:paren, :pipe, :minus].include?(type)
216
- x[:value] = build_searches(x[:value], fields, command_types)
212
+ build_searches!(x[:value], fields, command_types)
217
213
  x
218
214
  else
219
215
  build_search(x, fields)
220
216
  end
221
217
  end
218
+ ast.flatten!
222
219
  end
223
220
 
224
- def build_tree(ast)
225
- ast.flat_map do |x|
221
+ def build_tree!(ast)
222
+ mongo_types = { paren: '$and', pipe: '$or', minus: '$not' }
223
+ ast.each do |x|
226
224
  next x unless x[:nest_type]
227
- mongo_types = { paren: '$and', pipe: '$or', minus: '$not' }
225
+ build_tree!(x[:value])
228
226
  key = mongo_types[x[:nest_type]]
229
- { key => build_tree(x[:value]) }
227
+ x[key] = x[:value]
228
+ x.delete(:nest_type)
229
+ x.delete(:nest_op)
230
+ x.delete(:value)
231
+ x.delete(:type)
230
232
  end
231
233
  end
232
234
 
233
- def collapse_ors(ast)
234
- ast.flat_map do |x|
235
- ['$and', '$or', '$not'].map do |key|
236
- next unless x[key]
237
- x[key] = collapse_ors(x[key])
238
- end
239
- next x unless x['$or']
240
- val = x['$or'].flat_map { |kid| kid['$or'] || kid }
241
- { '$or' => val }
235
+ def collapse_ors!(ast)
236
+ ast.each do |x|
237
+ next unless x['$or']
238
+ x['$or'].map! { |kid| kid['$or'] || kid }.flatten!
242
239
  end
243
240
  end
244
241
 
@@ -257,12 +254,11 @@ module CommandSearch
257
254
  end
258
255
 
259
256
  def build_query(ast, fields, command_types = {})
260
- # Numbers are searched as strings unless part of a compare/command
261
257
  out = ast
262
258
  out = decompose_nots(out)
263
- out = build_searches(out, fields, command_types)
264
- out = build_tree(out)
265
- out = collapse_ors(out)
259
+ build_searches!(out, fields, command_types)
260
+ build_tree!(out)
261
+ collapse_ors!(out)
266
262
  out = {} if out == []
267
263
  out = out.first if out.count == 1
268
264
  out = { '$and' => out } if out.count > 1
@@ -2,18 +2,16 @@ module CommandSearch
2
2
  module Optimizer
3
3
  module_function
4
4
 
5
- def ands_and_ors(ast)
6
- ast.uniq.map do |node|
7
- next node unless node[:nest_type]
8
- next node if node[:nest_type] == :compare
9
- node[:value] = ands_and_ors(node[:value])
5
+ def ands_and_ors!(ast)
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
10
  node[:value] = node[:value].flat_map do |kid|
11
11
  next kid[:value] if kid[:nest_type] == :pipe
12
12
  kid
13
13
  end
14
- if node[:nest_type] == :pipe && node[:value].length == 1
15
- next node[:value].first
16
- end
14
+ node[:value].uniq!
17
15
  node
18
16
  end
19
17
  end
@@ -23,7 +21,6 @@ module CommandSearch
23
21
  next node unless node[:nest_type]
24
22
  node[:value] = negate_negate(node[:value])
25
23
  next [] if node[:value] == []
26
- next node if node[:value].count > 1
27
24
  type = node[:nest_type]
28
25
  child_type = node[:value].first[:nest_type]
29
26
  next node unless type == :minus && child_type == :minus
@@ -34,51 +31,30 @@ module CommandSearch
34
31
  def denest_parens(ast, parent_type = :root)
35
32
  ast.flat_map do |node|
36
33
  next node unless node[:nest_type]
37
-
38
34
  node[:value] = denest_parens(node[:value], node[:nest_type])
39
-
40
- valid_self = node[:nest_type] == :paren
41
- valid_parent = parent_type != :pipe
42
- valid_child = node[:value].count < 2
43
-
44
- next node[:value] if valid_self && valid_parent
45
- next node[:value] if valid_self && valid_child
35
+ # valid_self && (valid_parent || valid_child)
36
+ if node[:nest_type] == :paren && (parent_type != :pipe || node[:value].count < 2)
37
+ next node[:value]
38
+ end
46
39
  node
47
40
  end
48
41
  end
49
42
 
50
- def remove_empty_strings(ast)
51
- out = ast.flat_map do |node|
52
- next if node[:type] == :quoted_str && node[:value] == ''
53
- next node unless node[:nest_type]
54
- node[:value] = remove_empty_strings(node[:value])
55
- node
43
+ def remove_empty_strings!(ast)
44
+ ast.reject! do |node|
45
+ remove_empty_strings!(node[:value]) if node[:nest_type]
46
+ node[:type] == :quoted_str && node[:value] == ''
56
47
  end
57
- out.compact
58
48
  end
59
49
 
60
- def optimization_pass(ast)
61
- # '(a b)|(c d)' is the only current
62
- # situation where parens are needed.
63
- # 'a|(b|(c|d))' can be flattened by
64
- # repeated application of "ands_and_or"
65
- # and "denest_parens".
50
+ def optimize(ast)
66
51
  out = ast
67
52
  out = denest_parens(out)
53
+ remove_empty_strings!(out)
68
54
  out = negate_negate(out)
69
- out = ands_and_ors(out)
70
- out = remove_empty_strings(out)
55
+ ands_and_ors!(out)
56
+ out.uniq!
71
57
  out
72
58
  end
73
-
74
- def optimize(ast)
75
- out_a = optimization_pass(ast)
76
- out_b = optimization_pass(out_a)
77
- until out_a == out_b
78
- out_a = out_b
79
- out_b = optimization_pass(out_b)
80
- end
81
- out_b
82
- end
83
59
  end
84
60
  end
@@ -3,7 +3,6 @@ module CommandSearch
3
3
  module_function
4
4
 
5
5
  def parens_rindex(input)
6
- val_list = input.map { |x| x[:value] }
7
6
  open_i = input.rindex { |x| x[:value] == '(' && x[:type] == :paren}
8
7
  return unless open_i
9
8
  close_offset = input.drop(open_i).index { |x| x[:value] == ')' && x[:type] == :paren}
@@ -11,48 +10,41 @@ module CommandSearch
11
10
  [open_i, close_offset + open_i]
12
11
  end
13
12
 
14
- def group_parens(input)
15
- out = input
16
- while parens_rindex(out)
17
- (a, b) = parens_rindex(out)
18
- val = out[(a + 1)..(b - 1)]
19
- out[a..b] = { type: :nest, nest_type: :paren, value: val }
13
+ def group_parens!(input)
14
+ while parens_rindex(input)
15
+ (a, b) = parens_rindex(input)
16
+ val = input[(a + 1)..(b - 1)]
17
+ input[a..b] = { type: :nest, nest_type: :paren, value: val }
20
18
  end
21
- out
22
19
  end
23
20
 
24
21
  def cluster!(type, input, cluster_type = :binary)
25
22
  binary = (cluster_type == :binary)
26
- out = input
27
- out = out[:value] while out.is_a?(Hash)
28
- out.compact!
23
+ input.compact!
29
24
  # rindex (vs index) important for nested prefixes
30
- while (i = out.rindex { |x| x[:type] == type })
31
- val = [out[i + 1]]
32
- val.unshift(out[i - 1]) if binary && i > 0
25
+ while (i = input.rindex { |x| x[:type] == type })
26
+ val = [input[i + 1]]
27
+ val.unshift(input[i - 1]) if binary && i > 0
33
28
  front_offset = 0
34
29
  front_offset = 1 if binary && i > 0
35
- out[(i - front_offset)..(i + 1)] = {
30
+ input[(i - front_offset)..(i + 1)] = {
36
31
  type: :nest,
37
32
  nest_type: type,
38
- nest_op: out[i][:value],
33
+ nest_op: input[i][:value],
39
34
  value: val
40
35
  }
41
36
  end
42
- out.map! do |x|
43
- next x unless x[:type] == :nest
44
- x[:value] = cluster!(type, x[:value], cluster_type)
45
- x
37
+ input.each do |x|
38
+ cluster!(type, x[:value], cluster_type) if x[:type] == :nest
46
39
  end
47
40
  end
48
41
 
49
42
  def unchain!(types, input)
50
43
  i = 0
51
44
  while i < input.length - 2
52
- front = input[i][:type]
53
- mid = input[i + 1][:type]
54
- back = input[i + 2][:type]
55
- if types.include?(front) && !types.include?(mid) && types.include?(back)
45
+ left = input[i][:type]
46
+ right = input[i + 2][:type]
47
+ if types.include?(left) && types.include?(right)
56
48
  input.insert(i + 1, input[i + 1])
57
49
  end
58
50
  i += 1
@@ -60,7 +52,6 @@ module CommandSearch
60
52
  end
61
53
 
62
54
  def merge_strs(input, (x, y))
63
- return input if input.empty?
64
55
  if input[y] && input[y][:type] == :str
65
56
  values = input.map { |x| x[:value] }
66
57
  { type: :str, value: values.join() }
@@ -71,8 +62,6 @@ module CommandSearch
71
62
  end
72
63
 
73
64
  def clean_ununusable!(input)
74
- return unless input.any?
75
-
76
65
  i = 0
77
66
  while i < input.length
78
67
  next i += 1 unless input[i][:type] == :minus
@@ -97,31 +86,19 @@ module CommandSearch
97
86
  end
98
87
 
99
88
  def clean_ununused!(input)
100
- input.map! do |x|
101
- next if x[:type] == :paren && x[:value].is_a?(String)
102
- next if x[:nest_type] == :colon && x[:value].empty?
103
- if x[:nest_type] == :compare && x[:value].length < 2
104
- x = clean_ununused!(x[:value]).first
105
- end
106
- next x unless x && x[:type] == :nest
107
- x[:value] = clean_ununused!(x[:value])
108
- x
109
- end
110
- input.compact!
111
- input
89
+ input.reject! { |x| x[:type] == :paren && x[:value].is_a?(String) }
112
90
  end
113
91
 
114
- def parse(input)
115
- out = input
116
- clean_ununusable!(out)
117
- unchain!([:colon, :compare], out)
118
- out = group_parens(out)
119
- cluster!(:colon, out)
120
- cluster!(:compare, out)
121
- cluster!(:minus, out, :prefix)
122
- cluster!(:pipe, out)
123
- clean_ununused!(out)
124
- out
92
+ def parse!(input)
93
+ clean_ununusable!(input)
94
+ unchain!([:colon, :compare], input)
95
+ group_parens!(input)
96
+ cluster!(:colon, input)
97
+ cluster!(:compare, input)
98
+ cluster!(:minus, input, :prefix)
99
+ cluster!(:pipe, input)
100
+ clean_ununused!(input)
101
+ input
125
102
  end
126
103
  end
127
104
  end
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - zumbalogy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-08 00:00:00.000000000 Z
11
+ date: 2018-11-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chronic