chef-zero 4.2.3 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +201 -201
  3. data/README.md +155 -150
  4. data/Rakefile +31 -31
  5. data/bin/chef-zero +100 -100
  6. data/lib/chef_zero.rb +10 -7
  7. data/lib/chef_zero/chef_data/acl_path.rb +139 -139
  8. data/lib/chef_zero/chef_data/cookbook_data.rb +240 -240
  9. data/lib/chef_zero/chef_data/data_normalizer.rb +207 -207
  10. data/lib/chef_zero/chef_data/default_creator.rb +446 -446
  11. data/lib/chef_zero/data_store/data_already_exists_error.rb +29 -29
  12. data/lib/chef_zero/data_store/data_error.rb +31 -31
  13. data/lib/chef_zero/data_store/data_not_found_error.rb +28 -28
  14. data/lib/chef_zero/data_store/default_facade.rb +149 -149
  15. data/lib/chef_zero/data_store/interface_v1.rb +67 -67
  16. data/lib/chef_zero/data_store/interface_v2.rb +18 -18
  17. data/lib/chef_zero/data_store/memory_store.rb +33 -33
  18. data/lib/chef_zero/data_store/memory_store_v2.rb +155 -155
  19. data/lib/chef_zero/data_store/raw_file_store.rb +147 -147
  20. data/lib/chef_zero/data_store/v1_to_v2_adapter.rb +142 -142
  21. data/lib/chef_zero/data_store/v2_to_v1_adapter.rb +107 -107
  22. data/lib/chef_zero/endpoints/acl_endpoint.rb +38 -38
  23. data/lib/chef_zero/endpoints/acls_endpoint.rb +29 -29
  24. data/lib/chef_zero/endpoints/actor_endpoint.rb +94 -88
  25. data/lib/chef_zero/endpoints/actors_endpoint.rb +64 -64
  26. data/lib/chef_zero/endpoints/authenticate_user_endpoint.rb +31 -31
  27. data/lib/chef_zero/endpoints/container_endpoint.rb +22 -22
  28. data/lib/chef_zero/endpoints/containers_endpoint.rb +13 -13
  29. data/lib/chef_zero/endpoints/cookbook_endpoint.rb +39 -39
  30. data/lib/chef_zero/endpoints/cookbook_version_endpoint.rb +119 -119
  31. data/lib/chef_zero/endpoints/cookbooks_base.rb +65 -65
  32. data/lib/chef_zero/endpoints/cookbooks_endpoint.rb +19 -19
  33. data/lib/chef_zero/endpoints/data_bag_endpoint.rb +45 -45
  34. data/lib/chef_zero/endpoints/data_bag_item_endpoint.rb +25 -25
  35. data/lib/chef_zero/endpoints/data_bags_endpoint.rb +23 -23
  36. data/lib/chef_zero/endpoints/environment_cookbook_endpoint.rb +24 -24
  37. data/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb +123 -123
  38. data/lib/chef_zero/endpoints/environment_cookbooks_endpoint.rb +22 -22
  39. data/lib/chef_zero/endpoints/environment_endpoint.rb +33 -33
  40. data/lib/chef_zero/endpoints/environment_nodes_endpoint.rb +23 -23
  41. data/lib/chef_zero/endpoints/environment_recipes_endpoint.rb +22 -22
  42. data/lib/chef_zero/endpoints/environment_role_endpoint.rb +36 -36
  43. data/lib/chef_zero/endpoints/file_store_file_endpoint.rb +22 -22
  44. data/lib/chef_zero/endpoints/group_endpoint.rb +20 -20
  45. data/lib/chef_zero/endpoints/groups_endpoint.rb +13 -13
  46. data/lib/chef_zero/endpoints/license_endpoint.rb +25 -25
  47. data/lib/chef_zero/endpoints/node_endpoint.rb +17 -17
  48. data/lib/chef_zero/endpoints/node_identifiers_endpoint.rb +22 -0
  49. data/lib/chef_zero/endpoints/not_found_endpoint.rb +11 -11
  50. data/lib/chef_zero/endpoints/organization_association_request_endpoint.rb +22 -22
  51. data/lib/chef_zero/endpoints/organization_association_requests_endpoint.rb +30 -29
  52. data/lib/chef_zero/endpoints/organization_authenticate_user_endpoint.rb +26 -26
  53. data/lib/chef_zero/endpoints/organization_endpoint.rb +46 -41
  54. data/lib/chef_zero/endpoints/organization_user_base.rb +15 -0
  55. data/lib/chef_zero/endpoints/organization_user_endpoint.rb +26 -48
  56. data/lib/chef_zero/endpoints/organization_users_endpoint.rb +43 -14
  57. data/lib/chef_zero/endpoints/organization_validator_key_endpoint.rb +20 -20
  58. data/lib/chef_zero/endpoints/organizations_endpoint.rb +62 -55
  59. data/lib/chef_zero/endpoints/policies_endpoint.rb +151 -154
  60. data/lib/chef_zero/endpoints/principal_endpoint.rb +42 -42
  61. data/lib/chef_zero/endpoints/rest_list_endpoint.rb +42 -42
  62. data/lib/chef_zero/endpoints/rest_object_endpoint.rb +63 -63
  63. data/lib/chef_zero/endpoints/role_endpoint.rb +16 -16
  64. data/lib/chef_zero/endpoints/role_environments_endpoint.rb +14 -14
  65. data/lib/chef_zero/endpoints/sandbox_endpoint.rb +27 -27
  66. data/lib/chef_zero/endpoints/sandboxes_endpoint.rb +50 -50
  67. data/lib/chef_zero/endpoints/search_endpoint.rb +194 -192
  68. data/lib/chef_zero/endpoints/searches_endpoint.rb +18 -18
  69. data/lib/chef_zero/endpoints/server_api_version_endpoint.rb +14 -0
  70. data/lib/chef_zero/endpoints/system_recovery_endpoint.rb +30 -30
  71. data/lib/chef_zero/endpoints/user_association_request_endpoint.rb +40 -40
  72. data/lib/chef_zero/endpoints/user_association_requests_count_endpoint.rb +19 -19
  73. data/lib/chef_zero/endpoints/user_association_requests_endpoint.rb +19 -19
  74. data/lib/chef_zero/endpoints/user_organizations_endpoint.rb +22 -22
  75. data/lib/chef_zero/endpoints/version_endpoint.rb +12 -12
  76. data/lib/chef_zero/log.rb +7 -7
  77. data/lib/chef_zero/rest_base.rb +242 -214
  78. data/lib/chef_zero/rest_error_response.rb +11 -11
  79. data/lib/chef_zero/rest_request.rb +69 -65
  80. data/lib/chef_zero/rest_router.rb +45 -45
  81. data/lib/chef_zero/rspec.rb +308 -308
  82. data/lib/chef_zero/server.rb +642 -637
  83. data/lib/chef_zero/socketless_server_map.rb +92 -92
  84. data/lib/chef_zero/solr/query/binary_operator.rb +52 -52
  85. data/lib/chef_zero/solr/query/phrase.rb +23 -23
  86. data/lib/chef_zero/solr/query/range_query.rb +46 -46
  87. data/lib/chef_zero/solr/query/regexpable_query.rb +29 -29
  88. data/lib/chef_zero/solr/query/subquery.rb +37 -37
  89. data/lib/chef_zero/solr/query/term.rb +45 -45
  90. data/lib/chef_zero/solr/query/unary_operator.rb +43 -43
  91. data/lib/chef_zero/solr/solr_doc.rb +53 -53
  92. data/lib/chef_zero/solr/solr_parser.rb +203 -203
  93. data/lib/chef_zero/version.rb +3 -3
  94. data/spec/run_oc_pedant.rb +63 -56
  95. data/spec/search_spec.rb +32 -32
  96. data/spec/server_spec.rb +92 -91
  97. data/spec/socketless_server_map_spec.rb +76 -76
  98. data/spec/support/oc_pedant.rb +132 -134
  99. data/spec/support/stickywicket.pem +27 -27
  100. metadata +10 -15
  101. data/spec/run_pedant.rb +0 -103
  102. data/spec/support/pedant.rb +0 -129
