Skip to main content

canic_cli/restore/
options.rs

1use crate::args::{flag_arg, parse_matches, path_option, string_option, value_arg};
2use clap::{ArgMatches, Command as ClapCommand};
3use std::{ffi::OsString, path::PathBuf};
4
5use super::{RestoreCommandError, usage};
6
7///
8/// RestorePlanOptions
9///
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct RestorePlanOptions {
13    pub manifest: Option<PathBuf>,
14    pub backup_dir: Option<PathBuf>,
15    pub mapping: Option<PathBuf>,
16    pub out: Option<PathBuf>,
17    pub require_verified: bool,
18    pub require_restore_ready: bool,
19}
20
21impl RestorePlanOptions {
22    /// Parse restore planning options from CLI arguments.
23    pub fn parse<I>(args: I) -> Result<Self, RestoreCommandError>
24    where
25        I: IntoIterator<Item = OsString>,
26    {
27        let matches = parse_matches(restore_plan_command(), args)
28            .map_err(|_| RestoreCommandError::Usage(usage()))?;
29
30        let manifest = path_option(&matches, "manifest");
31        let backup_dir = path_option(&matches, "backup-dir");
32        let require_verified = matches.get_flag("require-verified");
33
34        if manifest.is_some() && backup_dir.is_some() {
35            return Err(RestoreCommandError::ConflictingManifestSources);
36        }
37
38        if manifest.is_none() && backup_dir.is_none() {
39            return Err(RestoreCommandError::MissingOption(
40                "--manifest or --backup-dir",
41            ));
42        }
43
44        if require_verified && backup_dir.is_none() {
45            return Err(RestoreCommandError::RequireVerifiedNeedsBackupDir);
46        }
47
48        Ok(Self {
49            manifest,
50            backup_dir,
51            mapping: path_option(&matches, "mapping"),
52            out: path_option(&matches, "out"),
53            require_verified,
54            require_restore_ready: matches.get_flag("require-restore-ready"),
55        })
56    }
57}
58
59// Build the restore plan parser.
60fn restore_plan_command() -> ClapCommand {
61    ClapCommand::new("restore-plan")
62        .disable_help_flag(true)
63        .arg(value_arg("manifest").long("manifest"))
64        .arg(value_arg("backup-dir").long("backup-dir"))
65        .arg(value_arg("mapping").long("mapping"))
66        .arg(value_arg("out").long("out"))
67        .arg(flag_arg("require-verified").long("require-verified"))
68        .arg(flag_arg("require-restore-ready").long("require-restore-ready"))
69}
70
71///
72/// RestoreApplyOptions
73///
74
75#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct RestoreApplyOptions {
77    pub plan: PathBuf,
78    pub backup_dir: Option<PathBuf>,
79    pub out: Option<PathBuf>,
80    pub journal_out: Option<PathBuf>,
81    pub dry_run: bool,
82}
83
84impl RestoreApplyOptions {
85    /// Parse restore apply options from CLI arguments.
86    pub fn parse<I>(args: I) -> Result<Self, RestoreCommandError>
87    where
88        I: IntoIterator<Item = OsString>,
89    {
90        let matches = parse_matches(restore_apply_command(), args)
91            .map_err(|_| RestoreCommandError::Usage(usage()))?;
92        let dry_run = matches.get_flag("dry-run");
93
94        if !dry_run {
95            return Err(RestoreCommandError::ApplyRequiresDryRun);
96        }
97
98        Ok(Self {
99            plan: path_option(&matches, "plan")
100                .ok_or(RestoreCommandError::MissingOption("--plan"))?,
101            backup_dir: path_option(&matches, "backup-dir"),
102            out: path_option(&matches, "out"),
103            journal_out: path_option(&matches, "journal-out"),
104            dry_run,
105        })
106    }
107}
108
109// Build the restore apply dry-run parser.
110fn restore_apply_command() -> ClapCommand {
111    ClapCommand::new("restore-apply")
112        .disable_help_flag(true)
113        .arg(value_arg("plan").long("plan"))
114        .arg(value_arg("backup-dir").long("backup-dir"))
115        .arg(value_arg("out").long("out"))
116        .arg(value_arg("journal-out").long("journal-out"))
117        .arg(flag_arg("dry-run").long("dry-run"))
118}
119
120///
121/// RestoreRunOptions
122///
123
124#[derive(Clone, Debug, Eq, PartialEq)]
125#[expect(
126    clippy::struct_excessive_bools,
127    reason = "CLI runner options mirror three mutually exclusive mode flags and two operator guard flags"
128)]
129pub struct RestoreRunOptions {
130    pub journal: PathBuf,
131    pub dfx: String,
132    pub network: Option<String>,
133    pub out: Option<PathBuf>,
134    pub dry_run: bool,
135    pub execute: bool,
136    pub unclaim_pending: bool,
137    pub max_steps: Option<usize>,
138    pub require_complete: bool,
139    pub require_no_attention: bool,
140}
141
142impl RestoreRunOptions {
143    /// Parse restore run options from CLI arguments.
144    pub fn parse<I>(args: I) -> Result<Self, RestoreCommandError>
145    where
146        I: IntoIterator<Item = OsString>,
147    {
148        let matches = parse_matches(restore_run_command(), args)
149            .map_err(|_| RestoreCommandError::Usage(usage()))?;
150
151        let dry_run = matches.get_flag("dry-run");
152        let execute = matches.get_flag("execute");
153        let unclaim_pending = matches.get_flag("unclaim-pending");
154
155        validate_restore_run_mode_selection(dry_run, execute, unclaim_pending)?;
156
157        Ok(Self {
158            journal: path_option(&matches, "journal")
159                .ok_or(RestoreCommandError::MissingOption("--journal"))?,
160            dfx: string_option(&matches, "dfx").unwrap_or_else(|| "dfx".to_string()),
161            network: string_option(&matches, "network"),
162            out: path_option(&matches, "out"),
163            dry_run,
164            execute,
165            unclaim_pending,
166            max_steps: positive_integer_option(&matches, "max-steps", "--max-steps")?,
167            require_complete: matches.get_flag("require-complete"),
168            require_no_attention: matches.get_flag("require-no-attention"),
169        })
170    }
171}
172
173// Build the native restore runner parser.
174fn restore_run_command() -> ClapCommand {
175    ClapCommand::new("restore-run")
176        .disable_help_flag(true)
177        .arg(value_arg("journal").long("journal"))
178        .arg(value_arg("dfx").long("dfx"))
179        .arg(value_arg("network").long("network"))
180        .arg(value_arg("out").long("out"))
181        .arg(flag_arg("dry-run").long("dry-run"))
182        .arg(flag_arg("execute").long("execute"))
183        .arg(flag_arg("unclaim-pending").long("unclaim-pending"))
184        .arg(value_arg("max-steps").long("max-steps"))
185        .arg(flag_arg("require-complete").long("require-complete"))
186        .arg(flag_arg("require-no-attention").long("require-no-attention"))
187}
188
189// Read one positive integer option from Clap matches.
190fn positive_integer_option(
191    matches: &ArgMatches,
192    id: &str,
193    option: &'static str,
194) -> Result<Option<usize>, RestoreCommandError> {
195    string_option(matches, id)
196        .map(|value| parse_positive_integer(option, value))
197        .transpose()
198}
199
200// Validate that restore run received exactly one execution mode.
201fn validate_restore_run_mode_selection(
202    dry_run: bool,
203    execute: bool,
204    unclaim_pending: bool,
205) -> Result<(), RestoreCommandError> {
206    let mode_count = [dry_run, execute, unclaim_pending]
207        .into_iter()
208        .filter(|enabled| *enabled)
209        .count();
210    if mode_count > 1 {
211        return Err(RestoreCommandError::RestoreRunConflictingModes);
212    }
213
214    if mode_count == 0 {
215        return Err(RestoreCommandError::RestoreRunRequiresMode);
216    }
217
218    Ok(())
219}
220
221// Parse a restore apply journal operation sequence value.
222fn parse_sequence(value: String) -> Result<usize, RestoreCommandError> {
223    value
224        .parse::<usize>()
225        .map_err(|_| RestoreCommandError::InvalidSequence)
226}
227
228// Parse a positive integer CLI value for options where zero is not meaningful.
229fn parse_positive_integer(
230    option: &'static str,
231    value: String,
232) -> Result<usize, RestoreCommandError> {
233    let parsed = parse_sequence(value)?;
234    if parsed == 0 {
235        return Err(RestoreCommandError::InvalidPositiveInteger { option });
236    }
237
238    Ok(parsed)
239}