1use anyhow::{Context, Result};
2use root_generator::RootGenerator;
3use sails_idl_parser_v2::{FsLoader, GitLoader, parse_idl, preprocess, visitor};
4use std::{collections::HashMap, fs, io::Write, path::Path};
5
6mod ctor_generators;
7mod events_generator;
8mod helpers;
9mod mock_generator;
10mod root_generator;
11mod service_generators;
12mod type_generators;
13
14const SAILS: &str = "sails_rs";
15
16pub struct IdlPath<'ast>(&'ast Path);
17pub struct IdlString<'ast>(&'ast str);
18pub struct ClientGenerator<'ast, S> {
19 sails_path: Option<&'ast str>,
20 mocks_feature_name: Option<&'ast str>,
21 external_types: HashMap<&'ast str, &'ast str>,
22 no_derive_traits: bool,
23 with_no_std: bool,
24 client_path: Option<&'ast Path>,
25 idl: S,
26}
27
28impl<'ast, S> ClientGenerator<'ast, S> {
29 pub fn with_mocks(self, mocks_feature_name: &'ast str) -> Self {
30 Self {
31 mocks_feature_name: Some(mocks_feature_name),
32 ..self
33 }
34 }
35
36 pub fn with_sails_crate(self, sails_path: &'ast str) -> Self {
37 Self {
38 sails_path: Some(sails_path),
39 ..self
40 }
41 }
42
43 pub fn with_no_std(self, with_no_std: bool) -> Self {
44 Self {
45 with_no_std,
46 ..self
47 }
48 }
49
50 pub fn with_external_type(self, name: &'ast str, path: &'ast str) -> Self {
62 let mut external_types = self.external_types;
63 external_types.insert(name, path);
64 Self {
65 external_types,
66 ..self
67 }
68 }
69
70 pub fn with_no_derive_traits(self) -> Self {
74 Self {
75 no_derive_traits: true,
76 ..self
77 }
78 }
79
80 pub fn with_client_path(self, client_path: &'ast Path) -> Self {
81 Self {
82 client_path: Some(client_path),
83 ..self
84 }
85 }
86}
87
88impl<'ast> ClientGenerator<'ast, IdlPath<'ast>> {
89 pub fn from_idl_path(idl_path: &'ast Path) -> Self {
90 Self {
91 sails_path: None,
92 mocks_feature_name: None,
93 external_types: HashMap::new(),
94 no_derive_traits: false,
95 with_no_std: false,
96 client_path: None,
97 idl: IdlPath(idl_path),
98 }
99 }
100
101 pub fn generate(self) -> Result<()> {
102 let client_path = self.client_path.context("client path not set")?;
103 let idl_path = self.idl.0;
104
105 let path_str = idl_path.to_string_lossy();
106 let idl = preprocess::preprocess(&path_str, &[&FsLoader, &GitLoader])
107 .with_context(|| format!("Failed to open {} for reading", idl_path.display()))?;
108
109 self.with_idl(&idl)
110 .generate_to(client_path)
111 .context("failed to generate client")?;
112 Ok(())
113 }
114
115 pub fn generate_to(self, out_path: impl AsRef<Path>) -> Result<()> {
116 let idl_path = self.idl.0;
117
118 let path_str = idl_path.to_string_lossy();
119 let idl = preprocess::preprocess(&path_str, &[&FsLoader, &GitLoader])
120 .with_context(|| format!("Failed to open {} for reading", idl_path.display()))?;
121
122 self.with_idl(&idl)
123 .generate_to(out_path)
124 .context("failed to generate client")?;
125 Ok(())
126 }
127
128 fn with_idl(self, idl: &'ast str) -> ClientGenerator<'ast, IdlString<'ast>> {
129 ClientGenerator {
130 sails_path: self.sails_path,
131 mocks_feature_name: self.mocks_feature_name,
132 external_types: self.external_types,
133 no_derive_traits: self.no_derive_traits,
134 with_no_std: self.with_no_std,
135 client_path: self.client_path,
136 idl: IdlString(idl),
137 }
138 }
139}
140
141impl<'ast> ClientGenerator<'ast, IdlString<'ast>> {
142 pub fn from_idl(idl: &'ast str) -> Self {
143 Self {
144 sails_path: None,
145 mocks_feature_name: None,
146 external_types: HashMap::new(),
147 no_derive_traits: false,
148 with_no_std: false,
149 client_path: None,
150 idl: IdlString(idl),
151 }
152 }
153
154 pub fn generate(self) -> Result<String> {
155 let idl = self.idl.0;
156 let sails_path = self.sails_path.unwrap_or(SAILS);
157 let mut generator = RootGenerator::new(
158 self.mocks_feature_name,
159 sails_path,
160 self.external_types,
161 self.no_derive_traits,
162 );
163 let doc = parse_idl(idl).context("Failed to parse IDL")?;
164 visitor::accept_idl_doc(&doc, &mut generator);
165
166 let code = generator.finalize(self.with_no_std);
167
168 let code = pretty_with_rustfmt(&code);
170
171 Ok(code)
172 }
173
174 pub fn generate_to(self, out_path: impl AsRef<Path>) -> Result<()> {
175 let out_path = out_path.as_ref();
176 let code = self.generate().context("failed to generate client")?;
177
178 fs::write(out_path, code).with_context(|| {
179 format!("Failed to write generated client to {}", out_path.display())
180 })?;
181
182 Ok(())
183 }
184}
185
186fn pretty_with_rustfmt(code: &str) -> String {
189 use std::process::Command;
190 let mut child = Command::new("rustfmt")
191 .arg("--config")
192 .arg("style_edition=2024")
193 .stdin(std::process::Stdio::piped())
194 .stdout(std::process::Stdio::piped())
195 .spawn()
196 .expect("Failed to spawn rustfmt");
197
198 let child_stdin = child.stdin.as_mut().expect("Failed to open stdin");
199 child_stdin
200 .write_all(code.as_bytes())
201 .expect("Failed to write to rustfmt");
202
203 let output = child
204 .wait_with_output()
205 .expect("Failed to wait for rustfmt");
206
207 if !output.status.success() {
208 panic!(
209 "rustfmt failed with status: {}\n{}",
210 output.status,
211 String::from_utf8(output.stderr).expect("Failed to read rustfmt stderr")
212 );
213 }
214
215 String::from_utf8(output.stdout).expect("Failed to read rustfmt output")
216}
217
218#[cfg(test)]
219mod tests {
220 use sails_idl_parser_v2::{FsLoader, GitLoader, preprocess};
221
222 #[test]
223 fn test_resolve_idl_from_path() {
224 let path = "tests/idls/recursive_main.idl";
225 let result =
226 preprocess::preprocess(path, &[&FsLoader]).expect("Failed to resolve nested IDL");
227
228 assert!(result.contains("service Leaf"));
229 assert!(result.contains("service Middle"));
230 assert!(result.contains("service Main"));
231 }
232
233 #[test]
234 fn test_resolve_nested_idl() {
235 let path = "tests/idls/nested/main.idl";
236 let result =
237 preprocess::preprocess(path, &[&FsLoader]).expect("Failed to resolve nested IDL");
238
239 assert!(result.contains("service A"));
240 assert!(result.contains("service B"));
241 assert!(result.contains("service Main"));
242
243 let common_count = result.matches("struct Common").count();
244 assert_eq!(
245 common_count, 1,
246 "struct Common should be included only once, but found {}",
247 common_count
248 );
249 }
250
251 #[test]
252 #[ignore]
253 fn test_git_include_demo() {
254 let path = "tests/idls/git_include/main.idl";
255 let result = preprocess::preprocess(path, &[&FsLoader, &GitLoader])
256 .expect("Failed to preprocess git include chain");
257
258 let doc = sails_idl_parser_v2::parse_idl(&result)
259 .expect("Failed to parse IDL from git include chain");
260
261 let service_names: Vec<_> = doc.services.iter().map(|s| s.name.to_string()).collect();
262 assert!(
263 service_names.iter().any(|n| n.contains("PingPong")),
264 "Expected PingPong service from demo_client.idl, got: {service_names:?}"
265 );
266 assert!(
267 service_names.iter().any(|n| n.contains("Counter")),
268 "Expected Counter service from demo_client.idl, got: {service_names:?}"
269 );
270 }
271}