gmail_search_syntax 0.1.0 → 0.1.2
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 +4 -4
- data/GMAIL_BEHAVIOR_COMPARISON.md +166 -0
- data/GMAIL_COMPATIBILITY_COMPLETE.md +236 -0
- data/IMPLEMENTATION_NOTES.md +174 -0
- data/README.md +2 -2
- data/examples/escaped_quotes_demo.rb +152 -0
- data/examples/gmail_comparison_demo.rb +82 -0
- data/examples/text_vs_substring_demo.rb +93 -0
- data/lib/gmail_search_syntax/ast.rb +14 -2
- data/lib/gmail_search_syntax/parser.rb +45 -27
- data/lib/gmail_search_syntax/sql_visitor.rb +22 -5
- data/lib/gmail_search_syntax/tokenizer.rb +47 -12
- data/lib/gmail_search_syntax/version.rb +1 -1
- data/test/gmail_search_syntax_test.rb +246 -186
- data/test/sql_visitor_test.rb +44 -1
- data/test/tokenizer_test.rb +204 -118
- metadata +7 -1
@@ -3,71 +3,91 @@ require "test_helper"
|
|
3
3
|
class GmailSearchSyntaxTest < Minitest::Test
|
4
4
|
include GmailSearchSyntax::AST
|
5
5
|
|
6
|
+
def assert_operator(expected_properties, actual_operator)
|
7
|
+
assert_instance_of Operator, actual_operator, "Expected Operator, got #{actual_operator.class}"
|
8
|
+
expected_properties.each do |property, expected_value|
|
9
|
+
actual_value = actual_operator.public_send(property)
|
10
|
+
if property == :operands && expected_value.is_a?(Array)
|
11
|
+
assert_equal expected_value.length, actual_value.length, "Expected #{expected_value.length} operands, got #{actual_value.length}"
|
12
|
+
expected_value.each_with_index do |expected_operand, index|
|
13
|
+
if expected_operand.is_a?(Hash)
|
14
|
+
if expected_operand.key?(:name) && expected_operand.key?(:value)
|
15
|
+
# This is an Operator specification
|
16
|
+
assert_operator(expected_operand, actual_value[index])
|
17
|
+
elsif expected_operand.key?(:value)
|
18
|
+
# This is a StringToken specification
|
19
|
+
assert_string_token(expected_operand, actual_value[index])
|
20
|
+
else
|
21
|
+
# Generic node assertion
|
22
|
+
assert_equal expected_operand, actual_value[index], "Operand #{index}: expected #{expected_operand.inspect}, got #{actual_value[index].inspect}"
|
23
|
+
end
|
24
|
+
else
|
25
|
+
assert_equal expected_operand, actual_value[index], "Operand #{index}: expected #{expected_operand.inspect}, got #{actual_value[index].inspect}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
elsif expected_value.is_a?(Class)
|
29
|
+
assert_instance_of expected_value, actual_value, "Operator: expected #{property} to be instance of #{expected_value}, got #{actual_value.class}"
|
30
|
+
else
|
31
|
+
assert_equal expected_value, actual_value, "Operator: expected #{property} to be #{expected_value.inspect}, got #{actual_value.inspect}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def assert_string_token(expected_properties, actual_string_token)
|
37
|
+
assert_instance_of StringToken, actual_string_token, "Expected StringToken, got #{actual_string_token.class}"
|
38
|
+
expected_properties.each do |property, expected_value|
|
39
|
+
actual_value = actual_string_token.public_send(property)
|
40
|
+
assert_equal expected_value, actual_value, "StringToken: expected #{property} to be #{expected_value.inspect}, got #{actual_value.inspect}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
6
44
|
def test_version
|
7
|
-
|
45
|
+
assert GmailSearchSyntax::VERSION
|
8
46
|
end
|
9
47
|
|
10
48
|
def test_simple_from_operator
|
11
49
|
ast = GmailSearchSyntax.parse!("from:amy@example.com")
|
12
|
-
|
13
|
-
assert_equal "from", ast.name
|
14
|
-
assert_equal "amy@example.com", ast.value
|
50
|
+
assert_operator({name: "from", value: "amy@example.com"}, ast)
|
15
51
|
end
|
16
52
|
|
17
53
|
def test_from_me
|
18
54
|
ast = GmailSearchSyntax.parse!("from:me")
|
19
|
-
|
20
|
-
assert_equal "from", ast.name
|
21
|
-
assert_equal "me", ast.value
|
55
|
+
assert_operator({name: "from", value: "me"}, ast)
|
22
56
|
end
|
23
57
|
|
24
58
|
def test_to_operator
|
25
59
|
ast = GmailSearchSyntax.parse!("to:john@example.com")
|
26
|
-
|
27
|
-
assert_equal "to", ast.name
|
28
|
-
assert_equal "john@example.com", ast.value
|
60
|
+
assert_operator({name: "to", value: "john@example.com"}, ast)
|
29
61
|
end
|
30
62
|
|
31
63
|
def test_subject_with_single_word
|
32
64
|
ast = GmailSearchSyntax.parse!("subject:dinner")
|
33
|
-
|
34
|
-
assert_equal "subject", ast.name
|
35
|
-
assert_equal "dinner", ast.value
|
65
|
+
assert_operator({name: "subject", value: "dinner"}, ast)
|
36
66
|
end
|
37
67
|
|
38
68
|
def test_subject_with_quoted_phrase
|
39
69
|
ast = GmailSearchSyntax.parse!('subject:"anniversary party"')
|
40
|
-
|
41
|
-
assert_equal "subject", ast.name
|
42
|
-
assert_equal "anniversary party", ast.value
|
70
|
+
assert_operator({name: "subject", value: "anniversary party"}, ast)
|
43
71
|
end
|
44
72
|
|
45
73
|
def test_after_date
|
46
74
|
ast = GmailSearchSyntax.parse!("after:2004/04/16")
|
47
|
-
|
48
|
-
assert_equal "after", ast.name
|
49
|
-
assert_equal "2004/04/16", ast.value
|
75
|
+
assert_operator({name: "after", value: "2004/04/16"}, ast)
|
50
76
|
end
|
51
77
|
|
52
78
|
def test_before_date
|
53
79
|
ast = GmailSearchSyntax.parse!("before:04/18/2004")
|
54
|
-
|
55
|
-
assert_equal "before", ast.name
|
56
|
-
assert_equal "04/18/2004", ast.value
|
80
|
+
assert_operator({name: "before", value: "04/18/2004"}, ast)
|
57
81
|
end
|
58
82
|
|
59
83
|
def test_older_than_relative
|
60
84
|
ast = GmailSearchSyntax.parse!("older_than:1y")
|
61
|
-
|
62
|
-
assert_equal "older_than", ast.name
|
63
|
-
assert_equal "1y", ast.value
|
85
|
+
assert_operator({name: "older_than", value: "1y"}, ast)
|
64
86
|
end
|
65
87
|
|
66
88
|
def test_newer_than_relative
|
67
89
|
ast = GmailSearchSyntax.parse!("newer_than:2d")
|
68
|
-
|
69
|
-
assert_equal "newer_than", ast.name
|
70
|
-
assert_equal "2d", ast.value
|
90
|
+
assert_operator({name: "newer_than", value: "2d"}, ast)
|
71
91
|
end
|
72
92
|
|
73
93
|
def test_or_operator_with_from
|
@@ -75,13 +95,8 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
75
95
|
assert_instance_of Or, ast
|
76
96
|
|
77
97
|
assert_equal 2, ast.operands.length
|
78
|
-
|
79
|
-
|
80
|
-
assert_equal "amy", ast.operands[0].value
|
81
|
-
|
82
|
-
assert_instance_of Operator, ast.operands[1]
|
83
|
-
assert_equal "from", ast.operands[1].name
|
84
|
-
assert_equal "david", ast.operands[1].value
|
98
|
+
assert_operator({name: "from", value: "amy"}, ast.operands[0])
|
99
|
+
assert_operator({name: "from", value: "david"}, ast.operands[1])
|
85
100
|
end
|
86
101
|
|
87
102
|
def test_braces_as_or
|
@@ -89,13 +104,8 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
89
104
|
assert_instance_of Or, ast
|
90
105
|
|
91
106
|
assert_equal 2, ast.operands.length
|
92
|
-
|
93
|
-
|
94
|
-
assert_equal "amy", ast.operands[0].value
|
95
|
-
|
96
|
-
assert_instance_of Operator, ast.operands[1]
|
97
|
-
assert_equal "from", ast.operands[1].name
|
98
|
-
assert_equal "david", ast.operands[1].value
|
107
|
+
assert_operator({name: "from", value: "amy"}, ast.operands[0])
|
108
|
+
assert_operator({name: "from", value: "david"}, ast.operands[1])
|
99
109
|
end
|
100
110
|
|
101
111
|
def test_and_operator
|
@@ -103,13 +113,8 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
103
113
|
assert_instance_of And, ast
|
104
114
|
|
105
115
|
assert_equal 2, ast.operands.length
|
106
|
-
|
107
|
-
|
108
|
-
assert_equal "amy", ast.operands[0].value
|
109
|
-
|
110
|
-
assert_instance_of Operator, ast.operands[1]
|
111
|
-
assert_equal "to", ast.operands[1].name
|
112
|
-
assert_equal "david", ast.operands[1].value
|
116
|
+
assert_operator({name: "from", value: "amy"}, ast.operands[0])
|
117
|
+
assert_operator({name: "to", value: "david"}, ast.operands[1])
|
113
118
|
end
|
114
119
|
|
115
120
|
def test_implicit_and
|
@@ -129,11 +134,11 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
129
134
|
assert_instance_of And, ast
|
130
135
|
|
131
136
|
assert_equal 2, ast.operands.length
|
132
|
-
assert_instance_of
|
137
|
+
assert_instance_of StringToken, ast.operands[0]
|
133
138
|
assert_equal "dinner", ast.operands[0].value
|
134
139
|
|
135
140
|
assert_instance_of Not, ast.operands[1]
|
136
|
-
assert_instance_of
|
141
|
+
assert_instance_of StringToken, ast.operands[1].child
|
137
142
|
assert_equal "movie", ast.operands[1].child.value
|
138
143
|
end
|
139
144
|
|
@@ -141,113 +146,84 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
141
146
|
ast = GmailSearchSyntax.parse!("holiday AROUND 10 vacation")
|
142
147
|
assert_instance_of Around, ast
|
143
148
|
|
144
|
-
assert_instance_of
|
149
|
+
assert_instance_of StringToken, ast.left
|
145
150
|
assert_equal "holiday", ast.left.value
|
146
151
|
assert_equal 10, ast.distance
|
147
152
|
|
148
|
-
assert_instance_of
|
153
|
+
assert_instance_of StringToken, ast.right
|
149
154
|
assert_equal "vacation", ast.right.value
|
150
155
|
end
|
151
156
|
|
152
157
|
def test_around_with_quoted_string
|
153
158
|
ast = GmailSearchSyntax.parse!('"secret AROUND 25 birthday"')
|
154
|
-
assert_instance_of
|
159
|
+
assert_instance_of Substring, ast
|
155
160
|
assert_equal "secret AROUND 25 birthday", ast.value
|
156
161
|
end
|
157
162
|
|
158
163
|
def test_label_operator
|
159
164
|
ast = GmailSearchSyntax.parse!("label:friends")
|
160
|
-
|
161
|
-
assert_equal "label", ast.name
|
162
|
-
assert_equal "friends", ast.value
|
165
|
+
assert_operator({name: "label", value: "friends"}, ast)
|
163
166
|
end
|
164
167
|
|
165
168
|
def test_category_operator
|
166
169
|
ast = GmailSearchSyntax.parse!("category:primary")
|
167
|
-
|
168
|
-
assert_equal "category", ast.name
|
169
|
-
assert_equal "primary", ast.value
|
170
|
+
assert_operator({name: "category", value: "primary"}, ast)
|
170
171
|
end
|
171
172
|
|
172
173
|
def test_has_attachment
|
173
174
|
ast = GmailSearchSyntax.parse!("has:attachment")
|
174
|
-
|
175
|
-
assert_equal "has", ast.name
|
176
|
-
assert_equal "attachment", ast.value
|
175
|
+
assert_operator({name: "has", value: "attachment"}, ast)
|
177
176
|
end
|
178
177
|
|
179
178
|
def test_filename_operator
|
180
179
|
ast = GmailSearchSyntax.parse!("filename:pdf")
|
181
|
-
|
182
|
-
assert_equal "filename", ast.name
|
183
|
-
assert_equal "pdf", ast.value
|
180
|
+
assert_operator({name: "filename", value: "pdf"}, ast)
|
184
181
|
end
|
185
182
|
|
186
183
|
def test_filename_with_extension
|
187
184
|
ast = GmailSearchSyntax.parse!("filename:homework.txt")
|
188
|
-
|
189
|
-
assert_equal "filename", ast.name
|
190
|
-
assert_equal "homework.txt", ast.value
|
185
|
+
assert_operator({name: "filename", value: "homework.txt"}, ast)
|
191
186
|
end
|
192
187
|
|
193
188
|
def test_quoted_exact_phrase
|
194
189
|
ast = GmailSearchSyntax.parse!('"dinner and movie tonight"')
|
195
|
-
assert_instance_of
|
190
|
+
assert_instance_of Substring, ast
|
196
191
|
assert_equal "dinner and movie tonight", ast.value
|
197
192
|
end
|
198
193
|
|
199
194
|
def test_parentheses_grouping
|
200
195
|
ast = GmailSearchSyntax.parse!("subject:(dinner movie)")
|
201
|
-
|
202
|
-
assert_equal "subject", ast.name
|
203
|
-
|
204
|
-
assert_instance_of And, ast.value
|
196
|
+
assert_operator({name: "subject", value: And}, ast)
|
205
197
|
assert_equal 2, ast.value.operands.length
|
206
|
-
|
207
|
-
|
208
|
-
assert_instance_of Text, ast.value.operands[1]
|
209
|
-
assert_equal "movie", ast.value.operands[1].value
|
198
|
+
assert_string_token({value: "dinner"}, ast.value.operands[0])
|
199
|
+
assert_string_token({value: "movie"}, ast.value.operands[1])
|
210
200
|
end
|
211
201
|
|
212
202
|
def test_in_anywhere
|
203
|
+
# With Gmail-compatible bareword consumption, "movie" gets consumed into operator value
|
204
|
+
# To search for "movie" as text, use: in:anywhere "movie" or use a different operator after
|
213
205
|
ast = GmailSearchSyntax.parse!("in:anywhere movie")
|
214
|
-
|
215
|
-
|
216
|
-
assert_equal 2, ast.operands.length
|
217
|
-
assert_instance_of Operator, ast.operands[0]
|
218
|
-
assert_equal "in", ast.operands[0].name
|
219
|
-
assert_equal "anywhere", ast.operands[0].value
|
220
|
-
|
221
|
-
assert_instance_of Text, ast.operands[1]
|
222
|
-
assert_equal "movie", ast.operands[1].value
|
206
|
+
assert_operator({name: "in", value: "anywhere movie"}, ast)
|
223
207
|
end
|
224
208
|
|
225
209
|
def test_is_starred
|
226
210
|
ast = GmailSearchSyntax.parse!("is:starred")
|
227
|
-
|
228
|
-
assert_equal "is", ast.name
|
229
|
-
assert_equal "starred", ast.value
|
211
|
+
assert_operator({name: "is", value: "starred"}, ast)
|
230
212
|
end
|
231
213
|
|
232
214
|
def test_is_unread
|
233
215
|
ast = GmailSearchSyntax.parse!("is:unread")
|
234
|
-
|
235
|
-
assert_equal "is", ast.name
|
236
|
-
assert_equal "unread", ast.value
|
216
|
+
assert_operator({name: "is", value: "unread"}, ast)
|
237
217
|
end
|
238
218
|
|
239
219
|
def test_size_operator
|
240
220
|
ast = GmailSearchSyntax.parse!("size:1000000")
|
241
|
-
|
242
|
-
assert_equal "size", ast.name
|
243
|
-
assert_equal 1000000, ast.value
|
221
|
+
assert_operator({name: "size", value: 1000000}, ast)
|
244
222
|
end
|
245
223
|
|
246
224
|
def test_larger_operator
|
247
225
|
ast = GmailSearchSyntax.parse!("larger:10M")
|
248
|
-
|
249
|
-
assert_equal "larger", ast.name
|
250
|
-
assert_equal "10M", ast.value
|
226
|
+
assert_operator({name: "larger", value: "10M"}, ast)
|
251
227
|
end
|
252
228
|
|
253
229
|
def test_complex_query_with_multiple_operators
|
@@ -292,42 +268,32 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
292
268
|
|
293
269
|
def test_list_operator
|
294
270
|
ast = GmailSearchSyntax.parse!("list:info@example.com")
|
295
|
-
|
296
|
-
assert_equal "list", ast.name
|
297
|
-
assert_equal "info@example.com", ast.value
|
271
|
+
assert_operator({name: "list", value: "info@example.com"}, ast)
|
298
272
|
end
|
299
273
|
|
300
274
|
def test_deliveredto_operator
|
301
275
|
ast = GmailSearchSyntax.parse!("deliveredto:username@example.com")
|
302
|
-
|
303
|
-
assert_equal "deliveredto", ast.name
|
304
|
-
assert_equal "username@example.com", ast.value
|
276
|
+
assert_operator({name: "deliveredto", value: "username@example.com"}, ast)
|
305
277
|
end
|
306
278
|
|
307
279
|
def test_rfc822msgid_operator
|
308
280
|
ast = GmailSearchSyntax.parse!("rfc822msgid:200503292@example.com")
|
309
|
-
|
310
|
-
assert_equal "rfc822msgid", ast.name
|
311
|
-
assert_equal "200503292@example.com", ast.value
|
281
|
+
assert_operator({name: "rfc822msgid", value: "200503292@example.com"}, ast)
|
312
282
|
end
|
313
283
|
|
314
284
|
def test_cc_operator
|
315
285
|
ast = GmailSearchSyntax.parse!("cc:john@example.com")
|
316
|
-
|
317
|
-
assert_equal "cc", ast.name
|
318
|
-
assert_equal "john@example.com", ast.value
|
286
|
+
assert_operator({name: "cc", value: "john@example.com"}, ast)
|
319
287
|
end
|
320
288
|
|
321
289
|
def test_bcc_operator
|
322
290
|
ast = GmailSearchSyntax.parse!("bcc:david@example.com")
|
323
|
-
|
324
|
-
assert_equal "bcc", ast.name
|
325
|
-
assert_equal "david@example.com", ast.value
|
291
|
+
assert_operator({name: "bcc", value: "david@example.com"}, ast)
|
326
292
|
end
|
327
293
|
|
328
294
|
def test_plain_text_search
|
329
295
|
ast = GmailSearchSyntax.parse!("meeting")
|
330
|
-
assert_instance_of
|
296
|
+
assert_instance_of StringToken, ast
|
331
297
|
assert_equal "meeting", ast.value
|
332
298
|
end
|
333
299
|
|
@@ -336,10 +302,10 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
336
302
|
assert_instance_of And, ast
|
337
303
|
|
338
304
|
assert_equal 2, ast.operands.length
|
339
|
-
assert_instance_of
|
305
|
+
assert_instance_of StringToken, ast.operands[0]
|
340
306
|
assert_equal "project", ast.operands[0].value
|
341
307
|
|
342
|
-
assert_instance_of
|
308
|
+
assert_instance_of StringToken, ast.operands[1]
|
343
309
|
assert_equal "report", ast.operands[1].value
|
344
310
|
end
|
345
311
|
|
@@ -417,15 +383,13 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
417
383
|
|
418
384
|
def test_quoted_string_with_operators_inside
|
419
385
|
ast = GmailSearchSyntax.parse!('"from:amy to:bob"')
|
420
|
-
assert_instance_of
|
386
|
+
assert_instance_of Substring, ast
|
421
387
|
assert_equal "from:amy to:bob", ast.value
|
422
388
|
end
|
423
389
|
|
424
390
|
def test_email_with_plus_sign
|
425
391
|
ast = GmailSearchSyntax.parse!("to:user+tag@example.com")
|
426
|
-
|
427
|
-
assert_equal "to", ast.name
|
428
|
-
assert_equal "user+tag@example.com", ast.value
|
392
|
+
assert_operator({name: "to", value: "user+tag@example.com"}, ast)
|
429
393
|
end
|
430
394
|
|
431
395
|
def test_in_operator_with_location
|
@@ -440,16 +404,12 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
440
404
|
|
441
405
|
def test_has_drive_operator
|
442
406
|
ast = GmailSearchSyntax.parse!("has:drive")
|
443
|
-
|
444
|
-
assert_equal "has", ast.name
|
445
|
-
assert_equal "drive", ast.value
|
407
|
+
assert_operator({name: "has", value: "drive"}, ast)
|
446
408
|
end
|
447
409
|
|
448
410
|
def test_category_updates
|
449
411
|
ast = GmailSearchSyntax.parse!("category:updates")
|
450
|
-
|
451
|
-
assert_equal "category", ast.name
|
452
|
-
assert_equal "updates", ast.value
|
412
|
+
assert_operator({name: "category", value: "updates"}, ast)
|
453
413
|
end
|
454
414
|
|
455
415
|
def test_around_default_distance
|
@@ -460,23 +420,17 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
460
420
|
|
461
421
|
def test_parentheses_with_single_term
|
462
422
|
ast = GmailSearchSyntax.parse!("(meeting)")
|
463
|
-
assert_instance_of
|
423
|
+
assert_instance_of StringToken, ast
|
464
424
|
assert_equal "meeting", ast.value
|
465
425
|
end
|
466
426
|
|
467
427
|
def test_subject_with_parentheses_multiple_words
|
468
428
|
ast = GmailSearchSyntax.parse!("subject:(project status update)")
|
469
|
-
|
470
|
-
assert_equal "subject", ast.name
|
471
|
-
|
472
|
-
assert_instance_of And, ast.value
|
429
|
+
assert_operator({name: "subject", value: And}, ast)
|
473
430
|
assert_equal 3, ast.value.operands.length
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
assert_equal "status", ast.value.operands[1].value
|
478
|
-
assert_instance_of Text, ast.value.operands[2]
|
479
|
-
assert_equal "update", ast.value.operands[2].value
|
431
|
+
assert_string_token({value: "project"}, ast.value.operands[0])
|
432
|
+
assert_string_token({value: "status"}, ast.value.operands[1])
|
433
|
+
assert_string_token({value: "update"}, ast.value.operands[2])
|
480
434
|
end
|
481
435
|
|
482
436
|
def test_and_explicit_with_text
|
@@ -484,52 +438,37 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
484
438
|
assert_instance_of And, ast
|
485
439
|
|
486
440
|
assert_equal 2, ast.operands.length
|
487
|
-
assert_instance_of
|
441
|
+
assert_instance_of StringToken, ast.operands[0]
|
488
442
|
assert_equal "meeting", ast.operands[0].value
|
489
443
|
|
490
|
-
assert_instance_of
|
444
|
+
assert_instance_of StringToken, ast.operands[1]
|
491
445
|
assert_equal "project", ast.operands[1].value
|
492
446
|
end
|
493
447
|
|
494
448
|
def test_smaller_operator
|
495
449
|
ast = GmailSearchSyntax.parse!("smaller:1M")
|
496
|
-
|
497
|
-
assert_equal "smaller", ast.name
|
498
|
-
assert_equal "1M", ast.value
|
450
|
+
assert_operator({name: "smaller", value: "1M"}, ast)
|
499
451
|
end
|
500
452
|
|
501
453
|
def test_or_inside_operator_value
|
502
454
|
ast = GmailSearchSyntax.parse!("from:(mischa@ OR julik@)")
|
503
|
-
|
504
|
-
assert_equal "from", ast.name
|
505
|
-
|
506
|
-
assert_instance_of Or, ast.value
|
455
|
+
assert_operator({name: "from", value: Or}, ast)
|
507
456
|
assert_equal 2, ast.value.operands.length
|
508
|
-
|
509
|
-
|
510
|
-
assert_instance_of Text, ast.value.operands[1]
|
511
|
-
assert_equal "julik@", ast.value.operands[1].value
|
457
|
+
assert_string_token({value: "mischa@"}, ast.value.operands[0])
|
458
|
+
assert_string_token({value: "julik@"}, ast.value.operands[1])
|
512
459
|
end
|
513
460
|
|
514
461
|
def test_or_with_emails_inside_operator
|
515
462
|
ast = GmailSearchSyntax.parse!("from:(amy@example.com OR bob@example.com)")
|
516
|
-
|
517
|
-
assert_equal "from", ast.name
|
518
|
-
|
519
|
-
assert_instance_of Or, ast.value
|
463
|
+
assert_operator({name: "from", value: Or}, ast)
|
520
464
|
assert_equal 2, ast.value.operands.length
|
521
|
-
|
522
|
-
|
523
|
-
assert_instance_of Text, ast.value.operands[1]
|
524
|
-
assert_equal "bob@example.com", ast.value.operands[1].value
|
465
|
+
assert_string_token({value: "amy@example.com"}, ast.value.operands[0])
|
466
|
+
assert_string_token({value: "bob@example.com"}, ast.value.operands[1])
|
525
467
|
end
|
526
468
|
|
527
469
|
def test_multiple_or_inside_operator
|
528
470
|
ast = GmailSearchSyntax.parse!("from:(a@ OR b@ OR c@)")
|
529
|
-
|
530
|
-
assert_equal "from", ast.name
|
531
|
-
|
532
|
-
assert_instance_of Or, ast.value
|
471
|
+
assert_operator({name: "from", value: Or}, ast)
|
533
472
|
assert_equal 3, ast.value.operands.length
|
534
473
|
assert_equal "a@", ast.value.operands[0].value
|
535
474
|
assert_equal "b@", ast.value.operands[1].value
|
@@ -538,15 +477,10 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
538
477
|
|
539
478
|
def test_and_inside_operator_value
|
540
479
|
ast = GmailSearchSyntax.parse!("subject:(urgent AND meeting)")
|
541
|
-
|
542
|
-
assert_equal "subject", ast.name
|
543
|
-
|
544
|
-
assert_instance_of And, ast.value
|
480
|
+
assert_operator({name: "subject", value: And}, ast)
|
545
481
|
assert_equal 2, ast.value.operands.length
|
546
|
-
|
547
|
-
|
548
|
-
assert_instance_of Text, ast.value.operands[1]
|
549
|
-
assert_equal "meeting", ast.value.operands[1].value
|
482
|
+
assert_string_token({value: "urgent"}, ast.value.operands[0])
|
483
|
+
assert_string_token({value: "meeting"}, ast.value.operands[1])
|
550
484
|
end
|
551
485
|
|
552
486
|
def test_operator_with_or_combined_with_other_conditions
|
@@ -565,13 +499,9 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
565
499
|
|
566
500
|
def test_negation_inside_operator_value
|
567
501
|
ast = GmailSearchSyntax.parse!("subject:(meeting -cancelled)")
|
568
|
-
|
569
|
-
assert_equal "subject", ast.name
|
570
|
-
|
571
|
-
assert_instance_of And, ast.value
|
502
|
+
assert_operator({name: "subject", value: And}, ast)
|
572
503
|
assert_equal 2, ast.value.operands.length
|
573
|
-
|
574
|
-
assert_equal "meeting", ast.value.operands[0].value
|
504
|
+
assert_string_token({value: "meeting"}, ast.value.operands[0])
|
575
505
|
assert_instance_of Not, ast.value.operands[1]
|
576
506
|
assert_equal "cancelled", ast.value.operands[1].child.value
|
577
507
|
end
|
@@ -604,21 +534,16 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
604
534
|
assert_equal 2, ast.value.operands[0].operands.length
|
605
535
|
assert_equal "urgent", ast.value.operands[0].operands[0].value
|
606
536
|
assert_equal "important", ast.value.operands[0].operands[1].value
|
607
|
-
assert_instance_of
|
537
|
+
assert_instance_of StringToken, ast.value.operands[1]
|
608
538
|
assert_equal "meeting", ast.value.operands[1].value
|
609
539
|
end
|
610
540
|
|
611
541
|
def test_curly_braces_inside_operator_value
|
612
542
|
ast = GmailSearchSyntax.parse!("from:{mischa@ marc@}")
|
613
|
-
|
614
|
-
assert_equal "from", ast.name
|
615
|
-
|
616
|
-
assert_instance_of Or, ast.value
|
543
|
+
assert_operator({name: "from", value: Or}, ast)
|
617
544
|
assert_equal 2, ast.value.operands.length
|
618
|
-
|
619
|
-
|
620
|
-
assert_instance_of Text, ast.value.operands[1]
|
621
|
-
assert_equal "marc@", ast.value.operands[1].value
|
545
|
+
assert_string_token({value: "mischa@"}, ast.value.operands[0])
|
546
|
+
assert_string_token({value: "marc@"}, ast.value.operands[1])
|
622
547
|
end
|
623
548
|
|
624
549
|
def test_curly_braces_with_emails_inside_operator
|
@@ -688,4 +613,139 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
688
613
|
assert_equal "subject", ast.operands[1].name
|
689
614
|
assert_instance_of And, ast.operands[1].value
|
690
615
|
end
|
616
|
+
|
617
|
+
def test_quoted_string_with_escaped_quotes
|
618
|
+
ast = GmailSearchSyntax.parse!('"She said \\"hello\\" to me"')
|
619
|
+
assert_instance_of Substring, ast
|
620
|
+
assert_equal 'She said "hello" to me', ast.value
|
621
|
+
end
|
622
|
+
|
623
|
+
def test_quoted_string_with_escaped_backslash
|
624
|
+
ast = GmailSearchSyntax.parse!('"path\\\\to\\\\file"')
|
625
|
+
assert_instance_of Substring, ast
|
626
|
+
assert_equal 'path\\to\\file', ast.value
|
627
|
+
end
|
628
|
+
|
629
|
+
def test_subject_with_escaped_quotes
|
630
|
+
ast = GmailSearchSyntax.parse!('subject:"Meeting: \\"Q1 Review\\""')
|
631
|
+
assert_instance_of Operator, ast
|
632
|
+
assert_equal "subject", ast.name
|
633
|
+
assert_equal 'Meeting: "Q1 Review"', ast.value
|
634
|
+
end
|
635
|
+
|
636
|
+
def test_unquoted_text_with_escaped_quote
|
637
|
+
ast = GmailSearchSyntax.parse!('meeting\\"room')
|
638
|
+
assert_instance_of StringToken, ast
|
639
|
+
assert_equal 'meeting"room', ast.value
|
640
|
+
end
|
641
|
+
|
642
|
+
def test_unquoted_text_with_escaped_backslash
|
643
|
+
ast = GmailSearchSyntax.parse!('path\\\\to\\\\file')
|
644
|
+
assert_instance_of StringToken, ast
|
645
|
+
assert_equal 'path\\to\\file', ast.value
|
646
|
+
end
|
647
|
+
|
648
|
+
def test_operator_with_unquoted_escaped_quote
|
649
|
+
ast = GmailSearchSyntax.parse!('subject:test\\"value')
|
650
|
+
assert_instance_of Operator, ast
|
651
|
+
assert_equal "subject", ast.name
|
652
|
+
assert_equal 'test"value', ast.value
|
653
|
+
end
|
654
|
+
|
655
|
+
def test_multiple_tokens_with_escapes
|
656
|
+
ast = GmailSearchSyntax.parse!('meeting\\"room project\\\\plan')
|
657
|
+
assert_instance_of And, ast
|
658
|
+
assert_equal 2, ast.operands.length
|
659
|
+
assert_equal 'meeting"room', ast.operands[0].value
|
660
|
+
assert_equal 'project\\plan', ast.operands[1].value
|
661
|
+
end
|
662
|
+
|
663
|
+
# Gmail behavior: barewords after operator values get consumed into the operator value
|
664
|
+
# until the next operator is encountered.
|
665
|
+
# We now implement this Gmail-compatible behavior.
|
666
|
+
|
667
|
+
def test_label_with_space_separated_value_gmail_behavior
|
668
|
+
# Gmail parses this as: label:"Cora/Google Drive", label:"Notes"
|
669
|
+
# We now match this behavior
|
670
|
+
ast = GmailSearchSyntax.parse!("label:Cora/Google Drive label:Notes")
|
671
|
+
assert_instance_of And, ast
|
672
|
+
assert_equal 2, ast.operands.length
|
673
|
+
|
674
|
+
# Gmail-compatible: barewords consumed into operator value
|
675
|
+
assert_instance_of Operator, ast.operands[0]
|
676
|
+
assert_equal "label", ast.operands[0].name
|
677
|
+
assert_equal "Cora/Google Drive", ast.operands[0].value
|
678
|
+
|
679
|
+
# Second operator parsed correctly
|
680
|
+
assert_instance_of Operator, ast.operands[1]
|
681
|
+
assert_equal "label", ast.operands[1].name
|
682
|
+
assert_equal "Notes", ast.operands[1].value
|
683
|
+
end
|
684
|
+
|
685
|
+
def test_subject_with_barewords_gmail_behavior
|
686
|
+
# Gmail parses: subject:"urgent meeting important"
|
687
|
+
# We now match this behavior
|
688
|
+
ast = GmailSearchSyntax.parse!("subject:urgent meeting important")
|
689
|
+
assert_instance_of Operator, ast
|
690
|
+
|
691
|
+
assert_equal "subject", ast.name
|
692
|
+
assert_equal "urgent meeting important", ast.value
|
693
|
+
end
|
694
|
+
|
695
|
+
def test_multiple_barewords_between_operators_gmail_behavior
|
696
|
+
# Gmail parses: label:"test one two three", label:"another"
|
697
|
+
# We now match this behavior
|
698
|
+
ast = GmailSearchSyntax.parse!("label:test one two three label:another")
|
699
|
+
assert_instance_of And, ast
|
700
|
+
assert_equal 2, ast.operands.length
|
701
|
+
|
702
|
+
assert_instance_of Operator, ast.operands[0]
|
703
|
+
assert_equal "label", ast.operands[0].name
|
704
|
+
assert_equal "test one two three", ast.operands[0].value
|
705
|
+
|
706
|
+
assert_instance_of Operator, ast.operands[1]
|
707
|
+
assert_equal "label", ast.operands[1].name
|
708
|
+
assert_equal "another", ast.operands[1].value
|
709
|
+
end
|
710
|
+
|
711
|
+
def test_barewords_stop_at_special_operators
|
712
|
+
# Bareword collection should stop at OR, AND, AROUND
|
713
|
+
ast = GmailSearchSyntax.parse!("subject:urgent meeting OR subject:important call")
|
714
|
+
assert_instance_of Or, ast
|
715
|
+
assert_equal 2, ast.operands.length
|
716
|
+
|
717
|
+
assert_instance_of Operator, ast.operands[0]
|
718
|
+
assert_equal "subject", ast.operands[0].name
|
719
|
+
assert_equal "urgent meeting", ast.operands[0].value
|
720
|
+
|
721
|
+
assert_instance_of Operator, ast.operands[1]
|
722
|
+
assert_equal "subject", ast.operands[1].name
|
723
|
+
assert_equal "important call", ast.operands[1].value
|
724
|
+
end
|
725
|
+
|
726
|
+
def test_barewords_with_mixed_tokens
|
727
|
+
# Numbers, dates, emails should all be collected as barewords
|
728
|
+
ast = GmailSearchSyntax.parse!("subject:meeting 2024 Q1 review")
|
729
|
+
assert_instance_of Operator, ast
|
730
|
+
assert_equal "subject", ast.name
|
731
|
+
assert_equal "meeting 2024 Q1 review", ast.value
|
732
|
+
end
|
733
|
+
|
734
|
+
def test_specific_gmail_example_cora_google_drive
|
735
|
+
# The specific example from the user: label:Cora/Google Drive label:Notes
|
736
|
+
# This should parse as two separate label operators with multi-word values
|
737
|
+
ast = GmailSearchSyntax.parse!("label:Cora/Google Drive label:Notes")
|
738
|
+
assert_instance_of And, ast
|
739
|
+
assert_equal 2, ast.operands.length
|
740
|
+
|
741
|
+
# First operator: label with "Cora/Google Drive"
|
742
|
+
assert_instance_of Operator, ast.operands[0]
|
743
|
+
assert_equal "label", ast.operands[0].name
|
744
|
+
assert_equal "Cora/Google Drive", ast.operands[0].value
|
745
|
+
|
746
|
+
# Second operator: label with "Notes"
|
747
|
+
assert_instance_of Operator, ast.operands[1]
|
748
|
+
assert_equal "label", ast.operands[1].name
|
749
|
+
assert_equal "Notes", ast.operands[1].value
|
750
|
+
end
|
691
751
|
end
|