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 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 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 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
198fn 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}