1use 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
13pub 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 SectionType::Address | SectionType::Endpoint => {
24 Ok(SectionContent::Single(content.to_string()))
25 }
26
27 SectionType::Request | SectionType::Error => {
29 let json_value = json_mod::from_str(content)?;
30 Ok(SectionContent::Json(json_value))
31 }
32 SectionType::Response => {
33 if let Ok(json_value) = json_mod::from_str(content) {
35 return Ok(SectionContent::Json(json_value));
36 }
37
38 if let Some(values) = json_stream_parser::parse_response_json_values(content) {
40 Ok(SectionContent::JsonLines(values))
41 } else {
42 let json_value = json_mod::from_str(content)?;
44 Ok(SectionContent::Json(json_value))
45 }
46 }
47
48 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 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 SectionType::Asserts => {
74 let assertions = parse_assertions(content)?;
75 Ok(SectionContent::Assertions(assertions))
76 }
77
78 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
87pub 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
110pub 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
146pub 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
246pub 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
271fn 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
284fn parse_assertions(content: &str) -> Result<Vec<String>> {
286 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 assert!(!result.redact.is_empty()); }
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}