Skip to main content

via/
capabilities.rs

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