apple_clis/shared/
traits.rs1use crate::prelude::*;
2
3pub trait ExecInstance: Sized {
5 const BINARY_NAME: &'static str;
7
8 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 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 fn new() -> Result<Self> {
49 let path = which::which(Self::BINARY_NAME)?;
50 let path = Utf8PathBuf::try_from(path)?;
51 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 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 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 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 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 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#[allow(private_bounds)] pub trait PublicCommandOutput: std::fmt::Debug + Serialize + CommandNomParsable {
235 type PrimarySuccess: std::fmt::Debug;
238
239 fn success(&self) -> Result<&Self::PrimarySuccess>;
241
242 fn successful(&self) -> bool {
244 self.success().is_ok()
245 }
246
247 fn failed(&self) -> bool {
248 !self.successful()
249 }
250}
251
252pub(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) => {
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
319macro_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 Ok(output)
335 }
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;