1use 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
22pub fn parse_gctf(file_path: &Path) -> Result<GctfDocument> {
24 let (document, _) = parse_gctf_with_diagnostics(file_path)?;
25 Ok(document)
26}
27
28pub 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 let documents = crate::document_splitter::split_sections_by_boundary_owned(all_sections);
37
38 if documents.is_empty() {
39 let mut document = GctfDocument::new(file_path.to_string());
41 document.metadata.source = Some(content.to_string());
42 return Ok(document);
43 }
44
45 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
60fn 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
93pub 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 let documents = crate::document_splitter::split_sections_by_boundary_owned(sections);
109
110 let build_start = Instant::now();
111 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
232pub 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 §ions {
240 for attr in §ion.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 §ion.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 §ions[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(§ions, &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 = §ions[0];
602 let request = §ions[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}