ready_set_sdk/
describe.rs1use std::ffi::OsString;
8use std::io::Write;
9
10use serde::{Deserialize, Serialize};
11
12use crate::capability::CapabilityDescriptor;
13use crate::error::{Error, Result};
14use crate::exit_code::ExitCode;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum Stability {
20 Stable,
22 Experimental,
24 Deprecated,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "lowercase")]
31pub enum Platform {
32 Linux,
34 Macos,
36 Windows,
38}
39
40impl Platform {
41 #[must_use]
43 pub const fn current() -> Option<Self> {
44 if cfg!(target_os = "linux") {
45 Some(Self::Linux)
46 } else if cfg!(target_os = "macos") {
47 Some(Self::Macos)
48 } else if cfg!(target_os = "windows") {
49 Some(Self::Windows)
50 } else {
51 None
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct Describe {
61 pub description: String,
63 pub version: semver::Version,
65 pub stability: Stability,
67 pub min_dispatcher_version: semver::Version,
69 pub platforms: Vec<Platform>,
71 pub requires_cargo_workspace: bool,
73 pub capabilities: Vec<CapabilityDescriptor>,
75}
76
77impl Describe {
78 pub fn emit_stdout(&self) -> Result<()> {
86 let line = serde_json::to_string(self)?;
87 let stdout = std::io::stdout();
88 let mut handle = stdout.lock();
89 writeln!(handle, "{line}").map_err(Error::Io)?;
90 handle.flush().map_err(Error::Io)?;
91 Ok(())
92 }
93
94 pub fn handle_arg0_describe(
107 &self,
108 args: impl IntoIterator<Item = OsString>,
109 ) -> Option<ExitCode> {
110 let mut iter = args.into_iter();
111 drop(iter.next());
113 let first = iter.next()?;
114 if first != "__describe" {
115 return None;
116 }
117 if iter.next().is_some() {
118 eprintln!("__describe accepts no extra arguments");
120 return Some(ExitCode::ContractViolation);
121 }
122 match self.emit_stdout() {
123 Ok(()) => Some(ExitCode::Ok),
124 Err(_) => Some(ExitCode::ContractViolation),
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn fixture() -> Describe {
134 Describe {
135 description: "test".into(),
136 version: semver::Version::new(0, 1, 0),
137 stability: Stability::Stable,
138 min_dispatcher_version: semver::Version::new(0, 1, 0),
139 platforms: vec![Platform::Linux, Platform::Macos, Platform::Windows],
140 requires_cargo_workspace: false,
141 capabilities: Vec::new(),
142 }
143 }
144
145 #[test]
146 fn skips_when_no_describe_arg() {
147 let d = fixture();
148 let args: Vec<OsString> = ["prog", "run", "--flag"]
149 .iter()
150 .map(OsString::from)
151 .collect();
152 let result = d.handle_arg0_describe(args);
153 assert!(result.is_none());
154 }
155
156 #[test]
157 fn skips_when_no_args_at_all() {
158 let d = fixture();
159 let result = d.handle_arg0_describe([OsString::from("prog")]);
160 assert!(result.is_none());
161 }
162
163 #[test]
164 fn rejects_extra_args() {
165 let d = fixture();
166 let args: Vec<OsString> = ["prog", "__describe", "extra"]
167 .iter()
168 .map(OsString::from)
169 .collect();
170 let result = d.handle_arg0_describe(args);
171 assert_eq!(result, Some(ExitCode::ContractViolation));
172 }
173
174 #[test]
175 fn json_round_trip() {
176 let d = fixture();
177 let json = serde_json::to_string(&d).unwrap();
178 let back: Describe = serde_json::from_str(&json).unwrap();
179 assert_eq!(d, back);
180 assert!(json.contains("\"capabilities\":[]"));
181 }
182
183 #[test]
184 fn rejects_json_without_capabilities() {
185 let json = r#"{"description":"test","version":"0.1.0","stability":"stable","min_dispatcher_version":"0.1.0","platforms":["linux"],"requires_cargo_workspace":false}"#;
186 assert!(serde_json::from_str::<Describe>(json).is_err());
187 }
188}