Skip to main content

pick/formats/
env.rs

1use crate::error::PickError;
2use serde_json::Value;
3
4pub fn parse(input: &str) -> Result<Value, PickError> {
5    let mut map = serde_json::Map::new();
6
7    for line in input.lines() {
8        let line = line.trim();
9
10        // Skip comments and empty lines
11        if line.is_empty() || line.starts_with('#') {
12            continue;
13        }
14
15        // Strip optional "export " prefix
16        let line = line.strip_prefix("export ").unwrap_or(line);
17
18        // Find the first = sign
19        if let Some(eq_pos) = line.find('=') {
20            let key = line[..eq_pos].trim().to_string();
21            let value = line[eq_pos + 1..].trim();
22
23            // Strip surrounding quotes (double or single)
24            let value = strip_quotes(value);
25
26            map.insert(key, Value::String(value));
27        }
28    }
29
30    if map.is_empty() {
31        return Err(PickError::ParseError(
32            "env".into(),
33            "no key-value pairs found".into(),
34        ));
35    }
36
37    Ok(Value::Object(map))
38}
39
40fn strip_quotes(s: &str) -> String {
41    if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
42        let inner = &s[1..s.len() - 1];
43        let mut result = String::with_capacity(inner.len());
44        let mut chars = inner.chars();
45        while let Some(c) = chars.next() {
46            if c == '\\' {
47                match chars.next() {
48                    Some('"') => result.push('"'),
49                    Some('\\') => result.push('\\'),
50                    Some('n') => result.push('\n'),
51                    Some('t') => result.push('\t'),
52                    Some('r') => result.push('\r'),
53                    Some(other) => {
54                        result.push('\\');
55                        result.push(other);
56                    }
57                    None => result.push('\\'),
58                }
59            } else {
60                result.push(c);
61            }
62        }
63        return result;
64    }
65    if s.len() >= 2 && s.starts_with('\'') && s.ends_with('\'') {
66        return s[1..s.len() - 1].to_string();
67    }
68    s.to_string()
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use serde_json::json;
75
76    #[test]
77    fn parse_simple() {
78        let v = parse("DATABASE_URL=postgres://localhost/db\nPORT=3000").unwrap();
79        assert_eq!(v["DATABASE_URL"], json!("postgres://localhost/db"));
80        assert_eq!(v["PORT"], json!("3000"));
81    }
82
83    #[test]
84    fn parse_with_comments() {
85        let input = "# Database\nDATABASE_URL=postgres://localhost/db\n# Port\nPORT=3000";
86        let v = parse(input).unwrap();
87        assert_eq!(v["DATABASE_URL"], json!("postgres://localhost/db"));
88        assert_eq!(v["PORT"], json!("3000"));
89    }
90
91    #[test]
92    fn parse_double_quoted() {
93        let v = parse("MSG=\"hello world\"").unwrap();
94        assert_eq!(v["MSG"], json!("hello world"));
95    }
96
97    #[test]
98    fn parse_single_quoted() {
99        let v = parse("MSG='hello world'").unwrap();
100        assert_eq!(v["MSG"], json!("hello world"));
101    }
102
103    #[test]
104    fn parse_empty_value() {
105        let v = parse("EMPTY=").unwrap();
106        assert_eq!(v["EMPTY"], json!(""));
107    }
108
109    #[test]
110    fn parse_value_with_equals() {
111        let v = parse("URL=postgres://host?opt=val").unwrap();
112        assert_eq!(v["URL"], json!("postgres://host?opt=val"));
113    }
114
115    #[test]
116    fn parse_export_prefix() {
117        let v = parse("export DATABASE_URL=test\nexport PORT=3000").unwrap();
118        assert_eq!(v["DATABASE_URL"], json!("test"));
119        assert_eq!(v["PORT"], json!("3000"));
120    }
121
122    #[test]
123    fn parse_empty_lines() {
124        let v = parse("\n\nKEY=val\n\n").unwrap();
125        assert_eq!(v["KEY"], json!("val"));
126    }
127
128    #[test]
129    fn parse_mixed_quotes() {
130        let v = parse("A=\"double\"\nB='single'\nC=none").unwrap();
131        assert_eq!(v["A"], json!("double"));
132        assert_eq!(v["B"], json!("single"));
133        assert_eq!(v["C"], json!("none"));
134    }
135
136    #[test]
137    fn parse_empty_input() {
138        assert!(parse("").is_err());
139    }
140
141    #[test]
142    fn parse_only_comments() {
143        assert!(parse("# comment\n# another").is_err());
144    }
145
146    #[test]
147    fn parse_lowercase_keys() {
148        let v = parse("lower_key=value").unwrap();
149        assert_eq!(v["lower_key"], json!("value"));
150    }
151}