1use crate::tokenizer::Span;
16
17#[derive(Debug, Clone, PartialEq)]
18pub struct GctfToken {
19 pub kind: GctfTokenKind,
20 pub line: usize,
21 pub span: Span,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub enum GctfTokenKind {
26 SectionHeader { name: String, raw_options: String },
27 Comment(String),
28 Blank,
29 Content(String),
30 AttributeBlock(String),
31}
32
33pub fn tokenize_gctf(source: &str) -> Vec<GctfToken> {
34 let mut tokens = Vec::new();
35 for (line_idx, line) in source.lines().enumerate() {
36 tokens.push(classify_line(line_idx, line));
37 }
38 tokens
39}
40
41fn classify_line(line_idx: usize, line: &str) -> GctfToken {
42 let bytes = line.as_bytes();
43 let len = bytes.len();
44 let mut pos = 0;
45
46 while pos < len && is_ws(bytes[pos]) {
47 pos += 1;
48 }
49
50 if pos == len {
51 return GctfToken {
52 kind: GctfTokenKind::Blank,
53 line: line_idx,
54 span: Span { start: 0, end: len },
55 };
56 }
57
58 if pos + 1 < len && bytes[pos] == b'#' && bytes[pos + 1] == b'[' {
59 return scan_attribute_block(line_idx, line, bytes, len);
60 }
61
62 if bytes[pos] == b'#' {
63 return GctfToken {
64 kind: GctfTokenKind::Comment(line.to_string()),
65 line: line_idx,
66 span: Span { start: 0, end: len },
67 };
68 }
69
70 if pos + 1 < len && bytes[pos] == b'/' && bytes[pos + 1] == b'/' {
71 return GctfToken {
72 kind: GctfTokenKind::Comment(line.to_string()),
73 line: line_idx,
74 span: Span { start: 0, end: len },
75 };
76 }
77
78 if let Some(header) = scan_section_header(line_idx, line, bytes, len) {
79 return header;
80 }
81
82 GctfToken {
83 kind: GctfTokenKind::Content(line.to_string()),
84 line: line_idx,
85 span: Span { start: 0, end: len },
86 }
87}
88
89fn scan_attribute_block(line_idx: usize, line: &str, bytes: &[u8], len: usize) -> GctfToken {
90 let mut pos = 0;
91
92 while pos < len && is_ws(bytes[pos]) {
93 pos += 1;
94 }
95
96 if pos + 1 >= len || bytes[pos] != b'#' || bytes[pos + 1] != b'[' {
97 return GctfToken {
98 kind: GctfTokenKind::Content(line.to_string()),
99 line: line_idx,
100 span: Span { start: 0, end: len },
101 };
102 }
103
104 pos += 2;
105
106 let content_start = pos;
107
108 while pos < len && bytes[pos] != b']' {
109 pos += 1;
110 }
111
112 let content_end = pos;
113
114 let content = if content_end > content_start {
115 slice_str(line, content_start, content_end)
116 } else {
117 String::new()
118 };
119
120 GctfToken {
121 kind: GctfTokenKind::AttributeBlock(content),
122 line: line_idx,
123 span: Span { start: 0, end: len },
124 }
125}
126
127fn scan_section_header(line_idx: usize, line: &str, bytes: &[u8], len: usize) -> Option<GctfToken> {
128 let mut pos = 0;
129
130 while pos < len && is_ws(bytes[pos]) {
131 pos += 1;
132 }
133
134 if pos + 2 >= len || bytes[pos] != b'-' || bytes[pos + 1] != b'-' || bytes[pos + 2] != b'-' {
135 return None;
136 }
137 pos += 3;
138
139 while pos < len && is_ws(bytes[pos]) {
140 pos += 1;
141 }
142
143 let name_start = pos;
144 while pos < len && is_section_name_char(bytes[pos]) {
145 pos += 1;
146 }
147 if pos == name_start {
148 return None;
149 }
150 let name = slice_str(line, name_start, pos);
151
152 let mut trailing = len;
153 while trailing > pos && is_ws(bytes[trailing - 1]) {
154 trailing -= 1;
155 }
156 if trailing < pos + 3
157 || bytes[trailing - 3] != b'-'
158 || bytes[trailing - 2] != b'-'
159 || bytes[trailing - 1] != b'-'
160 {
161 return None;
162 }
163 let options_end = trailing - 3;
164
165 let mut opts_start = pos;
166 while opts_start < options_end && is_ws(bytes[opts_start]) {
167 opts_start += 1;
168 }
169 let mut opts_end = options_end;
170 while opts_end > opts_start && is_ws(bytes[opts_end - 1]) {
171 opts_end -= 1;
172 }
173
174 let raw_options = if opts_start < opts_end {
175 slice_str(line, opts_start, opts_end)
176 } else {
177 String::new()
178 };
179
180 Some(GctfToken {
181 kind: GctfTokenKind::SectionHeader { name, raw_options },
182 line: line_idx,
183 span: Span { start: 0, end: len },
184 })
185}
186
187pub fn tokenize_kv_line(line: &str) -> Option<(String, String)> {
188 let bytes = line.as_bytes();
189 let len = bytes.len();
190 let mut pos = 0;
191
192 while pos < len && is_ws(bytes[pos]) {
193 pos += 1;
194 }
195
196 if pos == len {
197 return None;
198 }
199 if bytes[pos] == b'#' {
200 return None;
201 }
202 if pos + 1 < len && bytes[pos] == b'/' && bytes[pos + 1] == b'/' {
203 return None;
204 }
205
206 let key_start = pos;
207 while pos < len && bytes[pos] != b':' {
208 pos += 1;
209 }
210 if pos == len {
211 return None;
212 }
213
214 let mut key_end = pos;
215 while key_end > key_start && is_ws(bytes[key_end - 1]) {
216 key_end -= 1;
217 }
218 pos += 1;
219
220 while pos < len && is_ws(bytes[pos]) {
221 pos += 1;
222 }
223
224 let val_start = pos;
225 let mut val_end = len;
226 while val_end > val_start && is_ws(bytes[val_end - 1]) {
227 val_end -= 1;
228 }
229
230 Some((
231 slice_str(line, key_start, key_end),
232 slice_str(line, val_start, val_end),
233 ))
234}
235
236pub fn tokenize_extract_line(line: &str) -> Option<(String, String)> {
237 let (name, _, value) = tokenize_extract_line_full(line)?;
238 Some((name, value))
239}
240
241pub fn tokenize_extract_line_full(line: &str) -> Option<(String, Option<String>, String)> {
244 let bytes = line.as_bytes();
245 let len = bytes.len();
246 let mut pos = 0;
247
248 while pos < len && is_ws(bytes[pos]) {
249 pos += 1;
250 }
251
252 if pos == len {
253 return None;
254 }
255 if bytes[pos] == b'#' {
256 return None;
257 }
258 if pos + 1 < len && bytes[pos] == b'/' && bytes[pos + 1] == b'/' {
259 return None;
260 }
261
262 let name_start = pos;
263 while pos < len && bytes[pos] != b'=' {
264 pos += 1;
265 }
266 if pos == len {
267 return None;
268 }
269
270 let mut name_end = pos;
271 while name_end > name_start && is_ws(bytes[name_end - 1]) {
272 name_end -= 1;
273 }
274 let raw_name = slice_str(line, name_start, name_end);
275 pos += 1;
276
277 while pos < len && is_ws(bytes[pos]) {
278 pos += 1;
279 }
280
281 let val_start = pos;
282 let mut val_end = len;
283 while val_end > val_start && is_ws(bytes[val_end - 1]) {
284 val_end -= 1;
285 }
286 let value = slice_str(line, val_start, val_end);
287
288 let (name, type_annotation) = if let Some(colon_pos) = raw_name.rfind(':') {
290 let maybe_type = raw_name[colon_pos + 1..].trim();
291 if !maybe_type.is_empty()
292 && maybe_type
293 .chars()
294 .all(|c| c.is_ascii_alphanumeric() || c == '_')
295 {
296 (
297 raw_name[..colon_pos].trim().to_string(),
298 Some(maybe_type.to_string()),
299 )
300 } else {
301 (raw_name, None)
302 }
303 } else {
304 (raw_name, None)
305 };
306
307 Some((name, type_annotation, value))
308}
309
310pub fn tokenize_inline_options(raw: &str) -> Vec<(String, String)> {
311 let bytes = raw.as_bytes();
312 let len = bytes.len();
313 let mut pos = 0;
314 let mut result = Vec::new();
315
316 while pos < len {
317 while pos < len && is_ws(bytes[pos]) {
318 pos += 1;
319 }
320 if pos >= len {
321 break;
322 }
323
324 let tok_start = pos;
325 let mut in_quotes = false;
326 let mut escaped = false;
327
328 while pos < len {
329 if escaped {
330 escaped = false;
331 pos += 1;
332 continue;
333 }
334 match bytes[pos] {
335 b'\\' => {
336 escaped = true;
337 pos += 1;
338 }
339 b'"' => {
340 in_quotes = !in_quotes;
341 pos += 1;
342 }
343 b' ' | b'\t' if !in_quotes => break,
344 _ => pos += 1,
345 }
346 }
347 let tok_end = pos;
348
349 let token = slice_str(raw, tok_start, tok_end);
350
351 let mut eq_pos = None;
352 let tb = token.as_bytes();
353 for (i, &b) in tb.iter().enumerate() {
354 if b == b'=' {
355 eq_pos = Some(i);
356 break;
357 }
358 }
359
360 if let Some(eq) = eq_pos {
361 let mut key_end = eq;
362 while key_end > 0 && is_ws(tb[key_end - 1]) {
363 key_end -= 1;
364 }
365 let mut val_start = eq + 1;
366 while val_start < tb.len() && is_ws(tb[val_start]) {
367 val_start += 1;
368 }
369 let mut val_end = tb.len();
370 while val_end > val_start && is_ws(tb[val_end - 1]) {
371 val_end -= 1;
372 }
373
374 let mut key = slice_str(&token, 0, key_end);
375 let mut value = slice_str(&token, val_start, val_end);
376
377 strip_outer_quotes(&mut key);
378 strip_outer_quotes(&mut value);
379
380 result.push((key, value));
381 } else {
382 let mut key = token;
383 strip_outer_quotes(&mut key);
384 result.push((key, "true".to_string()));
385 }
386 }
387
388 result
389}
390
391fn strip_outer_quotes(s: &mut String) {
392 if s.len() >= 2 {
393 let b = s.as_bytes();
394 if (b[0] == b'"' && b[s.len() - 1] == b'"') || (b[0] == b'\'' && b[s.len() - 1] == b'\'') {
395 *s = s[1..s.len() - 1].to_string();
396 }
397 }
398}
399
400#[inline]
401fn is_ws(b: u8) -> bool {
402 matches!(b, b' ' | b'\t')
403}
404
405#[inline]
406fn is_section_name_char(b: u8) -> bool {
407 b.is_ascii_uppercase() || b == b'_'
408}
409
410#[inline]
411fn slice_str(s: &str, start: usize, end: usize) -> String {
412 if start >= end {
413 return String::new();
414 }
415 s[start..end].to_string()
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn test_tokenize_empty() {
424 let tokens = tokenize_gctf("");
425 assert!(tokens.is_empty());
426 }
427
428 #[test]
429 fn test_tokenize_blank_lines() {
430 let tokens = tokenize_gctf("\n\n \n");
431 assert_eq!(tokens.len(), 3);
432 assert!(matches!(tokens[0].kind, GctfTokenKind::Blank));
433 assert!(matches!(tokens[1].kind, GctfTokenKind::Blank));
434 assert!(matches!(tokens[2].kind, GctfTokenKind::Blank));
435 }
436
437 #[test]
438 fn test_tokenize_comments() {
439 let tokens = tokenize_gctf("# hello\n// world");
440 assert_eq!(tokens.len(), 2);
441 assert!(matches!(&tokens[0].kind, GctfTokenKind::Comment(t) if t == "# hello"));
442 assert!(matches!(&tokens[1].kind, GctfTokenKind::Comment(t) if t == "// world"));
443 }
444
445 #[test]
446 fn test_tokenize_section_headers() {
447 let tokens = tokenize_gctf("--- ENDPOINT ---\n--- RESPONSE partial=true ---");
448 assert_eq!(tokens.len(), 2);
449
450 match &tokens[0].kind {
451 GctfTokenKind::SectionHeader { name, raw_options } => {
452 assert_eq!(name, "ENDPOINT");
453 assert_eq!(raw_options, "");
454 }
455 _ => panic!("expected SectionHeader"),
456 }
457
458 match &tokens[1].kind {
459 GctfTokenKind::SectionHeader { name, raw_options } => {
460 assert_eq!(name, "RESPONSE");
461 assert_eq!(raw_options, "partial=true");
462 }
463 _ => panic!("expected SectionHeader"),
464 }
465 }
466
467 #[test]
468 fn test_tokenize_content() {
469 let tokens = tokenize_gctf("hello world\n{\"key\": \"value\"}");
470 assert_eq!(tokens.len(), 2);
471 assert!(matches!(&tokens[0].kind, GctfTokenKind::Content(t) if t == "hello world"));
472 assert!(
473 matches!(&tokens[1].kind, GctfTokenKind::Content(t) if t == "{\"key\": \"value\"}")
474 );
475 }
476
477 #[test]
478 fn test_tokenize_not_section_header() {
479 let tokens = tokenize_gctf("--- not uppercase ---\n---ABC");
480 assert!(matches!(tokens[0].kind, GctfTokenKind::Content(_)));
481 assert!(matches!(tokens[1].kind, GctfTokenKind::Content(_)));
482 }
483
484 #[test]
485 fn test_tokenize_full_document() {
486 let input = r#"--- ENDPOINT ---
487test.Service/Method
488
489--- REQUEST ---
490{}
491
492--- ASSERTS ---
493.x == 1
494"#;
495 let tokens = tokenize_gctf(input);
496 let mut kinds: Vec<&str> = Vec::new();
497 for t in &tokens {
498 match &t.kind {
499 GctfTokenKind::SectionHeader { .. } => kinds.push("H"),
500 GctfTokenKind::Comment(_) => kinds.push("C"),
501 GctfTokenKind::Blank => kinds.push("_"),
502 GctfTokenKind::Content(_) => kinds.push("T"),
503 GctfTokenKind::AttributeBlock(_) => kinds.push("A"),
504 }
505 }
506 assert_eq!(kinds, vec!["H", "T", "_", "H", "T", "_", "H", "T"]);
507 }
508
509 #[test]
510 fn test_tokenize_section_header_with_multiple_options() {
511 let tokens = tokenize_gctf("--- RESPONSE partial=true tolerance=0.1 ---");
512 match &tokens[0].kind {
513 GctfTokenKind::SectionHeader { name, raw_options } => {
514 assert_eq!(name, "RESPONSE");
515 assert_eq!(raw_options, "partial=true tolerance=0.1");
516 }
517 _ => panic!("expected SectionHeader"),
518 }
519 }
520
521 #[test]
522 fn test_kv_line_basic() {
523 let (key, value) = tokenize_kv_line("Authorization: Bearer token").unwrap();
524 assert_eq!(key, "Authorization");
525 assert_eq!(value, "Bearer token");
526 }
527
528 #[test]
529 fn test_kv_line_with_whitespace() {
530 let (key, value) = tokenize_kv_line(" key : value ").unwrap();
531 assert_eq!(key, "key");
532 assert_eq!(value, "value");
533 }
534
535 #[test]
536 fn test_kv_line_comment() {
537 assert_eq!(tokenize_kv_line("# comment"), None);
538 assert_eq!(tokenize_kv_line("// comment"), None);
539 }
540
541 #[test]
542 fn test_kv_line_empty() {
543 assert_eq!(tokenize_kv_line(""), None);
544 assert_eq!(tokenize_kv_line(" "), None);
545 }
546
547 #[test]
548 fn test_kv_line_no_colon() {
549 assert_eq!(tokenize_kv_line("no colon here"), None);
550 }
551
552 #[test]
553 fn test_extract_line_basic() {
554 let (name, value) = tokenize_extract_line("total = .response.total").unwrap();
555 assert_eq!(name, "total");
556 assert_eq!(value, ".response.total");
557 }
558
559 #[test]
560 fn test_extract_line_comment() {
561 assert_eq!(tokenize_extract_line("# comment"), None);
562 assert_eq!(tokenize_extract_line("// comment"), None);
563 }
564
565 #[test]
566 fn test_extract_line_empty() {
567 assert_eq!(tokenize_extract_line(""), None);
568 }
569
570 #[test]
571 fn test_tokenize_inline_options_basic() {
572 let opts = tokenize_inline_options("key1=value1 key2=value2");
573 assert_eq!(opts.len(), 2);
574 assert_eq!(opts[0], ("key1".into(), "value1".into()));
575 assert_eq!(opts[1], ("key2".into(), "value2".into()));
576 }
577
578 #[test]
579 fn test_tokenize_inline_options_quoted() {
580 let opts = tokenize_inline_options(r#"key="hello world""#);
581 assert_eq!(opts.len(), 1);
582 assert_eq!(opts[0], ("key".into(), "hello world".into()));
583 }
584
585 #[test]
586 fn test_tokenize_inline_options_boolean_short_form() {
587 let opts = tokenize_inline_options("partial");
588 assert_eq!(opts.len(), 1);
589 assert_eq!(opts[0], ("partial".into(), "true".into()));
590 }
591
592 #[test]
593 fn test_tokenize_inline_options_complex() {
594 let opts = tokenize_inline_options("with_asserts=true partial=false tolerance=0.1");
595 assert_eq!(opts.len(), 3);
596 assert_eq!(opts[0], ("with_asserts".into(), "true".into()));
597 assert_eq!(opts[1], ("partial".into(), "false".into()));
598 assert_eq!(opts[2], ("tolerance".into(), "0.1".into()));
599 }
600
601 #[test]
602 fn test_line_numbers() {
603 let tokens = tokenize_gctf("line0\nline1\nline2");
604 assert_eq!(tokens[0].line, 0);
605 assert_eq!(tokens[1].line, 1);
606 assert_eq!(tokens[2].line, 2);
607 }
608
609 #[test]
612 fn test_section_header_meta() {
613 let tokens = tokenize_gctf("--- META ---\nname: Test");
614 let GctfTokenKind::SectionHeader { name, .. } = &tokens[0].kind else {
615 panic!()
616 };
617 assert_eq!(name, "META");
618 }
619
620 #[test]
621 fn test_section_header_no_closing_dashes() {
622 let tokens = tokenize_gctf("--- ENDPOINT");
623 assert!(matches!(tokens[0].kind, GctfTokenKind::Content(_)));
624 }
625
626 #[test]
627 fn test_section_header_only_dashes() {
628 let tokens = tokenize_gctf("------");
629 assert!(matches!(tokens[0].kind, GctfTokenKind::Content(_)));
630 }
631
632 #[test]
633 fn test_section_header_leading_whitespace() {
634 let tokens = tokenize_gctf(" --- ENDPOINT ---");
635 match &tokens[0].kind {
636 GctfTokenKind::SectionHeader { name, .. } => assert_eq!(name, "ENDPOINT"),
637 _ => panic!("expected SectionHeader"),
638 }
639 }
640
641 #[test]
642 fn test_section_header_extra_whitespace() {
643 let tokens = tokenize_gctf("--- RESPONSE partial=true ---");
644 match &tokens[0].kind {
645 GctfTokenKind::SectionHeader { name, raw_options } => {
646 assert_eq!(name, "RESPONSE");
647 assert_eq!(raw_options, "partial=true");
648 }
649 _ => panic!("expected SectionHeader"),
650 }
651 }
652
653 #[test]
654 fn test_section_header_lowercase_rejected() {
655 let tokens = tokenize_gctf("--- endpoint ---");
656 assert!(matches!(tokens[0].kind, GctfTokenKind::Content(_)));
657 }
658
659 #[test]
660 fn test_section_header_mixed_case_treated_as_partial_name() {
661 let tokens = tokenize_gctf("--- Endpoint ---");
662 match &tokens[0].kind {
663 GctfTokenKind::SectionHeader { name, .. } => assert_eq!(name, "E"),
664 _ => panic!("expected SectionHeader with truncated name"),
665 }
666 }
667
668 #[test]
669 fn test_section_header_fully_lowercase_rejected() {
670 let tokens = tokenize_gctf("--- endpoint ---");
671 assert!(matches!(tokens[0].kind, GctfTokenKind::Content(_)));
672 }
673
674 #[test]
675 fn test_section_header_with_underscore() {
676 let tokens = tokenize_gctf("--- REQUEST_HEADERS ---");
677 match &tokens[0].kind {
678 GctfTokenKind::SectionHeader { name, .. } => assert_eq!(name, "REQUEST_HEADERS"),
679 _ => panic!("expected SectionHeader"),
680 }
681 }
682
683 #[test]
684 fn test_three_dashes_in_content() {
685 let tokens = tokenize_gctf("---ABC");
686 assert!(matches!(tokens[0].kind, GctfTokenKind::Content(_)));
687 }
688
689 #[test]
690 fn test_comment_with_leading_whitespace() {
691 let tokens = tokenize_gctf(" # indented comment");
692 assert!(
693 matches!(&tokens[0].kind, GctfTokenKind::Comment(t) if t == " # indented comment")
694 );
695 }
696
697 #[test]
698 fn test_slash_slash_not_at_start_is_content() {
699 let tokens = tokenize_gctf("foo // bar");
700 assert!(matches!(&tokens[0].kind, GctfTokenKind::Content(t) if t == "foo // bar"));
701 }
702
703 #[test]
704 fn test_tab_only_line_is_blank() {
705 let tokens = tokenize_gctf("\t\t");
706 assert!(matches!(tokens[0].kind, GctfTokenKind::Blank));
707 }
708
709 #[test]
712 fn test_kv_line_empty_value() {
713 let (key, value) = tokenize_kv_line("key:").unwrap();
714 assert_eq!(key, "key");
715 assert_eq!(value, "");
716 }
717
718 #[test]
719 fn test_kv_line_colon_in_value() {
720 let (key, value) = tokenize_kv_line("url: http://host:8080").unwrap();
721 assert_eq!(key, "url");
722 assert_eq!(value, "http://host:8080");
723 }
724
725 #[test]
726 fn test_kv_line_value_with_spaces() {
727 let (key, value) = tokenize_kv_line(" cert : /path/to/cert.pem ").unwrap();
728 assert_eq!(key, "cert");
729 assert_eq!(value, "/path/to/cert.pem");
730 }
731
732 #[test]
733 fn test_kv_line_tab_separator() {
734 let (key, value) = tokenize_kv_line("key\t:\tvalue").unwrap();
735 assert_eq!(key, "key");
736 assert_eq!(value, "value");
737 }
738
739 #[test]
740 fn test_kv_line_only_whitespace_key_produces_empty_key() {
741 let result = tokenize_kv_line(" : value");
742 assert!(result.is_some());
743 let (key, value) = result.unwrap();
744 assert_eq!(key, "");
745 assert_eq!(value, "value");
746 }
747
748 #[test]
751 fn test_extract_line_with_spaces() {
752 let (name, value) = tokenize_extract_line(" total = .response.total ").unwrap();
753 assert_eq!(name, "total");
754 assert_eq!(value, ".response.total");
755 }
756
757 #[test]
758 fn test_extract_line_empty_value() {
759 let (name, value) = tokenize_extract_line("name=").unwrap();
760 assert_eq!(name, "name");
761 assert_eq!(value, "");
762 }
763
764 #[test]
765 fn test_extract_line_no_equals() {
766 assert_eq!(tokenize_extract_line(".just.a.path"), None);
767 }
768
769 #[test]
770 fn test_extract_line_whitespace_only() {
771 assert_eq!(tokenize_extract_line(" "), None);
772 }
773
774 #[test]
775 fn test_extract_line_only_whitespace_value() {
776 let (name, value) = tokenize_extract_line("name= ").unwrap();
777 assert_eq!(name, "name");
778 assert_eq!(value, "");
779 }
780
781 #[test]
784 fn test_tokenize_options_empty() {
785 let opts = tokenize_inline_options("");
786 assert!(opts.is_empty());
787 }
788
789 #[test]
790 fn test_tokenize_options_only_spaces() {
791 let opts = tokenize_inline_options(" ");
792 assert!(opts.is_empty());
793 }
794
795 #[test]
796 fn test_tokenize_options_escaped_char() {
797 let opts = tokenize_inline_options(r#"key=va\"lue"#);
798 assert_eq!(opts.len(), 1);
799 assert_eq!(opts[0].0, "key");
800 }
801
802 #[test]
803 fn test_tokenize_options_single_quotes_not_special() {
804 let opts = tokenize_inline_options("key='hello world'");
805 assert_eq!(opts.len(), 2);
806 assert_eq!(opts[0], ("key".into(), "'hello".into()));
807 }
808
809 #[test]
810 fn test_tokenize_options_array_value() {
811 let opts = tokenize_inline_options(r#"redact=["field1","field2"]"#);
812 assert_eq!(opts.len(), 1);
813 assert_eq!(opts[0].0, "redact");
814 assert!(opts[0].1.contains("field1"));
815 }
816
817 #[test]
818 fn test_tokenize_options_multiple_spaces() {
819 let opts = tokenize_inline_options(" a=1 b=2 ");
820 assert_eq!(opts.len(), 2);
821 assert_eq!(opts[0], ("a".into(), "1".into()));
822 assert_eq!(opts[1], ("b".into(), "2".into()));
823 }
824
825 #[test]
826 fn test_tokenize_options_tab_separated() {
827 let opts = tokenize_inline_options("a=1\tb=2");
828 assert_eq!(opts.len(), 2);
829 }
830
831 #[test]
832 fn test_tokenize_options_quoted_key() {
833 let opts = tokenize_inline_options(r#""key"=value"#);
834 assert_eq!(opts.len(), 1);
835 assert_eq!(opts[0].0, "key");
836 }
837
838 #[test]
839 fn test_tokenize_options_empty_value() {
840 let opts = tokenize_inline_options("key=");
841 assert_eq!(opts.len(), 1);
842 assert_eq!(opts[0], ("key".into(), "".into()));
843 }
844
845 #[test]
848 fn test_span_tracking() {
849 let tokens = tokenize_gctf("# comment\n--- ENDPOINT ---\nhello");
850 assert_eq!(tokens[0].span, Span { start: 0, end: 9 });
851 assert_eq!(tokens[1].span, Span { start: 0, end: 16 });
852 assert_eq!(tokens[2].span, Span { start: 0, end: 5 });
853 }
854
855 #[test]
856 fn test_gctf_token_kind_equality() {
857 let t1 = GctfToken {
858 kind: GctfTokenKind::Blank,
859 line: 0,
860 span: Span { start: 0, end: 0 },
861 };
862 let t2 = GctfToken {
863 kind: GctfTokenKind::Blank,
864 line: 1,
865 span: Span { start: 0, end: 0 },
866 };
867 assert_ne!(t1, t2); }
869
870 #[test]
871 fn test_tokenize_attribute_block() {
872 let tokens = tokenize_gctf("#[timeout(30)]");
873 assert_eq!(tokens.len(), 1);
874 match &tokens[0].kind {
875 GctfTokenKind::AttributeBlock(content) => {
876 assert_eq!(content, "timeout(30)");
877 }
878 _ => panic!("expected AttributeBlock"),
879 }
880 }
881
882 #[test]
883 fn test_tokenize_attribute_block_with_spaces() {
884 let tokens = tokenize_gctf("#[ retry(3) ]");
885 assert_eq!(tokens.len(), 1);
886 match &tokens[0].kind {
887 GctfTokenKind::AttributeBlock(content) => {
888 assert_eq!(content, " retry(3) ");
889 }
890 _ => panic!("expected AttributeBlock"),
891 }
892 }
893
894 #[test]
895 fn test_tokenize_attribute_block_flag() {
896 let tokens = tokenize_gctf("#[skip]");
897 assert_eq!(tokens.len(), 1);
898 match &tokens[0].kind {
899 GctfTokenKind::AttributeBlock(content) => {
900 assert_eq!(content, "skip");
901 }
902 _ => panic!("expected AttributeBlock"),
903 }
904 }
905
906 #[test]
907 fn test_tokenize_attribute_block_empty() {
908 let tokens = tokenize_gctf("#[]");
909 assert_eq!(tokens.len(), 1);
910 match &tokens[0].kind {
911 GctfTokenKind::AttributeBlock(content) => {
912 assert_eq!(content, "");
913 }
914 _ => panic!("expected AttributeBlock"),
915 }
916 }
917
918 #[test]
919 fn test_tokenize_attribute_block_quoted_value() {
920 let tokens = tokenize_gctf(r#"#[tag("smoke, slow")]"#);
921 assert_eq!(tokens.len(), 1);
922 match &tokens[0].kind {
923 GctfTokenKind::AttributeBlock(content) => {
924 assert_eq!(content, r#"tag("smoke, slow")"#);
925 }
926 _ => panic!("expected AttributeBlock"),
927 }
928 }
929
930 #[test]
931 fn test_tokenize_attribute_before_section() {
932 let tokens = tokenize_gctf("#[timeout(30)]\n--- REQUEST ---");
933 assert_eq!(tokens.len(), 2);
934 match &tokens[0].kind {
935 GctfTokenKind::AttributeBlock(content) => {
936 assert_eq!(content, "timeout(30)");
937 }
938 _ => panic!("expected AttributeBlock"),
939 }
940 match &tokens[1].kind {
941 GctfTokenKind::SectionHeader { name, .. } => {
942 assert_eq!(name, "REQUEST");
943 }
944 _ => panic!("expected SectionHeader"),
945 }
946 }
947
948 #[test]
949 fn test_tokenize_attribute_after_section() {
950 let tokens = tokenize_gctf("--- REQUEST ---\n{}\n#[skip]");
951 assert_eq!(tokens.len(), 3);
952 match &tokens[0].kind {
953 GctfTokenKind::SectionHeader { name, .. } => {
954 assert_eq!(name, "REQUEST");
955 }
956 _ => panic!("expected SectionHeader"),
957 }
958 match &tokens[2].kind {
959 GctfTokenKind::AttributeBlock(content) => {
960 assert_eq!(content, "skip");
961 }
962 _ => panic!("expected AttributeBlock"),
963 }
964 }
965}