1use crate::{AgentSurfaceSpec, LicenseType, RepoInfo};
4
5pub const WORKSPACE_REPO: RepoInfo = RepoInfo::new("tftio-stuff", "tools");
7
8#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
10pub enum ContractError {
11 #[error("tool spec does not opt into the authoritative cli-common base contract")]
13 NotAuthoritative,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ToolContract {
19 CliCommonBase,
21 Legacy {
23 supports_json: bool,
25 supports_doctor: bool,
27 },
28}
29
30impl ToolContract {
31 #[must_use]
33 pub const fn from_support_flags(supports_json: bool, supports_doctor: bool) -> Self {
34 if supports_json && supports_doctor {
35 Self::CliCommonBase
36 } else {
37 Self::Legacy {
38 supports_json,
39 supports_doctor,
40 }
41 }
42 }
43
44 #[must_use]
46 pub const fn supports_json(self) -> bool {
47 match self {
48 Self::CliCommonBase => true,
49 Self::Legacy { supports_json, .. } => supports_json,
50 }
51 }
52
53 #[must_use]
55 pub const fn supports_doctor(self) -> bool {
56 match self {
57 Self::CliCommonBase => true,
58 Self::Legacy {
59 supports_doctor, ..
60 } => supports_doctor,
61 }
62 }
63
64 #[must_use]
66 pub const fn is_authoritative(self) -> bool {
67 matches!(self, Self::CliCommonBase)
68 }
69
70 pub const fn validate(self) -> Result<(), ContractError> {
77 match self {
78 Self::CliCommonBase => Ok(()),
79 Self::Legacy { .. } => Err(ContractError::NotAuthoritative),
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct ToolSpec {
87 pub(crate) bin_name: &'static str,
89 #[allow(
91 dead_code,
92 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"
93 )]
94 pub(crate) display_name: &'static str,
95 pub(crate) version: &'static str,
97 pub(crate) license: LicenseType,
99 #[allow(
101 dead_code,
102 reason = "set by the public ToolSpec constructor and read via its accessor; dead_code fires only in binaries that never read it"
103 )]
104 pub(crate) repo: RepoInfo,
105 pub(crate) contract: ToolContract,
107 pub(crate) agent_surface: Option<&'static AgentSurfaceSpec>,
109}
110
111impl ToolSpec {
112 #[must_use]
114 pub const fn new(
115 bin_name: &'static str,
116 display_name: &'static str,
117 version: &'static str,
118 license: LicenseType,
119 repo: RepoInfo,
120 supports_json: bool,
121 supports_doctor: bool,
122 ) -> Self {
123 Self {
124 bin_name,
125 display_name,
126 version,
127 license,
128 repo,
129 contract: ToolContract::from_support_flags(supports_json, supports_doctor),
130 agent_surface: None,
131 }
132 }
133
134 #[must_use]
136 pub const fn workspace(
137 bin_name: &'static str,
138 display_name: &'static str,
139 version: &'static str,
140 license: LicenseType,
141 supports_json: bool,
142 supports_doctor: bool,
143 ) -> Self {
144 Self::new(
145 bin_name,
146 display_name,
147 version,
148 license,
149 WORKSPACE_REPO,
150 supports_json,
151 supports_doctor,
152 )
153 }
154
155 #[must_use]
157 pub const fn with_agent_surface(self, agent_surface: &'static AgentSurfaceSpec) -> Self {
158 Self {
159 agent_surface: Some(agent_surface),
160 ..self
161 }
162 }
163
164 #[must_use]
166 pub const fn bin_name(&self) -> &'static str {
167 self.bin_name
168 }
169
170 #[must_use]
172 pub const fn display_name(&self) -> &'static str {
173 self.display_name
174 }
175
176 #[must_use]
178 pub const fn version(&self) -> &'static str {
179 self.version
180 }
181
182 #[must_use]
184 pub const fn agent_surface(&self) -> Option<&'static AgentSurfaceSpec> {
185 self.agent_surface
186 }
187
188 #[must_use]
190 pub const fn supports_json(&self) -> bool {
191 self.contract.supports_json()
192 }
193
194 #[must_use]
196 pub const fn supports_doctor(&self) -> bool {
197 self.contract.supports_doctor()
198 }
199
200 #[must_use]
202 pub const fn has_authoritative_contract(&self) -> bool {
203 self.contract.is_authoritative()
204 }
205
206 pub const fn validate_authoritative_contract(&self) -> Result<(), ContractError> {
213 self.contract.validate()
214 }
215}
216
217#[must_use]
219pub const fn workspace_tool(
220 bin_name: &'static str,
221 display_name: &'static str,
222 version: &'static str,
223 license: LicenseType,
224 supports_json: bool,
225 supports_doctor: bool,
226) -> ToolSpec {
227 ToolSpec::workspace(
228 bin_name,
229 display_name,
230 version,
231 license,
232 supports_json,
233 supports_doctor,
234 )
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn tool_spec_new_preserves_fields() {
243 let spec = ToolSpec::new(
244 "tool",
245 "Tool",
246 "1.2.3",
247 LicenseType::MIT,
248 RepoInfo::new("owner", "repo"),
249 true,
250 false,
251 );
252
253 assert_eq!(spec.bin_name, "tool");
254 assert_eq!(spec.display_name, "Tool");
255 assert_eq!(spec.version, "1.2.3");
256 assert_eq!(spec.license, LicenseType::MIT);
257 assert_eq!(spec.repo.owner, "owner");
258 assert_eq!(spec.repo.name, "repo");
259 assert_eq!(
260 spec.contract,
261 ToolContract::Legacy {
262 supports_json: true,
263 supports_doctor: false,
264 }
265 );
266 assert!(spec.supports_json());
267 assert!(!spec.supports_doctor());
268 assert!(!spec.has_authoritative_contract());
269 assert!(spec.validate_authoritative_contract().is_err());
270 assert!(spec.agent_surface().is_none());
271 }
272
273 #[test]
274 fn workspace_tool_uses_workspace_repo_defaults() {
275 let spec = workspace_tool("tool", "Tool", "1.2.3", LicenseType::MIT, true, false);
276
277 assert_eq!(spec.repo.owner, WORKSPACE_REPO.owner);
278 assert_eq!(spec.repo.name, WORKSPACE_REPO.name);
279 assert_eq!(spec.bin_name, "tool");
280 assert_eq!(spec.display_name, "Tool");
281 }
282
283 #[test]
284 fn tool_spec_with_all_mandatory_capabilities_is_authoritative() {
285 let spec = workspace_tool("tool", "Tool", "1.2.3", LicenseType::MIT, true, true);
286
287 assert_eq!(spec.contract, ToolContract::CliCommonBase);
288 assert!(spec.has_authoritative_contract());
289 assert!(spec.validate_authoritative_contract().is_ok());
290 }
291}