use std::ffi::OsString;
use std::io::Write;
use serde::{Deserialize, Serialize};
use crate::capability::CapabilityDescriptor;
use crate::error::{Error, Result};
use crate::exit_code::ExitCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Stability {
Stable,
Experimental,
Deprecated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Platform {
Linux,
Macos,
Windows,
}
impl Platform {
#[must_use]
pub const fn current() -> Option<Self> {
if cfg!(target_os = "linux") {
Some(Self::Linux)
} else if cfg!(target_os = "macos") {
Some(Self::Macos)
} else if cfg!(target_os = "windows") {
Some(Self::Windows)
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Describe {
pub description: String,
pub version: semver::Version,
pub stability: Stability,
pub min_dispatcher_version: semver::Version,
pub platforms: Vec<Platform>,
pub requires_cargo_workspace: bool,
pub capabilities: Vec<CapabilityDescriptor>,
}
impl Describe {
pub fn emit_stdout(&self) -> Result<()> {
let line = serde_json::to_string(self)?;
let stdout = std::io::stdout();
let mut handle = stdout.lock();
writeln!(handle, "{line}").map_err(Error::Io)?;
handle.flush().map_err(Error::Io)?;
Ok(())
}
pub fn handle_arg0_describe(
&self,
args: impl IntoIterator<Item = OsString>,
) -> Option<ExitCode> {
let mut iter = args.into_iter();
drop(iter.next());
let first = iter.next()?;
if first != "__describe" {
return None;
}
if iter.next().is_some() {
eprintln!("__describe accepts no extra arguments");
return Some(ExitCode::ContractViolation);
}
match self.emit_stdout() {
Ok(()) => Some(ExitCode::Ok),
Err(_) => Some(ExitCode::ContractViolation),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture() -> Describe {
Describe {
description: "test".into(),
version: semver::Version::new(0, 1, 0),
stability: Stability::Stable,
min_dispatcher_version: semver::Version::new(0, 1, 0),
platforms: vec![Platform::Linux, Platform::Macos, Platform::Windows],
requires_cargo_workspace: false,
capabilities: Vec::new(),
}
}
#[test]
fn skips_when_no_describe_arg() {
let d = fixture();
let args: Vec<OsString> = ["prog", "run", "--flag"]
.iter()
.map(OsString::from)
.collect();
let result = d.handle_arg0_describe(args);
assert!(result.is_none());
}
#[test]
fn skips_when_no_args_at_all() {
let d = fixture();
let result = d.handle_arg0_describe([OsString::from("prog")]);
assert!(result.is_none());
}
#[test]
fn rejects_extra_args() {
let d = fixture();
let args: Vec<OsString> = ["prog", "__describe", "extra"]
.iter()
.map(OsString::from)
.collect();
let result = d.handle_arg0_describe(args);
assert_eq!(result, Some(ExitCode::ContractViolation));
}
#[test]
fn json_round_trip() {
let d = fixture();
let json = serde_json::to_string(&d).unwrap();
let back: Describe = serde_json::from_str(&json).unwrap();
assert_eq!(d, back);
assert!(json.contains("\"capabilities\":[]"));
}
#[test]
fn rejects_json_without_capabilities() {
let json = r#"{"description":"test","version":"0.1.0","stability":"stable","min_dispatcher_version":"0.1.0","platforms":["linux"],"requires_cargo_workspace":false}"#;
assert!(serde_json::from_str::<Describe>(json).is_err());
}
}