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 if line.is_empty() || line.starts_with('#') {
12 continue;
13 }
14
15 let line = line.strip_prefix("export ").unwrap_or(line);
17
18 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 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}