Skip to main content

isoloom_spec/syntax/
os.rs

1//! A machine's `os`: `<family>@<version>`, or the family alone for its pinned default. Parsed once
2//! into the OS it names, so a parsed spec only holds known ones.
3
4use std::borrow::Cow;
5use std::fmt;
6
7use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use std::str::FromStr;
11
12use crate::Error;
13use crate::os::{Os, resolve};
14
15/// A known OS, as a machine's `os` names it.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct OsRef(Os);
18
19impl OsRef {
20    /// The OS, resolved.
21    #[must_use]
22    pub fn os(self) -> Os {
23        self.0
24    }
25
26    /// Its image-table name (`debian-12`), what the targets read.
27    #[must_use]
28    pub fn name(self) -> &'static str {
29        self.0.name
30    }
31
32    /// Whether it is a Windows OS (no container form in 1.0).
33    #[must_use]
34    pub fn is_windows(self) -> bool {
35        crate::images::is_windows(self.0.name)
36    }
37}
38
39impl FromStr for OsRef {
40    type Err = Error;
41
42    /// The OS `text` names: `<family>@<version>`, or the family alone for its pinned default.
43    ///
44    /// # Errors
45    ///
46    /// [`Error::UnknownOs`] listing the known families, or [`Error::UnknownOsVersion`] listing
47    /// the family's versions.
48    fn from_str(text: &str) -> Result<OsRef, Error> {
49        resolve(text).map(OsRef)
50    }
51}
52
53impl fmt::Display for OsRef {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        f.write_str(&self.0.id())
56    }
57}
58
59impl Serialize for OsRef {
60    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
61        s.serialize_str(&self.0.id())
62    }
63}
64
65impl<'de> Deserialize<'de> for OsRef {
66    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<OsRef, D::Error> {
67        String::deserialize(d)?.parse().map_err(serde::de::Error::custom)
68    }
69}
70
71impl JsonSchema for OsRef {
72    fn schema_name() -> Cow<'static, str> {
73        "Os".into()
74    }
75
76    fn json_schema(_: &mut SchemaGenerator) -> Schema {
77        json_schema!({
78            "description": "An OS as `<family>@<version>` (`debian@12`, `windows-server@2022`), or the family alone for its pinned default version (`debian` is `debian@12`).",
79            "type": "string",
80            "enum": crate::os::spellings(),
81        })
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn a_family_alone_resolves_to_its_pinned_default() {
91        let os: OsRef = "debian".parse().unwrap();
92        assert_eq!((os.to_string(), os.name()), ("debian@12".to_owned(), "debian-12"));
93        assert!("windows-server@2019".parse::<OsRef>().unwrap().is_windows());
94    }
95
96    #[test]
97    fn an_unknown_version_lists_the_versions_of_its_family() {
98        assert!("debian@10".parse::<OsRef>().unwrap_err().to_string().contains("11, 12, 13"));
99    }
100
101    proptest::proptest! {
102        #[test]
103        fn every_spelling_reads_back_as_itself(i in 0..crate::os::spellings().len()) {
104            let spelling = &crate::os::spellings()[i];
105            let os: OsRef = spelling.parse().unwrap();
106            // A family alone reads as its pinned default, written in full.
107            let in_full = format!("{spelling}@");
108            proptest::prop_assert!(os.to_string() == *spelling || os.to_string().starts_with(&in_full));
109            proptest::prop_assert_eq!(os.to_string().parse::<OsRef>().unwrap(), os);
110        }
111
112        #[test]
113        fn any_text_resolves_or_says_why(text in "\\PC{0,24}") {
114            if let Err(e) = text.parse::<OsRef>() {
115                proptest::prop_assert!(!e.to_string().is_empty());
116            }
117        }
118    }
119}