use std::path::{Path, PathBuf};
use log::info;
use serde_generate::{Encoding, SourceInstaller, java, swift, typescript};
use serde_reflection::Registry;
use std::{
fs::{self, File},
io::Write,
};
use thiserror::Error;
pub type Result = std::result::Result<(), TypeGenError>;
#[derive(Error, Debug)]
pub enum TypeGenError {
#[error("type generation failed: {0}")]
Generation(String),
#[error("error writing generated types")]
Io(#[from] std::io::Error),
#[error(
"`pnpm` is needed for TypeScript type generation, but it could not be found in PATH.\nPlease install it from https://pnpm.io/installation"
)]
PnpmNotFound(#[source] std::io::Error),
}
pub fn swift(registry: &Registry, module_name: &str, path: impl AsRef<Path>) -> Result {
let path = path.as_ref().join(module_name);
recreate_dir(&path)?;
info!("Generating Swift types at {}", path.display());
let installer = swift::Installer::new(path.clone());
installer
.install_serde_runtime()
.map_err(|e| TypeGenError::Generation(e.to_string()))?;
installer
.install_bincode_runtime()
.map_err(|e| TypeGenError::Generation(e.to_string()))?;
let config = serde_generate::CodeGeneratorConfig::new(module_name.to_string())
.with_encodings(vec![Encoding::Bincode]);
installer
.install_module(&config, registry)
.map_err(|e| TypeGenError::Generation(e.to_string()))?;
let mut output = File::create(
path.join("Sources")
.join(module_name)
.join("Requests.swift"),
)?;
let requests_path = extensions_path("swift/requests.swift");
let requests_data = fs::read_to_string(requests_path)?;
write!(output, "{requests_data}")?;
let mut output = File::create(path.join("Package.swift"))?;
let package_path = extensions_path("swift/Package.swift");
let package_data = fs::read_to_string(package_path)?;
write!(
output,
"{}",
package_data.replace("SharedTypes", module_name)
)?;
Ok(())
}
pub fn java(registry: &Registry, package_name: &str, path: impl AsRef<Path>) -> Result {
let path = path.as_ref();
fs::create_dir_all(path)?;
let package_path = package_name.replace('.', "/");
recreate_dir(path.join(&package_path))?;
recreate_dir(path.join("com/novi/bincode"))?;
recreate_dir(path.join("com/novi/serde"))?;
info!("Generating Java types at {}", path.display());
let config = serde_generate::CodeGeneratorConfig::new(package_name.to_string())
.with_encodings(vec![Encoding::Bincode]);
let installer = java::Installer::new(path.to_path_buf());
installer
.install_serde_runtime()
.map_err(|e| TypeGenError::Generation(e.to_string()))?;
installer
.install_bincode_runtime()
.map_err(|e| TypeGenError::Generation(e.to_string()))?;
installer
.install_module(&config, registry)
.map_err(|e| TypeGenError::Generation(e.to_string()))?;
let requests_path = extensions_path("java/Requests.java");
let requests_data = fs::read_to_string(requests_path)?;
let requests = format!("package {package_name};\n\n{requests_data}");
fs::write(path.join(package_path).join("Requests.java"), requests)?;
Ok(())
}
pub fn typescript(
registry: &Registry,
module_name: &str,
version: &str,
path: impl AsRef<Path>,
) -> Result {
let path = path.as_ref();
recreate_dir(path)?;
info!("Generating TypeScript types at {}", path.display());
let installer = typescript::Installer::new(path.to_path_buf());
installer
.install_serde_runtime()
.map_err(|e| TypeGenError::Generation(e.to_string()))?;
installer
.install_bincode_runtime()
.map_err(|e| TypeGenError::Generation(e.to_string()))?;
let extensions_dir = extensions_path("typescript");
copy(extensions_dir.as_ref(), path)?;
let config = serde_generate::CodeGeneratorConfig::new(module_name.to_string())
.with_encodings(vec![Encoding::Bincode]);
let generator = serde_generate::typescript::CodeGenerator::new(&config);
let mut source = Vec::new();
generator.output(&mut source, registry)?;
let out = String::from_utf8_lossy(&source)
.replace(
"import { BcsSerializer, BcsDeserializer } from '../bcs/mod.ts';",
"",
)
.replace(".ts'", "'");
let types_dir = path.join("types");
fs::create_dir_all(&types_dir)?;
let mut package_json = File::create(path.join("package.json"))?;
write!(
package_json,
"{{\"name\": \"{module_name}\", \"version\": \"{version}\"}}"
)?;
std::process::Command::new("pnpm")
.current_dir(path)
.arg("add")
.arg("--save-dev")
.arg("typescript")
.status()
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => TypeGenError::PnpmNotFound(e),
_ => TypeGenError::Io(e),
})?;
let mut output = File::create(types_dir.join(format!("{module_name}.ts")))?;
write!(output, "{out}")?;
std::process::Command::new("pnpm")
.current_dir(path)
.arg("install")
.status()
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => TypeGenError::PnpmNotFound(e),
_ => TypeGenError::Io(e),
})?;
std::process::Command::new("pnpm")
.current_dir(path)
.arg("exec")
.arg("tsc")
.arg("--build")
.status()
.map_err(TypeGenError::Io)?;
Ok(())
}
fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result {
fs::create_dir_all(&to)?;
let entries = fs::read_dir(from)?;
for entry in entries {
let entry = entry?;
let to = to.as_ref().to_path_buf().join(entry.file_name());
if entry.file_type()?.is_dir() {
copy(entry.path(), to)?;
} else {
fs::copy(entry.path(), to)?;
}
}
Ok(())
}
fn extensions_path(path: impl AsRef<Path>) -> impl AsRef<Path> {
let path = path.as_ref();
let custom = PathBuf::from("./typegen_extensions").join(path);
let default = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("typegen_extensions")
.join(path);
match custom.try_exists() {
Ok(true) => custom,
Ok(false) => default,
Err(e) => {
println!("cant check typegen extensions override: {e}");
default
}
}
}
fn recreate_dir<P>(dir: P) -> std::io::Result<()>
where
P: AsRef<Path>,
{
match fs::remove_dir_all(&dir) {
Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(e),
_ => fs::create_dir_all(&dir),
}
}