chef-zero 4.2.3 → 4.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.
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