use clap::builder::PossibleValue;
use clap::error::ErrorKind;
use clap::{CommandFactory, Parser, Subcommand};
use std::error::Error;
use std::path::PathBuf;
use tackler_api::txn_ts;
use tackler_core::config;
use tackler_core::kernel::settings::{FileInput, FsInput, GitInput, InputSettings};
use tackler_core::kernel::Settings;
use tackler_core::parser::GitInputSelector;
#[derive(Parser)]
#[command(author, version=env!("VERSION"), about, long_about = None)]
#[command(args_conflicts_with_subcommands = true)]
pub(crate) struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[clap(flatten)]
pub args: DefaultArgs,
}
impl Cli {
pub(crate) fn cmd(&self) -> Commands {
let command = self
.command
.clone()
.unwrap_or(Commands::Report(self.args.clone()));
match self.command {
Some(_) => command,
None => {
if self.args.conf_path.is_none() {
let mut cmd = Cli::command();
let msg = format!(
"config file is not provided, use: \n\n{} --config <path/to/config-file>",
cmd.get_name()
);
cmd.error(ErrorKind::MissingRequiredArgument, msg.as_str())
.exit();
}
command
}
}
}
}
#[derive(Debug, Clone, clap::Args)]
#[group(multiple = false)]
pub(crate) struct GitInputGroup {
#[arg(
long = "input.git.ref",
value_name = "refname",
group = "git_input_group"
)]
pub(crate) input_git_ref: Option<String>,
#[arg(
long = "input.git.commit",
value_name = "sha",
group = "git_input_group"
)]
pub(crate) input_git_commit: Option<String>,
}
#[derive(Clone, Subcommand)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Commands {
New { name: String },
Init {},
Report(DefaultArgs),
}
#[derive(Debug, Clone, clap::Args)]
pub(crate) struct DefaultArgs {
#[arg(long = "config", value_name = "path_to_config-file")]
pub(crate) conf_path: Option<PathBuf>,
#[arg(long = "strict.mode", value_name = "true|false")]
pub(crate) strict_mode: Option<bool>,
#[arg(long = "audit.mode", value_name = "true|false")]
pub(crate) audit_mode: Option<bool>,
#[arg(
long = "output.dir",
value_name = "path_to_output-directory",
requires("output_name")
)]
pub(crate) output_directory: Option<PathBuf>,
#[arg(
long = "output.prefix",
value_name = "filename-prefix",
requires("output_directory")
)]
pub(crate) output_name: Option<String>,
#[arg(long="input.file",
value_name = "path_to_journal-file",
conflicts_with_all([
"input_storage",
"input_fs_dir",
"input_fs_ext",
"input_git_repo",
"input_git_ref",
"input_git_dir"])
)]
pub(crate) input_filename: Option<PathBuf>,
#[arg(long="input.storage",
value_name = "type_of_storage",
value_parser([
PossibleValue::new(config::StorageType::STORAGE_FS),
PossibleValue::new(config::StorageType::STORAGE_GIT),
])
)]
pub(crate) input_storage: Option<String>,
#[arg(long="input.fs.dir",
value_name = "path_to_transaction-directory",
requires("input_fs_ext"),
conflicts_with_all([
"input_git_repo",
"input_git_ref",
"input_git_dir"])
)]
pub(crate) input_fs_dir: Option<PathBuf>,
#[arg(
long = "input.fs.ext",
value_name = "txn_file-suffix",
requires("input_fs_dir")
)]
pub(crate) input_fs_ext: Option<String>,
#[arg(
long = "input.git.repository",
value_name = "path",
requires("input_git_dir"),
requires("git_input_group")
)]
pub(crate) input_git_repo: Option<PathBuf>,
#[clap(flatten)]
git_input_selector: GitInputGroup,
#[arg(
long = "input.git.dir",
value_name = "path_to_transaction-directory",
requires("input_git_repo")
)]
pub(crate) input_git_dir: Option<String>,
#[arg(long = "accounts", value_name = "regex", num_args(1..))]
pub(crate) accounts: Option<Vec<String>>,
#[arg(long = "reports", value_name = "type", num_args(1..),
value_parser([
PossibleValue::new("register"),
PossibleValue::new("balance"),
PossibleValue::new("balance-group"),
])
)]
pub(crate) reports: Option<Vec<String>>,
#[arg(long = "group-by", value_name = "group-by", num_args(1),
value_parser([
PossibleValue::new(txn_ts::GroupBy::YEAR),
PossibleValue::new(txn_ts::GroupBy::MONTH),
PossibleValue::new(txn_ts::GroupBy::DATE),
PossibleValue::new(txn_ts::GroupBy::ISO_WEEK),
PossibleValue::new(txn_ts::GroupBy::ISO_WEEK_DATE),
])
)]
pub(crate) group_by: Option<String>,
#[arg(long = "exports", value_name = "type", num_args(1..),
value_parser([
PossibleValue::new("identity"),
PossibleValue::new("equity"),
])
)]
pub(crate) exports: Option<Vec<String>>,
#[arg(long = "api-filter-def", value_name = "txn_filter")]
pub(crate) api_filter_def: Option<String>,
}
impl DefaultArgs {
fn get_git_selector(&self) -> Option<GitInputSelector> {
match (
&self.git_input_selector.input_git_commit,
&self.git_input_selector.input_git_ref,
) {
(Some(commit), None) => Some(GitInputSelector::CommitId(commit.clone())),
(None, Some(git_ref)) => Some(GitInputSelector::Reference(git_ref.clone())),
(None, None) => None,
(Some(_), Some(_)) => {
panic!("IE: this should not be possible, Clap configuration is broken")
}
}
}
pub(crate) fn get_input_type(
&self,
settings: &Settings,
) -> Result<InputSettings, Box<dyn Error>> {
let git_selector = self.get_git_selector();
if let Some(filename) = &self.input_filename {
let i = FileInput {
path: filename.clone(),
};
Ok(InputSettings::File(i))
} else if self.input_fs_dir.is_some() {
let i = FsInput {
dir: self
.input_fs_dir
.clone()
.expect("IE: This should not be possible (Clap)"),
suffix: self
.input_fs_ext
.clone()
.expect("IE: This should not be possible (Clap)"),
};
Ok(InputSettings::Fs(i))
} else if self.input_git_repo.is_some() {
let i = GitInput {
repo: self.input_git_repo.clone().unwrap(),
git_ref: git_selector.expect("IE: This should not be possible (Clap)"),
dir: self
.input_git_dir
.clone()
.expect("IE: This should not be possible (Clap)"),
ext: String::from("txn"),
};
Ok(InputSettings::Git(i))
} else if self.input_git_repo.is_none() && git_selector.is_some() {
match settings.get_input_settings(
Some(&config::StorageType::STORAGE_GIT.to_string()),
Some(self.conf_path.as_ref().unwrap().as_path()),
)? {
InputSettings::Git(git) => Ok(InputSettings::Git(GitInput {
git_ref: git_selector.unwrap(),
..git
})),
_ => {
let msg = "CLI Arg handling: Internal logic error";
Err(msg.into())
}
}
} else {
settings.get_input_settings(
self.input_storage.as_ref(),
Some(self.conf_path.as_ref().unwrap().as_path()),
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_cli() {
use clap::CommandFactory;
Cli::command().debug_assert()
}
}