Skip to main content

sails_client_gen_v2/
lib.rs

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    /// Add an map from IDL type to crate path
51    ///
52    /// Instead of generating type in client code, use type path from external crate
53    ///
54    /// # Example
55    ///
56    /// Following code generates `use my_crate::MyParam as MyFuncParam;`
57    /// ```
58    /// let code = sails_client_gen_v2::ClientGenerator::from_idl("<idl>")
59    ///     .with_external_type("MyFuncParam", "my_crate::MyParam");
60    /// ```
61    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    /// Derive only nessessary [`parity_scale_codec::Encode`], [`parity_scale_codec::Decode`], [`type_info::TypeInfo`] and [`sails_reflect_hash::ReflectHash`] traits for the generated types
71    ///
72    /// By default, types additionally derive [`PartialEq`], [`Clone`] and [`Debug`]
73    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        // Check for parsing errors
169        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
186// not using prettyplease since it's bad at reporting syntax errors and also removes comments
187// TODO(holykol): Fallback if rustfmt is not in PATH would be nice
188fn 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}