supercode_interchange/orchestration/codec/
dotenv.rs1use std::collections::BTreeMap;
5
6pub fn parse_dotenv(text: &str) -> BTreeMap<String, String> {
8 let mut out = BTreeMap::new();
9 for line in text.lines() {
10 let t = line.trim();
11 if t.is_empty() || t.starts_with('#') {
12 continue;
13 }
14 let Some(eq) = t.find('=') else { continue };
15 let key = t[..eq].trim().to_string();
16 let mut value = t[eq + 1..].trim().to_string();
17 let quoted = (value.starts_with('"') && value.ends_with('"') && value.len() >= 2)
18 || (value.starts_with('\'') && value.ends_with('\'') && value.len() >= 2);
19 if quoted {
20 value = value[1..value.len() - 1].to_string();
21 }
22 out.insert(key, value);
23 }
24 out
25}
26
27pub fn render_dotenv(entries: &BTreeMap<String, String>) -> String {
29 let mut text = entries
30 .iter()
31 .map(|(k, v)| format!("{k}={}", serde_json::to_string(v).unwrap()))
32 .collect::<Vec<_>>()
33 .join("\n");
34 if !entries.is_empty() {
35 text.push('\n');
36 }
37 text
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43
44 #[test]
45 fn round_trip_and_quotes() {
46 let parsed = parse_dotenv("# c\nA=1\nB=\"two words\"\nC='x'\nbad line\n");
47 assert_eq!(parsed.get("A").map(String::as_str), Some("1"));
48 assert_eq!(parsed.get("B").map(String::as_str), Some("two words"));
49 assert_eq!(parsed.get("C").map(String::as_str), Some("x"));
50 assert_eq!(parsed.len(), 3);
51 assert_eq!(
52 render_dotenv(&parsed),
53 "A=\"1\"\nB=\"two words\"\nC=\"x\"\n"
54 );
55 assert_eq!(render_dotenv(&BTreeMap::new()), "");
56 }
57}