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}