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#[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 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
59fn 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#[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 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
109fn 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#[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 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
173fn 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
189fn 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
200fn 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
221fn parse_sequence(value: String) -> Result<usize, RestoreCommandError> {
223 value
224 .parse::<usize>()
225 .map_err(|_| RestoreCommandError::InvalidSequence)
226}
227
228fn 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}