use std::path::PathBuf;
use anyhow::{anyhow, bail, Result};
use atelier_core::model::Model;
use clap::Parser;
use console::style;
use wash_lib::cli::CommandOutput;
use weld_codegen::{
config::{CodegenConfig, ModelSource, OutputLanguage},
sources_to_model,
};
use wash_lib::generate::emoji;
type TomlValue = toml::Value;
const CODEGEN_CONFIG_FILE: &str = "codegen.toml";
#[derive(Debug, Parser, Clone)]
#[clap(name = "lint")]
pub struct LintCli {
#[clap(flatten)]
opt: LintOptions,
}
#[derive(Debug, Parser, Clone)]
#[clap(name = "validate")]
pub struct ValidateCli {
#[clap(flatten)]
opt: ValidateOptions,
}
#[derive(Debug, Parser, Clone)]
#[clap(name = "gen")]
pub struct GenerateCli {
#[clap(flatten)]
opt: GenerateOptions,
}
#[derive(Debug, Clone, Parser)]
pub struct LintOptions {
#[clap(short, long)]
config: Option<PathBuf>,
#[clap(short, long)]
verbose: bool,
#[clap(name = "input")]
input: Vec<String>,
}
#[derive(Debug, Clone, Parser)]
pub struct ValidateOptions {
#[clap(short, long)]
config: Option<PathBuf>,
#[clap(short, long)]
verbose: bool,
#[clap(name = "input")]
input: Vec<String>,
}
#[derive(Debug, Clone, Parser)]
pub struct GenerateOptions {
#[clap(short, long)]
config: Option<PathBuf>,
#[clap(long)]
output_dir: Option<PathBuf>,
#[clap(short = 'T', long)]
template_dir: Option<PathBuf>,
#[clap(short, long, number_of_values = 1)]
lang: Vec<OutputLanguage>,
#[clap(short = 'D', value_parser = parse_key_val, number_of_values = 1)]
defines: Vec<(String, TomlValue)>,
#[clap(short, long)]
verbose: bool,
#[clap(name = "input")]
input: Vec<String>,
}
pub async fn handle_lint_command(command: LintCli) -> Result<CommandOutput> {
let opt = command.opt;
let verbose = match opt.verbose {
true => 1u8,
false => 0u8,
};
use atelier_core::action::lint::{run_linter_actions, NamingConventions, UnwelcomeTerms};
let config = select_config(&opt.config)?;
let model = build_model(opt.input, config.models, config.base_dir, verbose)?;
let report = run_linter_actions(
&mut [
Box::<NamingConventions>::default(),
Box::<UnwelcomeTerms>::default(),
],
&model,
false,
)
.map_err(|e| anyhow!("lint error: {}", e.to_string()))?;
cargo_atelier::report::report_action_issues(report, true)
.map_err(|e| anyhow!("report error: {}", e))?;
Ok(CommandOutput::default())
}
pub async fn handle_validate_command(command: ValidateCli) -> Result<CommandOutput> {
use atelier_core::action::validate::{
run_validation_actions, CorrectTypeReferences, NoUnresolvedReferences,
};
let opt = command.opt;
let verbose = match opt.verbose {
true => 1u8,
false => 0u8,
};
let config = select_config(&opt.config)?;
let model = build_model(opt.input, config.models, config.base_dir, verbose)?;
let report = run_validation_actions(
&mut [
Box::<CorrectTypeReferences>::default(),
Box::<NoUnresolvedReferences>::default(),
],
&model,
false,
)
.map_err(|e| anyhow!("validation error: {}", e.to_string()))?;
cargo_atelier::report::report_action_issues(report, true)
.map_err(|e| anyhow!("report error: {}", e))?;
Ok(CommandOutput::default())
}
fn build_model(
input: Vec<String>,
models: Vec<ModelSource>,
base_dir: PathBuf,
verbose: u8,
) -> Result<Model, anyhow::Error> {
std::thread::spawn(move || {
if input.is_empty() {
sources_to_model(&models, &base_dir, verbose).map_err(|e| e.to_string())
} else {
inputs_to_model(&input, verbose).map_err(|e| e.to_string())
}
})
.join()
.map_err(|_| anyhow!("downloader thread paniced"))?
.map_err(|e| anyhow!("{}", e))
}
fn inputs_to_model(inputs: &[String], verbose: u8) -> Result<Model, anyhow::Error> {
use std::str::FromStr;
let inputs = inputs
.iter()
.map(|s| ModelSource::from_str(s).unwrap())
.collect::<Vec<ModelSource>>();
let current_dir = PathBuf::from(".");
Ok(sources_to_model(&inputs, ¤t_dir, verbose)?)
}
fn select_config(opt_config: &Option<PathBuf>) -> Result<CodegenConfig, anyhow::Error> {
let (cfile, folder) = if let Some(path) = &opt_config {
let path = std::fs::canonicalize(path)?;
(
std::fs::read_to_string(&path)
.map_err(|e| anyhow!("reading config file {}: {}", path.display(), e))?,
path.parent().unwrap().to_path_buf(),
)
} else if PathBuf::from(CODEGEN_CONFIG_FILE).is_file() {
(
std::fs::read_to_string(CODEGEN_CONFIG_FILE)
.map_err(|e| anyhow!("reading config file {}.toml: {}", CODEGEN_CONFIG_FILE, e))?,
PathBuf::from("."),
)
} else {
(String::new(), PathBuf::from("."))
};
let folder = std::fs::canonicalize(folder)?;
let mut config = cfile.parse::<CodegenConfig>()?;
config.base_dir = folder;
Ok(config)
}
pub fn handle_gen_command(command: GenerateCli) -> Result<CommandOutput> {
let opt = command.opt;
if let Some(ref tdir) = opt.template_dir {
if !tdir.is_dir() {
bail!("template_dir parameter must be an existing directory");
}
}
let output_dir = match &opt.output_dir {
Some(pb) => pb.to_owned(),
_ => PathBuf::from("."),
};
let verbose = match opt.verbose {
true => 1u8,
false => 0u8,
};
let mut config = select_config(&opt.config)?;
if !opt.lang.is_empty() {
config.output_languages = opt.lang.clone()
}
let mut input_models = Vec::new();
std::mem::swap(&mut config.models, &mut input_models);
let model = build_model(opt.input, input_models, config.base_dir.clone(), verbose)?;
let templates = if let Some(ref tdir) = opt.template_dir {
println!(
"{} {} {}",
emoji::INFO,
style("Importing templates from ").bold(),
style(&tdir.display()).underlined()
);
weld_codegen::templates_from_dir(tdir)?
} else {
Vec::new()
};
let g = weld_codegen::Generator::default();
g.gen(Some(&model), config, templates, &output_dir, opt.defines)?;
Ok(CommandOutput::default())
}
fn parse_key_val(
s: &str,
) -> Result<(String, TomlValue), Box<dyn std::error::Error + Send + Sync + 'static>> {
let pos = s
.find('=')
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
Ok((s[..pos].to_string(), as_toml(&s[pos + 1..])))
}
fn as_toml(s: &str) -> TomlValue {
if s == "true" {
return true.into();
}
if s == "false" {
return false.into();
}
if let Ok(num) = s.parse::<i32>() {
return num.into();
};
TomlValue::String(s.to_string())
}