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.
@@ -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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast.operands[0]
79
- assert_equal "from", ast.operands[0].name
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
- assert_instance_of Operator, ast.operands[0]
93
- assert_equal "from", ast.operands[0].name
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
- assert_instance_of Operator, ast.operands[0]
107
- assert_equal "from", ast.operands[0].name
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of StringToken, ast.value.operands[0]
207
- assert_equal "dinner", ast.value.operands[0].value
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
- 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 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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of StringToken, ast.value.operands[0]
475
- assert_equal "project", ast.value.operands[0].value
476
- assert_instance_of StringToken, ast.value.operands[1]
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of StringToken, ast.value.operands[0]
509
- assert_equal "mischa@", ast.value.operands[0].value
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
- assert_instance_of Operator, ast
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
- assert_instance_of StringToken, ast.value.operands[0]
522
- assert_equal "amy@example.com", ast.value.operands[0].value
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of StringToken, ast.value.operands[0]
547
- assert_equal "urgent", ast.value.operands[0].value
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
- assert_instance_of Operator, ast
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
- assert_instance_of StringToken, ast.value.operands[0]
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
- assert_instance_of Operator, ast
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
- assert_instance_of StringToken, ast.value.operands[0]
619
- assert_equal "mischa@", ast.value.operands[0].value
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