Skip to main content

apif_parser/
core.rs

1// GCTF file parser - converts .gctf text to AST
2// Handles section extraction, comment removal, and inline option parsing
3
4use crate::content_parser::{self, parse_attribute};
5use anyhow::{Context, Result};
6use apif_ast::gctf_tokenizer::{GctfTokenKind, tokenize_gctf};
7use apif_ast::*;
8use serde::Serialize;
9use std::collections::HashMap;
10use std::fs;
11use std::path::Path;
12use std::time::Instant;
13
14type CurrentSection = Option<(
15    SectionType,
16    usize,
17    Vec<String>,
18    InlineOptions,
19    Vec<GctfAttribute>,
20)>;
21
22/// Parse a .gctf file into an AST
23pub fn parse_gctf(file_path: &Path) -> Result<GctfDocument> {
24    let (document, _) = parse_gctf_with_diagnostics(file_path)?;
25    Ok(document)
26}
27
28/// Parse .gctf content from string (for LSP/editor use).
29/// Documents are determined implicitly: REQUEST after RESPONSE/ERROR/ASSERTS,
30/// or ENDPOINT/ADDRESS starts a new document.
31pub fn parse_gctf_from_str(content: &str, file_path: &str) -> Result<GctfDocument> {
32    let (all_sections, _) = parse_sections_from_str(content)?;
33    let source_lines: Vec<&str> = content.lines().collect();
34
35    // Split sections into documents based on implicit boundaries
36    let documents = crate::document_splitter::split_sections_by_boundary_owned(all_sections);
37
38    if documents.is_empty() {
39        // Return empty single document for backward compatibility
40        let mut document = GctfDocument::new(file_path.to_string());
41        document.metadata.source = Some(content.to_string());
42        return Ok(document);
43    }
44
45    // Build chain in reverse order
46    let mut head: Option<GctfDocument> = None;
47
48    for doc_sections in documents.into_iter().rev() {
49        let mut document = GctfDocument::new(file_path.to_string());
50        document.metadata.source =
51            Some(extract_doc_source_from_lines(&doc_sections, &source_lines));
52        document.sections = doc_sections;
53        document.next_document = head.map(Box::new);
54        head = Some(document);
55    }
56
57    head.ok_or_else(|| anyhow::anyhow!("No documents parsed"))
58}
59
60/// Split sections into documents based on implicit boundaries.
61///
62/// Extract source lines for a document from the original content.
63fn extract_doc_source_from_lines(sections: &[Section], lines: &[&str]) -> String {
64    if sections.is_empty() {
65        return String::new();
66    }
67
68    let (start, end) = match (sections.first(), sections.last()) {
69        (Some(first), Some(last)) => (first.start_line, last.end_line),
70        _ => return String::new(),
71    };
72    lines.get(start..end).unwrap_or(&[]).join("\n")
73}
74
75#[derive(Debug, Clone, Serialize, Default)]
76pub struct ParseTimings {
77    pub read_ms: f64,
78    pub parse_sections_ms: f64,
79    pub build_document_ms: f64,
80    pub total_ms: f64,
81}
82
83#[derive(Debug, Clone, Serialize, Default)]
84pub struct ParseDiagnostics {
85    pub file_path: String,
86    pub bytes: usize,
87    pub total_lines: usize,
88    pub section_headers: usize,
89    pub section_counts: HashMap<String, usize>,
90    pub timings: ParseTimings,
91}
92
93/// Parse .gctf and return AST + diagnostics useful for inspect/debug.
94/// Supports multiple documents with implicit boundaries (ENDPOINT after terminal section).
95pub fn parse_gctf_with_diagnostics(file_path: &Path) -> Result<(GctfDocument, ParseDiagnostics)> {
96    let total_start = Instant::now();
97
98    let read_start = Instant::now();
99    let source = fs::read_to_string(file_path)
100        .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
101    let read_ms = read_start.elapsed().as_secs_f64() * 1000.0;
102
103    let parse_sections_start = Instant::now();
104    let (sections, section_headers) = parse_sections_from_str(&source)?;
105    let parse_sections_ms = parse_sections_start.elapsed().as_secs_f64() * 1000.0;
106
107    // Split into documents using implicit boundaries
108    let documents = crate::document_splitter::split_sections_by_boundary_owned(sections);
109
110    let build_start = Instant::now();
111    // Build chain — return head document
112    let mut head: Option<GctfDocument> = None;
113    for doc_sections in documents.into_iter().rev() {
114        let mut document = GctfDocument::new(file_path.display().to_string());
115        document.metadata.source = Some(source.clone());
116        document.sections = doc_sections;
117        document.next_document = head.map(Box::new);
118        head = Some(document);
119    }
120
121    let document = head.unwrap_or_else(|| {
122        let mut doc = GctfDocument::new(file_path.display().to_string());
123        doc.metadata.source = Some(source.clone());
124        doc
125    });
126    let build_ms = build_start.elapsed().as_secs_f64() * 1000.0;
127    let total_ms = total_start.elapsed().as_secs_f64() * 1000.0;
128
129    let mut section_counts: HashMap<String, usize> = HashMap::new();
130    for d in document.iter_chain() {
131        for section in &d.sections {
132            *section_counts
133                .entry(section.section_type.as_str().to_string())
134                .or_insert(0) += 1;
135        }
136    }
137
138    let diagnostics = ParseDiagnostics {
139        file_path: file_path.display().to_string(),
140        bytes: source.len(),
141        total_lines: source.lines().count(),
142        section_headers,
143        section_counts,
144        timings: ParseTimings {
145            read_ms,
146            parse_sections_ms,
147            build_document_ms: build_ms,
148            total_ms,
149        },
150    };
151
152    Ok((document, diagnostics))
153}
154
155fn parse_sections_from_str(source: &str) -> Result<(Vec<Section>, usize)> {
156    let tokens = tokenize_gctf(source);
157    let mut sections = Vec::new();
158    let mut section_headers = 0;
159    let mut current_section: CurrentSection = None;
160    let mut pending_attributes: Vec<GctfAttribute> = Vec::new();
161
162    for token in tokens {
163        match token.kind {
164            GctfTokenKind::SectionHeader { name, raw_options } => {
165                if let Some((section_type, start_line, content, options, raw_attrs)) =
166                    current_section.take()
167                {
168                    let section = content_parser::build_section(
169                        section_type,
170                        start_line,
171                        token.line,
172                        &content,
173                        options,
174                        raw_attrs,
175                    )?;
176                    sections.push(section);
177                }
178
179                section_headers += 1;
180
181                if let Some(section_type) = SectionType::from_keyword(&name) {
182                    let inline_options =
183                        if section_type.supports_inline_options() && !raw_options.is_empty() {
184                            content_parser::parse_inline_options(&raw_options)?
185                        } else {
186                            InlineOptions::default()
187                        };
188                    current_section = Some((
189                        section_type,
190                        token.line,
191                        Vec::new(),
192                        inline_options,
193                        std::mem::take(&mut pending_attributes),
194                    ));
195                } else {
196                    return Err(anyhow::anyhow!("Unknown section type: {}", name));
197                }
198            }
199            GctfTokenKind::AttributeBlock(attr_content) => {
200                if let Some(attr) = parse_attribute(&attr_content) {
201                    pending_attributes.push(attr);
202                }
203            }
204            GctfTokenKind::Comment(text) | GctfTokenKind::Content(text) => {
205                if let Some((_, _, ref mut content, _, _)) = current_section {
206                    content.push(text);
207                }
208            }
209            GctfTokenKind::Blank => {
210                if let Some((_, _, ref mut content, _, _)) = current_section {
211                    content.push(String::new());
212                }
213            }
214        }
215    }
216
217    if let Some((section_type, start_line, content, options, raw_attrs)) = current_section {
218        let end_line = source.lines().count();
219        let section = content_parser::build_section(
220            section_type,
221            start_line,
222            end_line,
223            &content,
224            options,
225            raw_attrs,
226        )?;
227        sections.push(section);
228    }
229    Ok((sections, section_headers))
230}
231
232/// Format/serialize a GCTF document to string
233pub fn serialize_gctf(doc: &GctfDocument) -> String {
234    use std::fmt::Write;
235    let mut output = String::new();
236
237    let sections = sort_sections_for_fmt(&doc.sections);
238
239    for section in &sections {
240        for attr in &section.attributes {
241            let _ = writeln!(output, "{}", attr.format_directive());
242        }
243
244        let _ = write!(output, "--- {} ---", section.section_type.as_str());
245        output.push('\n');
246
247        match &section.content {
248            SectionContent::Single(s) => {
249                let _ = writeln!(output, "{}", s.trim());
250            }
251            SectionContent::Json(val) => {
252                if let Ok(pretty) = serde_json::to_string_pretty(val) {
253                    let _ = writeln!(output, "{}", pretty);
254                } else {
255                    let raw = section.raw_content.trim();
256                    let _ = writeln!(output, "{}", raw);
257                }
258            }
259            SectionContent::JsonLines(lines) => {
260                for val in lines {
261                    if let Ok(compact) = serde_json::to_string(val) {
262                        let _ = writeln!(output, "{}", compact);
263                    }
264                }
265            }
266            SectionContent::KeyValues(kv) => {
267                let mut sorted: Vec<_> = kv.iter().collect();
268                if section.section_type == SectionType::Bench {
269                    sorted.sort_by(|a, b| {
270                        bench_key_rank(a.0)
271                            .cmp(&bench_key_rank(b.0))
272                            .then_with(|| a.0.cmp(b.0))
273                    });
274                } else {
275                    sorted.sort_by(|a, b| a.0.cmp(b.0));
276                }
277                for (k, v) in sorted {
278                    let _ = writeln!(output, "{}: {}", k, v);
279                }
280            }
281            SectionContent::Assertions(lines) => {
282                for line in lines {
283                    let _ = writeln!(output, "{}", line.trim());
284                }
285            }
286            SectionContent::Empty => {}
287            SectionContent::Extract(vars) => {
288                let mut sorted: Vec<_> = vars.iter().collect();
289                sorted.sort_by(|a, b| a.0.cmp(b.0));
290                for (k, v) in sorted {
291                    let _ = writeln!(output, "{}: {}", k, v);
292                }
293            }
294            SectionContent::Meta(meta) => {
295                if let Ok(yaml) = serde_yaml_ng::to_string(meta) {
296                    output.push_str(yaml.trim_end());
297                }
298            }
299        }
300        output.push('\n');
301    }
302
303    output.trim_end().to_string() + "\n"
304}
305
306fn sort_sections_for_fmt(sections: &[Section]) -> Vec<Section> {
307    if sections.len() <= 1 {
308        return sections.to_vec();
309    }
310
311    let first_body_idx = sections
312        .iter()
313        .position(|s| s.section_type.preamble_rank().is_none())
314        .unwrap_or(sections.len());
315
316    let mut preamble: Vec<&Section> = sections[..first_body_idx].iter().collect();
317    preamble.sort_by_key(|s| s.section_type.preamble_rank().unwrap());
318
319    let mut result = Vec::with_capacity(sections.len());
320    for s in &preamble {
321        result.push((*s).clone());
322    }
323    for s in &sections[first_body_idx..] {
324        result.push((*s).clone());
325    }
326    result
327}
328
329fn bench_key_rank(key: &str) -> usize {
330    let canonical_order = [
331        "mode",
332        "profile",
333        "name",
334        "concurrency",
335        "requests",
336        "duration",
337        "max_duration",
338        "ramp_up",
339        "warmup",
340        "warmup_mode",
341        "cool_down",
342        "max_rps",
343        "load_schedule",
344        "load_start",
345        "load_step",
346        "load_end",
347        "load_step_duration",
348        "load_max_duration",
349        "load_midpoint",
350        "load_amplitude",
351        "load_frequency",
352        "load_spike_target",
353        "load_spike_after",
354        "load_spike_duration",
355        "load_profile",
356        "progress_interval",
357        "connections",
358        "connect_timeout",
359        "keepalive",
360        "cpus",
361        "assert_mode",
362        "no_assert",
363        "sample_rate",
364        "duration_stop",
365        "cache",
366        "cache_ttl",
367        "skip_first",
368        "count_errors_in_latency",
369        "latency_percentiles",
370        "sources",
371    ];
372
373    if let Some((idx, _)) = canonical_order.iter().enumerate().find(|(_, k)| **k == key) {
374        return idx;
375    }
376    if key.starts_with("thresholds.") || key == "thresholds" {
377        return canonical_order.len();
378    }
379    usize::MAX
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn test_parse_sections_basic() {
388        let input = "\
389--- ENDPOINT ---
390test.Service/Method
391
392--- REQUEST ---
393{}
394
395--- RESPONSE ---
396{}
397";
398        let (sections, count) = parse_sections_from_str(input).unwrap();
399        assert_eq!(count, 3);
400        assert_eq!(sections.len(), 3);
401    }
402
403    #[test]
404    fn test_section_header_tokenizer() {
405        let input = "\
406--- ENDPOINT ---
407test.Service/Method
408
409--- REQUEST ---
410{}
411
412--- RESPONSE partial=true ---
413{}
414";
415        let (sections, count) = parse_sections_from_str(input).unwrap();
416        assert_eq!(count, 3);
417        assert_eq!(sections.len(), 3);
418
419        let resp = sections
420            .iter()
421            .find(|s| s.section_type == SectionType::Response)
422            .unwrap();
423        assert!(resp.inline_options.partial);
424    }
425
426    #[test]
427    fn test_parse_multi_document() {
428        let input = "\
429--- ENDPOINT ---
430test.Service/Method
431
432--- REQUEST ---
433{}
434
435--- RESPONSE ---
436{}
437
438--- ENDPOINT ---
439test.Service/Method2
440
441--- REQUEST ---
442{\"a\": 1}
443
444--- RESPONSE ---
445{\"b\": 2}
446";
447        let doc = parse_gctf_from_str(input, "test.gctf").unwrap();
448        assert_eq!(doc.document_count(), 2);
449
450        let first_endpoint = doc.get_endpoint().unwrap();
451        assert_eq!(first_endpoint, "test.Service/Method");
452
453        let second = doc.get_document(1).unwrap();
454        assert_eq!(second.get_endpoint().unwrap(), "test.Service/Method2");
455    }
456
457    #[test]
458    fn test_parse_empty_content() {
459        let doc = parse_gctf_from_str("", "test.gctf").unwrap();
460        assert!(doc.sections.is_empty());
461    }
462
463    #[test]
464    fn test_parse_all_section_types() {
465        let input = "\
466--- ADDRESS ---
467localhost:50051
468
469--- ENDPOINT ---
470test.Service/Method
471
472--- TLS ---
473ca_cert: /path/ca.pem
474
475--- PROTO ---
476files: service.proto
477
478--- OPTIONS ---
479timeout: 10
480
481--- REQUEST_HEADERS ---
482Authorization: Bearer token
483
484--- REQUEST ---
485{}
486
487--- RESPONSE ---
488{}
489
490--- ASSERTS ---
491.x == 1
492
493--- EXTRACT ---
494total = .response.total
495";
496        let (sections, count) = parse_sections_from_str(input).unwrap();
497        assert_eq!(count, 10);
498
499        let types: Vec<SectionType> = sections.iter().map(|s| s.section_type).collect();
500        assert_eq!(types[0], SectionType::Address);
501        assert_eq!(types[1], SectionType::Endpoint);
502        assert_eq!(types[2], SectionType::Tls);
503        assert_eq!(types[3], SectionType::Proto);
504        assert_eq!(types[4], SectionType::Options);
505        assert_eq!(types[5], SectionType::RequestHeaders);
506        assert_eq!(types[6], SectionType::Request);
507        assert_eq!(types[7], SectionType::Response);
508        assert_eq!(types[8], SectionType::Asserts);
509        assert_eq!(types[9], SectionType::Extract);
510    }
511
512    #[test]
513    fn test_parse_unknown_section_type() {
514        let input = "--- UNKNOWN ---\nhello\n";
515        let result = parse_sections_from_str(input);
516        assert!(result.is_err());
517        assert!(
518            result
519                .unwrap_err()
520                .to_string()
521                .contains("Unknown section type")
522        );
523    }
524
525    #[test]
526    fn test_parse_preserves_comments_in_content() {
527        let input = "\
528--- RESPONSE ---
529// This is a comment
530{\"status\": \"OK\"}
531# Another comment
532";
533        let (sections, _) = parse_sections_from_str(input).unwrap();
534        let resp = sections
535            .into_iter()
536            .find(|s| s.section_type == SectionType::Response)
537            .unwrap();
538        assert!(resp.raw_content.contains("// This is a comment"));
539        assert!(resp.raw_content.contains("# Another comment"));
540    }
541
542    #[test]
543    fn test_parse_from_str_section_counts() {
544        let input = "\
545--- ENDPOINT ---
546test.Service/Method
547
548--- REQUEST ---
549{}
550
551--- ASSERTS ---
552.x == 1
553";
554        let doc = parse_gctf_from_str(input, "test.gctf").unwrap();
555        assert_eq!(doc.sections.len(), 3);
556        assert!(doc.get_endpoint().is_some());
557        let asserts = doc.get_assertions();
558        assert_eq!(asserts.len(), 1);
559    }
560
561    #[test]
562    fn test_extract_doc_source() {
563        let source = "line0\nline1\nline2\nline3\nline4";
564        let lines: Vec<&str> = source.lines().collect();
565        let sections = vec![Section {
566            section_type: SectionType::Endpoint,
567            content: SectionContent::Single("line1".into()),
568            inline_options: InlineOptions::default(),
569            raw_content: "line1".into(),
570            start_line: 1,
571            end_line: 2,
572            attributes: Vec::new(),
573        }];
574        let result = extract_doc_source_from_lines(&sections, &lines);
575        assert_eq!(result, "line1");
576    }
577
578    #[test]
579    fn test_extract_doc_source_empty() {
580        let result = extract_doc_source_from_lines(&[], &[]);
581        assert!(result.is_empty());
582    }
583
584    #[test]
585    fn test_attribute_before_section_attaches_to_following_section() {
586        let input = "\
587--- ENDPOINT ---
588test.Service/Method
589
590#[name(test)]
591--- REQUEST ---
592{}
593
594--- RESPONSE ---
595{}
596";
597
598        let (sections, _) = parse_sections_from_str(input).unwrap();
599        assert_eq!(sections.len(), 3);
600
601        let endpoint = &sections[0];
602        let request = &sections[1];
603
604        assert!(endpoint.attributes.is_empty());
605        assert_eq!(request.attributes.len(), 1);
606        assert_eq!(request.attributes[0].name, "name");
607        assert_eq!(request.attributes[0].value, "test");
608    }
609
610    #[test]
611    fn test_attribute_between_sections_not_attached_to_previous_section() {
612        let input = "\
613--- ENDPOINT ---
614test.Service/Method
615#[timeout(10)]
616--- REQUEST ---
617{}
618";
619
620        let (sections, _) = parse_sections_from_str(input).unwrap();
621        assert_eq!(sections.len(), 2);
622        assert!(sections[0].attributes.is_empty());
623        assert_eq!(sections[1].attributes.len(), 1);
624        assert_eq!(sections[1].attributes[0].name, "timeout");
625    }
626
627    #[test]
628    #[cfg(not(miri))]
629    fn bench_parse_small_doc() {
630        let header = "--- ENDPOINT ---
631";
632        let body = "test.Service/Method
633
634--- REQUEST ---
635{\"k\":\"v\"}
636
637--- RESPONSE ---
638{\"r\":\"ok\"}
639";
640        let input = format!("{}{}", header, body);
641        let start = std::time::Instant::now();
642        let n = 5000;
643        for _ in 0..n {
644            let _ = parse_sections_from_str(&input);
645        }
646        let d = start.elapsed();
647        eprintln!("bench: {} iterations in {:?} ({:?}/call)", n, d, d / n);
648    }
649}