sails_idl_parser_v2/preprocess/
mod.rs1use 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#[derive(Debug)]
12pub struct IdlSource {
13 pub content: String,
14 pub id: String,
16}
17
18pub trait IdlLoader {
25 fn load(&self, path: &str) -> Result<IdlSource>;
27
28 fn resolve(&self, base_path: &str, include_path: &str) -> Option<String>;
32}
33
34pub 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); }
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}