gmail_search_syntax 0.1.1 → 0.1.3
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/.github/workflows/ci.yml +24 -0
- data/AGENTS.md +104 -0
- data/Gemfile +3 -0
- data/README.md +19 -42
- data/SCHEMA.md +112 -128
- data/examples/demo.rb +2 -6
- data/examples/gmail_comparison_demo.rb +82 -0
- data/gmail_search_syntax.gemspec +23 -0
- data/lib/gmail_search_syntax/parser.rb +10 -22
- data/lib/gmail_search_syntax/tokenizer.rb +3 -3
- data/lib/gmail_search_syntax/version.rb +1 -1
- data/slop/GMAIL_BEHAVIOR_COMPARISON.md +166 -0
- data/slop/GMAIL_COMPATIBILITY_COMPLETE.md +236 -0
- data/slop/GREEDY_VS_NON_GREEDY_TOKENIZATION.md +84 -0
- data/test/gmail_search_syntax_test.rb +197 -169
- data/test/tokenizer_test.rb +176 -144
- metadata +11 -3
- /data/{ARCHITECTURE.md → slop/ARCHITECTURE.md} +0 -0
- /data/{IMPLEMENTATION_NOTES.md → slop/IMPLEMENTATION_NOTES.md} +0 -0
|
@@ -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
|
|
@@ -157,37 +162,27 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
|
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
|
|
@@ -198,56 +193,40 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
|
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 StringToken, 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
|
+
# Gmail treats barewords after operator as separate search terms
|
|
204
|
+
# in:anywhere movie → search for "movie" in all mail locations
|
|
213
205
|
ast = GmailSearchSyntax.parse!("in:anywhere movie")
|
|
214
206
|
assert_instance_of And, ast
|
|
215
|
-
|
|
216
207
|
assert_equal 2, ast.operands.length
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
assert_equal "anywhere", ast.operands[0].value
|
|
220
|
-
|
|
221
|
-
assert_instance_of StringToken, ast.operands[1]
|
|
222
|
-
assert_equal "movie", ast.operands[1].value
|
|
208
|
+
assert_operator({name: "in", value: "anywhere"}, ast.operands[0])
|
|
209
|
+
assert_string_token({value: "movie"}, ast.operands[1])
|
|
223
210
|
end
|
|
224
211
|
|
|
225
212
|
def test_is_starred
|
|
226
213
|
ast = GmailSearchSyntax.parse!("is:starred")
|
|
227
|
-
|
|
228
|
-
assert_equal "is", ast.name
|
|
229
|
-
assert_equal "starred", ast.value
|
|
214
|
+
assert_operator({name: "is", value: "starred"}, ast)
|
|
230
215
|
end
|
|
231
216
|
|
|
232
217
|
def test_is_unread
|
|
233
218
|
ast = GmailSearchSyntax.parse!("is:unread")
|
|
234
|
-
|
|
235
|
-
assert_equal "is", ast.name
|
|
236
|
-
assert_equal "unread", ast.value
|
|
219
|
+
assert_operator({name: "is", value: "unread"}, ast)
|
|
237
220
|
end
|
|
238
221
|
|
|
239
222
|
def test_size_operator
|
|
240
223
|
ast = GmailSearchSyntax.parse!("size:1000000")
|
|
241
|
-
|
|
242
|
-
assert_equal "size", ast.name
|
|
243
|
-
assert_equal 1000000, ast.value
|
|
224
|
+
assert_operator({name: "size", value: 1000000}, ast)
|
|
244
225
|
end
|
|
245
226
|
|
|
246
227
|
def test_larger_operator
|
|
247
228
|
ast = GmailSearchSyntax.parse!("larger:10M")
|
|
248
|
-
|
|
249
|
-
assert_equal "larger", ast.name
|
|
250
|
-
assert_equal "10M", ast.value
|
|
229
|
+
assert_operator({name: "larger", value: "10M"}, ast)
|
|
251
230
|
end
|
|
252
231
|
|
|
253
232
|
def test_complex_query_with_multiple_operators
|
|
@@ -292,37 +271,27 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
|
292
271
|
|
|
293
272
|
def test_list_operator
|
|
294
273
|
ast = GmailSearchSyntax.parse!("list:info@example.com")
|
|
295
|
-
|
|
296
|
-
assert_equal "list", ast.name
|
|
297
|
-
assert_equal "info@example.com", ast.value
|
|
274
|
+
assert_operator({name: "list", value: "info@example.com"}, ast)
|
|
298
275
|
end
|
|
299
276
|
|
|
300
277
|
def test_deliveredto_operator
|
|
301
278
|
ast = GmailSearchSyntax.parse!("deliveredto:username@example.com")
|
|
302
|
-
|
|
303
|
-
assert_equal "deliveredto", ast.name
|
|
304
|
-
assert_equal "username@example.com", ast.value
|
|
279
|
+
assert_operator({name: "deliveredto", value: "username@example.com"}, ast)
|
|
305
280
|
end
|
|
306
281
|
|
|
307
282
|
def test_rfc822msgid_operator
|
|
308
283
|
ast = GmailSearchSyntax.parse!("rfc822msgid:200503292@example.com")
|
|
309
|
-
|
|
310
|
-
assert_equal "rfc822msgid", ast.name
|
|
311
|
-
assert_equal "200503292@example.com", ast.value
|
|
284
|
+
assert_operator({name: "rfc822msgid", value: "200503292@example.com"}, ast)
|
|
312
285
|
end
|
|
313
286
|
|
|
314
287
|
def test_cc_operator
|
|
315
288
|
ast = GmailSearchSyntax.parse!("cc:john@example.com")
|
|
316
|
-
|
|
317
|
-
assert_equal "cc", ast.name
|
|
318
|
-
assert_equal "john@example.com", ast.value
|
|
289
|
+
assert_operator({name: "cc", value: "john@example.com"}, ast)
|
|
319
290
|
end
|
|
320
291
|
|
|
321
292
|
def test_bcc_operator
|
|
322
293
|
ast = GmailSearchSyntax.parse!("bcc:david@example.com")
|
|
323
|
-
|
|
324
|
-
assert_equal "bcc", ast.name
|
|
325
|
-
assert_equal "david@example.com", ast.value
|
|
294
|
+
assert_operator({name: "bcc", value: "david@example.com"}, ast)
|
|
326
295
|
end
|
|
327
296
|
|
|
328
297
|
def test_plain_text_search
|
|
@@ -423,9 +392,7 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
|
423
392
|
|
|
424
393
|
def test_email_with_plus_sign
|
|
425
394
|
ast = GmailSearchSyntax.parse!("to:user+tag@example.com")
|
|
426
|
-
|
|
427
|
-
assert_equal "to", ast.name
|
|
428
|
-
assert_equal "user+tag@example.com", ast.value
|
|
395
|
+
assert_operator({name: "to", value: "user+tag@example.com"}, ast)
|
|
429
396
|
end
|
|
430
397
|
|
|
431
398
|
def test_in_operator_with_location
|
|
@@ -440,16 +407,12 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
|
440
407
|
|
|
441
408
|
def test_has_drive_operator
|
|
442
409
|
ast = GmailSearchSyntax.parse!("has:drive")
|
|
443
|
-
|
|
444
|
-
assert_equal "has", ast.name
|
|
445
|
-
assert_equal "drive", ast.value
|
|
410
|
+
assert_operator({name: "has", value: "drive"}, ast)
|
|
446
411
|
end
|
|
447
412
|
|
|
448
413
|
def test_category_updates
|
|
449
414
|
ast = GmailSearchSyntax.parse!("category:updates")
|
|
450
|
-
|
|
451
|
-
assert_equal "category", ast.name
|
|
452
|
-
assert_equal "updates", ast.value
|
|
415
|
+
assert_operator({name: "category", value: "updates"}, ast)
|
|
453
416
|
end
|
|
454
417
|
|
|
455
418
|
def test_around_default_distance
|
|
@@ -466,17 +429,11 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
|
466
429
|
|
|
467
430
|
def test_subject_with_parentheses_multiple_words
|
|
468
431
|
ast = GmailSearchSyntax.parse!("subject:(project status update)")
|
|
469
|
-
|
|
470
|
-
assert_equal "subject", ast.name
|
|
471
|
-
|
|
472
|
-
assert_instance_of And, ast.value
|
|
432
|
+
assert_operator({name: "subject", value: And}, ast)
|
|
473
433
|
assert_equal 3, ast.value.operands.length
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
assert_equal "status", ast.value.operands[1].value
|
|
478
|
-
assert_instance_of StringToken, ast.value.operands[2]
|
|
479
|
-
assert_equal "update", ast.value.operands[2].value
|
|
434
|
+
assert_string_token({value: "project"}, ast.value.operands[0])
|
|
435
|
+
assert_string_token({value: "status"}, ast.value.operands[1])
|
|
436
|
+
assert_string_token({value: "update"}, ast.value.operands[2])
|
|
480
437
|
end
|
|
481
438
|
|
|
482
439
|
def test_and_explicit_with_text
|
|
@@ -493,43 +450,28 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
|
493
450
|
|
|
494
451
|
def test_smaller_operator
|
|
495
452
|
ast = GmailSearchSyntax.parse!("smaller:1M")
|
|
496
|
-
|
|
497
|
-
assert_equal "smaller", ast.name
|
|
498
|
-
assert_equal "1M", ast.value
|
|
453
|
+
assert_operator({name: "smaller", value: "1M"}, ast)
|
|
499
454
|
end
|
|
500
455
|
|
|
501
456
|
def test_or_inside_operator_value
|
|
502
457
|
ast = GmailSearchSyntax.parse!("from:(mischa@ OR julik@)")
|
|
503
|
-
|
|
504
|
-
assert_equal "from", ast.name
|
|
505
|
-
|
|
506
|
-
assert_instance_of Or, ast.value
|
|
458
|
+
assert_operator({name: "from", value: Or}, ast)
|
|
507
459
|
assert_equal 2, ast.value.operands.length
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
assert_instance_of StringToken, ast.value.operands[1]
|
|
511
|
-
assert_equal "julik@", ast.value.operands[1].value
|
|
460
|
+
assert_string_token({value: "mischa@"}, ast.value.operands[0])
|
|
461
|
+
assert_string_token({value: "julik@"}, ast.value.operands[1])
|
|
512
462
|
end
|
|
513
463
|
|
|
514
464
|
def test_or_with_emails_inside_operator
|
|
515
465
|
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
|
|
466
|
+
assert_operator({name: "from", value: Or}, ast)
|
|
520
467
|
assert_equal 2, ast.value.operands.length
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
assert_instance_of StringToken, ast.value.operands[1]
|
|
524
|
-
assert_equal "bob@example.com", ast.value.operands[1].value
|
|
468
|
+
assert_string_token({value: "amy@example.com"}, ast.value.operands[0])
|
|
469
|
+
assert_string_token({value: "bob@example.com"}, ast.value.operands[1])
|
|
525
470
|
end
|
|
526
471
|
|
|
527
472
|
def test_multiple_or_inside_operator
|
|
528
473
|
ast = GmailSearchSyntax.parse!("from:(a@ OR b@ OR c@)")
|
|
529
|
-
|
|
530
|
-
assert_equal "from", ast.name
|
|
531
|
-
|
|
532
|
-
assert_instance_of Or, ast.value
|
|
474
|
+
assert_operator({name: "from", value: Or}, ast)
|
|
533
475
|
assert_equal 3, ast.value.operands.length
|
|
534
476
|
assert_equal "a@", ast.value.operands[0].value
|
|
535
477
|
assert_equal "b@", ast.value.operands[1].value
|
|
@@ -538,15 +480,10 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
|
538
480
|
|
|
539
481
|
def test_and_inside_operator_value
|
|
540
482
|
ast = GmailSearchSyntax.parse!("subject:(urgent AND meeting)")
|
|
541
|
-
|
|
542
|
-
assert_equal "subject", ast.name
|
|
543
|
-
|
|
544
|
-
assert_instance_of And, ast.value
|
|
483
|
+
assert_operator({name: "subject", value: And}, ast)
|
|
545
484
|
assert_equal 2, ast.value.operands.length
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
assert_instance_of StringToken, ast.value.operands[1]
|
|
549
|
-
assert_equal "meeting", ast.value.operands[1].value
|
|
485
|
+
assert_string_token({value: "urgent"}, ast.value.operands[0])
|
|
486
|
+
assert_string_token({value: "meeting"}, ast.value.operands[1])
|
|
550
487
|
end
|
|
551
488
|
|
|
552
489
|
def test_operator_with_or_combined_with_other_conditions
|
|
@@ -565,13 +502,9 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
|
565
502
|
|
|
566
503
|
def test_negation_inside_operator_value
|
|
567
504
|
ast = GmailSearchSyntax.parse!("subject:(meeting -cancelled)")
|
|
568
|
-
|
|
569
|
-
assert_equal "subject", ast.name
|
|
570
|
-
|
|
571
|
-
assert_instance_of And, ast.value
|
|
505
|
+
assert_operator({name: "subject", value: And}, ast)
|
|
572
506
|
assert_equal 2, ast.value.operands.length
|
|
573
|
-
|
|
574
|
-
assert_equal "meeting", ast.value.operands[0].value
|
|
507
|
+
assert_string_token({value: "meeting"}, ast.value.operands[0])
|
|
575
508
|
assert_instance_of Not, ast.value.operands[1]
|
|
576
509
|
assert_equal "cancelled", ast.value.operands[1].child.value
|
|
577
510
|
end
|
|
@@ -610,15 +543,10 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
|
610
543
|
|
|
611
544
|
def test_curly_braces_inside_operator_value
|
|
612
545
|
ast = GmailSearchSyntax.parse!("from:{mischa@ marc@}")
|
|
613
|
-
|
|
614
|
-
assert_equal "from", ast.name
|
|
615
|
-
|
|
616
|
-
assert_instance_of Or, ast.value
|
|
546
|
+
assert_operator({name: "from", value: Or}, ast)
|
|
617
547
|
assert_equal 2, ast.value.operands.length
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
assert_instance_of StringToken, ast.value.operands[1]
|
|
621
|
-
assert_equal "marc@", ast.value.operands[1].value
|
|
548
|
+
assert_string_token({value: "mischa@"}, ast.value.operands[0])
|
|
549
|
+
assert_string_token({value: "marc@"}, ast.value.operands[1])
|
|
622
550
|
end
|
|
623
551
|
|
|
624
552
|
def test_curly_braces_with_emails_inside_operator
|
|
@@ -734,4 +662,104 @@ class GmailSearchSyntaxTest < Minitest::Test
|
|
|
734
662
|
assert_equal 'meeting"room', ast.operands[0].value
|
|
735
663
|
assert_equal 'project\\plan', ast.operands[1].value
|
|
736
664
|
end
|
|
665
|
+
|
|
666
|
+
# Gmail behavior: barewords after operator values are treated as separate search terms.
|
|
667
|
+
# Multi-word operator values must be explicitly quoted: label:"Cora/Google Drive"
|
|
668
|
+
|
|
669
|
+
def test_label_with_space_separated_value_gmail_behavior
|
|
670
|
+
# Gmail treats barewords as separate search terms
|
|
671
|
+
# To search for label "Cora/Google Drive", you must quote it: label:"Cora/Google Drive"
|
|
672
|
+
ast = GmailSearchSyntax.parse!("label:Cora/Google Drive label:Notes")
|
|
673
|
+
assert_instance_of And, ast
|
|
674
|
+
assert_equal 3, ast.operands.length
|
|
675
|
+
|
|
676
|
+
# First operator takes only the first token
|
|
677
|
+
assert_instance_of Operator, ast.operands[0]
|
|
678
|
+
assert_equal "label", ast.operands[0].name
|
|
679
|
+
assert_equal "Cora/Google", ast.operands[0].value
|
|
680
|
+
|
|
681
|
+
# "Drive" becomes a separate search term
|
|
682
|
+
assert_instance_of StringToken, ast.operands[1]
|
|
683
|
+
assert_equal "Drive", ast.operands[1].value
|
|
684
|
+
|
|
685
|
+
# Second operator parsed correctly
|
|
686
|
+
assert_instance_of Operator, ast.operands[2]
|
|
687
|
+
assert_equal "label", ast.operands[2].name
|
|
688
|
+
assert_equal "Notes", ast.operands[2].value
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
def test_subject_with_barewords_gmail_behavior
|
|
692
|
+
# Gmail treats barewords as separate search terms
|
|
693
|
+
# subject:urgent meeting important → subject contains "urgent" AND body contains "meeting" AND "important"
|
|
694
|
+
ast = GmailSearchSyntax.parse!("subject:urgent meeting important")
|
|
695
|
+
assert_instance_of And, ast
|
|
696
|
+
assert_equal 3, ast.operands.length
|
|
697
|
+
|
|
698
|
+
assert_operator({name: "subject", value: "urgent"}, ast.operands[0])
|
|
699
|
+
assert_string_token({value: "meeting"}, ast.operands[1])
|
|
700
|
+
assert_string_token({value: "important"}, ast.operands[2])
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def test_multiple_barewords_between_operators_gmail_behavior
|
|
704
|
+
# Gmail treats each bareword as a separate search term
|
|
705
|
+
# label:test one two three label:another → 5 terms
|
|
706
|
+
ast = GmailSearchSyntax.parse!("label:test one two three label:another")
|
|
707
|
+
assert_instance_of And, ast
|
|
708
|
+
assert_equal 5, ast.operands.length
|
|
709
|
+
|
|
710
|
+
assert_operator({name: "label", value: "test"}, ast.operands[0])
|
|
711
|
+
assert_string_token({value: "one"}, ast.operands[1])
|
|
712
|
+
assert_string_token({value: "two"}, ast.operands[2])
|
|
713
|
+
assert_string_token({value: "three"}, ast.operands[3])
|
|
714
|
+
assert_operator({name: "label", value: "another"}, ast.operands[4])
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def test_barewords_stop_at_special_operators
|
|
718
|
+
# Barewords are separate terms, OR separates two groups
|
|
719
|
+
ast = GmailSearchSyntax.parse!("subject:urgent meeting OR subject:important call")
|
|
720
|
+
assert_instance_of Or, ast
|
|
721
|
+
assert_equal 2, ast.operands.length
|
|
722
|
+
|
|
723
|
+
# Left side: subject:urgent AND meeting (implicit AND)
|
|
724
|
+
assert_instance_of And, ast.operands[0]
|
|
725
|
+
assert_equal 2, ast.operands[0].operands.length
|
|
726
|
+
assert_operator({name: "subject", value: "urgent"}, ast.operands[0].operands[0])
|
|
727
|
+
assert_string_token({value: "meeting"}, ast.operands[0].operands[1])
|
|
728
|
+
|
|
729
|
+
# Right side: subject:important AND call (implicit AND)
|
|
730
|
+
assert_instance_of And, ast.operands[1]
|
|
731
|
+
assert_equal 2, ast.operands[1].operands.length
|
|
732
|
+
assert_operator({name: "subject", value: "important"}, ast.operands[1].operands[0])
|
|
733
|
+
assert_string_token({value: "call"}, ast.operands[1].operands[1])
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def test_barewords_with_mixed_tokens
|
|
737
|
+
# Numbers, dates, emails are all separate search terms
|
|
738
|
+
ast = GmailSearchSyntax.parse!("subject:meeting 2024 Q1 review")
|
|
739
|
+
assert_instance_of And, ast
|
|
740
|
+
assert_equal 4, ast.operands.length
|
|
741
|
+
|
|
742
|
+
assert_operator({name: "subject", value: "meeting"}, ast.operands[0])
|
|
743
|
+
assert_string_token({value: 2024}, ast.operands[1])
|
|
744
|
+
assert_string_token({value: "Q1"}, ast.operands[2])
|
|
745
|
+
assert_string_token({value: "review"}, ast.operands[3])
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def test_specific_gmail_example_cora_google_drive
|
|
749
|
+
# label:Cora/Google Drive label:Notes
|
|
750
|
+
# "Drive" is a separate search term - to include it in the label, quote it:
|
|
751
|
+
# label:"Cora/Google Drive" label:Notes
|
|
752
|
+
ast = GmailSearchSyntax.parse!("label:Cora/Google Drive label:Notes")
|
|
753
|
+
assert_instance_of And, ast
|
|
754
|
+
assert_equal 3, ast.operands.length
|
|
755
|
+
|
|
756
|
+
# First operator: label with "Cora/Google" only
|
|
757
|
+
assert_operator({name: "label", value: "Cora/Google"}, ast.operands[0])
|
|
758
|
+
|
|
759
|
+
# "Drive" becomes a separate search term
|
|
760
|
+
assert_string_token({value: "Drive"}, ast.operands[1])
|
|
761
|
+
|
|
762
|
+
# Second operator: label with "Notes"
|
|
763
|
+
assert_operator({name: "label", value: "Notes"}, ast.operands[2])
|
|
764
|
+
end
|
|
737
765
|
end
|