Skip to main content

sails_idl_parser_v2/preprocess/
mod.rs

1use crate::error::{Error, Result};
2use alloc::collections::BTreeSet;
3use alloc::string::{String, ToString};
4
5#[cfg(feature = "std")]
6pub mod fs;
7#[cfg(feature = "std")]
8pub mod git;
9
10/// The result of loading an IDL source — content and a unique id used for deduplication.
11#[derive(Debug)]
12pub struct IdlSource {
13    pub content: String,
14    /// Unique identifier (e.g. canonical file path or full git:// URL).
15    pub id: String,
16}
17
18/// Trait for loading IDL content from a path or URL.
19///
20/// Implement this trait to support custom IDL sources (local files, git, HTTP, etc.).
21/// A loader is responsible for three things:
22/// - loading the raw IDL content (`load`)
23/// - resolving relative include paths relative to a base (`resolve`)
24pub trait IdlLoader {
25    /// Loads the IDL source at `path`, returning its content and a unique id.
26    fn load(&self, path: &str) -> Result<IdlSource>;
27
28    /// Resolves a relative `include_path` against `base_path`.
29    ///
30    /// Returns `None` when this loader does not handle `base_path`.
31    fn resolve(&self, base_path: &str, include_path: &str) -> Option<String>;
32}
33
34/// Preprocesses the IDL source starting from `path`, resolving `!@include` directives.
35///
36/// `loaders` are tried in order — the first one that resolves the path is used.
37/// Each file (identified by `IdlSource::id`) is included at most once.
38pub fn preprocess(path: &str, loaders: &[&dyn IdlLoader]) -> Result<String> {
39    let mut visited = BTreeSet::new();
40    let mut result = String::new();
41    preprocess_recursive(path, loaders, &mut visited, &mut result)?;
42    Ok(result)
43}
44
45fn preprocess_recursive(
46    path: &str,
47    loaders: &[&dyn IdlLoader],
48    visited: &mut BTreeSet<String>,
49    out: &mut String,
50) -> Result<()> {
51    let loader = loaders
52        .iter()
53        .find(|loader| loader.resolve(path, path).is_some())
54        .ok_or_else(|| Error::Preprocess(alloc::format!("No loader can handle path: {path}")))?;
55
56    let source = loader.load(path)?;
57
58    if !visited.insert(source.id) {
59        return Ok(());
60    }
61
62    for line in source.content.lines() {
63        let trimmed = line.trim();
64
65        if let Some(rest) = trimmed.strip_prefix("!@include:") {
66            let include_path = rest.trim().trim_matches(|c| c == '"' || c == '\'');
67
68            if include_path.is_empty() {
69                return Err(Error::Preprocess("Invalid include directive".to_string()));
70            }
71
72            let next_path = loaders
73                .iter()
74                .filter_map(|loader| loader.resolve(path, include_path))
75                .next()
76                .ok_or_else(|| {
77                    Error::Preprocess(alloc::format!(
78                        "No loader can resolve include '{include_path}' from: {path}"
79                    ))
80                })?;
81            preprocess_recursive(&next_path, loaders, visited, out)?;
82
83            if !out.is_empty() && !out.ends_with('\n') {
84                out.push('\n');
85            }
86        } else {
87            out.push_str(line);
88            out.push('\n');
89        }
90    }
91
92    Ok(())
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use alloc::collections::BTreeMap;
99    use alloc::format;
100
101    struct MapLoader(BTreeMap<String, String>);
102
103    impl IdlLoader for MapLoader {
104        fn load(&self, path: &str) -> Result<IdlSource> {
105            let content = self
106                .0
107                .get(path)
108                .cloned()
109                .ok_or_else(|| Error::Preprocess(format!("File not found: {path}")))?;
110            Ok(IdlSource {
111                content,
112                id: path.to_string(),
113            })
114        }
115
116        fn resolve(&self, base_path: &str, include_path: &str) -> Option<String> {
117            if let Some(pos) = base_path.rfind('/') {
118                Some(format!("{}{}", &base_path[..pos + 1], include_path))
119            } else {
120                Some(String::from(include_path))
121            }
122        }
123    }
124
125    #[test]
126    fn test_preprocess_recursive() {
127        let mut files = BTreeMap::new();
128        files.insert("leaf.idl".into(), "service Leaf {}".into());
129        files.insert(
130            "middle.idl".into(),
131            "!@include: leaf.idl\nservice Middle {}".into(),
132        );
133        files.insert(
134            "main.idl".into(),
135            "!@include: middle.idl\nservice Main {}".into(),
136        );
137
138        let loader = MapLoader(files);
139        let result = preprocess("main.idl", &[&loader]).unwrap();
140        assert!(result.contains("service Leaf"));
141        assert!(result.contains("service Middle"));
142        assert!(result.contains("service Main"));
143    }
144
145    #[test]
146    fn test_preprocess_duplicate_prevented() {
147        let mut files = BTreeMap::new();
148        files.insert("common.idl".into(), "struct Common {}".into());
149        files.insert("a.idl".into(), "!@include: common.idl\nservice A {}".into());
150        files.insert("b.idl".into(), "!@include: common.idl\nservice B {}".into());
151        files.insert(
152            "main.idl".into(),
153            "!@include: a.idl\n!@include: b.idl".into(),
154        );
155
156        let loader = MapLoader(files);
157        let result = preprocess("main.idl", &[&loader]).unwrap();
158
159        let count = result.matches("struct Common").count();
160        assert_eq!(count, 1); // Should be included only once
161    }
162
163    #[test]
164    fn test_preprocess_complex_includes() {
165        let mut files = BTreeMap::new();
166        files.insert(
167            "common.idl".into(),
168            r#"!@sails: 0.1.0
169            !@author: gear
170
171            service CommonSvc {
172                types {
173                    struct Common {
174                        id: u64,
175                    }
176                }
177            }"#
178            .into(),
179        );
180        files.insert(
181            "service_a.idl".into(),
182            r#"!@include: common.idl
183
184            service ServiceA {
185                functions {
186                    Do(c: u64);
187                }
188            }"#
189            .into(),
190        );
191        files.insert(
192            "main.idl".into(),
193            r#"!@sails: 0.1.0
194            !@include: service_a.idl
195
196            program Main {
197                services {
198                    ServiceA: ServiceA,
199                }
200            }"#
201            .into(),
202        );
203
204        let loader = MapLoader(files);
205        let result = preprocess("main.idl", &[&loader]).unwrap();
206
207        let doc = crate::parse_idl(&result).expect("Failed to parse preprocessed IDL");
208
209        assert_eq!(doc.globals.len(), 3);
210        assert_eq!(doc.services.len(), 2);
211        assert!(doc.program.is_some());
212    }
213}