crux_cli/codegen/
generate.rs

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