use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::capability::CapabilityDescriptor;
use crate::describe::{Platform, Stability};
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Manifest {
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 Manifest {
pub fn load(path: &Path) -> Result<Self> {
let raw = std::fs::read_to_string(path)?;
let manifest: Self = toml::from_str(&raw)?;
manifest.validate()?;
Ok(manifest)
}
#[must_use]
pub fn sibling_of(binary: &Path) -> PathBuf {
let mut path = binary.to_path_buf();
path.set_extension("toml");
path
}
fn validate(&self) -> Result<()> {
if self.description.is_empty() {
return Err(Error::contract("manifest description is empty"));
}
if self.description.len() > 80 {
return Err(Error::contract(format!(
"manifest description is {} chars (max 80)",
self.description.len()
)));
}
if self.description.contains('\n') || self.description.contains('\r') {
return Err(Error::contract(
"manifest description must be a single line",
));
}
if self.platforms.is_empty() {
return Err(Error::contract(
"manifest platforms must list at least one OS",
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sibling_of_strips_extension() {
let unix = Path::new("/usr/local/bin/ready-set-foo");
assert_eq!(
Manifest::sibling_of(unix),
PathBuf::from("/usr/local/bin/ready-set-foo.toml")
);
let win = Path::new("C:/tools/ready-set-foo.exe");
assert_eq!(
Manifest::sibling_of(win),
PathBuf::from("C:/tools/ready-set-foo.toml")
);
}
#[test]
fn parses_a_full_manifest() {
let toml_src = r#"
description = "Reference plugin"
version = "0.1.0"
stability = "stable"
min_dispatcher_version = "0.1.0"
platforms = ["linux", "macos", "windows"]
requires_cargo_workspace = false
capabilities = []
"#;
let m: Manifest = toml::from_str(toml_src).unwrap();
m.validate().unwrap();
assert_eq!(m.description, "Reference plugin");
assert_eq!(m.platforms.len(), 3);
assert!(m.capabilities.is_empty());
}
#[test]
fn parses_manifest_with_capability() {
let toml_src = r#"
description = "Reference plugin"
version = "0.1.0"
stability = "stable"
min_dispatcher_version = "0.1.0"
platforms = ["linux", "macos", "windows"]
requires_cargo_workspace = false
[[capabilities]]
id = "linting"
title = "Linting"
provider = "rust"
verbs = ["ready", "set", "go"]
default_relevance = "required"
"#;
let m: Manifest = toml::from_str(toml_src).unwrap();
m.validate().unwrap();
assert_eq!(m.capabilities.len(), 1);
assert_eq!(m.capabilities[0].id.as_str(), "linting");
}
#[test]
fn rejects_manifest_without_capabilities() {
let toml_src = r#"
description = "Reference plugin"
version = "0.1.0"
stability = "stable"
min_dispatcher_version = "0.1.0"
platforms = ["linux", "macos", "windows"]
requires_cargo_workspace = false
"#;
assert!(toml::from_str::<Manifest>(toml_src).is_err());
}
#[test]
fn rejects_overlong_description() {
let mut m = Manifest {
description: "x".repeat(81),
version: semver::Version::new(0, 1, 0),
stability: Stability::Stable,
min_dispatcher_version: semver::Version::new(0, 1, 0),
platforms: vec![Platform::Linux],
requires_cargo_workspace: false,
capabilities: Vec::new(),
};
assert!(m.validate().is_err());
m.description = "ok".into();
m.validate().unwrap();
}
}