1use serde::Serialize;
2
3use crate::config::{CapabilityMode, Config};
4use crate::error::ViaError;
5
6#[derive(Serialize)]
7struct Capabilities<'a> {
8 services: Vec<ServiceCapabilities<'a>>,
9}
10
11#[derive(Serialize)]
12struct ServiceCapabilities<'a> {
13 name: &'a str,
14 description: Option<&'a str>,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 hint: Option<&'a str>,
17 capabilities: Vec<CapabilitySummary<'a>>,
18}
19
20#[derive(Serialize)]
21struct CapabilitySummary<'a> {
22 name: &'a str,
23 description: Option<&'a str>,
24 mode: CapabilityMode,
25}
26
27pub fn print(config: &Config, json: bool) -> Result<(), ViaError> {
28 print!("{}", render(config, json)?);
29 Ok(())
30}
31
32pub fn render(config: &Config, json: bool) -> Result<String, ViaError> {
33 if json {
34 let capabilities = Capabilities {
35 services: config
36 .services
37 .iter()
38 .map(|(name, service)| ServiceCapabilities {
39 name,
40 description: service.description.as_deref(),
41 hint: service.hint.as_deref(),
42 capabilities: service
43 .commands
44 .iter()
45 .map(|(command_name, command)| CapabilitySummary {
46 name: command_name,
47 description: command.description().map(String::as_str),
48 mode: command.mode(),
49 })
50 .collect(),
51 })
52 .collect(),
53 };
54 return Ok(format!(
55 "{}\n",
56 serde_json::to_string_pretty(&capabilities)?
57 ));
58 }
59
60 let mut output = String::new();
61 for (service_name, service) in &config.services {
62 match &service.description {
63 Some(description) => output.push_str(&format!("{service_name}: {description}\n")),
64 None => output.push_str(&format!("{service_name}\n")),
65 }
66 if let Some(hint) = &service.hint {
67 output.push_str(&format!(" hint: {hint}\n"));
68 }
69
70 for (command_name, command) in &service.commands {
71 match command.description() {
72 Some(description) => output.push_str(&format!(
73 " {command_name} ({:?}): {description}\n",
74 command.mode()
75 )),
76 None => output.push_str(&format!(" {command_name} ({:?})\n", command.mode())),
77 }
78 }
79 }
80
81 Ok(output)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 fn config() -> Config {
89 Config::from_toml_str(
90 r#"
91version = 1
92
93[providers.onepassword]
94type = "1password"
95
96[services.github]
97description = "GitHub access"
98hint = "via github api /user"
99provider = "onepassword"
100
101[services.github.secrets]
102token = "op://Private/GitHub/token"
103
104[services.github.commands.api]
105description = "REST access"
106mode = "rest"
107base_url = "https://api.github.com"
108
109[services.github.commands.api.auth]
110type = "bearer"
111secret = "token"
112"#,
113 )
114 .unwrap()
115 }
116
117 #[test]
118 fn renders_human_capabilities() {
119 let output = render(&config(), false).unwrap();
120
121 assert!(output.contains("github: GitHub access"));
122 assert!(output.contains("hint: via github api /user"));
123 assert!(output.contains("api (Rest): REST access"));
124 }
125
126 #[test]
127 fn renders_json_capabilities_without_secret_refs() {
128 let output = render(&config(), true).unwrap();
129
130 assert!(output.contains("\"name\": \"github\""));
131 assert!(output.contains("\"hint\": \"via github api /user\""));
132 assert!(output.contains("\"mode\": \"rest\""));
133 assert!(!output.contains("op://"));
134 }
135}