use convert_case::{Case, Casing};
use core::marker::PhantomData;
pub use sails_client_gen_v2::{ClientGenerator, IdlPath};
use sails_idl_meta::ProgramMeta;
use std::{
env,
ffi::OsStr,
path::{Path, PathBuf},
string::{String, ToString},
};
pub fn build_client<P: ProgramMeta>() -> (Option<PathBuf>, Option<PathBuf>) {
ClientBuilder::<P>::from_env().build()
}
pub fn build_client_as_lib<P: ProgramMeta>() -> (Option<PathBuf>, Option<PathBuf>) {
ClientBuilder::<P>::from_env().no_std().build()
}
#[derive(Debug, Clone)]
pub struct ClientBuilder<P> {
idl_path: Option<PathBuf>,
client_path: Option<PathBuf>,
wasm_path: Option<PathBuf>,
program_name: String,
no_std: bool,
_marker: PhantomData<P>,
}
impl<P: ProgramMeta> Default for ClientBuilder<P> {
fn default() -> Self {
Self::from_env()
}
}
impl<P: ProgramMeta> ClientBuilder<P> {
pub fn from_env() -> Self {
let out_dir =
env::var("CARGO_MANIFEST_DIR").expect("Default builder can only be used in crate");
let package_name =
env::var("CARGO_PKG_NAME").expect("Default builder can only be used in crate");
let program_name = package_name.to_case(Case::Snake);
Self {
idl_path: Some(
PathBuf::from(&out_dir)
.join(&program_name)
.with_extension("idl"),
),
client_path: Some(
PathBuf::from(&out_dir)
.join("src")
.join(&program_name)
.with_extension("rs"),
),
wasm_path: None,
program_name,
no_std: false,
_marker: Default::default(),
}
}
pub fn from_wasm_path<T: AsRef<Path>>(path: T) -> Self {
let path = path.as_ref();
let program_name = path
.file_name()
.expect("Invalid path to wasm")
.to_string_lossy()
.split('.')
.next()
.expect("path file_name must have at least one segment before a dot")
.to_string();
Self {
idl_path: Some(path.with_file_name(&program_name).with_extension("idl")),
client_path: Some(path.with_file_name(&program_name).with_extension("rs")),
wasm_path: Some(path.to_path_buf()),
program_name,
no_std: false,
_marker: Default::default(),
}
}
pub fn empty(program_name: String) -> Self {
Self {
idl_path: None,
client_path: None,
wasm_path: None,
program_name,
no_std: false,
_marker: Default::default(),
}
}
pub fn with_idl_path<T: AsRef<Path>>(self, path: T) -> Self {
Self {
idl_path: Some(PathBuf::from(path.as_ref())),
..self
}
}
pub fn with_idl_file_name<S: AsRef<OsStr>>(self, file_name: S) -> Self {
let Self { idl_path, .. } = self;
Self {
idl_path: idl_path.map(|p| p.with_file_name(file_name)),
..self
}
}
pub fn with_client_path<T: AsRef<Path>>(self, path: T) -> Self {
Self {
client_path: Some(PathBuf::from(path.as_ref())),
..self
}
}
pub fn with_client_file_name<S: AsRef<OsStr>>(self, file_name: S) -> Self {
let Self { client_path, .. } = self;
Self {
client_path: client_path.map(|p| p.with_file_name(file_name)),
..self
}
}
pub fn with_program_name(self, program_name: &str) -> Self {
Self {
program_name: program_name.to_case(Case::Snake),
..self
}
}
pub fn no_std(self) -> Self {
let Self { client_path, .. } = self;
Self {
client_path: client_path.map(|p| p.with_file_name("lib.rs")),
no_std: true,
..self
}
}
pub fn build_idl<'a>(&'a self) -> ClientGenerator<'a, IdlPath<'a>> {
let program_name = self.program_name.to_case(Case::Pascal);
let idl_path = self.idl_path.as_ref().expect("idl path not set");
let client_path = self.client_path.as_ref().expect("client path not set");
sails_idl_gen::generate_idl_to_file::<P>(Some(program_name.as_str()), idl_path.as_path())
.expect("Error generating IDL from program");
self.try_embed_idl(idl_path);
ClientGenerator::from_idl_path(idl_path.as_path()).with_client_path(client_path.as_path())
}
pub fn build(self) -> (Option<PathBuf>, Option<PathBuf>) {
let program_name = self.program_name.to_case(Case::Pascal);
if let Some(idl_path) = self.idl_path.as_ref() {
sails_idl_gen::generate_idl_to_file::<P>(
Some(program_name.as_str()),
idl_path.as_path(),
)
.expect("Error generating IDL from program");
self.try_embed_idl(idl_path);
if let Some(client_path) = self.client_path.as_ref() {
ClientGenerator::from_idl_path(idl_path.as_path())
.with_no_std(self.no_std)
.generate_to(client_path.as_path())
.expect("Error generating client from IDL");
}
} else if let Some(client_path) = self.client_path.as_ref() {
let mut idl = String::new();
sails_idl_gen::generate_idl::<P>(Some(program_name.as_str()), &mut idl)
.expect("Error generating IDL from program");
ClientGenerator::from_idl(&idl)
.with_no_std(self.no_std)
.generate_to(client_path.as_path())
.expect("Error generating client from IDL");
}
(self.idl_path, self.client_path)
}
#[cfg(feature = "idl-embed")]
fn try_embed_idl(&self, idl_path: &Path) {
if env::var("SAILS_NO_EMBED_IDL")
.ok()
.is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("true"))
{
return;
}
let Some(wasm_path) = self.wasm_path.as_ref() else {
return;
};
let idl = match std::fs::read_to_string(idl_path) {
Ok(idl) => idl,
Err(e) => {
std::eprintln!("cargo::warning=Failed to read IDL for embedding: {e}");
return;
}
};
match sails_idl_embed::embed_idl_to_file(wasm_path, &idl) {
Ok(()) => {
let raw_size = idl.len();
let wasm_size = std::fs::metadata(wasm_path).map(|m| m.len()).unwrap_or(0);
std::eprintln!(
"Embedded IDL in WASM ({raw_size} bytes IDL, {wasm_size} bytes WASM total)"
);
}
Err(e) => {
std::eprintln!("cargo::warning=Failed to embed IDL in WASM: {e}");
}
}
}
#[cfg(not(feature = "idl-embed"))]
fn try_embed_idl(&self, _idl_path: &Path) {}
}
#[cfg(test)]
mod tests {
use super::*;
struct P;
#[derive(crate::TypeInfo)]
#[type_info(crate = crate::type_info)]
struct Meta;
impl ProgramMeta for P {
type ConstructorsMeta = Meta;
const SERVICES: &'static [(&'static str, sails_idl_meta::AnyServiceMeta)] = &[];
const ASYNC: bool = false;
}
#[test]
fn builder_new() {
let ClientBuilder {
client_path,
idl_path,
program_name,
..
} = ClientBuilder::<P>::from_env();
assert_eq!("sails_rs", program_name);
assert!(client_path.is_some());
assert!(idl_path.is_some());
assert!(client_path.unwrap().ends_with("src/sails_rs.rs"));
assert!(idl_path.unwrap().ends_with("sails_rs.idl"));
}
#[test]
fn from_wasm_path_strips_extensions() {
let path = "target/release/foo.opt.wasm";
let builder = ClientBuilder::<P>::from_wasm_path(path);
assert_eq!("foo", builder.program_name);
assert!(builder.idl_path.unwrap().ends_with("foo.idl"));
assert!(builder.client_path.unwrap().ends_with("foo.rs"));
assert!(builder.wasm_path.unwrap().ends_with("foo.opt.wasm"));
}
}