Skip to main content

holodeck_simctl_core/models/
runtime.rs

1use std::cmp::Ordering;
2
3use super::platform::Platform;
4use super::semantic_version::SemanticVersion;
5use super::simctl_identifiers::RUNTIME_PREFIX;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct Runtime {
9    pub platform: Platform,
10    pub version: SemanticVersion,
11    pub identifier: String,
12}
13
14impl Runtime {
15    pub fn new(platform: Platform, version: SemanticVersion, identifier: impl Into<String>) -> Self {
16        Self { platform, version, identifier: identifier.into() }
17    }
18
19    pub fn from_identifier(identifier: &str) -> Option<Self> {
20        let suffix = identifier.strip_prefix(RUNTIME_PREFIX)?;
21        let dash = suffix.find('-')?;
22        let platform_name = &suffix[..dash];
23        let version_part = suffix[dash + 1..].replace('-', ".");
24        let platform = Platform::from_simctl_name(platform_name)?;
25        let version = SemanticVersion::parse(&version_part)?;
26        Some(Self { platform, version, identifier: identifier.to_string() })
27    }
28
29    /// Apple sometimes ships point releases under the same identifier
30    /// (e.g. iOS-26-4 hosts both 26.4 and 26.4.1). Prefer the explicit
31    /// version string from `simctl list --json runtimes` when available.
32    pub fn from_identifier_with_version(identifier: &str, version_string: Option<&str>) -> Option<Self> {
33        let parsed = Self::from_identifier(identifier)?;
34        let resolved_version = version_string.and_then(SemanticVersion::parse).unwrap_or(parsed.version);
35        Some(Self { platform: parsed.platform, version: resolved_version, identifier: identifier.to_string() })
36    }
37
38    pub fn display_name(&self) -> String {
39        format!("{} {}", self.platform.raw_value(), self.version)
40    }
41}
42
43impl PartialOrd for Runtime {
44    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
45        Some(self.cmp(other))
46    }
47}
48
49impl Ord for Runtime {
50    fn cmp(&self, other: &Self) -> Ordering {
51        self.platform.raw_value().cmp(other.platform.raw_value()).then(self.version.cmp(&other.version))
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn parses_identifier() {
61        let runtime = Runtime::from_identifier("com.apple.CoreSimulator.SimRuntime.iOS-18-0").unwrap();
62        assert_eq!(runtime.platform, Platform::IOS);
63        assert_eq!(runtime.version, SemanticVersion::new(18, 0, 0));
64    }
65
66    #[test]
67    fn version_string_overrides_identifier_derived_version() {
68        let runtime =
69            Runtime::from_identifier_with_version("com.apple.CoreSimulator.SimRuntime.iOS-26-4", Some("26.4.1")).unwrap();
70        assert_eq!(runtime.version, SemanticVersion::new(26, 4, 1));
71    }
72
73    #[test]
74    fn falls_back_to_identifier_version_when_string_absent() {
75        let runtime = Runtime::from_identifier_with_version("com.apple.CoreSimulator.SimRuntime.iOS-26-4", None).unwrap();
76        assert_eq!(runtime.version, SemanticVersion::new(26, 4, 0));
77    }
78
79    #[test]
80    fn display_name_combines_platform_and_version() {
81        let runtime = Runtime::from_identifier("com.apple.CoreSimulator.SimRuntime.iOS-18-0").unwrap();
82        assert_eq!(runtime.display_name(), "iOS 18.0");
83    }
84}