use std::fs::File;
use std::fs::{self};
use std::io::Write;
use std::io::{self};
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use clap::Command;
use crate::model::CliSpec;
use crate::reflect::ReflectOptions;
use crate::reflect::reflect_command_with_options;
use crate::reflect_command_with_name;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum OutputContractGeneration {
#[default]
Omit,
Emit,
}
impl OutputContractGeneration {
#[must_use]
pub const fn is_enabled(self) -> bool {
matches!(self, Self::Emit)
}
}
pub trait Generator {
fn file_name(&self, bin_name: &str) -> String;
fn generate(&self, spec: &CliSpec, buf: &mut dyn Write) -> io::Result<()>;
fn generate_files(&self, spec: &CliSpec) -> io::Result<Vec<GeneratedFile>> {
let mut contents = Vec::<u8>::new();
self.generate(spec, &mut contents)?;
Ok(vec![GeneratedFile::new(
self.file_name(&spec.bin_name),
contents,
)])
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedFile {
pub relative_path: PathBuf,
pub contents: Vec<u8>,
}
impl GeneratedFile {
#[must_use]
pub fn new(relative_path: impl Into<PathBuf>, contents: impl Into<Vec<u8>>) -> Self {
Self {
relative_path: relative_path.into(),
contents: contents.into(),
}
}
#[must_use]
pub fn text(relative_path: impl Into<PathBuf>, contents: impl Into<String>) -> Self {
Self::new(relative_path, contents.into().into_bytes())
}
}
pub fn generate<G, S>(
generator: G,
cmd: &Command,
bin_name: S,
buf: &mut dyn Write,
) -> io::Result<()>
where
G: Generator,
S: Into<String>,
{
let spec = reflect_command_with_name(cmd.clone(), bin_name);
generator.generate(&spec, buf)
}
pub fn generate_to<G, S, P>(
generator: G,
cmd: &Command,
bin_name: S,
out_dir: P,
) -> io::Result<PathBuf>
where
G: Generator,
S: Into<String>,
P: AsRef<Path>,
{
generate_to_with_options(generator, cmd, bin_name, out_dir, ReflectOptions::default())
}
pub fn generate_to_with_options<G, S, P>(
generator: G,
cmd: &Command,
bin_name: S,
out_dir: P,
opts: ReflectOptions,
) -> io::Result<PathBuf>
where
G: Generator,
S: Into<String>,
P: AsRef<Path>,
{
let bin_name = bin_name.into();
let out_dir = out_dir.as_ref();
let spec = reflect_command_with_options(cmd.clone(), bin_name.clone(), opts);
let files = generator.generate_files(&spec)?;
fs::create_dir_all(out_dir)?;
for generated in files {
let path = safe_output_path(out_dir, &generated.relative_path)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = File::create(path)?;
file.write_all(&generated.contents)?;
file.flush()?;
}
Ok(out_dir.join(generator.file_name(&bin_name)))
}
fn safe_output_path(out_dir: &Path, relative_path: &Path) -> io::Result<PathBuf> {
if relative_path.as_os_str().is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"generated file path cannot be empty",
));
}
if relative_path.components().any(|component| {
matches!(
component,
Component::Prefix(_) | Component::RootDir | Component::ParentDir
)
}) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"generated file path `{}` must be relative and stay inside the output directory",
relative_path.display()
),
));
}
Ok(out_dir.join(relative_path))
}