use std::ffi::OsString;
use std::path::PathBuf;
use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::config_file::{self, FileConfig};
use crate::report::display_path;
use crate::{Config, Derive, Extractor, Report};
#[derive(Debug, Parser)]
#[command(
name = "rok-sqlc",
version,
about = "Generate SQLx Rust modules from SQL schema files"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
#[command(visible_alias = "g")]
Generate {
#[arg(short, long, value_name = "PATH")]
schema: Vec<PathBuf>,
#[arg(short, long, value_name = "DIR")]
output: Option<PathBuf>,
#[arg(short, long, value_name = "TABLE")]
resource: Option<String>,
#[arg(short = 'n', long)]
dry_run: bool,
#[arg(short = 'k', long)]
check: bool,
#[arg(short, long)]
force: bool,
#[arg(short, long, value_name = "PATH")]
queries: Vec<PathBuf>,
#[arg(short, long)]
lenient: bool,
#[arg(short, long, value_name = "FILE")]
config: Option<PathBuf>,
#[arg(short, long, value_name = "JSON|@FILE|-", allow_hyphen_values = true)]
json: Option<String>,
#[arg(short, long, value_name = "NAME", value_enum)]
derive: Vec<DeriveArg>,
#[arg(short, long, value_name = "FRAMEWORK", value_enum)]
extractor: Vec<ExtractorArg>,
},
}
pub fn command() -> clap::Command {
<Cli as clap::CommandFactory>::command()
}
pub fn run() -> Result<()> {
run_from(std::env::args_os())
}
pub fn run_from<I, T>(args: I) -> Result<()>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let cli = match Cli::try_parse_from(args) {
Ok(cli) => cli,
Err(err) if is_informational(&err) => {
err.print()?;
return Ok(());
}
Err(err) => return Err(err.into()),
};
match cli.command {
Command::Generate {
schema,
output,
resource,
dry_run,
check,
force,
queries,
lenient,
config,
json,
derive,
extractor,
} => {
let payload = match json {
Some(arg) => config_file::load_json(&arg)?,
None => FileConfig::default(),
};
let file = match config {
Some(path) => config_file::load(&path)?,
None => match config_file::discover(&std::env::current_dir()?) {
Some(path) => config_file::load(&path)?,
None => FileConfig::default(),
},
};
let flags = FileConfig {
schema,
queries,
output,
resource,
conventions: Default::default(),
derive: (!derive.is_empty())
.then(|| derive.into_iter().map(Derive::from).collect()),
extractors: (!extractor.is_empty())
.then(|| extractor.into_iter().map(Extractor::from).collect()),
relationships: Default::default(),
modules: Default::default(),
};
let merged = flags.over(payload.over(file));
if merged.schema.is_empty() {
return Err(missing_schema().into());
}
let defaults = Config::default();
generate(&Config {
schema: merged.schema,
queries: merged.queries,
output: merged.output.unwrap_or(defaults.output),
resource: merged.resource,
dry_run,
check,
force,
lenient,
conventions: merged.conventions.resolve(),
derive: merged.derive.unwrap_or_default(),
extractors: merged.extractors.unwrap_or_default(),
relationships: merged.relationships,
modules: merged.modules,
})
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
enum DeriveArg {
Serde,
Validate,
}
impl From<DeriveArg> for Derive {
fn from(arg: DeriveArg) -> Self {
match arg {
DeriveArg::Serde => Derive::Serde,
DeriveArg::Validate => Derive::Validate,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
enum ExtractorArg {
Axum,
#[value(name = "actix_web")]
ActixWeb,
}
impl From<ExtractorArg> for Extractor {
fn from(arg: ExtractorArg) -> Self {
match arg {
ExtractorArg::Axum => Extractor::Axum,
ExtractorArg::ActixWeb => Extractor::ActixWeb,
}
}
}
fn missing_schema() -> clap::Error {
let mut cmd = command();
cmd.build();
let generate = cmd
.find_subcommand_mut("generate")
.expect("the generate subcommand exists");
generate.error(
clap::error::ErrorKind::MissingRequiredArgument,
format!(
"no schema given: pass --schema <PATH>, or set `schema` in the --json payload or {}",
config_file::FILE_NAME
),
)
}
fn is_informational(err: &clap::Error) -> bool {
use clap::error::ErrorKind;
matches!(
err.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
)
}
fn generate(config: &Config) -> Result<()> {
match crate::generate(config) {
Ok(report) => {
print_report(config, &report);
Ok(())
}
Err(err) => {
print_report(config, err.report());
Err(err.into())
}
}
}
fn print_report(config: &Config, report: &Report) {
for failure in &report.table_failures {
if config.lenient {
eprintln!(
"warning: {}table `{}` skipped: {}",
failure.location(),
failure.table,
failure.reason
);
} else {
eprintln!("error: {failure}");
}
}
for failure in &report.query_failures {
if config.lenient {
eprintln!(
"warning: {}query `{}` skipped: {}",
failure.location(),
failure.query,
failure.reason
);
} else {
eprintln!("error: {failure}");
}
}
for path in &report.written {
if config.dry_run {
println!("would write {}", display_path(path));
} else {
println!("generated {}", display_path(path));
}
}
for path in &report.unchanged {
println!("unchanged {}", display_path(path));
}
for path in &report.missing {
println!("missing {}", display_path(path));
}
for path in &report.changed {
println!("changed {}", display_path(path));
}
for path in &report.stale {
if config.check {
println!("stale {}", display_path(path));
} else if config.dry_run {
println!("would remove {}", display_path(path));
} else {
println!("removed {}", display_path(path));
}
}
for refused in &report.refused {
let (path, reason) = (display_path(&refused.path), &refused.reason);
if config.dry_run {
println!("would refuse {path} ({reason})");
} else {
eprintln!("error: refused to write {path} ({reason})");
}
}
}