@@ -1,37 +1,37 @@
1
- module ChefZero
2
- module Solr
3
- module Query
4
- class Subquery
5
- def initialize(subquery)
6
- @subquery = subquery
7
- end
8
-
9
- attr_reader :subquery
10
-
11
- def to_s
12
- "(#{subquery})"
13
- end
14
-
15
- def literal_string
16
- subquery.literal_string
17
- end
18
-
19
- def regexp
20
- subquery.regexp
21
- end
22
-
23
- def regexp_string
24
- subquery.regexp_string
25
- end
26
-
27
- def matches_doc?(doc)
28
- subquery.matches_doc?(doc)
29
- end
30
-
31
- def matches_values?(values)
32
- subquery.matches_values?(values)
33
- end
34
- end
35
- end
36
- end
37
- end
1
+ module ChefZero
2
+ module Solr
3
+ module Query
4
+ class Subquery
5
+ def initialize(subquery)
6
+ @subquery = subquery
7
+ end
8
+
9
+ attr_reader :subquery
10
+
11
+ def to_s
12
+ "(#{subquery})"
13
+ end
14
+
15
+ def literal_string
16
+ subquery.literal_string
17
+ end
18
+
19
+ def regexp
20
+ subquery.regexp
21
+ end
22
+
23
+ def regexp_string
24
+ subquery.regexp_string
25
+ end
26
+
27
+ def matches_doc?(doc)
28
+ subquery.matches_doc?(doc)
29
+ end
30
+
31
+ def matches_values?(values)
32
+ subquery.matches_values?(values)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,45 +1,45 @@
1
- require 'chef_zero/solr/query/regexpable_query'
2
-
3
- module ChefZero
4
- module Solr
5
- module Query
6
- class Term < RegexpableQuery
7
- def initialize(term)
8
- # Get rid of escape characters, turn * and ? into .* and . for regex, and
9
- # escape everything that needs escaping
10
- literal_string = ""
11
- regexp_string = ""
12
- index = 0
13
- while index < term.length
14
- if term[index] == '*'
15
- regexp_string << "#{WORD_CHARACTER}*"
16
- literal_string = nil
17
- index += 1
18
- elsif term[index] == '?'
19
- regexp_string << WORD_CHARACTER
20
- literal_string = nil
21
- index += 1
22
- elsif term[index] == '~'
23
- raise "~ unsupported"
24
- else
25
- if term[index] == '\\'
26
- index = index+1
27
- if index >= term.length
28
- raise "Backslash at end of string '#{term}'"
29
- end
30
- end
31
- literal_string << term[index] if literal_string
32
- regexp_string << Regexp.escape(term[index])
33
- index += 1
34
- end
35
- end
36
- super(regexp_string, literal_string)
37
- end
38
-
39
- def to_s
40
- "Term(#{regexp_string})"
41
- end
42
- end
43
- end
44
- end
45
- end
1
+ require 'chef_zero/solr/query/regexpable_query'
2
+
3
+ module ChefZero
4
+ module Solr
5
+ module Query
6
+ class Term < RegexpableQuery
7
+ def initialize(term)
8
+ # Get rid of escape characters, turn * and ? into .* and . for regex, and
9
+ # escape everything that needs escaping
10
+ literal_string = ""
11
+ regexp_string = ""
12
+ index = 0
13
+ while index < term.length
14
+ if term[index] == '*'
15
+ regexp_string << "#{WORD_CHARACTER}*"
16
+ literal_string = nil
17
+ index += 1
18
+ elsif term[index] == '?'
19
+ regexp_string << WORD_CHARACTER
20
+ literal_string = nil
21
+ index += 1
22
+ elsif term[index] == '~'
23
+ raise "~ unsupported"
24
+ else
25
+ if term[index] == '\\'
26
+ index = index+1
27
+ if index >= term.length
28
+ raise "Backslash at end of string '#{term}'"
29
+ end
30
+ end
31
+ literal_string << term[index] if literal_string
32
+ regexp_string << Regexp.escape(term[index])
33
+ index += 1
34
+ end
35
+ end
36
+ super(regexp_string, literal_string)
37
+ end
38
+
39
+ def to_s
40
+ "Term(#{regexp_string})"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,43 +1,43 @@
1
- module ChefZero
2
- module Solr
3
- module Query
4
- class UnaryOperator
5
- def initialize(operator, operand)
6
- @operator = operator
7
- @operand = operand
8
- end
9
-
10
- def to_s
11
- "#{operator} #{operand}"
12
- end
13
-
14
- attr_reader :operator
15
- attr_reader :operand
16
-
17
- def matches_doc?(doc)
18
- case @operator
19
- when '-'
20
- when 'NOT'
21
- !operand.matches_doc?(doc)
22
- when '+'
23
- # TODO This operator uses relevance to eliminate other, unrelated
24
- # expressions. +a OR b means "if it has b but not a, don't return it"
25
- raise "+ not supported yet, because it is hard."
26
- end
27
- end
28
-
29
- def matches_values?(values)
30
- case @operator
31
- when '-'
32
- when 'NOT'
33
- !operand.matches_values?(values)
34
- when '+'
35
- # TODO This operator uses relevance to eliminate other, unrelated
36
- # expressions. +a OR b means "if it has b but not a, don't return it"
37
- raise "+ not supported yet, because it is hard."
38
- end
39
- end
40
- end
41
- end
42
- end
43
- end
1
+ module ChefZero
2
+ module Solr
3
+ module Query
4
+ class UnaryOperator
5
+ def initialize(operator, operand)
6
+ @operator = operator
7
+ @operand = operand
8
+ end
9
+
10
+ def to_s
11
+ "#{operator} #{operand}"
12
+ end
13
+
14
+ attr_reader :operator
15
+ attr_reader :operand
16
+
17
+ def matches_doc?(doc)
18
+ case @operator
19
+ when '-'
20
+ when 'NOT'
21
+ !operand.matches_doc?(doc)
22
+ when '+'
23
+ # TODO This operator uses relevance to eliminate other, unrelated
24
+ # expressions. +a OR b means "if it has b but not a, don't return it"
25
+ raise "+ not supported yet, because it is hard."
26
+ end
27
+ end
28
+
29
+ def matches_values?(values)
30
+ case @operator
31
+ when '-'
32
+ when 'NOT'
33
+ !operand.matches_values?(values)
34
+ when '+'
35
+ # TODO This operator uses relevance to eliminate other, unrelated
36
+ # expressions. +a OR b means "if it has b but not a, don't return it"
37
+ raise "+ not supported yet, because it is hard."
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,53 +1,53 @@
1
- module ChefZero
2
- module Solr
3
- # This does what expander does, flattening the json doc into keys and values
4
- # so that solr can search them.
5
- class SolrDoc
6
- def initialize(json, id)
7
- @json = json
8
- @id = id
9
- end
10
-
11
- def [](key)
12
- matching_values { |match_key| match_key == key }
13
- end
14
-
15
- def matching_values(&block)
16
- result = []
17
- key_values(nil, @json) do |key, value|
18
- if block.call(key)
19
- result << value.to_s
20
- end
21
- end
22
- # Handle manufactured value(s)
23
- if block.call('X_CHEF_id_CHEF_X')
24
- result << @id.to_s
25
- end
26
-
27
- result.uniq
28
- end
29
-
30
- private
31
-
32
- def key_values(key_so_far, value, &block)
33
- if value.is_a?(Hash)
34
- value.each_pair do |child_key, child_value|
35
- block.call(child_key, child_value.to_s)
36
- if key_so_far
37
- new_key = "#{key_so_far}_#{child_key}"
38
- key_values(new_key, child_value, &block)
39
- else
40
- key_values(child_key, child_value, &block) if child_value.is_a?(Hash) || child_value.is_a?(Array)
41
- end
42
- end
43
- elsif value.is_a?(Array)
44
- value.each do |child_value|
45
- key_values(key_so_far, child_value, &block)
46
- end
47
- else
48
- block.call(key_so_far || 'text', value.to_s)
49
- end
50
- end
51
- end
52
- end
53
- end
1
+ module ChefZero
2
+ module Solr
3
+ # This does what expander does, flattening the json doc into keys and values
4
+ # so that solr can search them.
5
+ class SolrDoc
6
+ def initialize(json, id)
7
+ @json = json
8
+ @id = id
9
+ end
10
+
11
+ def [](key)
12
+ matching_values { |match_key| match_key == key }
13
+ end
14
+
15
+ def matching_values(&block)
16
+ result = []
17
+ key_values(nil, @json) do |key, value|
18
+ if block.call(key)
19
+ result << value.to_s
20
+ end
21
+ end
22
+ # Handle manufactured value(s)
23
+ if block.call('X_CHEF_id_CHEF_X')
24
+ result << @id.to_s
25
+ end
26
+
27
+ result.uniq
28
+ end
29
+
30
+ private
31
+
32
+ def key_values(key_so_far, value, &block)
33
+ if value.is_a?(Hash)
34
+ value.each_pair do |child_key, child_value|
35
+ block.call(child_key, child_value.to_s)
36
+ if key_so_far
37
+ new_key = "#{key_so_far}_#{child_key}"
38
+ key_values(new_key, child_value, &block)
39
+ else
40
+ key_values(child_key, child_value, &block) if child_value.is_a?(Hash) || child_value.is_a?(Array)
41
+ end
42
+ end
43
+ elsif value.is_a?(Array)
44
+ value.each do |child_value|
45
+ key_values(key_so_far, child_value, &block)
46
+ end
47
+ else
48
+ block.call(key_so_far || 'text', value.to_s)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,203 +1,203 @@
1
- require 'chef_zero/solr/query/binary_operator'
2
- require 'chef_zero/solr/query/unary_operator'
3
- require 'chef_zero/solr/query/term'
4
- require 'chef_zero/solr/query/phrase'
5
- require 'chef_zero/solr/query/range_query'
6
- require 'chef_zero/solr/query/subquery'
7
-
8
- module ChefZero
9
- module Solr
10
- class SolrParser
11
- def initialize(query_string)
12
- @query_string = query_string
13
- @index = 0
14
- end
15
-
16
- def parse
17
- read_expression
18
- end
19
-
20
- #
21
- # Tokenization
22
- #
23
- def peek_token
24
- @next_token ||= parse_token
25
- end
26
-
27
- def next_token
28
- result = peek_token
29
- @next_token = nil
30
- result
31
- end
32
-
33
- def parse_token
34
- # Skip whitespace
35
- skip_whitespace
36
- return nil if eof?
37
-
38
- # Operators
39
- operator = peek_operator_token
40
- if operator
41
- @index+=operator.length
42
- operator
43
- else
44
- # Everything that isn't whitespace or an operator, is part of a term
45
- # (characters plus backslashed escaped characters)
46
- start_index = @index
47
- begin
48
- if @query_string[@index] == '\\'
49
- @index+=1
50
- end
51
- @index+=1 if !eof?
52
- end while !eof? && peek_term_token
53
- @query_string[start_index..@index-1]
54
- end
55
- end
56
-
57
- def skip_whitespace
58
- if @query_string[@index] =~ /\s/
59
- whitespace = /\s+/.match(@query_string, @index) || peek
60
- @index += whitespace[0].length
61
- end
62
- end
63
-
64
- def peek_term_token
65
- return nil if @query_string[@index] =~ /\s/
66
- op = peek_operator_token
67
- return !op || op == '-'
68
- end
69
-
70
- def peek_operator_token
71
- if ['"', '+', '-', '!', '(', ')', '{', '}', '[', ']', '^', ':'].include?(@query_string[@index])
72
- return @query_string[@index]
73
- else
74
- result = @query_string[@index..@index+1]
75
- if ['&&', '||'].include?(result)
76
- return result
77
- end
78
- end
79
- nil
80
- end
81
-
82
- def eof?
83
- !@next_token && @index >= @query_string.length
84
- end
85
-
86
- # Parse tree creation
87
- def read_expression
88
- result = read_single_expression
89
- # Expression is over when we hit a close paren or eof
90
- # (peek_token has the side effect of skipping whitespace for us, so we
91
- # really know if we're at eof or not)
92
- until peek_token == ')' || eof?
93
- operator = peek_token
94
- if binary_operator?(operator)
95
- next_token
96
- else
97
- # If 2 terms are next to each other, the default operator is OR
98
- operator = 'OR'
99
- end
100
- next_expression = read_single_expression
101
-
102
- # Build the operator, taking precedence into account
103
- if result.is_a?(Query::BinaryOperator) &&
104
- binary_operator_precedence(operator) > binary_operator_precedence(result.operator)
105
- # a+b*c -> a+(b*c)
106
- new_right = Query::BinaryOperator.new(result.right, operator, next_expression)
107
- result = Query::BinaryOperator.new(result.left, result.operator, new_right)
108
- else
109
- # a*b+c -> (a*b)+c
110
- result = Query::BinaryOperator.new(result, operator, next_expression)
111
- end
112
- end
113
- result
114
- end
115
-
116
- def parse_error(token, str)
117
- raise "Error on token '#{token}' at #{@index} of '#{@query_string}': #{str}"
118
- end
119
-
120
- def read_single_expression
121
- token = next_token
122
- # If EOF, we have a problem Houston
123
- if !token
124
- parse_error(nil, "Expected expression!")
125
-
126
- # If it's an unary operand, build that
127
- elsif unary_operator?(token)
128
- operand = read_single_expression
129
- # TODO We rely on all unary operators having higher precedence than all
130
- # binary operators. Check if this is the case.
131
- Query::UnaryOperator.new(token, operand)
132
-
133
- # If it's the start of a phrase, read the terms in the phrase
134
- elsif token == '"'
135
- # Read terms until close "
136
- phrase_terms = []
137
- until (term = next_token) == '"'
138
- phrase_terms << Query::Term.new(term)
139
- end
140
- Query::Phrase.new(phrase_terms)
141
-
142
- # If it's the start of a range query, build that
143
- elsif token == '{' || token == '['
144
- left = next_token
145
- parse_error(left, "Expected left term in range query") if !left
146
- to = next_token
147
- parse_error(left, "Expected TO in range query") if to != "TO"
148
- right = next_token
149
- parse_error(right, "Expected left term in range query") if !right
150
- end_range = next_token
151
- parse_error(right, "Expected end range '#{end_range}") if !['}', ']'].include?(end_range)
152
- Query::RangeQuery.new(left, right, token == '[', end_range == ']')
153
-
154
- elsif token == '('
155
- subquery = read_expression
156
- close_paren = next_token
157
- parse_error(close_paren, "Expected ')'") if close_paren != ')'
158
- Query::Subquery.new(subquery)
159
-
160
- # If it's the end of a closure, raise an exception
161
- elsif ['}',']',')'].include?(token)
162
- parse_error(token, "Unexpected end paren")
163
-
164
- # If it's a binary operator, raise an exception
165
- elsif binary_operator?(token)
166
- parse_error(token, "Unexpected binary operator")
167
-
168
- # Otherwise it's a term.
169
- else
170
- term = Query::Term.new(token)
171
- if peek_token == ':'
172
- Query::BinaryOperator.new(term, next_token, read_single_expression)
173
- else
174
- term
175
- end
176
- end
177
- end
178
-
179
- def unary_operator?(token)
180
- [ 'NOT', '+', '-' ].include?(token)
181
- end
182
-
183
- def binary_operator?(token)
184
- [ 'AND', 'OR', '^', ':'].include?(token)
185
- end
186
-
187
- def binary_operator_precedence(token)
188
- case token
189
- when '^'
190
- 4
191
- when ':'
192
- 3
193
- when 'AND'
194
- 2
195
- when 'OR'
196
- 1
197
- end
198
- end
199
-
200
- DEFAULT_FIELD = 'text'
201
- end
202
- end
203
- end
1
+ require 'chef_zero/solr/query/binary_operator'
2
+ require 'chef_zero/solr/query/unary_operator'
3
+ require 'chef_zero/solr/query/term'
4
+ require 'chef_zero/solr/query/phrase'
5
+ require 'chef_zero/solr/query/range_query'
6
+ require 'chef_zero/solr/query/subquery'
7
+
8
+ module ChefZero
9
+ module Solr
10
+ class SolrParser
11
+ def initialize(query_string)
12
+ @query_string = query_string
13
+ @index = 0
14
+ end
15
+
16
+ def parse
17
+ read_expression
18
+ end
19
+
20
+ #
21
+ # Tokenization
22
+ #
23
+ def peek_token
24
+ @next_token ||= parse_token
25
+ end
26
+
27
+ def next_token
28
+ result = peek_token
29
+ @next_token = nil
30
+ result
31
+ end
32
+
33
+ def parse_token
34
+ # Skip whitespace
35
+ skip_whitespace
36
+ return nil if eof?
37
+
38
+ # Operators
39
+ operator = peek_operator_token
40
+ if operator
41
+ @index+=operator.length
42
+ operator
43
+ else
44
+ # Everything that isn't whitespace or an operator, is part of a term
45
+ # (characters plus backslashed escaped characters)
46
+ start_index = @index
47
+ begin
48
+ if @query_string[@index] == '\\'
49
+ @index+=1
50
+ end
51
+ @index+=1 if !eof?
52
+ end while !eof? && peek_term_token
53
+ @query_string[start_index..@index-1]
54
+ end
55
+ end
56
+
57
+ def skip_whitespace
58
+ if @query_string[@index] =~ /\s/
59
+ whitespace = /\s+/.match(@query_string, @index) || peek
60
+ @index += whitespace[0].length
61
+ end
62
+ end
63
+
64
+ def peek_term_token
65
+ return nil if @query_string[@index] =~ /\s/
66
+ op = peek_operator_token
67
+ return !op || op == '-'
68
+ end
69
+
70
+ def peek_operator_token
71
+ if ['"', '+', '-', '!', '(', ')', '{', '}', '[', ']', '^', ':'].include?(@query_string[@index])
72
+ return @query_string[@index]
73
+ else
74
+ result = @query_string[@index..@index+1]
75
+ if ['&&', '||'].include?(result)
76
+ return result
77
+ end
78
+ end
79
+ nil
80
+ end
81
+
82
+ def eof?
83
+ !@next_token && @index >= @query_string.length
84
+ end
85
+
86
+ # Parse tree creation
87
+ def read_expression
88
+ result = read_single_expression
89
+ # Expression is over when we hit a close paren or eof
90
+ # (peek_token has the side effect of skipping whitespace for us, so we
91
+ # really know if we're at eof or not)
92
+ until peek_token == ')' || eof?
93
+ operator = peek_token
94
+ if binary_operator?(operator)
95
+ next_token
96
+ else
97
+ # If 2 terms are next to each other, the default operator is OR
98
+ operator = 'OR'
99
+ end
100
+ next_expression = read_single_expression
101
+
102
+ # Build the operator, taking precedence into account
103
+ if result.is_a?(Query::BinaryOperator) &&
104
+ binary_operator_precedence(operator) > binary_operator_precedence(result.operator)
105
+ # a+b*c -> a+(b*c)
106
+ new_right = Query::BinaryOperator.new(result.right, operator, next_expression)
107
+ result = Query::BinaryOperator.new(result.left, result.operator, new_right)
108
+ else
109
+ # a*b+c -> (a*b)+c
110
+ result = Query::BinaryOperator.new(result, operator, next_expression)
111
+ end
112
+ end
113
+ result
114
+ end
115
+
116
+ def parse_error(token, str)
117
+ raise "Error on token '#{token}' at #{@index} of '#{@query_string}': #{str}"
118
+ end
119
+
120
+ def read_single_expression
121
+ token = next_token
122
+ # If EOF, we have a problem Houston
123
+ if !token
124
+ parse_error(nil, "Expected expression!")
125
+
126
+ # If it's an unary operand, build that
127
+ elsif unary_operator?(token)
128
+ operand = read_single_expression
129
+ # TODO We rely on all unary operators having higher precedence than all
130
+ # binary operators. Check if this is the case.
131
+ Query::UnaryOperator.new(token, operand)
132
+
133
+ # If it's the start of a phrase, read the terms in the phrase
134
+ elsif token == '"'
135
+ # Read terms until close "
136
+ phrase_terms = []
137
+ until (term = next_token) == '"'
138
+ phrase_terms << Query::Term.new(term)
139
+ end
140
+ Query::Phrase.new(phrase_terms)
141
+
142
+ # If it's the start of a range query, build that
143
+ elsif token == '{' || token == '['
144
+ left = next_token
145
+ parse_error(left, "Expected left term in range query") if !left
146
+ to = next_token
147
+ parse_error(left, "Expected TO in range query") if to != "TO"
148
+ right = next_token
149
+ parse_error(right, "Expected left term in range query") if !right
150
+ end_range = next_token
151
+ parse_error(right, "Expected end range '#{end_range}") if !['}', ']'].include?(end_range)
152
+ Query::RangeQuery.new(left, right, token == '[', end_range == ']')
153
+
154
+ elsif token == '('
155
+ subquery = read_expression
156
+ close_paren = next_token
157
+ parse_error(close_paren, "Expected ')'") if close_paren != ')'
158
+ Query::Subquery.new(subquery)
159
+
160
+ # If it's the end of a closure, raise an exception
161
+ elsif ['}',']',')'].include?(token)
162
+ parse_error(token, "Unexpected end paren")
163
+
164
+ # If it's a binary operator, raise an exception
165
+ elsif binary_operator?(token)
166
+ parse_error(token, "Unexpected binary operator")
167
+
168
+ # Otherwise it's a term.
169
+ else
170
+ term = Query::Term.new(token)
171
+ if peek_token == ':'
172
+ Query::BinaryOperator.new(term, next_token, read_single_expression)
173
+ else
174
+ term
175
+ end
176
+ end
177
+ end
178
+
179
+ def unary_operator?(token)
180
+ [ 'NOT', '+', '-' ].include?(token)
181
+ end
182
+
183
+ def binary_operator?(token)
184
+ [ 'AND', 'OR', '^', ':'].include?(token)
185
+ end
186
+
187
+ def binary_operator_precedence(token)
188
+ case token
189
+ when '^'
190
+ 4
191
+ when ':'
192
+ 3
193
+ when 'AND'
194
+ 2
195
+ when 'OR'
196
+ 1
197
+ end
198
+ end
199
+
200
+ DEFAULT_FIELD = 'text'
201
+ end
202
+ end
203
+ end