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.
@@ -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
- assert_equal "0.1.0", GmailSearchSyntax::VERSION
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
@@ -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 Text, ast.operands[0]
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 Text, ast.operands[1].child
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 Text, ast.left
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 Text, ast.right
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 Text, ast
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
- 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
194
189
  ast = GmailSearchSyntax.parse!('"dinner and movie tonight"')
195
- assert_instance_of Text, ast
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
- 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 Text, ast.value.operands[0]
207
- assert_equal "dinner", ast.value.operands[0].value
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
- assert_instance_of And, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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 Text, ast
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 Text, ast.operands[0]
305
+ assert_instance_of StringToken, ast.operands[0]
340
306
  assert_equal "project", ast.operands[0].value
341
307
 
342
- assert_instance_of Text, ast.operands[1]
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 Text, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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 Text, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Text, ast.value.operands[0]
475
- assert_equal "project", ast.value.operands[0].value
476
- assert_instance_of Text, ast.value.operands[1]
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 Text, ast.operands[0]
441
+ assert_instance_of StringToken, ast.operands[0]
488
442
  assert_equal "meeting", ast.operands[0].value
489
443
 
490
- assert_instance_of Text, ast.operands[1]
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Text, ast.value.operands[0]
509
- assert_equal "mischa@", ast.value.operands[0].value
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
- assert_instance_of Operator, ast
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
- assert_instance_of Text, ast.value.operands[0]
522
- assert_equal "amy@example.com", ast.value.operands[0].value
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
- assert_instance_of Operator, ast
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
- assert_instance_of Operator, ast
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
- assert_instance_of Text, ast.value.operands[0]
547
- assert_equal "urgent", ast.value.operands[0].value
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
- assert_instance_of Operator, ast
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
- assert_instance_of Text, ast.value.operands[0]
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 Text, ast.value.operands[1]
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
- assert_instance_of Operator, ast
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
- assert_instance_of Text, ast.value.operands[0]
619
- assert_equal "mischa@", ast.value.operands[0].value
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