1use std::collections::HashMap;
4
5use serde_json::Value;
6
7use apif_ast::{
8 DocumentMetadata, FileMeta, GctfDocument, InlineOptions, Section, SectionContent, SectionType,
9};
10
11#[derive(Debug, Clone)]
12pub struct GctfDocumentBuilder {
13 file_path: String,
14 sections: Vec<Section>,
15}
16
17impl GctfDocumentBuilder {
18 pub fn new() -> Self {
19 Self {
20 file_path: String::new(),
21 sections: Vec::new(),
22 }
23 }
24
25 pub fn with_file_path(mut self, file_path: impl Into<String>) -> Self {
26 self.file_path = file_path.into();
27 self
28 }
29
30 pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
31 self.push_section(
32 SectionType::Endpoint,
33 SectionContent::Single(endpoint.into()),
34 );
35 self
36 }
37
38 pub fn address(mut self, address: impl Into<String>) -> Self {
39 self.push_section(SectionType::Address, SectionContent::Single(address.into()));
40 self
41 }
42
43 pub fn request_headers(mut self, headers: HashMap<String, String>) -> Self {
44 if !headers.is_empty() {
45 self.push_section(
46 SectionType::RequestHeaders,
47 SectionContent::KeyValues(headers),
48 );
49 }
50 self
51 }
52
53 pub fn request(mut self, request: Value) -> Self {
54 self.push_section(SectionType::Request, SectionContent::Json(request));
55 self
56 }
57
58 pub fn response(mut self, response: Value) -> Self {
59 self.push_section(SectionType::Response, SectionContent::Json(response));
60 self
61 }
62
63 pub fn error(mut self, error: impl Into<String>) -> Self {
64 self.push_section(SectionType::Error, SectionContent::Single(error.into()));
65 self
66 }
67
68 pub fn tls(mut self, tls: HashMap<String, String>) -> Self {
69 if !tls.is_empty() {
70 self.push_section(SectionType::Tls, SectionContent::KeyValues(tls));
71 }
72 self
73 }
74
75 pub fn options(mut self, options: HashMap<String, String>) -> Self {
76 if !options.is_empty() {
77 self.push_section(SectionType::Options, SectionContent::KeyValues(options));
78 }
79 self
80 }
81
82 pub fn proto(mut self, proto: HashMap<String, String>) -> Self {
83 if !proto.is_empty() {
84 self.push_section(SectionType::Proto, SectionContent::KeyValues(proto));
85 }
86 self
87 }
88
89 pub fn meta(mut self, meta: FileMeta) -> Self {
90 if !meta.is_empty() {
91 self.push_section(SectionType::Meta, SectionContent::Meta(meta));
92 }
93 self
94 }
95
96 pub fn build(self) -> GctfDocument {
97 GctfDocument {
98 file_path: self.file_path,
99 sections: self.sections,
100 metadata: DocumentMetadata {
101 source: None,
102 mtime: None,
103 parsed_at: std::time::SystemTime::now()
104 .duration_since(std::time::UNIX_EPOCH)
105 .unwrap_or_default()
106 .as_secs() as i64,
107 ..Default::default()
108 },
109 next_document: None,
110 }
111 }
112
113 pub fn render(self) -> String {
114 let doc = self.build();
115 crate::core::serialize_gctf(&doc)
116 }
117
118 fn push_section(&mut self, section_type: SectionType, content: SectionContent) {
119 self.sections.push(Section {
120 section_type,
121 content,
122 inline_options: InlineOptions::default(),
123 raw_content: String::new(),
124 start_line: 0,
125 end_line: 0,
126 attributes: Vec::new(),
127 });
128 }
129}
130
131impl Default for GctfDocumentBuilder {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use serde_json::json;
141
142 #[test]
143 fn builder_renders_minimal_document() {
144 let output = GctfDocumentBuilder::new()
145 .address("localhost:4770")
146 .endpoint("auth.AuthService/CheckAccess")
147 .request(json!({"action": "delete"}))
148 .render();
149
150 assert!(output.contains("--- ADDRESS ---\nlocalhost:4770"));
151 assert!(output.contains("--- ENDPOINT ---\nauth.AuthService/CheckAccess"));
152 assert!(output.contains("--- REQUEST ---"));
153 }
154
155 #[test]
156 fn builder_skips_empty_maps() {
157 let output = GctfDocumentBuilder::new()
158 .address("localhost:4770")
159 .endpoint("svc/method")
160 .request_headers(HashMap::new())
161 .options(HashMap::new())
162 .proto(HashMap::new())
163 .request(json!({}))
164 .render();
165
166 assert!(!output.contains("REQUEST_HEADERS"));
167 assert!(!output.contains("OPTIONS"));
168 assert!(!output.contains("PROTO"));
169 }
170}