apple_clis/shared/
traits.rs

1use crate::prelude::*;
2
3/// Wrapper of binary
4pub trait ExecInstance: Sized {
5	/// E.g. `codesign` or `xcrun`
6	const BINARY_NAME: &'static str;
7
8	/// # Safety
9	/// Must point to a valid executable file.
10	///
11	/// Prefer [ExecInstance::new]
12	unsafe fn new_unchecked(exec_path: impl AsRef<Utf8Path>) -> Self;
13
14	fn get_inner_exec_path(&self) -> &Utf8Path;
15
16	fn bossy_command(&self) -> bossy::Command {
17		bossy::Command::pure(self.get_inner_exec_path())
18	}
19
20	fn version_command(&self) -> bossy::Command {
21		self.bossy_command().with_arg("--version")
22	}
23
24	fn validate_version(&self) -> std::result::Result<bool, bossy::Error> {
25		self
26			.version_command()
27			.run_and_wait_for_output()
28			.map(|status| status.success())
29	}
30
31	fn from_path(path: impl AsRef<Utf8Path>) -> Result<Self> {
32		// check path exists
33		let path = path.as_ref();
34		match path.try_exists() {
35			Ok(true) => Ok(unsafe { Self::new_unchecked(path) }),
36			Ok(false) => Err(Error::PathDoesNotExist {
37				path: path.to_owned(),
38				err: None,
39			}),
40			Err(e) => Err(Error::PathDoesNotExist {
41				path: path.to_owned(),
42				err: Some(e),
43			}),
44		}
45	}
46
47	/// Uses `which` to find the binary automatically
48	fn new() -> Result<Self> {
49		let path = which::which(Self::BINARY_NAME)?;
50		let path = Utf8PathBuf::try_from(path)?;
51		// Safety: `path` is a valid path to the binary
52		let instance = unsafe { Self::new_unchecked(path) };
53		match instance.validate_version() {
54			Ok(true) => Ok(instance),
55			Ok(false) => Err(Error::VersionCheckFailed(None)),
56			Err(e) => Err(Error::VersionCheckFailed(Some(e))),
57		}
58	}
59}
60
61pub trait ExecChild<'src>: Sized {
62	const SUBCOMMAND_NAME: &'static str;
63
64	type Parent: ExecInstance;
65
66	/// Unsafe constructor for a child command.
67	/// Assumes that the subcommand has been validated to exist.
68	///
69	/// # Safety
70	/// Parent must be validated. Type system should validate this,
71	/// this method being unsafe is to reflect [ExecInstance::new_unchecked].
72	///
73	/// Prefer [ChildExec::new]
74	unsafe fn new_unchecked(parent: &'src Self::Parent) -> Self;
75	fn get_inner_parent(&self) -> &Self::Parent;
76
77	fn bossy_command(&self) -> bossy::Command {
78		self
79			.get_inner_parent()
80			.bossy_command()
81			.with_arg(Self::SUBCOMMAND_NAME)
82	}
83
84	fn version_command(&self) -> bossy::Command {
85		self.bossy_command().with_arg("--version")
86	}
87
88	fn validate_version(&self) -> std::result::Result<bool, bossy::Error> {
89		self
90			.version_command()
91			.run_and_wait_for_output()
92			.map(|status| status.success())
93	}
94
95	fn new(parent: &'src Self::Parent) -> Result<Self> {
96		let instance = unsafe { Self::new_unchecked(parent) };
97		match instance.validate_version() {
98			Ok(true) => Ok(instance),
99			Ok(false) => Err(Error::VersionCheckFailed(None)),
100			Err(e) => Err(Error::VersionCheckFailed(Some(e))),
101		}
102	}
103}
104
105macro_rules! impl_exec_instance {
106	($t:ty, $name:expr) => {
107		impl $crate::shared::ExecInstance for $t {
108			const BINARY_NAME: &'static str = $name;
109
110			unsafe fn new_unchecked(exec_path: impl AsRef<::camino::Utf8Path>) -> Self {
111				Self {
112					exec_path: exec_path.as_ref().to_path_buf(),
113				}
114			}
115
116			fn get_inner_exec_path(&self) -> &::camino::Utf8Path {
117				&self.exec_path
118			}
119		}
120
121		impl $t {
122			/// Constructs an instance of `Self` using `which`.
123			pub fn new() -> $crate::error::Result<Self> {
124				$crate::shared::ExecInstance::new()
125			}
126		}
127	};
128	($t:ty, $name:expr, skip_version_check) => {
129		impl $crate::shared::ExecInstance for $t {
130			const BINARY_NAME: &'static str = $name;
131
132			unsafe fn new_unchecked(exec_path: impl AsRef<::camino::Utf8Path>) -> Self {
133				Self {
134					exec_path: exec_path.as_ref().to_path_buf(),
135				}
136			}
137
138			fn get_inner_exec_path(&self) -> &::camino::Utf8Path {
139				&self.exec_path
140			}
141
142			fn validate_version(&self) -> std::result::Result<bool, bossy::Error> {
143				Ok(true)
144			}
145		}
146
147		impl $t {
148			/// Constructs an instance of `Self` using `which`.
149			pub fn new() -> $crate::error::Result<Self> {
150				$crate::shared::ExecInstance::new()
151			}
152		}
153	};
154	($t:ty, $name:expr, skip_version_check, extra_flags = $extra_flags:expr) => {
155		impl $crate::shared::ExecInstance for $t {
156			const BINARY_NAME: &'static str = $name;
157
158			unsafe fn new_unchecked(exec_path: impl AsRef<::camino::Utf8Path>) -> Self {
159				Self {
160					exec_path: exec_path.as_ref().to_path_buf(),
161				}
162			}
163
164			fn get_inner_exec_path(&self) -> &::camino::Utf8Path {
165				&self.exec_path
166			}
167
168			fn bossy_command(&self) -> ::bossy::Command {
169				bossy::Command::pure(&self.get_inner_exec_path()).with_args($extra_flags)
170			}
171
172			fn validate_version(&self) -> std::result::Result<bool, bossy::Error> {
173				Ok(true)
174			}
175		}
176
177		impl $t {
178			/// Constructs an instance of `Self` using `which`.
179			pub fn new() -> $crate::error::Result<Self> {
180				$crate::shared::ExecInstance::new()
181			}
182		}
183	};
184}
185pub(crate) use impl_exec_instance;
186
187macro_rules! impl_exec_child {
188	($t:ty, parent = $parent:ty, subcommand = $name:expr) => {
189		impl<'src> $crate::shared::ExecChild<'src> for $t {
190			const SUBCOMMAND_NAME: &'static str = $name;
191			type Parent = $parent;
192
193			unsafe fn new_unchecked(parent: &'src Self::Parent) -> Self {
194				Self {
195					exec_parent: parent,
196				}
197			}
198
199			fn get_inner_parent(&self) -> &Self::Parent {
200				&self.exec_parent
201			}
202		}
203
204		impl<'src> $t {
205			/// Constructs an instance of `Self` using `which`.
206			pub fn new(
207				parent: &'src <Self as $crate::shared::ExecChild<'src>>::Parent,
208			) -> $crate::error::Result<Self> {
209				$crate::shared::ExecChild::new(parent)
210			}
211		}
212	};
213}
214pub(crate) use impl_exec_child;
215
216pub(crate) trait NomFromStr: Sized {
217	fn nom_from_str(input: &str) -> IResult<&str, Self>;
218}
219
220impl NomFromStr for NonZeroU8 {
221	#[tracing::instrument(level = "trace", skip(input))]
222	fn nom_from_str(input: &str) -> IResult<&str, Self> {
223		map_res(digit1, |s: &str| s.parse())(input)
224	}
225}
226
227/// Common operations that should be implemented on all command outputs.
228/// Often the 'success' of a command is a semantics issue, and is hence
229/// not by default implemented as a plain [std::result::Result].
230/// This trait bridges the gap between custom command output types and
231/// [Result], but it is good to check the documentation for the specific command
232/// to see if you agree with what outputs are classes as 'errors'.
233#[allow(private_bounds)] // private bounds seals this trait
234pub trait PublicCommandOutput: std::fmt::Debug + Serialize + CommandNomParsable {
235	/// If applicable, what the commands successful output should be.
236	/// If no output is application, often a [bool] as returned in [PublicCommandOutput::successful]
237	type PrimarySuccess: std::fmt::Debug;
238
239	/// Will return [Error::OutputErrored] if the command was not successful.
240	fn success(&self) -> Result<&Self::PrimarySuccess>;
241
242	/// Did command exit with an 'expected' success case?
243	fn successful(&self) -> bool {
244		self.success().is_ok()
245	}
246
247	fn failed(&self) -> bool {
248		!self.successful()
249	}
250}
251
252/// Used to wrap command output types
253pub(crate) trait CommandNomParsable: Sized + std::fmt::Debug {
254	fn success_unimplemented(stdout: String) -> Self;
255	fn error_unimplemented(stderr: String) -> Self;
256
257	fn success_nom_from_str(input: &str) -> IResult<&str, Self> {
258		map(rest, |s: &str| Self::success_unimplemented(s.to_owned()))(input)
259	}
260
261	fn success_from_str(input: &str) -> Self {
262		match Self::success_nom_from_str(input) {
263			Ok((remaining, output)) => {
264				if !remaining.is_empty() {
265					tracing::warn!(?remaining, ?output, "Remaining input after parsing a command success output. This likely indicates a potential improvement to the output parser. PRs welcome!")
266				}
267				output
268			}
269			Err(err) => {
270				error!(?err, "Failed to parse success output");
271				Self::success_unimplemented(input.to_owned())
272			}
273		}
274	}
275
276	fn errored_nom_from_str(input: &str) -> IResult<&str, Self> {
277		map(rest, |s: &str| Self::error_unimplemented(s.to_owned()))(input)
278	}
279
280	fn errored_from_str(input: &str) -> Self {
281		match Self::errored_nom_from_str(input) {
282			Ok((remaining, output)) => {
283				if !remaining.is_empty() {
284					warn!(?remaining, ?output, "Remaining input after parsing a command error output. This likely indicates a potential improvement to the output parser. PRs welcome!")
285				}
286				output
287			}
288			// Err(err) => Err(Error::NomParsingFailed {
289			// 	name: Self::name().to_owned(),
290			// 	err: err.to_owned(),
291			// }),
292			Err(err) => {
293				tracing::error!(?err, "Failed to parse error output");
294				Self::error_unimplemented(input.to_owned())
295			}
296		}
297	}
298
299	fn from_success_stream(success: bool, string: String) -> Self {
300		if success {
301			Self::success_from_str(&string)
302		} else {
303			Self::errored_from_str(&string)
304		}
305	}
306
307	fn from_bossy_result(result: bossy::Result<Output>) -> Result<Self> {
308		let (success, string) = match result {
309			Ok(output) => (true, output.stdout_str()?.to_owned()),
310			Err(err) => match err.output() {
311				Some(output) => (false, output.stderr_str()?.to_owned()),
312				None => Err(Error::CannotLocateStderrStream { err })?,
313			},
314		};
315		Ok(Self::from_success_stream(success, string))
316	}
317}
318
319/// Not public API. [impl]s [std::str::FromStr] for a type
320// #[macro_export]
321macro_rules! impl_from_str_nom {
322($type:ty) => {
323		impl std::str::FromStr for $type {
324			type Err = $crate::error::Error;
325
326			#[tracing::instrument(level = "trace", skip(input))]
327			fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
328				match <$type>::nom_from_str(input) {
329					Ok((remaining, output)) => {
330						if !remaining.is_empty() {
331							tracing::warn!(?remaining, ?output, "Remaining input after parsing. This likely indicates a potential improvement to the output parser. PRs welcome!")
332						}
333						// if remaining.is_empty() {
334							Ok(output)
335						// } else {
336							// Err(Error::ParsingRemainingNotEmpty {
337								// input: input.to_owned(),
338						// 		remaining: remaining.to_owned(),
339						// 		parsed_debug: format!("{:#?}", output),
340						// 	})
341						// }
342					}
343					Err(e) => Err(Error::NomParsingFailed {
344						err: e.to_owned(),
345						name: stringify!($type).into(),
346					}),
347				}
348			}
349		}
350	};
351
352	($type:ty, unimplemented = $unimplemented:expr) => {
353		$crate::nom_from_str!($type);
354
355		impl $crate::shared::SuccessfullyParsed for $type {
356			fn successfully_parsed(&self) -> bool {
357				matches!(self, $unimplemented)
358			}
359		}
360	};
361}
362pub(crate) use impl_from_str_nom;