creator-simctl 0.1.1

Rust wrapper around Xcode's `simctl`.
Documentation
//! Supporting types for the `simctl list` subcommand.

use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Stdio;

use super::{Device, Result, Simctl};

/// Indicates the state of a device.
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
pub enum DeviceState {
    /// Indicates that the device is booted.
    Booted,

    /// Indicates that the device is shutdown.
    Shutdown,

    /// Indicates that the device is in an unknown state.
    #[serde(other)]
    Unknown,
}

/// Indicates the state of a pair of devices.
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
pub enum DevicePairState {
    /// Indicates that this pair is unavailable because one of its components is
    /// unavailable.
    #[serde(rename = "(unavailable)")]
    Unavailable,

    /// Indicates that this pair is active but not connected.
    #[serde(rename = "(active, disconnected)")]
    ActiveDisconnected,

    /// Indicates that this pair is in a state that is not (yet) recognized by
    /// this library.
    #[serde(other)]
    Unknown,
}

/// Information about a device type.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct DeviceType {
    /// Contains the minimum runtime version that this device type supports.
    /// This is relevant for devices that are newer than the oldest runtime that
    /// has been registered with `simctl`.
    #[serde(rename = "minRuntimeVersion")]
    pub min_runtime_version: usize,

    /// Contains the maximum runtime version that this device type supports.
    /// This is relevant for devices that have been deprecated before the newest
    /// runtime that has been registered with `simctl`.
    #[serde(rename = "maxRuntimeVersion")]
    pub max_runtime_version: usize,

    /// Contains a path to the bundle of this device type. This is usually not
    /// relevant to end-users.
    #[serde(rename = "bundlePath")]
    pub bundle_path: PathBuf,

    /// Contains a human-readable name for this device type.
    pub name: String,

    /// Contains a unique identifier for this device type.
    pub identifier: String,

    /// Contains a machine-readable name for the product family of this device
    /// type.
    #[serde(rename = "productFamily")]
    pub product_family: String,
}

/// Information about a runtime.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct Runtime {
    /// Contains a path to the bundle of this runtime. This is usually not
    /// relevant to end-users.
    #[serde(rename = "bundlePath")]
    pub bundle_path: PathBuf,

    /// Contains the build version of this runtime. This is usually not relevant
    /// to end-users.
    #[serde(rename = "buildversion")]
    pub build_version: String,

    /// Contains the root of this runtime. This is usually not relevant to
    /// end-users.
    #[serde(rename = "runtimeRoot")]
    pub runtime_root: PathBuf,

    /// Contains a unique identifier for this runtime.
    pub identifier: String,

    /// Contains a human-readable version string for this runtime.
    pub version: String,

    /// Indicates if this runtime is available. This is false when the runtime
    /// was first created (automatically) with an older version of Xcode that
    /// shipped with an older version of the iOS simulator and after upgrading
    /// to a newer version. In that case, Xcode no longer has the runtime bundle
    /// for this older runtime, but it will still be registered by `simctl`.
    /// However, it's not possible to boot a device with an unavailable runtime.
    #[serde(rename = "isAvailable")]
    pub is_available: bool,

    /// Contains a human-readable name for this runtime.
    pub name: String,
}

/// Information about a device.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct DeviceInfo {
    /// Note: this field is not directly present in JSON. Instead, the JSON
    /// representation is a hashmap of runtime IDs (keys) and devices (values)
    /// that we later connect during deserialization.
    #[serde(skip_deserializing)]
    pub runtime_identifier: String,

    /// If this device is not available (see [`DeviceInfo::is_available`]), this
    /// will contain a (slightly) more detailed explanation for its
    /// unavailability.
    #[serde(default, rename = "availabilityError")]
    pub availability_error: Option<String>,

    /// Contains the path where application data is stored.
    #[serde(rename = "dataPath")]
    pub data_path: PathBuf,

    /// Contains the path where logs are written to.
    #[serde(rename = "logPath")]
    pub log_path: PathBuf,

    /// Contains a unique identifier for this device.
    pub udid: String,

    /// Indicates if this device is available. Also see
    /// [`Runtime::is_available`].
    #[serde(rename = "isAvailable")]
    pub is_available: bool,

    /// This corresponds to [`DeviceType::identifier`]. This is missing for
    /// devices whose device type has since been removed from Xcode.
    #[serde(default, rename = "deviceTypeIdentifier")]
    pub device_type_identifier: String,

    /// Contains the state of this device.
    pub state: DeviceState,

    /// Contains the name of this device.
    pub name: String,
}

