use crate::args::{flag_arg, parse_matches, path_option, string_option, value_arg};
use clap::{ArgMatches, Command as ClapCommand};
use std::{ffi::OsString, path::PathBuf};
use super::{RestoreCommandError, usage};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RestorePlanOptions {
pub manifest: Option<PathBuf>,
pub backup_dir: Option<PathBuf>,
pub mapping: Option<PathBuf>,
pub out: Option<PathBuf>,
pub require_verified: bool,
pub require_restore_ready: bool,
}
impl RestorePlanOptions {
pub fn parse<I>(args: I) -> Result<Self, RestoreCommandError>
where
I: IntoIterator<Item = OsString>,
{
let matches = parse_matches(restore_plan_command(), args)
.map_err(|_| RestoreCommandError::Usage(usage()))?;
let manifest = path_option(&matches, "manifest");
let backup_dir = path_option(&matches, "backup-dir");
let require_verified = matches.get_flag("require-verified");
if manifest.is_some() && backup_dir.is_some() {
return Err(RestoreCommandError::ConflictingManifestSources);
}
if manifest.is_none() && backup_dir.is_none() {
return Err(RestoreCommandError::MissingOption(
"--manifest or --backup-dir",
));
}
if require_verified && backup_dir.is_none() {
return Err(RestoreCommandError::RequireVerifiedNeedsBackupDir);
}
Ok(Self {
manifest,
backup_dir,
mapping: path_option(&matches, "mapping"),
out: path_option(&matches, "out"),
require_verified,
require_restore_ready: matches.get_flag("require-restore-ready"),
})
}
}
fn restore_plan_command() -> ClapCommand {
ClapCommand::new("restore-plan")
.disable_help_flag(true)
.arg(value_arg("manifest").long("manifest"))
.arg(value_arg("backup-dir").long("backup-dir"))
.arg(value_arg("mapping").long("mapping"))
.arg(value_arg("out").long("out"))
.arg(flag_arg("require-verified").long("require-verified"))
.arg(flag_arg("require-restore-ready").long("require-restore-ready"))
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RestoreApplyOptions {
pub plan: PathBuf,
pub backup_dir: Option<PathBuf>,
pub out: Option<PathBuf>,
pub journal_out: Option<PathBuf>,
pub dry_run: bool,
}
impl RestoreApplyOptions {
pub fn parse<I>(args: I) -> Result<Self, RestoreCommandError>
where
I: IntoIterator<Item = OsString>,
{
let matches = parse_matches(restore_apply_command(), args)
.map_err(|_| RestoreCommandError::Usage(usage()))?;
let dry_run = matches.get_flag("dry-run");
if !dry_run {
return Err(RestoreCommandError::ApplyRequiresDryRun);
}
Ok(Self {
plan: path_option(&matches, "plan")
.ok_or(RestoreCommandError::MissingOption("--plan"))?,
backup_dir: path_option(&matches, "backup-dir"),
out: path_option(&matches, "out"),
journal_out: path_option(&matches, "journal-out"),
dry_run,
})
}
}
fn restore_apply_command() -> ClapCommand {
ClapCommand::new("restore-apply")
.disable_help_flag(true)
.arg(value_arg("plan").long("plan"))
.arg(value_arg("backup-dir").long("backup-dir"))
.arg(value_arg("out").long("out"))
.arg(value_arg("journal-out").long("journal-out"))
.arg(flag_arg("dry-run").long("dry-run"))
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[expect(
clippy::struct_excessive_bools,
reason = "CLI runner options mirror three mutually exclusive mode flags and two operator guard flags"
)]
pub struct RestoreRunOptions {
pub journal: PathBuf,
pub dfx: String,
pub network: Option<String>,
pub out: Option<PathBuf>,
pub dry_run: bool,
pub execute: bool,
pub unclaim_pending: bool,
pub max_steps: Option<usize>,
pub require_complete: bool,
pub require_no_attention: bool,
}
impl RestoreRunOptions {
pub fn parse<I>(args: I) -> Result<Self, RestoreCommandError>
where
I: IntoIterator<Item = OsString>,
{
let matches = parse_matches(restore_run_command(), args)
.map_err(|_| RestoreCommandError::Usage(usage()))?;
let dry_run = matches.get_flag("dry-run");
let execute = matches.get_flag("execute");
let unclaim_pending = matches.get_flag("unclaim-pending");
validate_restore_run_mode_selection(dry_run, execute, unclaim_pending)?;
Ok(Self {
journal: path_option(&matches, "journal")
.ok_or(RestoreCommandError::MissingOption("--journal"))?,
dfx: string_option(&matches, "dfx").unwrap_or_else(|| "dfx".to_string()),
network: string_option(&matches, "network"),
out: path_option(&matches, "out"),
dry_run,
execute,
unclaim_pending,
max_steps: positive_integer_option(&matches, "max-steps", "--max-steps")?,
require_complete: matches.get_flag("require-complete"),
require_no_attention: matches.get_flag("require-no-attention"),
})
}
}
fn restore_run_command() -> ClapCommand {
ClapCommand::new("restore-run")
.disable_help_flag(true)
.arg(value_arg("journal").long("journal"))
.arg(value_arg("dfx").long("dfx"))
.arg(value_arg("network").long("network"))
.arg(value_arg("out").long("out"))
.arg(flag_arg("dry-run").long("dry-run"))
.arg(flag_arg("execute").long("execute"))
.arg(flag_arg("unclaim-pending").long("unclaim-pending"))
.arg(value_arg("max-steps").long("max-steps"))
.arg(flag_arg("require-complete").long("require-complete"))
.arg(flag_arg("require-no-attention").long("require-no-attention"))
}
fn positive_integer_option(
matches: &ArgMatches,
id: &str,
option: &'static str,
) -> Result<Option<usize>, RestoreCommandError> {
string_option(matches, id)
.map(|value| parse_positive_integer(option, value))
.transpose()
}
fn validate_restore_run_mode_selection(
dry_run: bool,
execute: bool,
unclaim_pending: bool,
) -> Result<(), RestoreCommandError> {
let mode_count = [dry_run, execute, unclaim_pending]
.into_iter()
.filter(|enabled| *enabled)
.count();
if mode_count > 1 {
return Err(RestoreCommandError::RestoreRunConflictingModes);
}
if mode_count == 0 {
return Err(RestoreCommandError::RestoreRunRequiresMode);
}
Ok(())
}
fn parse_sequence(value: String) -> Result<usize, RestoreCommandError> {
value
.parse::<usize>()
.map_err(|_| RestoreCommandError::InvalidSequence)
}
fn parse_positive_integer(
option: &'static str,
value: String,
) -> Result<usize, RestoreCommandError> {
let parsed = parse_sequence(value)?;
if parsed == 0 {
return Err(RestoreCommandError::InvalidPositiveInteger { option });
}
Ok(parsed)
}