Skip to main content

via/
capabilities.rs

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