Skip to main content

apif_parser/
content_parser.rs

1//! Section content parser for GCTF files.
2//!
3//! Parses the content of different section types based on their structure.
4
5use anyhow::Result;
6
7use crate::assertions::strip_assertion_comments;
8use crate::ast::{FileMeta, GctfAttribute, InlineOptions, Section, SectionContent, SectionType};
9use crate::gctf_tokenizer::{tokenize_extract_line, tokenize_inline_options, tokenize_kv_line};
10use crate::json_mod;
11use crate::json_stream_parser;
12
13/// Parse section content based on section type.
14pub fn parse_section_content(section_type: SectionType, content: &str) -> Result<SectionContent> {
15    let content = content.trim();
16
17    if content.is_empty() {
18        return Ok(SectionContent::Empty);
19    }
20
21    match section_type {
22        // Single value sections
23        SectionType::Address | SectionType::Endpoint => {
24            Ok(SectionContent::Single(content.to_string()))
25        }
26
27        // JSON sections
28        SectionType::Request | SectionType::Error => {
29            let json_value = json_mod::from_str(content)?;
30            Ok(SectionContent::Json(json_value))
31        }
32        SectionType::Response => {
33            // Primary mode: a single JSON/JSON5 value
34            if let Ok(json_value) = json_mod::from_str(content) {
35                return Ok(SectionContent::Json(json_value));
36            }
37
38            // Streaming mode: multiple JSON payloads within one RESPONSE block
39            if let Some(values) = json_stream_parser::parse_response_json_values(content) {
40                Ok(SectionContent::JsonLines(values))
41            } else {
42                // Preserve original parse error behavior for malformed single-content responses
43                let json_value = json_mod::from_str(content)?;
44                Ok(SectionContent::Json(json_value))
45            }
46        }
47
48        // Key-value sections
49        SectionType::RequestHeaders
50        | SectionType::Tls
51        | SectionType::Proto
52        | SectionType::Options
53        | SectionType::Bench => {
54            let key_values = parse_key_value_section(content)?;
55            Ok(SectionContent::KeyValues(key_values))
56        }
57
58        // Extract section - support ternary expressions via AST
59        SectionType::Extract => {
60            let mut key_values = std::collections::HashMap::new();
61            for line in content.lines() {
62                if let Some((name, value)) = tokenize_extract_line(line)
63                    && let Some(extract_var) =
64                        crate::ternary_ast::ExtractVar::parse_raw(&name, &value)
65                {
66                    key_values.insert(extract_var.name, extract_var.value.to_jq());
67                }
68            }
69            Ok(SectionContent::Extract(key_values))
70        }
71
72        // Assertion sections
73        SectionType::Asserts => {
74            let assertions = parse_assertions(content)?;
75            Ok(SectionContent::Assertions(assertions))
76        }
77
78        // META section - parse as YAML (comments allowed)
79        SectionType::Meta => {
80            let meta = serde_yaml_ng::from_str::<FileMeta>(content)
81                .unwrap_or_else(|_| FileMeta::default());
82            Ok(SectionContent::Meta(meta))
83        }
84    }
85}
86
87/// Build a section from parsed content.
88pub fn build_section(
89    section_type: SectionType,
90    start_line: usize,
91    end_line: usize,
92    content: &[String],
93    inline_options: InlineOptions,
94    attributes: Vec<GctfAttribute>,
95) -> Result<Section> {
96    let raw_content = content.join("\n");
97    let section_content = parse_section_content(section_type, &raw_content)?;
98
99    Ok(Section {
100        section_type,
101        content: section_content,
102        inline_options,
103        raw_content,
104        start_line,
105        end_line,
106        attributes,
107    })
108}
109
110/// Parse key=value options from section header inline options string.
111pub fn parse_inline_options(s: &str) -> Result<InlineOptions> {
112    let mut inline_options = InlineOptions::default();
113
114    for (key, value) in tokenize_inline_options(s) {
115        match key.as_str() {
116            "with_asserts" => {
117                inline_options.with_asserts = matches!(value.as_str(), "true" | "1");
118            }
119            "partial" => {
120                inline_options.partial = matches!(value.as_str(), "true" | "1");
121            }
122            "tolerance" => {
123                if let Ok(t) = value.parse::<f64>() {
124                    inline_options.tolerance = Some(t);
125                }
126            }
127            "redact" => {
128                let redact_str = value.trim().trim_matches('[').trim_matches(']');
129                let strings: Vec<String> = redact_str
130                    .split(',')
131                    .map(|s| s.trim().trim_matches('"').to_string())
132                    .filter(|s| !s.is_empty())
133                    .collect();
134                inline_options.redact = strings;
135            }
136            "unordered_arrays" => {
137                inline_options.unordered_arrays = matches!(value.as_str(), "true" | "1");
138            }
139            _ => {}
140        }
141    }
142
143    Ok(inline_options)
144}
145
146/// Parse a GCTF attribute from `#[name(value)]` content string.
147/// Returns `None` if content is empty or invalid.
148pub fn parse_attribute(content: &str) -> Option<GctfAttribute> {
149    let content = content.trim();
150    if content.is_empty() {
151        return None;
152    }
153
154    let bytes = content.as_bytes();
155    let len = bytes.len();
156    let mut pos = 0;
157
158    while pos < len && is_attr_name_char(bytes[pos]) {
159        pos += 1;
160    }
161
162    if pos == 0 {
163        return None;
164    }
165
166    let name = content[..pos].to_string();
167
168    while pos < len && is_ws(bytes[pos]) {
169        pos += 1;
170    }
171
172    if pos == len {
173        return Some(GctfAttribute::flag(&name));
174    }
175
176    if bytes[pos] != b'(' {
177        return None;
178    }
179
180    pos += 1;
181
182    let value_start = pos;
183    let mut paren_depth = 1;
184    let mut escaped = false;
185
186    while pos < len && paren_depth > 0 {
187        if escaped {
188            escaped = false;
189            pos += 1;
190            continue;
191        }
192        match bytes[pos] {
193            b'\\' => {
194                escaped = true;
195                pos += 1;
196            }
197            b'"' | b'\'' => {
198                let quote = bytes[pos];
199                pos += 1;
200                while pos < len {
201                    if escaped {
202                        escaped = false;
203                        pos += 1;
204                        continue;
205                    }
206                    if bytes[pos] == b'\\' {
207                        escaped = true;
208                        pos += 1;
209                        continue;
210                    }
211                    if bytes[pos] == quote {
212                        pos += 1;
213                        break;
214                    }
215                    pos += 1;
216                }
217            }
218            b'(' => {
219                paren_depth += 1;
220                pos += 1;
221            }
222            b')' => {
223                paren_depth -= 1;
224                pos += 1;
225            }
226            _ => pos += 1,
227        }
228    }
229
230    if paren_depth != 0 {
231        return None;
232    }
233
234    let value = content[value_start..pos - 1].to_string();
235    Some(GctfAttribute::new(&name, &value))
236}
237
238fn is_attr_name_char(b: u8) -> bool {
239    b.is_ascii_alphanumeric() || b == b'_' || b == b'-'
240}
241
242fn is_ws(b: u8) -> bool {
243    b == b' ' || b == b'\t'
244}
245
246/// Resolve attributes for a section, applying inheritance rules:
247/// - Attributes from parent sections apply to child sections
248/// - Child section attributes override parent attributes
249/// - Attributes with the same name are overridden (not merged)
250pub fn resolve_attributes(
251    section_attrs: &[GctfAttribute],
252    inherited_attrs: &[GctfAttribute],
253) -> Vec<GctfAttribute> {
254    let mut resolved: Vec<GctfAttribute> = inherited_attrs.to_vec();
255    let mut seen: std::collections::HashSet<String> =
256        inherited_attrs.iter().map(|a| a.name.clone()).collect();
257
258    for attr in section_attrs {
259        if seen.contains(&attr.name) {
260            let idx = resolved.iter().position(|a| a.name == attr.name).unwrap();
261            resolved[idx] = attr.clone();
262        } else {
263            resolved.push(attr.clone());
264            seen.insert(attr.name.clone());
265        }
266    }
267
268    resolved
269}
270
271/// Parse key-value section (one per line: key: value).
272fn parse_key_value_section(content: &str) -> Result<std::collections::HashMap<String, String>> {
273    let mut key_values = std::collections::HashMap::new();
274
275    for line in content.lines() {
276        if let Some((key, value)) = tokenize_kv_line(line) {
277            key_values.insert(key, value);
278        }
279    }
280
281    Ok(key_values)
282}
283
284/// Parse assertions section (one assertion per line).
285fn parse_assertions(content: &str) -> Result<Vec<String>> {
286    // No normalization needed — regex literals /pattern/ are now handled
287    // by the assertion AST parser as Expr::RegExp nodes.
288    let assertions: Vec<String> = content
289        .lines()
290        .filter_map(strip_assertion_comments)
291        .collect();
292
293    Ok(assertions)
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_tokenize_options() {
302        let result = tokenize_inline_options("key1=value1 key2=value2");
303        assert_eq!(result.len(), 2);
304        assert_eq!(result[0], ("key1".into(), "value1".into()));
305        assert_eq!(result[1], ("key2".into(), "value2".into()));
306    }
307
308    #[test]
309    fn test_parse_inline_options() {
310        let result = parse_inline_options("with_asserts=true partial=false tolerance=0.1").unwrap();
311        assert!(result.with_asserts);
312        assert!(!result.partial);
313        assert_eq!(result.tolerance, Some(0.1));
314    }
315
316    #[test]
317    fn test_parse_section_content_single_value() {
318        let result = parse_section_content(SectionType::Address, "localhost:50051").unwrap();
319        assert_eq!(
320            result,
321            SectionContent::Single("localhost:50051".to_string())
322        );
323    }
324
325    #[test]
326    fn test_parse_section_content_empty() {
327        let result = parse_section_content(SectionType::Address, "").unwrap();
328        assert_eq!(result, SectionContent::Empty);
329    }
330
331    #[test]
332    fn test_parse_section_content_whitespace() {
333        let result = parse_section_content(SectionType::Address, "   ").unwrap();
334        assert_eq!(result, SectionContent::Empty);
335    }
336
337    #[test]
338    fn test_parse_section_content_endpoint() {
339        let result = parse_section_content(SectionType::Endpoint, "pkg.Service/Method").unwrap();
340        assert_eq!(
341            result,
342            SectionContent::Single("pkg.Service/Method".to_string())
343        );
344    }
345
346    #[test]
347    fn test_parse_section_content_request_json() {
348        let result = parse_section_content(SectionType::Request, r#"{"key": "value"}"#).unwrap();
349        assert!(matches!(result, SectionContent::Json(_)));
350    }
351
352    #[test]
353    fn test_parse_section_content_error_json() {
354        let result = parse_section_content(SectionType::Error, r#"{"code": 5}"#).unwrap();
355        assert!(matches!(result, SectionContent::Json(_)));
356    }
357
358    #[test]
359    fn test_parse_section_content_response_json() {
360        let result = parse_section_content(SectionType::Response, r#"{"status": "ok"}"#).unwrap();
361        assert!(matches!(result, SectionContent::Json(_)));
362    }
363
364    #[test]
365    fn test_parse_section_content_response_jsonlines() {
366        let input = "{\"a\":1}\n{\"b\":2}";
367        let result = parse_section_content(SectionType::Response, input).unwrap();
368        assert!(matches!(result, SectionContent::JsonLines(v) if v.len() == 2));
369    }
370
371    #[test]
372    fn test_parse_section_content_key_values() {
373        let input = "ca_cert: /path/to/ca.pem\nserver_name: example.com";
374        let result = parse_section_content(SectionType::Tls, input).unwrap();
375        if let SectionContent::KeyValues(kv) = result {
376            assert_eq!(kv.get("ca_cert"), Some(&"/path/to/ca.pem".to_string()));
377            assert_eq!(kv.get("server_name"), Some(&"example.com".to_string()));
378        } else {
379            panic!("expected KeyValues");
380        }
381    }
382
383    #[test]
384    fn test_parse_section_content_key_values_with_comments() {
385        let input = "# comment\nca_cert: /path/ca.pem\n\nkey: value";
386        let result = parse_section_content(SectionType::Options, input).unwrap();
387        if let SectionContent::KeyValues(kv) = result {
388            assert_eq!(kv.len(), 2);
389        } else {
390            panic!("expected KeyValues");
391        }
392    }
393
394    #[test]
395    fn test_parse_section_content_extract() {
396        let input = "total = .response.total\ncount = .items | length";
397        let result = parse_section_content(SectionType::Extract, input).unwrap();
398        if let SectionContent::Extract(kv) = result {
399            assert_eq!(kv.get("total"), Some(&".response.total".to_string()));
400            assert!(kv.contains_key("count"));
401        } else {
402            panic!("expected Extract");
403        }
404    }
405
406    #[test]
407    fn test_parse_section_content_extract_with_comments() {
408        let input = "# ignore\n// ignore\ntotal = .response.total";
409        let result = parse_section_content(SectionType::Extract, input).unwrap();
410        if let SectionContent::Extract(kv) = result {
411            assert_eq!(kv.len(), 1);
412        } else {
413            panic!("expected Extract");
414        }
415    }
416
417    #[test]
418    fn test_parse_section_content_asserts() {
419        let input = ".x == 1\n.y != \"hello\"";
420        let result = parse_section_content(SectionType::Asserts, input).unwrap();
421        if let SectionContent::Assertions(asserts) = result {
422            assert_eq!(asserts.len(), 2);
423            assert_eq!(asserts[0], ".x == 1");
424        } else {
425            panic!("expected Assertions");
426        }
427    }
428
429    #[test]
430    fn test_parse_section_content_asserts_with_comments() {
431        let input = ".x == 1 # inline\n# full line\n.y == 2 // comment";
432        let result = parse_section_content(SectionType::Asserts, input).unwrap();
433        if let SectionContent::Assertions(asserts) = result {
434            assert_eq!(asserts.len(), 2);
435        } else {
436            panic!("expected Assertions");
437        }
438    }
439
440    #[test]
441    fn test_build_section() {
442        let content = vec!["localhost:50051".to_string()];
443        let section = build_section(
444            SectionType::Address,
445            5,
446            6,
447            &content,
448            InlineOptions::default(),
449            Vec::new(),
450        )
451        .unwrap();
452        assert_eq!(section.section_type, SectionType::Address);
453        assert_eq!(section.start_line, 5);
454        assert_eq!(section.end_line, 6);
455    }
456
457    #[test]
458    fn test_parse_inline_options_all_fields() {
459        let result = parse_inline_options(
460            "with_asserts=true partial=true tolerance=0.5 unordered_arrays=true",
461        )
462        .unwrap();
463        assert!(result.with_asserts);
464        assert!(result.partial);
465        assert_eq!(result.tolerance, Some(0.5));
466        assert!(result.unordered_arrays);
467    }
468
469    #[test]
470    fn test_parse_inline_options_redact() {
471        let result = parse_inline_options(r#"redact=["token","password"]"#).unwrap();
472        assert_eq!(result.redact, vec!["token", "password"]);
473    }
474
475    #[test]
476    fn test_parse_inline_options_empty() {
477        let result = parse_inline_options("").unwrap();
478        assert_eq!(result, InlineOptions::default());
479    }
480
481    #[test]
482    fn test_parse_inline_options_unknown_key_ignored() {
483        let result = parse_inline_options("unknown_key=value").unwrap();
484        assert_eq!(result, InlineOptions::default());
485    }
486
487    #[test]
488    fn test_parse_inline_options_tolerance_negative() {
489        let result = parse_inline_options("tolerance=-0.5").unwrap();
490        assert_eq!(result.tolerance, Some(-0.5));
491    }
492
493    #[test]
494    fn test_parse_inline_options_tolerance_invalid() {
495        let result = parse_inline_options("tolerance=not_a_number").unwrap();
496        assert_eq!(result.tolerance, None);
497    }
498
499    #[test]
500    fn test_parse_inline_options_redact_empty_array() {
501        let result = parse_inline_options("redact=[]").unwrap();
502        assert!(result.redact.is_empty());
503    }
504
505    #[test]
506    fn test_parse_inline_options_redact_malformed() {
507        let result = parse_inline_options("redact=not_an_array").unwrap();
508        // Current tokenizer splits by spaces, so this becomes tokens
509        // This is a known limitation - redact with spaces in value
510        assert!(!result.redact.is_empty()); // tokenizer splits "not_an_array" into parts
511    }
512
513    #[test]
514    fn test_parse_section_content_meta_full() {
515        let result = parse_section_content(
516            SectionType::Meta,
517            r#"name: Test
518summary: Summary
519tags: [a, b]
520owner: backend
521links:
522  - https://example.com
523"#,
524        )
525        .unwrap();
526        let SectionContent::Meta(m) = result else {
527            panic!()
528        };
529        assert_eq!(m.name.as_deref(), Some("Test"));
530        assert_eq!(m.summary.as_deref(), Some("Summary"));
531        assert_eq!(m.tags, ["a", "b"]);
532        assert_eq!(m.owner.as_deref(), Some("backend"));
533        assert_eq!(m.links, ["https://example.com"]);
534    }
535
536    #[test]
537    fn test_parse_section_content_meta_comments() {
538        let result = parse_section_content(
539            SectionType::Meta,
540            r#"# comment
541name: Test
542tags: [a]
543"#,
544        )
545        .unwrap();
546        let SectionContent::Meta(m) = result else {
547            panic!()
548        };
549        assert_eq!(m.name.as_deref(), Some("Test"));
550        assert_eq!(m.tags, ["a"]);
551    }
552
553    #[test]
554    fn test_parse_attribute_with_value() {
555        let attr = parse_attribute("timeout(30)").unwrap();
556        assert_eq!(attr.name, "timeout");
557        assert_eq!(attr.value, "30");
558        assert_eq!(attr.parse_u64(), Some(30));
559    }
560
561    #[test]
562    fn test_parse_attribute_flag() {
563        let attr = parse_attribute("skip").unwrap();
564        assert_eq!(attr.name, "skip");
565        assert_eq!(attr.value, "true");
566        assert_eq!(attr.parse_bool(), Some(true));
567    }
568
569    #[test]
570    fn test_parse_attribute_quoted_value() {
571        let attr = parse_attribute(r#"tag("smoke, slow")"#).unwrap();
572        assert_eq!(attr.name, "tag");
573        assert_eq!(attr.value, r#""smoke, slow""#);
574    }
575
576    #[test]
577    fn test_parse_attribute_with_spaces() {
578        let attr = parse_attribute("  retry(3)  ").unwrap();
579        assert_eq!(attr.name, "retry");
580        assert_eq!(attr.value, "3");
581    }
582
583    #[test]
584    fn test_parse_attribute_empty() {
585        assert!(parse_attribute("").is_none());
586        assert!(parse_attribute("   ").is_none());
587    }
588
589    #[test]
590    fn test_parse_attribute_no_paren() {
591        let attr = parse_attribute("just_a_name").unwrap();
592        assert_eq!(attr.name, "just_a_name");
593        assert_eq!(attr.value, "true");
594    }
595
596    #[test]
597    fn test_resolve_attributes_inheritance() {
598        let parent = vec![GctfAttribute::new("timeout", "10")];
599        let child = vec![GctfAttribute::new("retry", "3")];
600        let resolved = resolve_attributes(&child, &parent);
601
602        let timeout = resolved.iter().find(|a| a.name == "timeout");
603        let retry = resolved.iter().find(|a| a.name == "retry");
604
605        assert_eq!(timeout.map(|a| a.value.as_str()), Some("10"));
606        assert_eq!(retry.map(|a| a.value.as_str()), Some("3"));
607    }
608
609    #[test]
610    fn test_resolve_attributes_override() {
611        let parent = vec![GctfAttribute::new("timeout", "10")];
612        let child = vec![GctfAttribute::new("timeout", "30")];
613        let resolved = resolve_attributes(&child, &parent);
614
615        assert_eq!(resolved.len(), 1);
616        assert_eq!(resolved[0].value, "30");
617    }
618
619    #[test]
620    fn test_resolve_attributes_empty() {
621        let resolved = resolve_attributes(&[], &[]);
622        assert!(resolved.is_empty());
623
624        let parent = vec![GctfAttribute::new("timeout", "10")];
625        let resolved = resolve_attributes(&[], &parent);
626        assert_eq!(resolved.len(), 1);
627        assert_eq!(resolved[0].value, "10");
628    }
629}