use clap::{Parser, Subcommand};
use convert_case::{Case, Casing};
use sails_cli::{idlgen::CrateIdlGenerator, program_new, solgen::SolidityGenerator};
use sails_client_gen::ClientGenerator as ClientGeneratorV1;
use sails_client_gen_js::JsClientGenerator;
use sails_client_gen_v2::ClientGenerator as ClientGeneratorV2;
use sails_idl_parser_v2::parse_tokens;
use std::{
error::Error,
path::{Path, PathBuf},
};
const SAILBOAT: &str = "\u{26F5}";
const ICON_CONFIG: &str = "📋";
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Parser)]
#[command(bin_name = "cargo")]
enum CliCommand {
#[command(name = "sails", subcommand)]
Sails(SailsCommands),
}
#[derive(Subcommand)]
enum SailsCommands {
#[command(name = "new")]
New {
#[arg(value_hint = clap::ValueHint::DirPath)]
path: PathBuf,
#[arg(long)]
name: Option<String>,
#[arg(long)]
author: Option<String>,
#[arg(long)]
username: Option<String>,
#[arg(long, value_hint = clap::ValueHint::DirPath)]
sails_path: Option<PathBuf>,
#[arg(long)]
offline: bool,
#[arg(long)]
eth: bool,
},
#[command(name = "client-rs")]
ClientRs {
#[arg(value_hint = clap::ValueHint::FilePath)]
idl_path: PathBuf,
#[arg(value_hint = clap::ValueHint::FilePath)]
out_path: Option<PathBuf>,
#[arg(long)]
mocks: Option<String>,
#[arg(long)]
sails_crate: Option<String>,
#[arg(long, short = 'T', value_parser = parse_key_val::<String, String>)]
external_types: Vec<(String, String)>,
#[arg(long)]
no_derive_traits: bool,
#[arg(long)]
v1: bool,
},
#[command(name = "client-js")]
ClientJs {
#[arg(value_hint = clap::ValueHint::FilePath)]
idl_path: PathBuf,
#[arg(value_hint = clap::ValueHint::FilePath)]
out_path: Option<PathBuf>,
},
#[command(name = "idl")]
IdlGen {
#[arg(long, value_hint = clap::ValueHint::FilePath)]
manifest_path: Option<PathBuf>,
#[arg(long, value_hint = clap::ValueHint::DirPath)]
target_dir: Option<PathBuf>,
#[arg(long)]
deps_level: Option<usize>,
#[arg(long, short = 'n')]
program_name: Option<String>,
},
#[command(name = "idl-embed")]
IdlEmbed {
#[arg(long, value_hint = clap::ValueHint::FilePath)]
wasm: PathBuf,
#[arg(long, value_hint = clap::ValueHint::FilePath)]
idl: PathBuf,
},
#[command(name = "idl-extract")]
IdlExtract {
#[arg(long, value_hint = clap::ValueHint::FilePath)]
wasm: PathBuf,
#[arg(long, short, value_hint = clap::ValueHint::FilePath)]
output: Option<PathBuf>,
},
#[command(name = "sol")]
SolGen {
#[arg(long, value_hint = clap::ValueHint::FilePath)]
idl_path: PathBuf,
#[arg(long, value_hint = clap::ValueHint::DirPath)]
target_dir: Option<PathBuf>,
#[arg(long, short = 'n')]
contract_name: Option<String>,
},
}
fn parse_key_val<T, U>(s: &str) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
where
T: std::str::FromStr,
T::Err: Error + Send + Sync + 'static,
U: std::str::FromStr,
U::Err: Error + Send + Sync + 'static,
{
let pos = s
.find('=')
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
}
fn should_print_banner(command: &SailsCommands) -> bool {
!matches!(command, SailsCommands::IdlExtract { output: None, .. })
}
fn format_client_rs_config(
idl_path: &Path,
out_path: &Path,
is_v1: bool,
sails_crate: Option<&str>,
external_types: &[(String, String)],
) -> String {
let version = if is_v1 { "v1" } else { "v2" };
let sails_crate = sails_crate.unwrap_or("-");
let external_types = if external_types.is_empty() {
"-".to_string()
} else {
external_types
.iter()
.map(|(name, path)| format!("{name}={path}"))
.collect::<Vec<_>>()
.join(", ")
};
format!(
"{ICON_CONFIG} Rust client:\n {source:<10} {}\n {output:<10} {}\n {version_label:<10} {version}\n {sails_label:<10} {sails_crate}\n {ext_label:<10} {external_types}\n",
idl_path.display(),
out_path.display(),
source = "source:",
output = "output:",
version_label = "version:",
sails_label = "sails-rs:",
ext_label = "ext types:",
)
}
fn main() -> Result<(), i32> {
let CliCommand::Sails(command) = CliCommand::parse();
if should_print_banner(&command) {
println!("{SAILBOAT} Sails CLI {VERSION}");
}
let result = match command {
SailsCommands::New {
path,
name,
author,
username,
sails_path,
offline,
eth,
} => program_new::ProgramGenerator::new(
path, name, author, username, sails_path, offline, eth,
)
.generate(),
SailsCommands::ClientRs {
idl_path,
out_path,
mocks,
sails_crate,
external_types,
no_derive_traits,
v1,
} => {
let out_path = out_path.unwrap_or_else(|| idl_path.with_extension("rs"));
let Ok(idl) = std::fs::read_to_string(&idl_path) else {
eprintln!("Error: failed to read {:?}", idl_path);
return Err(-1);
};
let is_v1 = v1 || parse_tokens(&idl).is_err();
print!(
"{}",
format_client_rs_config(
&idl_path,
&out_path,
is_v1,
sails_crate.as_deref(),
&external_types,
)
);
if is_v1 {
let mut client_gen = ClientGeneratorV1::from_idl_path(idl_path.as_ref());
if let Some(mocks) = mocks.as_ref() {
client_gen = client_gen.with_mocks(mocks);
}
if let Some(sails_crate) = sails_crate.as_ref() {
client_gen = client_gen.with_sails_crate(sails_crate);
}
for (name, path) in external_types.iter() {
client_gen = client_gen.with_external_type(name, path);
}
if no_derive_traits {
client_gen = client_gen.with_no_derive_traits();
}
client_gen.generate_to(out_path)
} else {
let mut client_gen = ClientGeneratorV2::from_idl_path(idl_path.as_ref());
if let Some(mocks) = mocks.as_ref() {
client_gen = client_gen.with_mocks(mocks);
}
if let Some(sails_crate) = sails_crate.as_ref() {
client_gen = client_gen.with_sails_crate(sails_crate);
}
for (name, path) in external_types.iter() {
client_gen = client_gen.with_external_type(name, path);
}
if no_derive_traits {
client_gen = client_gen.with_no_derive_traits();
}
client_gen.generate_to(out_path)
}
}
SailsCommands::ClientJs { idl_path, out_path } => {
let client_gen = JsClientGenerator::from_idl_path(idl_path.as_ref());
let out_path = out_path.unwrap_or_else(|| idl_path.with_extension("ts"));
client_gen.generate_to(out_path)
}
SailsCommands::IdlGen {
manifest_path,
target_dir,
deps_level,
program_name,
} => CrateIdlGenerator::new(
manifest_path,
target_dir,
deps_level,
program_name.map(|s| s.to_case(Case::Pascal)),
)
.generate(),
SailsCommands::IdlEmbed { wasm, idl } => (|| -> anyhow::Result<()> {
let idl_text = std::fs::read_to_string(&idl)?;
sails_idl_embed::embed_idl_to_file(&wasm, &idl_text)?;
println!(
"Embedded IDL ({} bytes) into {}",
idl_text.len(),
wasm.display()
);
Ok(())
})(),
SailsCommands::IdlExtract { wasm, output } => (|| -> anyhow::Result<()> {
let idl = sails_idl_embed::extract_idl_from_file(&wasm)?;
match idl {
Some(text) => {
if let Some(out_path) = output {
std::fs::write(&out_path, &text)?;
println!(
"Extracted IDL ({} bytes) to {}",
text.len(),
out_path.display()
);
} else {
print!("{text}");
}
}
None => {
eprintln!("No sails:idl section found in {}", wasm.display());
}
}
Ok(())
})(),
SailsCommands::SolGen {
idl_path,
target_dir,
contract_name,
} => SolidityGenerator::new(idl_path, target_dir, contract_name).generate(),
};
if let Err(e) = result {
eprintln!("Error: {e:#}");
return Err(-1);
}
Ok(())
}