#![deny(missing_docs)]
use std::path::PathBuf;
use clap::{ArgGroup, Args};
use color_eyre::eyre::{Context, Result};
use typify::{CrateVers, TypeSpace, TypeSpaceSettings, UnknownPolicy};
#[derive(Args)]
#[command(author, version, about)]
#[command(group(
ArgGroup::new("build")
.args(["builder", "no_builder"]),
))]
pub struct CliArgs {
pub input: PathBuf,
#[arg(short, long, default_value = "false", group = "build")]
pub builder: bool,
#[arg(short = 'B', long, default_value = "false", group = "build")]
pub no_builder: bool,
#[arg(short, long = "additional-derive", value_name = "derive")]
pub additional_derives: Vec<String>,
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(long = "crate")]
crates: Vec<CrateSpec>,
#[arg(
long = "unknown-crates",
value_parser = ["generate", "allow", "deny"]
)]
unknown_crates: Option<String>,
}
impl CliArgs {
pub fn output_path(&self) -> Option<PathBuf> {
match &self.output {
Some(output_path) => {
if output_path == &PathBuf::from("-") {
None
} else {
Some(output_path.clone())
}
}
None => {
let mut output = self.input.clone();
output.set_extension("rs");
Some(output)
}
}
}
pub fn use_builder(&self) -> bool {
!self.no_builder
}
}
#[derive(Debug, Clone)]
struct CrateSpec {
name: String,
version: CrateVers,
rename: Option<String>,
}
impl std::str::FromStr for CrateSpec {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
fn is_crate(s: &str) -> bool {
!s.contains(|cc: char| !cc.is_alphabetic() && cc != '-' && cc != '_')
}
fn convert(s: &str) -> Option<CrateSpec> {
let (rename, s) = if let Some(ii) = s.find('=') {
let rename = &s[..ii];
let rest = &s[ii + 1..];
if !is_crate(rename) {
return None;
}
(Some(rename.to_string()), rest)
} else {
(None, s)
};
let ii = s.find('@')?;
let crate_str = &s[..ii];
let vers_str = &s[ii + 1..];
if !is_crate(crate_str) {
return None;
}
let version = CrateVers::parse(vers_str)?;
Some(CrateSpec {
name: crate_str.to_string(),
version,
rename,
})
}
convert(s).ok_or("crate specifier must be of the form 'cratename@version'")
}
}
pub fn convert(args: &CliArgs) -> Result<String> {
let content = std::fs::read_to_string(&args.input)
.wrap_err_with(|| format!("Failed to open input file: {}", &args.input.display()))?;
let schema = serde_json::from_str::<schemars::schema::RootSchema>(&content)
.wrap_err("Failed to parse input file as JSON Schema")?;
let mut settings = TypeSpaceSettings::default();
settings.with_struct_builder(args.use_builder());
for derive in &args.additional_derives {
settings.with_derive(derive.clone());
}
for CrateSpec {
name,
version,
rename,
} in &args.crates
{
settings.with_crate(name, version.clone(), rename.as_ref());
}
if let Some(unknown_crates) = &args.unknown_crates {
let unknown_crates = match unknown_crates.as_str() {
"generate" => UnknownPolicy::Generate,
"allow" => UnknownPolicy::Allow,
"deny" => UnknownPolicy::Deny,
_ => unreachable!(),
};
settings.with_unknown_crates(unknown_crates);
}
let mut type_space = TypeSpace::new(&settings);
type_space
.add_root_schema(schema)
.wrap_err("Schema conversion failed")?;
let intro = "#![allow(clippy::redundant_closure_call)]
#![allow(clippy::needless_lifetimes)]
#![allow(clippy::match_single_binding)]
#![allow(clippy::clone_on_copy)]
";
let contents = format!("{intro}\n{}", type_space.to_stream());
let contents = rustfmt_wrapper::rustfmt(contents).wrap_err("Failed to format Rust code")?;
Ok(contents)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_output_parsing_stdout() {
let args = CliArgs {
input: PathBuf::from("input.json"),
builder: false,
additional_derives: vec![],
output: Some(PathBuf::from("-")),
no_builder: false,
crates: vec![],
unknown_crates: Default::default(),
};
assert_eq!(args.output_path(), None);
}
#[test]
fn test_output_parsing_file() {
let args = CliArgs {
input: PathBuf::from("input.json"),
builder: false,
additional_derives: vec![],
output: Some(PathBuf::from("some_file.rs")),
no_builder: false,
crates: vec![],
unknown_crates: Default::default(),
};
assert_eq!(args.output_path(), Some(PathBuf::from("some_file.rs")));
}
#[test]
fn test_output_parsing_default() {
let args = CliArgs {
input: PathBuf::from("input.json"),
builder: false,
additional_derives: vec![],
output: None,
no_builder: false,
crates: vec![],
unknown_crates: Default::default(),
};
assert_eq!(args.output_path(), Some(PathBuf::from("input.rs")));
}
#[test]
fn test_builder_as_default_style() {
let args = CliArgs {
input: PathBuf::from("input.json"),
builder: false,
additional_derives: vec![],
output: None,
no_builder: false,
crates: vec![],
unknown_crates: Default::default(),
};
assert!(args.use_builder());
}
#[test]
fn test_no_builder() {
let args = CliArgs {
input: PathBuf::from("input.json"),
builder: false,
additional_derives: vec![],
output: None,
no_builder: true,
crates: vec![],
unknown_crates: Default::default(),
};
assert!(!args.use_builder());
}
#[test]
fn test_builder_opt_in() {
let args = CliArgs {
input: PathBuf::from("input.json"),
builder: true,
additional_derives: vec![],
output: None,
no_builder: false,
crates: vec![],
unknown_crates: Default::default(),
};
assert!(args.use_builder());
}
}