apple_clis/xcrun/simctl/list/
output.rs

1use crate::prelude::*;
2use crate::shared::identifiers::IPadVariant;
3use crate::shared::identifiers::{DeviceName, IPhoneVariant, RuntimeIdentifier};
4
5#[derive(Debug, Serialize)]
6#[non_exhaustive]
7#[must_use = include_doc!(must_use_cmd_output)]
8pub enum ListOutput {
9	/// Contains the actual output of the command, parsed
10	SuccessJson(ListJson),
11
12	#[doc = include_doc!(cmd_success)]
13	SuccessUnImplemented { stdout: String },
14
15	#[doc = include_doc!(cmd_error)]
16	ErrorUnImplemented { stderr: String },
17}
18
19impl ListOutput {
20	/// Returns [Error::OutputErrored] if it didn't succeed.
21	/// Used to make error handling of non-successful commands explicit
22	pub fn success(&self) -> Result<&ListJson> {
23		match self {
24			ListOutput::SuccessJson(output) => Ok(output),
25			_ => Err(Error::output_errored(self)),
26		}
27	}
28
29	pub fn unwrap_success(&self) -> &ListJson {
30		match self {
31			ListOutput::SuccessJson(output) => output,
32			_ => {
33				error!(?self, "Tried to unwrap a non-successful ListOutput");
34				panic!("Tried to unwrap a non-successful ListOutput")
35			}
36		}
37	}
38
39	pub fn get_success(&self) -> Option<&ListJson> {
40		match self {
41			ListOutput::SuccessJson(output) => Some(output),
42			_ => None,
43		}
44	}
45
46	/// Only used in CLI
47	/// prefer [Self::get_success]
48	#[cfg(feature = "cli")]
49	pub fn get_success_reported(&self) -> std::result::Result<&ListJson, color_eyre::Report> {
50		match self {
51			Self::SuccessJson(output) => Ok(output),
52			Self::ErrorUnImplemented { stderr } => Err(eyre!(
53				"xcrun simctl list output didn't exist successfully: {:?}",
54				stderr
55			)),
56			Self::SuccessUnImplemented { stdout } => Err(eyre!(
57				"xcrun simctl list output didn't produce valid output: {:?}",
58				stdout
59			)),
60		}
61	}
62}
63
64impl CommandNomParsable for ListOutput {
65	fn success_unimplemented(stdout: String) -> Self {
66		Self::SuccessUnImplemented { stdout }
67	}
68
69	fn error_unimplemented(stderr: String) -> Self {
70		Self::ErrorUnImplemented { stderr }
71	}
72
73	fn success_from_str(input: &str) -> Self {
74		match serde_json::from_str(input) {
75			Ok(output) => Self::SuccessJson(output),
76			Err(e) => {
77				error!(?e, "Failed to parse JSON");
78				Self::success_unimplemented(input.to_owned())
79			}
80		}
81	}
82}
83
84impl PublicCommandOutput for ListOutput {
85	type PrimarySuccess = ListJson;
86
87	fn success(&self) -> Result<&Self::PrimarySuccess> {
88		match self {
89			ListOutput::SuccessJson(output) => Ok(output),
90			_ => Err(Error::output_errored(self)),
91		}
92	}
93}
94
95impl ListJson {
96	/// Returns an iterator over the returned devices
97	pub fn devices(&self) -> impl Iterator<Item = &ListDevice> + '_ {
98		self.devices.values().flatten()
99	}
100}
101
102#[derive(Deserialize, Debug, Serialize)]
103pub struct ListJson {
104	devices: HashMap<RuntimeIdentifier, Vec<ListDevice>>,
105}
106
107/// Allows for easier extraction of semantic information from
108/// an iterator over [ListDevice]s.
109#[extension_traits::extension(pub trait ListDevicesExt)]
110impl<'src, T> T
111where
112	T: Iterator<Item = &'src ListDevice>,
113{
114	/// Consumes self,
115	fn names(self) -> impl Iterator<Item = &'src DeviceName> {
116		self.map(|device| &device.name)
117	}
118
119	fn a_device(mut self) -> Option<&'src ListDevice> {
120		self.next()
121	}
122}
123
124#[extension_traits::extension(pub trait ListDeviceNamesExt)]
125impl<'src, T> T
126where
127	T: Iterator<Item = &'src DeviceName>,
128{
129	/// Tries to find the latest iPad in the list of devices
130	/// Not necessarily booted already
131	fn an_ipad(self) -> Option<&'src IPadVariant> {
132		self.ipads().max()
133	}
134
135	/// Tries to find the latest iPhone in the list of devices
136	/// Not necessarily booted already
137	fn an_iphone(self) -> Option<&'src IPhoneVariant> {
138		self.iphones().max()
139	}
140
141	fn iphones(self) -> impl Iterator<Item = &'src IPhoneVariant> {
142		self.filter_map(|names| match names {
143			DeviceName::IPhone(ref variant) => Some(variant),
144			_ => None,
145		})
146	}
147
148	fn ipads(self) -> impl Iterator<Item = &'src IPadVariant> {
149		self.filter_map(|names| match names {
150			DeviceName::IPad(ref variant) => Some(variant),
151			_ => None,
152		})
153	}
154}
155
156#[derive(Deserialize, Serialize, Debug, Clone)]
157#[serde(rename_all(deserialize = "camelCase"))]
158pub struct ListDevice {
159	pub availability_error: Option<String>,
160	pub data_path: Utf8PathBuf,
161	pub log_path: Utf8PathBuf,
162	pub udid: String,
163	pub is_available: bool,
164	pub device_type_identifier: String,
165	pub state: State,
166
167	pub name: DeviceName,
168}
169
170#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
171pub enum State {
172	Shutdown,
173	Booted,
174}
175
176impl State {
177	pub fn ready(&self) -> bool {
178		matches!(self, State::Booted)
179	}
180}
181
182impl ListDevice {
183	pub fn ready(&self) -> bool {
184		self.state.ready() && self.is_available
185	}
186}
187
188#[cfg(test)]
189mod tests {
190	use tracing::debug;
191
192	use super::*;
193
194	#[test]
195	fn test_simctl_list() {
196		let example = include_str!(concat!(
197			env!("CARGO_MANIFEST_DIR"),
198			"/tests/simctl-list-full.json"
199		));
200		let output = serde_json::from_str::<ListJson>(example);
201		match output {
202			Ok(output) => {
203				debug!("Output: {:?}", output);
204			}
205			Err(e) => {
206				panic!("Error parsing: {:?}", e)
207			}
208		}
209	}
210}