isoloom_spec/syntax/
os.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct OsRef(Os);
18
19impl OsRef {
20 #[must_use]
22 pub fn os(self) -> Os {
23 self.0
24 }
25
26 #[must_use]
28 pub fn name(self) -> &'static str {
29 self.0.name
30 }
31
32 #[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 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 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}