crux_cli/codegen/
generate.rs

1use std::path::{Path, PathBuf};
2
3use log::{debug, info};
4use serde_generate::{java, swift, typescript, Encoding, SourceInstaller};
5use serde_reflection::Registry;
6use std::{
7    fs::{self, File},
8    io::Write,
9};
10use thiserror::Error;
11
12pub type Result = std::result::Result<(), TypeGenError>;
13
14#[derive(Error, Debug)]
15pub enum TypeGenError {
16    #[error("type generation failed: {0}")]
17    Generation(String),
18    #[error("error writing generated types")]
19    Io(#[from] std::io::Error),
20    #[error("`pnpm` is needed for TypeScript type generation, but it could not be found in PATH.\nPlease install it from https://pnpm.io/installation")]
21    PnpmNotFound(#[source] std::io::Error),
22}
23
24/// Generates types for Swift
25/// e.g.
26/// ```
27/// # use crux_cli::codegen::generate;
28/// # let registry = serde_reflection::Registry::new();
29/// # let output_root = std::env::temp_dir().join("crux_cli_codegen_doctest");
30/// generate::swift(&registry, "SharedTypes", output_root.join("swift"))?;
31/// # Ok::<(), generate::TypeGenError>(())
32/// ```
33pub fn swift(registry: &Registry, module_name: &str, path: impl AsRef<Path>) -> Result {
34    let path = path.as_ref().join(module_name);
35
36    // remove any existing generated shared types, this ensures that we remove no longer used types
37    remove_dir(&path);
38
39    fs::create_dir_all(&path)?;
40
41    info!("Generating Swift types at {}", path.display());
42
43    let installer = swift::Installer::new(path.clone());
44    installer
45        .install_serde_runtime()
46        .map_err(|e| TypeGenError::Generation(e.to_string()))?;
47    installer
48        .install_bincode_runtime()
49        .map_err(|e| TypeGenError::Generation(e.to_string()))?;
50
51    let config = serde_generate::CodeGeneratorConfig::new(module_name.to_string())
52        .with_encodings(vec![Encoding::Bincode]);
53
54    installer
55        .install_module(&config, registry)
56        .map_err(|e| TypeGenError::Generation(e.to_string()))?;
57
58    // add bincode deserialization for Vec<Request>
59    let mut output = File::create(
60        path.join("Sources")
61            .join(module_name)
62            .join("Requests.swift"),
63    )?;
64
65    let requests_path = extensions_path("swift/requests.swift");
66
67    let requests_data = fs::read_to_string(requests_path)?;
68
69    write!(output, "{}", requests_data)?;
70
71    // wrap it all up in a swift package
72    let mut output = File::create(path.join("Package.swift"))?;
73
74    let package_path = extensions_path("swift/Package.swift");
75
76    let package_data = fs::read_to_string(package_path)?;
77
78    write!(
79        output,
80        "{}",
81        package_data.replace("SharedTypes", module_name)
82    )?;
83
84    Ok(())
85}
86
87/// Generates types for Java (for use with Kotlin)
88/// e.g.
89/// ```
90/// # use crux_cli::codegen::generate;
91/// # let registry = serde_reflection::Registry::new();
92/// # let output_root = std::env::temp_dir().join("crux_cli_codegen_doctest");
93/// generate::java(
94///     &registry,
95///     "com.redbadger.crux_core.shared_types",
96///     output_root.join("java"),
97/// )?;
98/// # Ok::<(), generate::TypeGenError>(())
99/// ```
100pub fn java(registry: &Registry, package_name: &str, path: impl AsRef<Path>) -> Result {
101    let path = path.as_ref();
102    fs::create_dir_all(&path)?;
103
104    let package_path = package_name.replace('.', "/");
105
106    // remove any existing generated shared types, this ensures that we remove no longer used types
107    remove_dir(path.join(&package_path));
108    remove_dir(path.join("com/novi/bincode"));
109    remove_dir(path.join("com/novi/serde"));
110
111    info!("Generating Java types at {}", path.display());
112
113    let config = serde_generate::CodeGeneratorConfig::new(package_name.to_string())
114        .with_encodings(vec![Encoding::Bincode]);
115
116    let installer = java::Installer::new(path.to_path_buf());
117    installer
118        .install_serde_runtime()
119        .map_err(|e| TypeGenError::Generation(e.to_string()))?;
120    installer
121        .install_bincode_runtime()
122        .map_err(|e| TypeGenError::Generation(e.to_string()))?;
123
124    installer
125        .install_module(&config, registry)
126        .map_err(|e| TypeGenError::Generation(e.to_string()))?;
127
128    let requests_path = extensions_path("java/Requests.java");
129
130    let requests_data = fs::read_to_string(requests_path)?;
131
132    let requests = format!("package {package_name};\n\n{}", requests_data);
133
134    fs::write(path.join(package_path).join("Requests.java"), requests)?;
135
136    Ok(())
137}
138
139/// Generates types for TypeScript
140/// e.g.
141/// ```
142/// # use crux_cli::codegen::generate;
143/// # let registry = serde_reflection::Registry::new();
144/// # let output_root = std::env::temp_dir().join("crux_cli_codegen_doctest");
145/// generate::typescript(&registry, "shared_types", "0.1.0", output_root.join("typescript"))?;
146/// # Ok::<(), generate::TypeGenError>(())
147/// ```
148pub fn typescript(
149    registry: &Registry,
150    module_name: &str,
151    version: &str,
152    path: impl AsRef<Path>,
153) -> Result {
154    let path = path.as_ref();
155
156    // remove any existing generated shared types, this ensures that we remove no longer used types
157    remove_dir(path);
158
159    fs::create_dir_all(path)?;
160
161    info!("Generating TypeScript types at {}", path.display());
162
163    let installer = typescript::Installer::new(path.to_path_buf());
164    installer
165        .install_serde_runtime()
166        .map_err(|e| TypeGenError::Generation(e.to_string()))?;
167    installer
168        .install_bincode_runtime()
169        .map_err(|e| TypeGenError::Generation(e.to_string()))?;
170
171    let extensions_dir = extensions_path("typescript");
172    copy(extensions_dir.as_ref(), path)?;
173
174    let config = serde_generate::CodeGeneratorConfig::new(module_name.to_string())
175        .with_encodings(vec![Encoding::Bincode]);
176
177    let generator = serde_generate::typescript::CodeGenerator::new(&config);
178    let mut source = Vec::new();
179    generator.output(&mut source, registry)?;
180
181    // FIXME fix import paths in generated code which assume running on Deno
182    let out = String::from_utf8_lossy(&source)
183        .replace(
184            "import { BcsSerializer, BcsDeserializer } from '../bcs/mod.ts';",
185            "",
186        )
187        .replace(".ts'", "'");
188
189    let types_dir = path.join("types");
190    fs::create_dir_all(&types_dir)?;
191
192    // write package.json
193    let mut package_json = File::create(path.join("package.json"))?;
194    write!(
195        package_json,
196        "{{\"name\": \"{module_name}\", \"version\": \"{version}\"}}"
197    )?;
198
199    // add Typescript package using pnpm
200    std::process::Command::new("pnpm")
201        .current_dir(path)
202        .arg("add")
203        .arg("typescript")
204        .status()
205        .map_err(|e| match e.kind() {
206            std::io::ErrorKind::NotFound => TypeGenError::PnpmNotFound(e),
207            _ => TypeGenError::Io(e),
208        })?;
209
210    let mut output = File::create(types_dir.join(format!("{module_name}.ts")))?;
211    write!(output, "{out}")?;
212
213    // Install dependencies
214    std::process::Command::new("pnpm")
215        .current_dir(path)
216        .arg("install")
217        .status()
218        .map_err(|e| match e.kind() {
219            std::io::ErrorKind::NotFound => TypeGenError::PnpmNotFound(e),
220            _ => TypeGenError::Io(e),
221        })?;
222
223    // Build TS code and emit declarations
224    std::process::Command::new("pnpm")
225        .current_dir(path)
226        .arg("exec")
227        .arg("tsc")
228        .arg("--build")
229        .status()
230        .map_err(TypeGenError::Io)?;
231
232    Ok(())
233}
234
235fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result {
236    fs::create_dir_all(&to)?;
237
238    let entries = fs::read_dir(from)?;
239    for entry in entries {
240        let entry = entry?;
241
242        let to = to.as_ref().to_path_buf().join(entry.file_name());
243        if entry.file_type()?.is_dir() {
244            copy(entry.path(), to)?;
245        } else {
246            fs::copy(entry.path(), to)?;
247        };
248    }
249
250    Ok(())
251}
252
253fn extensions_path(path: impl AsRef<Path>) -> impl AsRef<Path> {
254    let path = path.as_ref();
255    let custom = PathBuf::from("./typegen_extensions").join(path);
256    let default = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
257        .join("typegen_extensions")
258        .join(path);
259
260    match custom.try_exists() {
261        Ok(true) => custom,
262        Ok(false) => default,
263        Err(e) => {
264            println!("cant check typegen extensions override: {}", e);
265            default
266        }
267    }
268}
269
270fn remove_dir(path: impl AsRef<Path>) {
271    let path = path.as_ref();
272    if path.exists() {
273        debug!("Removing directory: {}", path.display());
274        fs::remove_dir_all(path).unwrap_or_default();
275    }
276}