Skip to main content

apif_parser/
document_splitter.rs

1// Split GCTF sections into multiple documents based on ENDPOINT boundaries.
2//
3// "Preamble" sections (ADDRESS, TLS, PROTO, OPTIONS, REQUEST_HEADERS) that appear
4// immediately before ENDPOINT are moved to the new document — they configure the
5// next request, not the previous one.
6
7use crate::ast::{Section, SectionType};
8
9/// Sections that are "preambles" — they belong to the next document, not the current one.
10fn is_preamble(t: &SectionType) -> bool {
11    matches!(
12        t,
13        SectionType::Address
14            | SectionType::Tls
15            | SectionType::Proto
16            | SectionType::Options
17            | SectionType::RequestHeaders
18    )
19}
20
21fn is_content_section(t: &SectionType) -> bool {
22    matches!(
23        t,
24        SectionType::Request
25            | SectionType::Response
26            | SectionType::Error
27            | SectionType::Asserts
28            | SectionType::Extract
29    )
30}
31
32/// Split owned sections into documents based on ENDPOINT boundaries.
33///
34/// Each ENDPOINT with meaningful content before it starts a new document.
35pub fn split_sections_by_boundary_owned(sections: Vec<Section>) -> Vec<Vec<Section>> {
36    if sections.is_empty() {
37        return Vec::new();
38    }
39
40    let mut docs: Vec<Vec<Section>> = Vec::new();
41    let mut current: Vec<Section> = Vec::new();
42    let mut current_has_content = false;
43
44    for section in sections {
45        if section.section_type == SectionType::Endpoint && current_has_content {
46            let mut preamble: Vec<Section> = Vec::new();
47            while let Some(last) = current.last() {
48                if is_preamble(&last.section_type) {
49                    if let Some(section) = current.pop() {
50                        preamble.push(section);
51                    } else {
52                        break;
53                    }
54                } else {
55                    break;
56                }
57            }
58            preamble.reverse();
59            docs.push(std::mem::take(&mut current));
60            current = preamble;
61            current_has_content = false;
62        }
63
64        if is_content_section(&section.section_type) {
65            current_has_content = true;
66        }
67        current.push(section);
68    }
69
70    if !current.is_empty() {
71        docs.push(current);
72    }
73
74    docs
75}
76
77/// Split sections into documents based on ENDPOINT boundaries.
78///
79/// Convenience wrapper for borrowed input.
80pub fn split_sections_by_boundary(sections: &[Section]) -> Vec<Vec<Section>> {
81    split_sections_by_boundary_owned(sections.to_vec())
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::ast::{Section, SectionContent, SectionType};
88
89    fn section(stype: SectionType, line: usize) -> Section {
90        Section {
91            section_type: stype,
92            content: SectionContent::Empty,
93            inline_options: Default::default(),
94            raw_content: String::new(),
95            start_line: line,
96            end_line: line,
97            attributes: Vec::new(),
98        }
99    }
100
101    #[test]
102    fn test_split_single_document() {
103        let sections = vec![
104            section(SectionType::Endpoint, 0),
105            section(SectionType::Request, 2),
106            section(SectionType::Response, 4),
107        ];
108        let docs = split_sections_by_boundary(&sections);
109        assert_eq!(docs.len(), 1);
110        assert_eq!(docs[0].len(), 3);
111    }
112
113    #[test]
114    fn test_split_two_documents_with_preamble() {
115        let sections = vec![
116            section(SectionType::Endpoint, 0),
117            section(SectionType::Request, 2),
118            section(SectionType::Response, 4),
119            section(SectionType::Address, 7), // preamble for next
120            section(SectionType::Endpoint, 9),
121            section(SectionType::Request, 11),
122            section(SectionType::Response, 13),
123        ];
124        let docs = split_sections_by_boundary(&sections);
125        assert_eq!(docs.len(), 2);
126        assert_eq!(docs[0].len(), 3); // endpoint + request + response
127        assert_eq!(docs[1].len(), 4); // address + endpoint + request + response
128    }
129
130    #[test]
131    fn test_split_empty_input() {
132        let sections: Vec<Section> = vec![];
133        let docs = split_sections_by_boundary(&sections);
134        assert!(docs.is_empty());
135    }
136
137    #[test]
138    fn test_split_no_endpoint_single_doc() {
139        let sections = vec![
140            section(SectionType::Address, 0),
141            section(SectionType::Request, 2),
142            section(SectionType::Response, 4),
143        ];
144        let docs = split_sections_by_boundary(&sections);
145        assert_eq!(docs.len(), 1);
146        assert_eq!(docs[0].len(), 3);
147    }
148
149    #[test]
150    fn test_split_only_preamble_sections() {
151        let sections = vec![
152            section(SectionType::Address, 0),
153            section(SectionType::Tls, 2),
154            section(SectionType::Proto, 4),
155        ];
156        let docs = split_sections_by_boundary(&sections);
157        assert_eq!(docs.len(), 1);
158        assert_eq!(docs[0].len(), 3);
159    }
160
161    #[test]
162    fn test_split_preambles_without_content_no_split() {
163        // Without content before first endpoint, preambles stay in single doc
164        let sections = vec![
165            section(SectionType::Address, 0),
166            section(SectionType::Tls, 2),
167            section(SectionType::Proto, 4),
168            section(SectionType::Options, 6),
169            section(SectionType::RequestHeaders, 8),
170            section(SectionType::Endpoint, 10),
171            section(SectionType::Request, 12),
172            section(SectionType::Response, 14),
173        ];
174        let docs = split_sections_by_boundary(&sections);
175        // No split - no content before first endpoint
176        assert_eq!(docs.len(), 1);
177        assert_eq!(docs[0].len(), 8); // all sections in one doc
178    }
179
180    #[test]
181    fn test_split_endpoint_at_start() {
182        let sections = vec![
183            section(SectionType::Endpoint, 0),
184            section(SectionType::Request, 2),
185            section(SectionType::Response, 4),
186            section(SectionType::Endpoint, 6),
187            section(SectionType::Request, 8),
188        ];
189        let docs = split_sections_by_boundary(&sections);
190        assert_eq!(docs.len(), 2);
191        assert_eq!(docs[0].len(), 3);
192        assert_eq!(docs[1].len(), 2);
193    }
194
195    #[test]
196    fn test_split_extract_as_terminal() {
197        let sections = vec![
198            section(SectionType::Endpoint, 0),
199            section(SectionType::Request, 2),
200            section(SectionType::Response, 4),
201            section(SectionType::Extract, 6), // terminal
202            section(SectionType::Endpoint, 8),
203            section(SectionType::Request, 10),
204        ];
205        let docs = split_sections_by_boundary(&sections);
206        assert_eq!(docs.len(), 2);
207    }
208
209    #[test]
210    fn test_split_asserts_as_terminal() {
211        let sections = vec![
212            section(SectionType::Endpoint, 0),
213            section(SectionType::Request, 2),
214            section(SectionType::Asserts, 4), // terminal
215            section(SectionType::Endpoint, 6),
216            section(SectionType::Request, 8),
217        ];
218        let docs = split_sections_by_boundary(&sections);
219        assert_eq!(docs.len(), 2);
220    }
221
222    #[test]
223    fn test_split_error_as_terminal() {
224        let sections = vec![
225            section(SectionType::Endpoint, 0),
226            section(SectionType::Request, 2),
227            section(SectionType::Error, 4), // terminal
228            section(SectionType::Endpoint, 6),
229            section(SectionType::Request, 8),
230        ];
231        let docs = split_sections_by_boundary(&sections);
232        assert_eq!(docs.len(), 2);
233    }
234
235    #[test]
236    fn test_preamble_selection() {
237        let sections = vec![
238            section(SectionType::Endpoint, 0),
239            section(SectionType::Request, 2),
240            section(SectionType::Response, 4),
241            section(SectionType::Address, 6),         // preamble
242            section(SectionType::Tls, 8),             // preamble
243            section(SectionType::RequestHeaders, 10), // preamble
244            section(SectionType::Endpoint, 12),
245            section(SectionType::Request, 14),
246        ];
247        let docs = split_sections_by_boundary(&sections);
248        assert_eq!(docs.len(), 2);
249        assert_eq!(docs[0].len(), 3);
250        assert_eq!(docs[1].len(), 5); // 3 preambles + 2 new sections
251    }
252}