use crate::{
generate::{errors::*, parameters, utils},
testing,
};
use chrono::{DateTime, Utc};
use clap::{Parser, Subcommand};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use serde_yaml::Error as SerdeYAMLError;
use std::{env, io::Error as IOError, ops::Deref, path::PathBuf};
use strum::EnumProperty;
use thiserror::Error;
use url::Url;
mod defaults {
use super::*;
pub const CWD: Lazy<PathBuf> = Lazy::new(|| env::current_dir().expect("must get current dir"));
}
use defaults::*;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Cli {
pub inner_cli: InnerCli,
pub generation_timestamp: DateTime<Utc>,
}
impl Deref for Cli {
type Target = InnerCli;
fn deref(&self) -> &Self::Target {
&self.inner_cli
}
}
impl Cli {
pub fn get_generation_timestamp_string(&self) -> String {
self.generation_timestamp.to_rfc3339()
}
pub async fn new() -> Result<Self, CLIError> {
let mut inner_cli = InnerCli::parse();
let InnerCli {
command,
output_project_dir_opt,
local_api_spec_filepath_opt,
..
} = &mut inner_cli;
if let Some(SubCommands::TestGeneration { .. }) = command.as_mut() {
let temp_subdir_path = utils::get_temp_subdir();
if local_api_spec_filepath_opt.is_none() {
let yaml_spec_file_name = testing::TESTING_SPEC_FILE_NAME;
let yaml_test_spec_path = temp_subdir_path.join(yaml_spec_file_name);
let _ = local_api_spec_filepath_opt.replace(yaml_test_spec_path);
}
if output_project_dir_opt.is_none() {
let _ = output_project_dir_opt.replace(temp_subdir_path);
}
} else if inner_cli.local_api_spec_filepath_opt.is_none()
&& inner_cli.api_spec_url_opt.is_none()
{
return Err(ParameterError::APIUrlNeededIfNoLocalFile.into());
}
Ok(Self {
generation_timestamp: Utc::now(),
inner_cli,
})
}
}
#[derive(Error, Debug)]
pub enum CLIError {
#[error(transparent)]
IOError(#[from] IOError),
#[error(transparent)]
CargoConfigError(#[from] CargoConfigError),
#[error(transparent)]
CrateScaffoldingError(#[from] CrateScaffoldingError),
#[error(transparent)]
MakefileGenerationError(#[from] MakefileGenerationError),
#[error(transparent)]
ParameterError(#[from] ParameterError),
#[error(transparent)]
ProcessError(#[from] utils::ProcessError),
#[error(transparent)]
READMEGenerationError(#[from] READMEGenerationError),
#[error(transparent)]
SerdeYAMLError(#[from] SerdeYAMLError),
#[error(transparent)]
YAMLGenerationError(#[from] YAMLGenerationError),
#[error(transparent)]
TestingError(#[from] testing::TestingError),
}
#[derive(Clone, Debug, Deserialize, Serialize, Subcommand)]
pub enum SubCommands {
#[command(rename_all = "kebab-case", verbatim_doc_comment)]
TestGeneration {
#[arg(
short = 'p',
long = "generator-crate-local-path",
required_unless_present("generator_crate_repo_url_opt")
)]
generator_crate_local_path_opt: Option<PathBuf>,
#[arg(short = 'u', long = "generator-crate-repo-url")]
generator_crate_repo_url_opt: Option<Url>,
},
}
#[derive(Clone, Debug, Deserialize, Parser, Serialize)]
#[command(author, version, about, verbatim_doc_comment)]
pub struct InnerCli {
#[arg(long = "name")]
pub site_or_api_name: String,
#[arg(long = "api-url")]
pub api_url: Url,
#[arg(long = "autogenerate", default_value_t = true)]
pub autogenerate: bool,
#[arg(long = "spec-url")]
pub api_spec_url_opt: Option<Url>,
#[arg(long = "spec-file")]
pub local_api_spec_filepath_opt: Option<PathBuf>,
#[arg(long = "lib_name")]
pub lib_name_opt: Option<String>,
#[arg(long = "authors")]
pub extra_authors: Option<String>,
#[arg(long = "output")]
output_project_dir_opt: Option<PathBuf>,
#[command(subcommand)]
pub command: Option<SubCommands>,
}
impl InnerCli {
pub const TEMP_DIR_NAME: &'static str = "temp";
pub fn get_extra_authors(&self) -> Vec<String> {
Self::parse_authors_string(&self.extra_authors.clone().unwrap_or_default())
}
fn get_default_lib_name(&self) -> String {
let Self {
site_or_api_name, ..
} = self;
format!("{site_or_api_name}_openapi_client")
}
fn get_default_spec_file_name(&self) -> String {
let mut name_path = PathBuf::from(self.get_lib_name());
name_path.set_extension("yaml");
name_path.to_string_lossy().to_string()
}
pub fn get_lib_name(&self) -> String {
self
.lib_name_opt
.clone()
.unwrap_or_else(|| self.get_default_lib_name())
}
pub fn get_output_project_dir(&self) -> PathBuf {
self
.output_project_dir_opt
.clone()
.unwrap_or_else(|| CWD.clone())
}
pub fn get_output_project_dir_string(&self) -> String {
self.get_output_project_dir().to_string_lossy().to_string()
}
pub fn get_output_project_subpath(
&self,
subpath: &Paths,
) -> PathBuf {
self
.get_output_project_dir()
.join(subpath.get_str("path").expect("must get subpath"))
}
pub fn get_output_project_subpath_string(
&self,
subpath: &Paths,
) -> String {
self
.get_output_project_subpath(subpath)
.to_string_lossy()
.to_string()
}
pub fn parse_authors_string(s: &str) -> Vec<String> {
s.split(";")
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
}
pub fn try_get_spec_file_name(&self) -> Result<String, ParameterError> {
if let Some(local_api_spec_filepath) = self.local_api_spec_filepath_opt.as_ref() {
Ok(local_api_spec_filepath.to_string_lossy().to_string())
} else {
let api_spec_url = self.api_spec_url_opt.clone().expect("must get spec url");
parameters::try_file_name_from_path_url(&api_spec_url).map(|mut s| {
if s.is_empty() {
s.push_str(&self.get_default_spec_file_name())
}
s
})
}
}
}
#[derive(Clone, Copy, Debug, Error, strum::EnumProperty)]
pub enum Paths {
#[error(".gitignore file")]
#[strum(props(path = ".gitignore"))]
GitignoreFile,
#[error(".git dir")]
#[strum(props(path = ".git"))]
GitDir,
#[error("Cargo Make make file")]
#[strum(props(path = "Makefile.toml"))]
CargoMakefile,
#[error("Cargo.toml file")]
#[strum(props(path = "Cargo.toml"))]
CargoTomlFile,
#[error("README.md file")]
#[strum(props(path = "README.md"))]
ReadmeMdFile,
#[error("temp dir")]
#[strum(props(path = "temp"))]
TempDir,
}