1use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct GctfDocument {
10 pub file_path: String,
12
13 pub sections: Vec<Section>,
15
16 pub metadata: DocumentMetadata,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub next_document: Option<Box<GctfDocument>>,
22}
23
24pub struct DocumentChainIter<'a> {
25 current: Option<&'a GctfDocument>,
26}
27
28impl<'a> Iterator for DocumentChainIter<'a> {
29 type Item = &'a GctfDocument;
30
31 fn next(&mut self) -> Option<Self::Item> {
32 let current = self.current?;
33 self.current = current.next_document.as_deref();
34 Some(current)
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct DocumentMetadata {
41 pub source: Option<String>,
43
44 pub mtime: Option<i64>,
46
47 pub parsed_at: i64,
49
50 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
54 pub variable_types: HashMap<String, String>,
55}
56
57impl Default for DocumentMetadata {
58 fn default() -> Self {
59 Self {
60 source: None,
61 mtime: None,
62 parsed_at: if cfg!(miri) {
63 0
64 } else {
65 std::time::SystemTime::now()
66 .duration_since(std::time::UNIX_EPOCH)
67 .map(|d| d.as_secs() as i64)
68 .unwrap_or(0)
69 },
70 variable_types: HashMap::new(),
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
77#[serde(default)]
78pub struct FileMeta {
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub name: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub summary: Option<String>,
85 #[serde(skip_serializing_if = "Vec::is_empty")]
87 pub tags: Vec<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub owner: Option<String>,
91 #[serde(skip_serializing_if = "Vec::is_empty")]
93 pub links: Vec<String>,
94}
95
96impl FileMeta {
97 #[must_use]
99 pub fn is_empty(&self) -> bool {
100 self.name.is_none()
101 && self.summary.is_none()
102 && self.tags.is_empty()
103 && self.owner.is_none()
104 && self.links.is_empty()
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110pub struct GctfAttribute {
111 pub name: String,
112 pub value: String,
113}
114
115impl GctfAttribute {
116 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
117 Self {
118 name: name.into(),
119 value: value.into(),
120 }
121 }
122
123 pub fn flag(name: impl Into<String>) -> Self {
124 Self {
125 name: name.into(),
126 value: "true".into(),
127 }
128 }
129
130 pub fn parse_u64(&self) -> Option<u64> {
131 self.value.trim().parse::<u64>().ok()
132 }
133
134 pub fn parse_u32(&self) -> Option<u32> {
135 self.value.trim().parse::<u32>().ok()
136 }
137
138 pub fn parse_f64(&self) -> Option<f64> {
139 self.value.trim().parse::<f64>().ok()
140 }
141
142 pub fn parse_bool(&self) -> Option<bool> {
143 match self.value.trim().to_lowercase().as_str() {
144 "true" | "1" | "yes" | "on" => Some(true),
145 "false" | "0" | "no" | "off" | "" => Some(false),
146 _ => None,
147 }
148 }
149
150 pub fn as_str(&self) -> &str {
151 &self.value
152 }
153
154 pub fn format_directive(&self) -> String {
155 if self.value == "true" {
156 format!("#[{}]", self.name)
157 } else {
158 format!("#[{}({})]", self.name, self.value)
159 }
160 }
161}
162
163impl Section {
164 pub fn get_attribute(&self, name: &str) -> Option<&GctfAttribute> {
165 self.attributes.iter().find(|a| a.name == name)
166 }
167
168 pub fn get_timeout(&self) -> Option<u64> {
169 self.get_attribute("timeout")
170 .and_then(|a| a.parse_u64())
171 .filter(|&v| v > 0)
172 }
173
174 pub fn get_retry(&self) -> Option<u32> {
175 self.get_attribute("retry").and_then(|a| a.parse_u32())
176 }
177
178 pub fn get_skip(&self) -> bool {
179 self.get_attribute("skip")
180 .and_then(|a| a.parse_bool())
181 .unwrap_or(false)
182 }
183
184 #[must_use]
185 pub fn has_tag(&self, tag: &str) -> bool {
186 self.get_attribute("tag")
187 .is_some_and(|a| a.value.split(',').any(|t| t.trim() == tag))
188 }
189
190 pub fn get_compression(&self) -> Option<String> {
191 self.get_attribute("compression")
192 .map(|a| a.value.trim().to_lowercase())
193 .filter(|v| v == "gzip" || v == "none")
194 }
195
196 pub fn get_repeat(&self) -> Option<u32> {
197 self.get_attribute("repeat")
198 .and_then(|a| a.parse_u32())
199 .filter(|&v| v >= 1)
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
205pub struct Section {
206 pub section_type: SectionType,
208
209 pub content: SectionContent,
211
212 pub inline_options: InlineOptions,
214
215 pub raw_content: String,
217
218 pub start_line: usize,
220
221 pub end_line: usize,
223
224 #[serde(default, skip_serializing_if = "Vec::is_empty")]
226 pub attributes: Vec<GctfAttribute>,
227}
228
229impl Default for Section {
230 fn default() -> Self {
231 Self {
232 section_type: SectionType::Address,
233 content: SectionContent::Empty,
234 inline_options: InlineOptions::default(),
235 raw_content: String::new(),
236 start_line: 0,
237 end_line: 0,
238 attributes: Vec::new(),
239 }
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub enum SectionContent {
246 Single(String),
248
249 Json(serde_json::Value),
251
252 JsonLines(Vec<serde_json::Value>),
254
255 KeyValues(HashMap<String, String>),
257
258 Extract(HashMap<String, String>),
260
261 Assertions(Vec<String>),
263
264 Meta(FileMeta),
266
267 Empty,
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
273pub enum SectionType {
274 Address,
276
277 Endpoint,
279
280 Request,
282
283 Response,
285
286 Error,
288
289 RequestHeaders,
291
292 Asserts,
294
295 Proto,
297
298 Tls,
300
301 Options,
303
304 Extract,
306
307 Meta,
309
310 Bench,
312}
313
314impl SectionType {
315 #[must_use]
317 pub fn is_terminal(&self) -> bool {
318 matches!(
319 self,
320 SectionType::Response | SectionType::Error | SectionType::Asserts
321 )
322 }
323
324 pub fn as_str(&self) -> &'static str {
326 match self {
327 SectionType::Address => "ADDRESS",
328 SectionType::Endpoint => "ENDPOINT",
329 SectionType::Request => "REQUEST",
330 SectionType::Response => "RESPONSE",
331 SectionType::Error => "ERROR",
332 SectionType::RequestHeaders => "REQUEST_HEADERS",
333 SectionType::Asserts => "ASSERTS",
334 SectionType::Proto => "PROTO",
335 SectionType::Tls => "TLS",
336 SectionType::Options => "OPTIONS",
337 SectionType::Extract => "EXTRACT",
338 SectionType::Meta => "META",
339 SectionType::Bench => "BENCH",
340 }
341 }
342
343 pub fn from_keyword(s: &str) -> Option<SectionType> {
345 match s.trim() {
346 "ADDRESS" => Some(SectionType::Address),
347 "ENDPOINT" => Some(SectionType::Endpoint),
348 "REQUEST" => Some(SectionType::Request),
349 "RESPONSE" => Some(SectionType::Response),
350 "ERROR" => Some(SectionType::Error),
351 "REQUEST_HEADERS" | "HEADERS" => Some(SectionType::RequestHeaders),
352 "ASSERTS" => Some(SectionType::Asserts),
353 "PROTO" => Some(SectionType::Proto),
354 "TLS" => Some(SectionType::Tls),
355 "OPTIONS" => Some(SectionType::Options),
356 "EXTRACT" => Some(SectionType::Extract),
357 "META" => Some(SectionType::Meta),
358 "BENCH" => Some(SectionType::Bench),
359 _ => None,
360 }
361 }
362
363 #[must_use]
365 pub fn is_multiple_allowed(&self) -> bool {
366 matches!(
367 self,
368 SectionType::Request
369 | SectionType::Response
370 | SectionType::Asserts
371 | SectionType::Extract
372 )
373 }
374
375 #[must_use]
377 pub fn is_file_level(&self) -> bool {
378 matches!(self, SectionType::Meta | SectionType::Bench)
379 }
380
381 pub fn supports_inline_options(&self) -> bool {
382 matches!(self, SectionType::Response | SectionType::Error)
383 }
384
385 pub fn preamble_rank(&self) -> Option<usize> {
386 match self {
387 SectionType::Meta => Some(0),
388 SectionType::Bench => Some(1),
389 SectionType::Address => Some(2),
390 SectionType::Endpoint => Some(3),
391 SectionType::Tls => Some(4),
392 SectionType::Proto => Some(5),
393 SectionType::Options => Some(6),
394 _ => None,
395 }
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
401pub struct InlineOptions {
402 pub with_asserts: bool,
404
405 pub partial: bool,
407
408 pub tolerance: Option<f64>,
410
411 pub redact: Vec<String>,
413
414 pub unordered_arrays: bool,
416}
417
418impl InlineOptions {
419 pub fn to_header_tokens(&self) -> Vec<String> {
420 let mut parts = Vec::new();
421
422 if self.partial {
423 parts.push("partial".to_string());
424 }
425
426 if let Some(tolerance) = self.tolerance {
427 parts.push(format!("tolerance={}", tolerance));
428 }
429
430 if !self.redact.is_empty() {
431 let mut sorted_redact = self.redact.clone();
432 sorted_redact.sort();
433 let quoted = sorted_redact
434 .iter()
435 .map(|field| format!("\"{}\"", field))
436 .collect::<Vec<_>>()
437 .join(",");
438 parts.push(format!("redact=[{}]", quoted));
439 }
440
441 if self.unordered_arrays {
442 parts.push("unordered_arrays".to_string());
443 }
444
445 if self.with_asserts {
446 parts.push("with_asserts".to_string());
447 }
448
449 parts
450 }
451
452 #[must_use]
453 pub fn is_empty(&self) -> bool {
454 !self.with_asserts
455 && !self.partial
456 && self.tolerance.is_none()
457 && self.redact.is_empty()
458 && !self.unordered_arrays
459 }
460}
461
462impl Section {
463 pub fn format_header(&self) -> String {
464 let section = self.section_type.as_str();
465 if self.section_type.supports_inline_options() {
466 let parts = self.inline_options.to_header_tokens();
467 if parts.is_empty() {
468 format!("--- {} ---", section)
469 } else {
470 format!("--- {} {} ---", section, parts.join(" "))
471 }
472 } else {
473 format!("--- {} ---", section)
474 }
475 }
476
477 pub fn header_keyword_from_source<'a>(&self, source: &'a str) -> Option<&'a str> {
478 let header_line = source.lines().nth(self.start_line)?.trim();
479 let inner = header_line.strip_prefix("---")?.strip_suffix("---")?.trim();
480 inner.split_whitespace().next()
481 }
482}
483
484#[derive(Debug, Clone, PartialEq)]
487pub struct SectionHeader {
488 pub section_type: SectionType,
490
491 pub options: HashMap<String, String>,
493}
494
495impl GctfDocument {
496 pub fn new(file_path: String) -> Self {
498 Self {
499 file_path,
500 sections: Vec::new(),
501 metadata: DocumentMetadata::default(),
502 next_document: None,
503 }
504 }
505
506 pub fn get_document(&self, index: usize) -> Option<&GctfDocument> {
508 self.iter_chain().nth(index)
509 }
510
511 pub fn iter_chain(&self) -> DocumentChainIter<'_> {
512 DocumentChainIter {
513 current: Some(self),
514 }
515 }
516
517 pub fn document_count(&self) -> usize {
518 let mut count = 1;
519 let mut current = &self.next_document;
520 while let Some(doc) = current {
521 count += 1;
522 current = &doc.next_document;
523 }
524 count
525 }
526
527 #[must_use]
528 pub fn is_single_document(&self) -> bool {
529 self.next_document.is_none()
530 }
531
532 pub fn sections_by_type(&self, section_type: SectionType) -> Vec<&Section> {
534 self.sections
535 .iter()
536 .filter(|s| s.section_type == section_type)
537 .collect()
538 }
539
540 pub fn first_section(&self, section_type: SectionType) -> Option<&Section> {
542 self.sections
543 .iter()
544 .find(|s| s.section_type == section_type)
545 }
546
547 pub fn get_address(&self, env_address: Option<&str>) -> Option<String> {
549 if let Some(section) = self.first_section(SectionType::Address)
550 && let SectionContent::Single(addr) = §ion.content
551 {
552 return Some(addr.clone());
553 }
554 env_address.map(|s| s.to_string())
555 }
556
557 pub fn get_endpoint(&self) -> Option<String> {
559 if let Some(section) = self.first_section(SectionType::Endpoint)
560 && let SectionContent::Single(endpoint) = §ion.content
561 {
562 return Some(endpoint.clone());
563 }
564 None
565 }
566
567 pub fn parse_endpoint(&self) -> Option<(String, String, String)> {
569 let endpoint = self.get_endpoint()?;
570 let parts: Vec<&str> = endpoint.split('/').collect();
571 if parts.len() == 2 {
572 let full_service = parts[0];
573 let service_parts: Vec<&str> = full_service.split('.').collect();
574 if service_parts.len() >= 2 {
575 let package = service_parts[..service_parts.len() - 1].join(".");
576 let service = service_parts[service_parts.len() - 1].to_string();
577 let method = parts[1].to_string();
578 return Some((package, service, method));
579 } else if service_parts.len() == 1 {
580 let package = String::new();
581 let service = service_parts[0].to_string();
582 let method = parts[1].to_string();
583 return Some((package, service, method));
584 }
585 }
586 None
587 }
588
589 pub fn get_requests(&self) -> Vec<serde_json::Value> {
591 self.sections_by_type(SectionType::Request)
592 .into_iter()
593 .filter_map(|s| {
594 if let SectionContent::Json(json) = &s.content {
595 Some(json.clone())
596 } else {
597 None
598 }
599 })
600 .collect()
601 }
602
603 pub fn get_assertions(&self) -> Vec<Vec<String>> {
605 self.sections_by_type(SectionType::Asserts)
606 .into_iter()
607 .filter_map(|s| {
608 if let SectionContent::Assertions(asserts) = &s.content {
609 Some(asserts.clone())
610 } else {
611 None
612 }
613 })
614 .collect()
615 }
616
617 pub fn get_request_headers(&self) -> Option<HashMap<String, String>> {
619 if let Some(section) = self.first_section(SectionType::RequestHeaders)
620 && let SectionContent::KeyValues(headers) = §ion.content
621 {
622 return Some(headers.clone());
623 }
624 None
625 }
626
627 pub fn get_tls_config(&self) -> Option<HashMap<String, String>> {
629 if let Some(section) = self.first_section(SectionType::Tls)
630 && let SectionContent::KeyValues(config) = §ion.content
631 {
632 return Some(config.clone());
633 }
634 None
635 }
636
637 pub fn get_options(&self) -> Option<HashMap<String, String>> {
639 if let Some(section) = self.first_section(SectionType::Options)
640 && let SectionContent::KeyValues(config) = §ion.content
641 {
642 return Some(config.clone());
643 }
644 None
645 }
646
647 pub fn get_tls_config_with_defaults(
649 &self,
650 defaults: &HashMap<String, String>,
651 ) -> Option<HashMap<String, String>> {
652 let mut merged = defaults.clone();
653
654 if let Some(section) = self.first_section(SectionType::Tls)
655 && let SectionContent::KeyValues(config) = §ion.content
656 {
657 for (key, value) in config {
658 merged.insert(key.clone(), value.clone());
659 }
660 }
661
662 if merged.is_empty() {
663 None
664 } else {
665 Some(merged)
666 }
667 }
668
669 pub fn get_proto_config(&self) -> Option<HashMap<String, String>> {
671 if let Some(section) = self.first_section(SectionType::Proto)
672 && let SectionContent::KeyValues(config) = §ion.content
673 {
674 return Some(config.clone());
675 }
676 None
677 }
678
679 #[must_use]
681 pub fn has_response_error_conflict(&self) -> bool {
682 self.first_section(SectionType::Response).is_some()
683 && self.first_section(SectionType::Error).is_some()
684 }
685
686 pub fn section_uses_deprecated_headers_alias(&self, section: &Section) -> bool {
687 if section.section_type != SectionType::RequestHeaders {
688 return false;
689 }
690
691 self.metadata
692 .source
693 .as_deref()
694 .and_then(|source| section.header_keyword_from_source(source))
695 .is_some_and(|keyword| keyword.eq_ignore_ascii_case("HEADERS"))
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702 use serde_json::json;
703
704 #[test]
705 fn test_section_type_from_str() {
706 assert_eq!(
707 SectionType::from_keyword("ADDRESS"),
708 Some(SectionType::Address)
709 );
710 assert_eq!(
711 SectionType::from_keyword("ENDPOINT"),
712 Some(SectionType::Endpoint)
713 );
714 assert_eq!(SectionType::from_keyword("INVALID"), None);
715 }
716
717 #[test]
718 fn test_section_type_multiple_allowed() {
719 assert!(SectionType::Request.is_multiple_allowed());
720 assert!(SectionType::Response.is_multiple_allowed());
721 assert!(SectionType::Asserts.is_multiple_allowed());
722 assert!(!SectionType::Address.is_multiple_allowed());
723 assert!(!SectionType::Endpoint.is_multiple_allowed());
724 }
725
726 #[test]
727 fn test_section_type_supports_inline_options() {
728 assert!(SectionType::Response.supports_inline_options());
729 assert!(SectionType::Error.supports_inline_options());
730 assert!(!SectionType::Request.supports_inline_options());
731 assert!(!SectionType::Address.supports_inline_options());
732 }
733
734 #[test]
735 fn test_section_type_as_str() {
736 assert_eq!(SectionType::Address.as_str(), "ADDRESS");
737 assert_eq!(SectionType::Endpoint.as_str(), "ENDPOINT");
738 assert_eq!(SectionType::Request.as_str(), "REQUEST");
739 assert_eq!(SectionType::Response.as_str(), "RESPONSE");
740 assert_eq!(SectionType::Error.as_str(), "ERROR");
741 assert_eq!(SectionType::RequestHeaders.as_str(), "REQUEST_HEADERS");
742 assert_eq!(SectionType::Asserts.as_str(), "ASSERTS");
743 assert_eq!(SectionType::Proto.as_str(), "PROTO");
744 assert_eq!(SectionType::Tls.as_str(), "TLS");
745 assert_eq!(SectionType::Options.as_str(), "OPTIONS");
746 assert_eq!(SectionType::Extract.as_str(), "EXTRACT");
747 assert_eq!(SectionType::Meta.as_str(), "META");
748 assert_eq!(SectionType::Bench.as_str(), "BENCH");
749 }
750
751 #[test]
752 fn test_section_type_from_keyword_aliases() {
753 assert_eq!(
754 SectionType::from_keyword("HEADERS"),
755 Some(SectionType::RequestHeaders)
756 );
757 assert_eq!(
758 SectionType::from_keyword("REQUEST_HEADERS"),
759 Some(SectionType::RequestHeaders)
760 );
761 assert_eq!(SectionType::from_keyword("BENCH"), Some(SectionType::Bench));
762 }
763
764 #[test]
765 fn test_section_type_from_keyword_case_insensitive() {
766 assert_eq!(SectionType::from_keyword("address"), None);
768 assert_eq!(
769 SectionType::from_keyword(" ADDRESS "),
770 Some(SectionType::Address)
771 );
772 }
773
774 #[test]
775 fn test_section_type_is_terminal() {
776 assert!(SectionType::Response.is_terminal());
777 assert!(SectionType::Error.is_terminal());
778 assert!(SectionType::Asserts.is_terminal());
779 assert!(!SectionType::Request.is_terminal());
780 assert!(!SectionType::Endpoint.is_terminal());
781 assert!(!SectionType::Extract.is_terminal());
782 assert!(!SectionType::Address.is_terminal());
783 }
784
785 #[test]
786 fn test_gctf_document_new() {
787 let doc = GctfDocument::new("test.gctf".to_string());
788 assert_eq!(doc.file_path, "test.gctf");
789 assert!(doc.sections.is_empty());
790 assert!(doc.metadata.source.is_none());
791 assert!(doc.metadata.mtime.is_none());
792 }
793
794 #[test]
795 fn test_gctf_document_sections_by_type() {
796 let mut doc = GctfDocument::new("test.gctf".to_string());
797 doc.sections.push(Section {
798 section_type: SectionType::Request,
799 content: SectionContent::Json(json!({"key": "value1"})),
800 inline_options: InlineOptions::default(),
801 raw_content: "".to_string(),
802 start_line: 1,
803 end_line: 2,
804 attributes: Vec::new(),
805 });
806 doc.sections.push(Section {
807 section_type: SectionType::Request,
808 content: SectionContent::Json(json!({"key": "value2"})),
809 inline_options: InlineOptions::default(),
810 raw_content: "".to_string(),
811 start_line: 3,
812 end_line: 4,
813 attributes: Vec::new(),
814 });
815 doc.sections.push(Section {
816 section_type: SectionType::Response,
817 content: SectionContent::Json(json!({"result": "ok"})),
818 inline_options: InlineOptions::default(),
819 raw_content: "".to_string(),
820 start_line: 5,
821 end_line: 6,
822 attributes: Vec::new(),
823 });
824
825 let requests = doc.sections_by_type(SectionType::Request);
826 assert_eq!(requests.len(), 2);
827
828 let responses = doc.sections_by_type(SectionType::Response);
829 assert_eq!(responses.len(), 1);
830
831 let errors = doc.sections_by_type(SectionType::Error);
832 assert_eq!(errors.len(), 0);
833 }
834
835 #[test]
836 fn test_gctf_document_first_section() {
837 let mut doc = GctfDocument::new("test.gctf".to_string());
838 doc.sections.push(Section {
839 section_type: SectionType::Request,
840 content: SectionContent::Json(json!({"key": "value"})),
841 inline_options: InlineOptions::default(),
842 raw_content: "".to_string(),
843 start_line: 1,
844 end_line: 2,
845 attributes: Vec::new(),
846 });
847
848 let first_request = doc.first_section(SectionType::Request);
849 assert!(first_request.is_some());
850
851 let first_error = doc.first_section(SectionType::Error);
852 assert!(first_error.is_none());
853 }
854
855 #[test]
856 fn test_gctf_document_get_address() {
857 let mut doc = GctfDocument::new("test.gctf".to_string());
858 doc.sections.push(Section {
859 section_type: SectionType::Address,
860 content: SectionContent::Single("localhost:4770".to_string()),
861 inline_options: InlineOptions::default(),
862 raw_content: "".to_string(),
863 start_line: 1,
864 end_line: 1,
865 attributes: Vec::new(),
866 });
867
868 assert_eq!(doc.get_address(None), Some("localhost:4770".to_string()));
869 assert_eq!(
870 doc.get_address(Some("env:5000")),
871 Some("localhost:4770".to_string())
872 );
873
874 let doc2 = GctfDocument::new("test.gctf".to_string());
875 assert_eq!(
876 doc2.get_address(Some("env:5000")),
877 Some("env:5000".to_string())
878 );
879 assert_eq!(doc2.get_address(None), None);
880 }
881
882 #[test]
883 fn test_gctf_document_get_endpoint() {
884 let mut doc = GctfDocument::new("test.gctf".to_string());
885 doc.sections.push(Section {
886 section_type: SectionType::Endpoint,
887 content: SectionContent::Single("my.Service/Method".to_string()),
888 inline_options: InlineOptions::default(),
889 raw_content: "".to_string(),
890 start_line: 1,
891 end_line: 1,
892 attributes: Vec::new(),
893 });
894
895 assert_eq!(doc.get_endpoint(), Some("my.Service/Method".to_string()));
896
897 let doc2 = GctfDocument::new("test.gctf".to_string());
898 assert_eq!(doc2.get_endpoint(), None);
899 }
900
901 #[test]
902 fn test_gctf_document_parse_endpoint() {
903 let mut doc = GctfDocument::new("test.gctf".to_string());
904 doc.sections.push(Section {
905 section_type: SectionType::Endpoint,
906 content: SectionContent::Single("package.Service/Method".to_string()),
907 inline_options: InlineOptions::default(),
908 raw_content: "".to_string(),
909 start_line: 1,
910 end_line: 1,
911 attributes: Vec::new(),
912 });
913
914 let (package, service, method) = doc.parse_endpoint().unwrap();
915 assert_eq!(package, "package");
916 assert_eq!(service, "Service");
917 assert_eq!(method, "Method");
918 }
919
920 #[test]
921 fn test_gctf_document_parse_endpoint_no_package() {
922 let mut doc = GctfDocument::new("test.gctf".to_string());
923 doc.sections.push(Section {
924 section_type: SectionType::Endpoint,
925 content: SectionContent::Single("Service/Method".to_string()),
926 inline_options: InlineOptions::default(),
927 raw_content: "".to_string(),
928 start_line: 1,
929 end_line: 1,
930 attributes: Vec::new(),
931 });
932
933 let (package, service, method) = doc.parse_endpoint().unwrap();
934 assert_eq!(package, "");
935 assert_eq!(service, "Service");
936 assert_eq!(method, "Method");
937 }
938
939 #[test]
940 fn test_gctf_document_parse_endpoint_invalid() {
941 let mut doc = GctfDocument::new("test.gctf".to_string());
942 doc.sections.push(Section {
943 section_type: SectionType::Endpoint,
944 content: SectionContent::Single("invalid".to_string()),
945 inline_options: InlineOptions::default(),
946 raw_content: "".to_string(),
947 start_line: 1,
948 end_line: 1,
949 attributes: Vec::new(),
950 });
951
952 assert!(doc.parse_endpoint().is_none());
953 }
954
955 #[test]
956 fn test_gctf_document_get_requests() {
957 let mut doc = GctfDocument::new("test.gctf".to_string());
958 doc.sections.push(Section {
959 section_type: SectionType::Request,
960 content: SectionContent::Json(json!({"key": "value1"})),
961 inline_options: InlineOptions::default(),
962 raw_content: "".to_string(),
963 start_line: 1,
964 end_line: 2,
965 attributes: Vec::new(),
966 });
967 doc.sections.push(Section {
968 section_type: SectionType::Request,
969 content: SectionContent::Json(json!({"key": "value2"})),
970 inline_options: InlineOptions::default(),
971 raw_content: "".to_string(),
972 start_line: 3,
973 end_line: 4,
974 attributes: Vec::new(),
975 });
976
977 let requests = doc.get_requests();
978 assert_eq!(requests.len(), 2);
979 assert_eq!(requests[0], json!({"key": "value1"}));
980 assert_eq!(requests[1], json!({"key": "value2"}));
981 }
982
983 #[test]
984 fn test_gctf_document_get_assertions() {
985 let mut doc = GctfDocument::new("test.gctf".to_string());
986 doc.sections.push(Section {
987 section_type: SectionType::Asserts,
988 content: SectionContent::Assertions(vec![".id == 1".to_string()]),
989 inline_options: InlineOptions::default(),
990 raw_content: "".to_string(),
991 start_line: 1,
992 end_line: 2,
993 attributes: Vec::new(),
994 });
995 doc.sections.push(Section {
996 section_type: SectionType::Asserts,
997 content: SectionContent::Assertions(vec![".name == \"test\"".to_string()]),
998 inline_options: InlineOptions::default(),
999 raw_content: "".to_string(),
1000 start_line: 3,
1001 end_line: 4,
1002 attributes: Vec::new(),
1003 });
1004
1005 let assertions = doc.get_assertions();
1006 assert_eq!(assertions.len(), 2);
1007 assert_eq!(assertions[0], vec![".id == 1"]);
1008 assert_eq!(assertions[1], vec![".name == \"test\""]);
1009 }
1010
1011 #[test]
1012 fn test_gctf_document_get_request_headers() {
1013 let mut doc = GctfDocument::new("test.gctf".to_string());
1014 let mut headers = HashMap::new();
1015 headers.insert("Authorization".to_string(), "Bearer token".to_string());
1016 doc.sections.push(Section {
1017 section_type: SectionType::RequestHeaders,
1018 content: SectionContent::KeyValues(headers.clone()),
1019 inline_options: InlineOptions::default(),
1020 raw_content: "".to_string(),
1021 start_line: 1,
1022 end_line: 2,
1023 attributes: Vec::new(),
1024 });
1025
1026 let result = doc.get_request_headers().unwrap();
1027 assert_eq!(
1028 result.get("Authorization"),
1029 Some(&"Bearer token".to_string())
1030 );
1031 }
1032
1033 #[test]
1034 fn test_gctf_document_get_tls_config() {
1035 let mut doc = GctfDocument::new("test.gctf".to_string());
1036 let mut config = HashMap::new();
1037 config.insert("ca_cert".to_string(), "/path/to/ca.pem".to_string());
1038 doc.sections.push(Section {
1039 section_type: SectionType::Tls,
1040 content: SectionContent::KeyValues(config.clone()),
1041 inline_options: InlineOptions::default(),
1042 raw_content: "".to_string(),
1043 start_line: 1,
1044 end_line: 2,
1045 attributes: Vec::new(),
1046 });
1047
1048 let result = doc.get_tls_config().unwrap();
1049 assert_eq!(result.get("ca_cert"), Some(&"/path/to/ca.pem".to_string()));
1050 }
1051
1052 #[test]
1053 fn test_gctf_document_get_options() {
1054 let mut doc = GctfDocument::new("test.gctf".to_string());
1055 let mut options = HashMap::new();
1056 options.insert("dry_run".to_string(), "true".to_string());
1057 options.insert("timeout".to_string(), "10".to_string());
1058 doc.sections.push(Section {
1059 section_type: SectionType::Options,
1060 content: SectionContent::KeyValues(options.clone()),
1061 inline_options: InlineOptions::default(),
1062 raw_content: "".to_string(),
1063 start_line: 1,
1064 end_line: 2,
1065 attributes: Vec::new(),
1066 });
1067
1068 let result = doc.get_options().unwrap();
1069 assert_eq!(result.get("dry_run"), Some(&"true".to_string()));
1070 assert_eq!(result.get("timeout"), Some(&"10".to_string()));
1071 }
1072
1073 #[test]
1074 fn test_gctf_document_get_tls_config_with_defaults_env_only() {
1075 let doc = GctfDocument::new("test.gctf".to_string());
1076 let mut defaults = HashMap::new();
1077 defaults.insert("server_name".to_string(), "example.com".to_string());
1078
1079 let result = doc.get_tls_config_with_defaults(&defaults).unwrap();
1080 assert_eq!(result.get("server_name"), Some(&"example.com".to_string()));
1081 }
1082
1083 #[test]
1084 fn test_gctf_document_get_tls_config_with_defaults_section_overrides() {
1085 let mut doc = GctfDocument::new("test.gctf".to_string());
1086 let mut config = HashMap::new();
1087 config.insert("insecure".to_string(), "true".to_string());
1088 doc.sections.push(Section {
1089 section_type: SectionType::Tls,
1090 content: SectionContent::KeyValues(config),
1091 inline_options: InlineOptions::default(),
1092 raw_content: "".to_string(),
1093 start_line: 1,
1094 end_line: 2,
1095 attributes: Vec::new(),
1096 });
1097
1098 let mut defaults = HashMap::new();
1099 defaults.insert("insecure".to_string(), "false".to_string());
1100 defaults.insert("server_name".to_string(), "example.com".to_string());
1101
1102 let result = doc.get_tls_config_with_defaults(&defaults).unwrap();
1103 assert_eq!(result.get("insecure"), Some(&"true".to_string()));
1104 assert_eq!(result.get("server_name"), Some(&"example.com".to_string()));
1105 }
1106
1107 #[test]
1108 fn test_gctf_document_get_proto_config() {
1109 let mut doc = GctfDocument::new("test.gctf".to_string());
1110 let mut config = HashMap::new();
1111 config.insert("files".to_string(), "service.proto".to_string());
1112 doc.sections.push(Section {
1113 section_type: SectionType::Proto,
1114 content: SectionContent::KeyValues(config.clone()),
1115 inline_options: InlineOptions::default(),
1116 raw_content: "".to_string(),
1117 start_line: 1,
1118 end_line: 2,
1119 attributes: Vec::new(),
1120 });
1121
1122 let result = doc.get_proto_config().unwrap();
1123 assert_eq!(result.get("files"), Some(&"service.proto".to_string()));
1124 }
1125
1126 #[test]
1127 fn test_gctf_document_has_response_error_conflict() {
1128 let mut doc = GctfDocument::new("test.gctf".to_string());
1129 assert!(!doc.has_response_error_conflict());
1130
1131 doc.sections.push(Section {
1132 section_type: SectionType::Response,
1133 content: SectionContent::Json(json!({"result": "ok"})),
1134 inline_options: InlineOptions::default(),
1135 raw_content: "".to_string(),
1136 start_line: 1,
1137 end_line: 2,
1138 attributes: Vec::new(),
1139 });
1140 assert!(!doc.has_response_error_conflict());
1141
1142 doc.sections.push(Section {
1143 section_type: SectionType::Error,
1144 content: SectionContent::Json(json!({"code": 5})),
1145 inline_options: InlineOptions::default(),
1146 raw_content: "".to_string(),
1147 start_line: 3,
1148 end_line: 4,
1149 attributes: Vec::new(),
1150 });
1151 assert!(doc.has_response_error_conflict());
1152 }
1153
1154 #[test]
1155 fn test_inline_options_default() {
1156 let options = InlineOptions::default();
1157 assert!(!options.with_asserts);
1158 assert!(!options.partial);
1159 assert!(options.tolerance.is_none());
1160 assert!(options.redact.is_empty());
1161 assert!(!options.unordered_arrays);
1162 }
1163
1164 #[test]
1165 fn test_section_format_header_with_inline_options() {
1166 let section = Section {
1167 section_type: SectionType::Response,
1168 content: SectionContent::Json(serde_json::json!({"ok": true})),
1169 inline_options: InlineOptions {
1170 with_asserts: true,
1171 partial: true,
1172 tolerance: Some(0.1),
1173 redact: vec!["token".to_string()],
1174 unordered_arrays: true,
1175 },
1176 raw_content: "".to_string(),
1177 start_line: 0,
1178 end_line: 0,
1179 attributes: Vec::new(),
1180 };
1181
1182 let header = section.format_header();
1183 assert_eq!(
1184 header,
1185 "--- RESPONSE partial tolerance=0.1 redact=[\"token\"] unordered_arrays with_asserts ---"
1186 );
1187 }
1188
1189 #[test]
1190 fn test_section_content_debug() {
1191 let content = SectionContent::Single("test".to_string());
1192 let debug_str = format!("{:?}", content);
1193 assert!(debug_str.contains("Single"));
1194 }
1195
1196 #[test]
1197 fn test_section_header_keyword_from_source() {
1198 let section = Section {
1199 section_type: SectionType::Response,
1200 content: SectionContent::Json(serde_json::json!({"ok": true})),
1201 inline_options: InlineOptions::default(),
1202 raw_content: "{\"ok\":true}".to_string(),
1203 start_line: 0,
1204 end_line: 2,
1205 attributes: Vec::new(),
1206 };
1207
1208 let source = "--- RESPONSE with_asserts=true ---\n{\"ok\":true}\n";
1209 assert_eq!(section.header_keyword_from_source(source), Some("RESPONSE"));
1210 }
1211
1212 #[test]
1213 fn test_document_detects_deprecated_headers_alias() {
1214 let mut doc = GctfDocument::new("test.gctf".to_string());
1215 doc.metadata.source = Some("--- HEADERS ---\nAuthorization: Bearer t\n".to_string());
1216 doc.sections.push(Section {
1217 section_type: SectionType::RequestHeaders,
1218 content: SectionContent::KeyValues(HashMap::from([(
1219 "Authorization".to_string(),
1220 "Bearer t".to_string(),
1221 )])),
1222 inline_options: InlineOptions::default(),
1223 raw_content: "Authorization: Bearer t".to_string(),
1224 start_line: 0,
1225 end_line: 2,
1226 attributes: Vec::new(),
1227 });
1228
1229 assert!(doc.section_uses_deprecated_headers_alias(&doc.sections[0]));
1230 }
1231
1232 #[test]
1233 fn test_gctf_document_debug() {
1234 let doc = GctfDocument::new("test.gctf".to_string());
1235 let debug_str = format!("{:?}", doc);
1236 assert!(debug_str.contains("test.gctf"));
1237 }
1238
1239 #[test]
1242 fn test_document_chain_single() {
1243 let doc = GctfDocument::new("test.gctf".to_string());
1244 assert!(doc.is_single_document());
1245 assert_eq!(doc.document_count(), 1);
1246 }
1247
1248 #[test]
1249 fn test_document_chain_two_docs() {
1250 let mut doc1 = GctfDocument::new("test.gctf".to_string());
1251 let doc2 = GctfDocument::new("test.gctf".to_string());
1252 doc1.next_document = Some(Box::new(doc2));
1253
1254 assert!(!doc1.is_single_document());
1255 assert_eq!(doc1.document_count(), 2);
1256 }
1257
1258 #[test]
1259 fn test_document_chain_three_docs() {
1260 let mut doc3 = GctfDocument::new("test.gctf".to_string());
1261 doc3.file_path = "doc3".to_string();
1262
1263 let mut doc2 = GctfDocument::new("test.gctf".to_string());
1264 doc2.file_path = "doc2".to_string();
1265 doc2.next_document = Some(Box::new(doc3));
1266
1267 let mut doc1 = GctfDocument::new("test.gctf".to_string());
1268 doc1.file_path = "doc1".to_string();
1269 doc1.next_document = Some(Box::new(doc2));
1270
1271 assert_eq!(doc1.document_count(), 3);
1272
1273 let docs: Vec<_> = doc1.iter_chain().collect();
1274 assert_eq!(docs.len(), 3);
1275 assert_eq!(docs[0].file_path, "doc1");
1276 assert_eq!(docs[1].file_path, "doc2");
1277 assert_eq!(docs[2].file_path, "doc3");
1278 }
1279
1280 #[test]
1281 fn test_document_chain_get_document() {
1282 let mut doc2 = GctfDocument::new("test.gctf".to_string());
1283 doc2.file_path = "doc2".to_string();
1284
1285 let mut doc1 = GctfDocument::new("test.gctf".to_string());
1286 doc1.file_path = "doc1".to_string();
1287 doc1.next_document = Some(Box::new(doc2));
1288
1289 assert_eq!(doc1.get_document(0).unwrap().file_path, "doc1");
1290 assert_eq!(doc1.get_document(1).unwrap().file_path, "doc2");
1291 assert!(doc1.get_document(2).is_none());
1292 }
1293
1294 #[test]
1295 fn test_document_chain_iter_on_last() {
1296 let doc = GctfDocument::new("test.gctf".to_string());
1297 let docs: Vec<_> = doc.iter_chain().collect();
1298 assert_eq!(docs.len(), 1);
1299 assert_eq!(docs[0].file_path, "test.gctf");
1300 }
1301
1302 #[test]
1303 fn test_gctf_attribute_parse_u64() {
1304 assert_eq!(GctfAttribute::new("timeout", "30").parse_u64(), Some(30));
1305 assert_eq!(
1306 GctfAttribute::new("timeout", " 30 ").parse_u64(),
1307 Some(30)
1308 );
1309 assert_eq!(GctfAttribute::new("timeout", "0").parse_u64(), Some(0));
1310 assert_eq!(GctfAttribute::new("timeout", "abc").parse_u64(), None);
1311 assert_eq!(GctfAttribute::new("timeout", "-1").parse_u64(), None);
1312 }
1313
1314 #[test]
1315 fn test_gctf_attribute_parse_u32() {
1316 assert_eq!(GctfAttribute::new("retry", "3").parse_u32(), Some(3));
1317 assert_eq!(GctfAttribute::new("retry", " 5 ").parse_u32(), Some(5));
1318 assert_eq!(GctfAttribute::new("retry", "0").parse_u32(), Some(0));
1319 assert_eq!(GctfAttribute::new("retry", "abc").parse_u32(), None);
1320 }
1321
1322 #[test]
1323 fn test_gctf_attribute_parse_f64() {
1324 assert_eq!(
1325 GctfAttribute::new("tolerance", "0.1").parse_f64(),
1326 Some(0.1)
1327 );
1328 assert_eq!(
1329 GctfAttribute::new("tolerance", " 1.5 ").parse_f64(),
1330 Some(1.5)
1331 );
1332 assert_eq!(GctfAttribute::new("tolerance", "abc").parse_f64(), None);
1333 }
1334
1335 #[test]
1336 fn test_gctf_attribute_parse_bool() {
1337 let cases_true = vec!["true", "1", "yes", "on", "True", "TRUE", "YES"];
1338 for v in cases_true {
1339 assert_eq!(
1340 GctfAttribute::new("skip", v).parse_bool(),
1341 Some(true),
1342 "failed for {}",
1343 v
1344 );
1345 }
1346 let cases_false = vec!["false", "0", "no", "off", "", "False", "FALSE"];
1347 for v in cases_false {
1348 assert_eq!(
1349 GctfAttribute::new("skip", v).parse_bool(),
1350 Some(false),
1351 "failed for {}",
1352 v
1353 );
1354 }
1355 assert_eq!(GctfAttribute::new("skip", "maybe").parse_bool(), None);
1356 }
1357
1358 #[test]
1359 fn test_gctf_attribute_as_str() {
1360 assert_eq!(GctfAttribute::new("name", "hello").as_str(), "hello");
1361 assert_eq!(GctfAttribute::flag("skip").as_str(), "true");
1362 }
1363
1364 #[test]
1365 fn test_section_get_attribute() {
1366 let section = Section {
1367 section_type: SectionType::Request,
1368 content: SectionContent::Empty,
1369 inline_options: InlineOptions::default(),
1370 raw_content: String::new(),
1371 start_line: 0,
1372 end_line: 0,
1373 attributes: vec![
1374 GctfAttribute::new("timeout", "30"),
1375 GctfAttribute::new("retry", "2"),
1376 ],
1377 };
1378 assert!(section.get_attribute("timeout").is_some());
1379 assert!(section.get_attribute("retry").is_some());
1380 assert!(section.get_attribute("skip").is_none());
1381 assert_eq!(
1382 section.get_attribute("timeout").unwrap().parse_u64(),
1383 Some(30)
1384 );
1385 }
1386
1387 #[test]
1388 fn test_section_get_timeout() {
1389 let section = Section {
1390 section_type: SectionType::Request,
1391 content: SectionContent::Empty,
1392 inline_options: InlineOptions::default(),
1393 raw_content: String::new(),
1394 start_line: 0,
1395 end_line: 0,
1396 attributes: vec![GctfAttribute::new("timeout", "10")],
1397 };
1398 assert_eq!(section.get_timeout(), Some(10));
1399 }
1400
1401 #[test]
1402 fn test_section_get_timeout_zero() {
1403 let section = Section {
1404 section_type: SectionType::Request,
1405 content: SectionContent::Empty,
1406 inline_options: InlineOptions::default(),
1407 raw_content: String::new(),
1408 start_line: 0,
1409 end_line: 0,
1410 attributes: vec![GctfAttribute::new("timeout", "0")],
1411 };
1412 assert_eq!(section.get_timeout(), None);
1413 }
1414
1415 #[test]
1416 fn test_section_get_timeout_missing() {
1417 let section = Section::default();
1418 assert_eq!(section.get_timeout(), None);
1419 }
1420
1421 #[test]
1422 fn test_section_get_retry() {
1423 let section = Section {
1424 section_type: SectionType::Request,
1425 content: SectionContent::Empty,
1426 inline_options: InlineOptions::default(),
1427 raw_content: String::new(),
1428 start_line: 0,
1429 end_line: 0,
1430 attributes: vec![GctfAttribute::new("retry", "3")],
1431 };
1432 assert_eq!(section.get_retry(), Some(3));
1433 }
1434
1435 #[test]
1436 fn test_section_get_retry_missing() {
1437 let section = Section::default();
1438 assert_eq!(section.get_retry(), None);
1439 }
1440
1441 #[test]
1442 fn test_section_get_skip() {
1443 let section = Section {
1444 section_type: SectionType::Request,
1445 content: SectionContent::Empty,
1446 inline_options: InlineOptions::default(),
1447 raw_content: String::new(),
1448 start_line: 0,
1449 end_line: 0,
1450 attributes: vec![GctfAttribute::flag("skip")],
1451 };
1452 assert!(section.get_skip());
1453 }
1454
1455 #[test]
1456 fn test_section_get_skip_explicit() {
1457 let section = Section {
1458 section_type: SectionType::Request,
1459 content: SectionContent::Empty,
1460 inline_options: InlineOptions::default(),
1461 raw_content: String::new(),
1462 start_line: 0,
1463 end_line: 0,
1464 attributes: vec![GctfAttribute::new("skip", "true")],
1465 };
1466 assert!(section.get_skip());
1467 }
1468
1469 #[test]
1470 fn test_section_get_skip_false() {
1471 let section = Section::default();
1472 assert!(!section.get_skip());
1473 }
1474
1475 #[test]
1476 fn test_section_has_tag() {
1477 let section = Section {
1478 section_type: SectionType::Request,
1479 content: SectionContent::Empty,
1480 inline_options: InlineOptions::default(),
1481 raw_content: String::new(),
1482 start_line: 0,
1483 end_line: 0,
1484 attributes: vec![GctfAttribute::new("tag", "smoke,slow")],
1485 };
1486 assert!(section.has_tag("smoke"));
1487 assert!(section.has_tag("slow"));
1488 assert!(!section.has_tag("integration"));
1489 }
1490
1491 #[test]
1492 fn test_section_has_tag_single() {
1493 let section = Section {
1494 section_type: SectionType::Request,
1495 content: SectionContent::Empty,
1496 inline_options: InlineOptions::default(),
1497 raw_content: String::new(),
1498 start_line: 0,
1499 end_line: 0,
1500 attributes: vec![GctfAttribute::new("tag", "smoke")],
1501 };
1502 assert!(section.has_tag("smoke"));
1503 assert!(!section.has_tag("slow"));
1504 }
1505
1506 #[test]
1507 fn test_section_has_tag_missing() {
1508 let section = Section::default();
1509 assert!(!section.has_tag("smoke"));
1510 }
1511}