Skip to main content

apif_parser/
builder.rs

1// Builder utilities for constructing .gctf documents programmatically.
2
3use 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}