/// Short summary of a device that is used as part of a device pair.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct DeviceSummary {
    /// Contains the name of this device.
    pub name: String,

    /// Contains a unique identifier for this device.
    pub udid: String,

    /// Contains the state of this device.
    pub state: DeviceState,
}

/// Information about a device pair.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct DevicePair {
    /// Note: this field is not directly present in JSON. Instead, the JSON
    /// representation is a hashmap of runtime IDs (keys) and devices (values)
    /// that we later connect during deserialization.
    #[serde(skip_deserializing)]
    pub udid: String,

    /// Contains a summary of the watch device.
    pub watch: DeviceSummary,

    /// Contains a summary of the phone device.
    pub phone: DeviceSummary,

    /// Contains the state of this device pair.
    pub state: DevicePairState,
}

/// Wrapper around the `simctl list` subcommand's output.
#[derive(Debug)]
pub struct List {
    simctl: Simctl,
    device_types: Vec<DeviceType>,
    runtimes: Vec<Runtime>,
    devices: Vec<Device>,
    pairs: Vec<DevicePair>,
}

impl List {
    /// Refreshes the `simctl list` subcommand's output.
    pub fn refresh(&mut self) -> Result<()> {
        let mut command = self.simctl.command("list");
        command.arg("-j");
        command.stdout(Stdio::piped());
        let output = command.output()?;
        let output: ListOutput = serde_json::from_slice(&output.stdout)?;
        self.device_types = output.device_types;
        self.runtimes = output.runtimes;
        self.devices = output
            .devices
            .into_iter()
            .map(|(runtime, devices)| {
                let simctl = self.simctl.clone();

                devices.into_iter().map(move |device| {
                    Device::new(
                        simctl.clone(),
                        DeviceInfo {
                            runtime_identifier: runtime.clone(),
                            ..device
                        },
                    )
                })
            })
            .flatten()
            .collect();
        self.pairs = output
            .pairs
            .into_iter()
            .map(move |(udid, pair)| DevicePair { udid, ..pair })
            .collect();
        Ok(())
    }

    /// Returns all device types that have been registered with `simctl`.
    pub fn device_types(&self) -> &[DeviceType] {
        &self.device_types
    }

    /// Returns all runtimes that have been registered with `simctl`.
    pub fn runtimes(&self) -> &[Runtime] {
        &self.runtimes
    }

    /// Returns all devices that have been registered with `simctl`.
    pub fn devices(&self) -> &[Device] {
        &self.devices
    }

    /// Returns all device pairs that have been registered with `simctl`.
    pub fn pairs(&self) -> &[DevicePair] {
        &self.pairs
    }
}

#[derive(Debug, Default, Deserialize)]
struct ListOutput {
    #[serde(rename = "devicetypes")]
    device_types: Vec<DeviceType>,
    runtimes: Vec<Runtime>,
    devices: HashMap<String, Vec<DeviceInfo>>,
    pairs: HashMap<String, DevicePair>,
}

impl Simctl {
    /// Returns a list of all device types, runtimes, devices and device pairs
    /// that have been registered with `simctl`.
    pub fn list(&self) -> Result<List> {
        let mut list = List {
            simctl: self.clone(),
            device_types: vec![],
            devices: vec![],
            pairs: vec![],
            runtimes: vec![],
        };
        list.refresh()?;
        Ok(list)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_list() -> Result<()> {
        let simctl = Simctl::new();
        let _ = simctl.list()?;
        Ok(())
    }
}