Skip to main content

cli/env/
catalog.rs

1use crate::{config::Config, presets};
2use anyhow::{Context, Result, bail};
3use serde::Deserialize;
4use std::collections::BTreeMap;
5use tokio::fs;
6
7const ENV_CATALOG_FILE: &str = "env.toml";
8
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct EnvMetadata {
11    pub description: String,
12    pub sensitive: bool,
13}
14
15#[derive(Deserialize)]
16struct EnvCatalogToml {
17    #[serde(default)]
18    variables: Vec<EnvMetadataToml>,
19}
20
21#[derive(Deserialize)]
22struct EnvMetadataToml {
23    key: String,
24    description: String,
25    #[serde(default)]
26    sensitive: bool,
27}
28
29pub async fn load(config: &Config) -> Result<BTreeMap<String, EnvMetadata>> {
30    let bytes = if config.is_external_presets {
31        let path = config.preset_path(ENV_CATALOG_FILE);
32        match fs::read(&path).await {
33            Ok(bytes) => Some(bytes),
34            Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
35            Err(error) => {
36                return Err(error).with_context(|| format!("reading {}", path.display()));
37            }
38        }
39    } else {
40        presets::read_asset_bytes(ENV_CATALOG_FILE)
41    };
42
43    bytes
44        .as_deref()
45        .map(parse)
46        .transpose()
47        .map(Option::unwrap_or_default)
48}
49
50fn parse(bytes: &[u8]) -> Result<BTreeMap<String, EnvMetadata>> {
51    let parsed: EnvCatalogToml = toml::from_slice(bytes).context("failed to parse env.toml")?;
52    let mut catalog = BTreeMap::new();
53    for variable in parsed.variables {
54        if variable.key.trim().is_empty() {
55            bail!("env.toml contains an empty variable key");
56        }
57        if catalog
58            .insert(
59                variable.key.clone(),
60                EnvMetadata {
61                    description: variable.description,
62                    sensitive: variable.sensitive,
63                },
64            )
65            .is_some()
66        {
67            bail!(
68                "env.toml contains duplicate variable key `{}`",
69                variable.key
70            );
71        }
72    }
73    Ok(catalog)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn parses_catalog_metadata() {
82        let catalog = parse(
83            br#"
84                [[variables]]
85                key = "API_TOKEN"
86                description = "API access token"
87                sensitive = true
88            "#,
89        )
90        .unwrap();
91
92        assert_eq!(catalog["API_TOKEN"].description, "API access token");
93        assert!(catalog["API_TOKEN"].sensitive);
94    }
95
96    #[test]
97    fn rejects_duplicate_keys() {
98        let error = parse(
99            br#"
100                [[variables]]
101                key = "PORT"
102                description = "First"
103                [[variables]]
104                key = "PORT"
105                description = "Second"
106            "#,
107        )
108        .unwrap_err();
109
110        assert!(error.to_string().contains("duplicate variable key `PORT`"));
111    }
112}