sails_client_gen/
lib.rs

1use anyhow::{Context, Result};
2use convert_case::{Case, Casing};
3use root_generator::RootGenerator;
4use sails_idl_parser::ast::visitor;
5use std::{collections::HashMap, ffi::OsStr, fs, io::Write, path::Path};
6
7mod ctor_generators;
8mod events_generator;
9mod helpers;
10mod mock_generator;
11mod root_generator;
12mod service_generators;
13mod type_generators;
14
15const SAILS: &str = "sails_rs";
16
17pub struct IdlPath<'a>(&'a Path);
18pub struct IdlString<'a>(&'a str);
19pub struct ClientGenerator<'a, S> {
20    sails_path: Option<&'a str>,
21    mocks_feature_name: Option<&'a str>,
22    external_types: HashMap<&'a str, &'a str>,
23    no_derive_traits: bool,
24    with_no_std: bool,
25    client_path: Option<&'a Path>,
26    idl: S,
27}
28
29impl<'a, S> ClientGenerator<'a, S> {
30    pub fn with_mocks(self, mocks_feature_name: &'a str) -> Self {
31        Self {
32            mocks_feature_name: Some(mocks_feature_name),
33            ..self
34        }
35    }
36
37    pub fn with_sails_crate(self, sails_path: &'a str) -> Self {
38        Self {
39            sails_path: Some(sails_path),
40            ..self
41        }
42    }
43
44    pub fn with_no_std(self, with_no_std: bool) -> Self {
45        Self {
46            with_no_std,
47            ..self
48        }
49    }
50
51    /// Add an map from IDL type to crate path
52    ///
53    /// Instead of generating type in client code, use type path from external crate
54    ///
55    /// # Example
56    ///
57    /// Following code generates `use my_crate::MyParam as MyFuncParam;`
58    /// ```
59    /// let code = sails_client_gen::ClientGenerator::from_idl("<idl>")
60    ///     .with_external_type("MyFuncParam", "my_crate::MyParam");
61    /// ```
62    pub fn with_external_type(self, name: &'a str, path: &'a str) -> Self {
63        let mut external_types = self.external_types;
64        external_types.insert(name, path);
65        Self {
66            external_types,
67            ..self
68        }
69    }
70
71    /// Derive only nessessary [`parity_scale_codec::Encode`], [`parity_scale_codec::Decode`] and [`scale_info::TypeInfo`] traits for the generated types
72    ///
73    /// By default, types additionally derive [`PartialEq`], [`Clone`] and [`Debug`]
74    pub fn with_no_derive_traits(self) -> Self {
75        Self {
76            no_derive_traits: true,
77            ..self
78        }
79    }
80
81    pub fn with_client_path(self, client_path: &'a Path) -> Self {
82        Self {
83            client_path: Some(client_path),
84            ..self
85        }
86    }
87}
88
89impl<'a> ClientGenerator<'a, IdlPath<'a>> {
90    pub fn from_idl_path(idl_path: &'a Path) -> Self {
91        Self {
92            sails_path: None,
93            mocks_feature_name: None,
94            external_types: HashMap::new(),
95            no_derive_traits: false,
96            with_no_std: false,
97            client_path: None,
98            idl: IdlPath(idl_path),
99        }
100    }
101
102    pub fn generate(self) -> Result<()> {
103        let client_path = self.client_path.context("client path not set")?;
104        let idl_path = self.idl.0;
105
106        let idl = fs::read_to_string(idl_path)
107            .with_context(|| format!("Failed to open {} for reading", idl_path.display()))?;
108
109        let file_name = idl_path.file_stem().unwrap_or(OsStr::new("service"));
110        let service_name = file_name.to_string_lossy().to_case(Case::Pascal);
111
112        self.with_idl(&idl)
113            .generate_to(&service_name, client_path)
114            .context("failed to generate client")?;
115        Ok(())
116    }
117
118    pub fn generate_to(self, out_path: impl AsRef<Path>) -> Result<()> {
119        let idl_path = self.idl.0;
120
121        let idl = fs::read_to_string(idl_path)
122            .with_context(|| format!("Failed to open {} for reading", idl_path.display()))?;
123
124        let file_name = idl_path.file_stem().unwrap_or(OsStr::new("service"));
125        let service_name = file_name.to_string_lossy().to_case(Case::Pascal);
126
127        self.with_idl(&idl)
128            .generate_to(&service_name, out_path)
129            .context("failed to generate client")?;
130        Ok(())
131    }
132
133    fn with_idl(self, idl: &'a str) -> ClientGenerator<'a, IdlString<'a>> {
134        ClientGenerator {
135            sails_path: self.sails_path,
136            mocks_feature_name: self.mocks_feature_name,
137            external_types: self.external_types,
138            no_derive_traits: self.no_derive_traits,
139            with_no_std: self.with_no_std,
140            client_path: self.client_path,
141            idl: IdlString(idl),
142        }
143    }
144}
145
146impl<'a> ClientGenerator<'a, IdlString<'a>> {
147    pub fn from_idl(idl: &'a str) -> Self {
148        Self {
149            sails_path: None,
150            mocks_feature_name: None,
151            external_types: HashMap::new(),
152            no_derive_traits: false,
153            with_no_std: false,
154            client_path: None,
155            idl: IdlString(idl),
156        }
157    }
158
159    pub fn generate(self, anonymous_service_name: &str) -> Result<String> {
160        let idl = self.idl.0;
161        let sails_path = self.sails_path.unwrap_or(SAILS);
162        let mut generator = RootGenerator::new(
163            anonymous_service_name,
164            self.mocks_feature_name,
165            sails_path,
166            self.external_types,
167            self.no_derive_traits,
168        );
169        let program = sails_idl_parser::ast::parse_idl(idl).context("Failed to parse IDL")?;
170        visitor::accept_program(&program, &mut generator);
171
172        let code = generator.finalize(self.with_no_std);
173
174        // Check for parsing errors
175        let code = pretty_with_rustfmt(&code);
176
177        Ok(code)
178    }
179
180    pub fn generate_to(
181        self,
182        anonymous_service_name: &str,
183        out_path: impl AsRef<Path>,
184    ) -> Result<()> {
185        let out_path = out_path.as_ref();
186        let code = self
187            .generate(anonymous_service_name)
188            .context("failed to generate client")?;
189
190        fs::write(out_path, code).with_context(|| {
191            format!("Failed to write generated client to {}", out_path.display())
192        })?;
193
194        Ok(())
195    }
196}
197
198// not using prettyplease since it's bad at reporting syntax errors and also removes comments
199// TODO(holykol): Fallback if rustfmt is not in PATH would be nice
200fn pretty_with_rustfmt(code: &str) -> String {
201    use std::process::Command;
202    let mut child = Command::new("rustfmt")
203        .arg("--config")
204        .arg("style_edition=2024")
205        .stdin(std::process::Stdio::piped())
206        .stdout(std::process::Stdio::piped())
207        .spawn()
208        .expect("Failed to spawn rustfmt");
209
210    let child_stdin = child.stdin.as_mut().expect("Failed to open stdin");
211    child_stdin
212        .write_all(code.as_bytes())
213        .expect("Failed to write to rustfmt");
214
215    let output = child
216        .wait_with_output()
217        .expect("Failed to wait for rustfmt");
218
219    if !output.status.success() {
220        panic!(
221            "rustfmt failed with status: {}\n{}",
222            output.status,
223            String::from_utf8(output.stderr).expect("Failed to read rustfmt stderr")
224        );
225    }
226
227    String::from_utf8(output.stdout).expect("Failed to read rustfmt output")
228}