use crate::{AgentSurfaceSpec, LicenseType, RepoInfo};
pub const WORKSPACE_REPO: RepoInfo = RepoInfo::new("tftio-stuff", "tools");
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum ContractError {
#[error("tool spec does not opt into the authoritative cli-common base contract")]
NotAuthoritative,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolContract {
CliCommonBase,
Legacy {
supports_json: bool,
supports_doctor: bool,
},
}
impl ToolContract {
#[must_use]
pub const fn from_support_flags(supports_json: bool, supports_doctor: bool) -> Self {
if supports_json && supports_doctor {
Self::CliCommonBase
} else {
Self::Legacy {
supports_json,
supports_doctor,
}
}
}
#[must_use]
pub const fn supports_json(self) -> bool {
match self {
Self::CliCommonBase => true,
Self::Legacy { supports_json, .. } => supports_json,
}
}
#[must_use]
pub const fn supports_doctor(self) -> bool {
match self {
Self::CliCommonBase => true,
Self::Legacy {
supports_doctor, ..
} => supports_doctor,
}
}
#[must_use]
pub const fn is_authoritative(self) -> bool {
matches!(self, Self::CliCommonBase)
}
pub const fn validate(self) -> Result<(), ContractError> {
match self {
Self::CliCommonBase => Ok(()),
Self::Legacy { .. } => Err(ContractError::NotAuthoritative),
}
}
}
#[derive(Debug, Clone)]
pub struct ToolSpec {
pub(crate) bin_name: &'static str,
#[allow(
dead_code,
reason = "set by the public ToolSpec constructor and read via the display_name() accessor; dead_code fires only in binaries that never call that accessor"
)]
pub(crate) display_name: &'static str,
pub(crate) version: &'static str,
pub(crate) license: LicenseType,
#[allow(
dead_code,
reason = "set by the public ToolSpec constructor and read via its accessor; dead_code fires only in binaries that never read it"
)]
pub(crate) repo: RepoInfo,
pub(crate) contract: ToolContract,
pub(crate) agent_surface: Option<&'static AgentSurfaceSpec>,
}
impl ToolSpec {
#[must_use]
pub const fn new(
bin_name: &'static str,
display_name: &'static str,
version: &'static str,
license: LicenseType,
repo: RepoInfo,
supports_json: bool,
supports_doctor: bool,
) -> Self {
Self {
bin_name,
display_name,
version,
license,
repo,
contract: ToolContract::from_support_flags(supports_json, supports_doctor),
agent_surface: None,
}
}
#[must_use]
pub const fn workspace(
bin_name: &'static str,
display_name: &'static str,
version: &'static str,
license: LicenseType,
supports_json: bool,
supports_doctor: bool,
) -> Self {
Self::new(
bin_name,
display_name,
version,
license,
WORKSPACE_REPO,
supports_json,
supports_doctor,
)
}
#[must_use]
pub const fn with_agent_surface(self, agent_surface: &'static AgentSurfaceSpec) -> Self {
Self {
agent_surface: Some(agent_surface),
..self
}
}
#[must_use]
pub const fn bin_name(&self) -> &'static str {
self.bin_name
}
#[must_use]
pub const fn display_name(&self) -> &'static str {
self.display_name
}
#[must_use]
pub const fn version(&self) -> &'static str {
self.version
}
#[must_use]
pub const fn agent_surface(&self) -> Option<&'static AgentSurfaceSpec> {
self.agent_surface
}
#[must_use]
pub const fn supports_json(&self) -> bool {
self.contract.supports_json()
}
#[must_use]
pub const fn supports_doctor(&self) -> bool {
self.contract.supports_doctor()
}
#[must_use]
pub const fn has_authoritative_contract(&self) -> bool {
self.contract.is_authoritative()
}
pub const fn validate_authoritative_contract(&self) -> Result<(), ContractError> {
self.contract.validate()
}
}
#[must_use]
pub const fn workspace_tool(
bin_name: &'static str,
display_name: &'static str,
version: &'static str,
license: LicenseType,
supports_json: bool,
supports_doctor: bool,
) -> ToolSpec {
ToolSpec::workspace(
bin_name,
display_name,
version,
license,
supports_json,
supports_doctor,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_spec_new_preserves_fields() {
let spec = ToolSpec::new(
"tool",
"Tool",
"1.2.3",
LicenseType::MIT,
RepoInfo::new("owner", "repo"),
true,
false,
);
assert_eq!(spec.bin_name, "tool");
assert_eq!(spec.display_name, "Tool");
assert_eq!(spec.version, "1.2.3");
assert_eq!(spec.license, LicenseType::MIT);
assert_eq!(spec.repo.owner, "owner");
assert_eq!(spec.repo.name, "repo");
assert_eq!(
spec.contract,
ToolContract::Legacy {
supports_json: true,
supports_doctor: false,
}
);
assert!(spec.supports_json());
assert!(!spec.supports_doctor());
assert!(!spec.has_authoritative_contract());
assert!(spec.validate_authoritative_contract().is_err());
assert!(spec.agent_surface().is_none());
}
#[test]
fn workspace_tool_uses_workspace_repo_defaults() {
let spec = workspace_tool("tool", "Tool", "1.2.3", LicenseType::MIT, true, false);
assert_eq!(spec.repo.owner, WORKSPACE_REPO.owner);
assert_eq!(spec.repo.name, WORKSPACE_REPO.name);
assert_eq!(spec.bin_name, "tool");
assert_eq!(spec.display_name, "Tool");
}
#[test]
fn tool_spec_with_all_mandatory_capabilities_is_authoritative() {
let spec = workspace_tool("tool", "Tool", "1.2.3", LicenseType::MIT, true, true);
assert_eq!(spec.contract, ToolContract::CliCommonBase);
assert!(spec.has_authoritative_contract());
assert!(spec.validate_authoritative_contract().is_ok());
}
}