1use std::collections::BTreeMap;
2
3use pest::iterators::Pair;
4use pest::Parser;
5
6#[derive(Parser)]
7#[grammar = "dotenv.pest"]
8struct DotenvLineParser;
9
10pub fn parse_dotenv(
12 source: &str,
13) -> Result<BTreeMap<String, String>, Box<dyn std::error::Error + Send + Sync>>
14{
15 let mut map = BTreeMap::new();
16 let pairs = DotenvLineParser::parse(Rule::env, source)?;
17 for pair in pairs {
18 match pair.as_rule() {
19 Rule::kv => {
20 if let Some((key, value)) = parse_kv(pair) {
21 map.insert(key, value);
22 }
23 }
24 _ => {}
25 }
26 }
27 Ok(map)
28}
29
30fn parse_kv(pair: Pair<Rule>) -> Option<(String, String)> {
32 match pair.as_rule() {
33 Rule::kv => {
34 let mut inner_rules = pair.into_inner(); let name: &str = inner_rules.next().unwrap().as_str();
36 parse_value(inner_rules.next().unwrap()).map(|v| (name.into(), v))
37 }
38 _ => None,
39 }
40}
41
42fn parse_value(pair: Pair<Rule>) -> Option<String> {
44 match pair.as_rule() {
45 Rule::value => {
46 let inner = pair.clone().into_inner().next();
47 match inner {
51 None => Some(pair.as_str().into()),
52 Some(inner_pair) => match inner_pair.into_inner().next() {
53 None => None,
54 Some(inner_string) => Some(inner_string.as_str().into()),
55 },
56 }
57 }
58 _ => None,
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::parse_dotenv;
65 use std::collections::BTreeMap;
66
67 #[test]
68 fn empty_file() {
69 assert_eq!(parse_dotenv("").unwrap(), BTreeMap::new());
70 }
71
72 #[test]
73 fn one_kv() {
74 let bm = vec![("key", "value")]
75 .into_iter()
76 .map(|(a, b)| (a.into(), b.into()))
77 .collect();
78 assert_eq!(parse_dotenv("key = value").unwrap(), bm);
79 }
80
81 #[test]
82 fn one_line() {
83 let bm = vec![("key", "value")]
84 .into_iter()
85 .map(|(a, b)| (a.into(), b.into()))
86 .collect();
87 assert_eq!(parse_dotenv("key = value\n").unwrap(), bm);
88 }
89
90 #[test]
91 fn two_lines() {
92 let bm = vec![("key", "value"), ("key2", "value2")]
93 .into_iter()
94 .map(|(a, b)| (a.into(), b.into()))
95 .collect();
96 assert_eq!(parse_dotenv("key = value\nkey2 = value2").unwrap(), bm);
97 }
98
99 #[test]
100 fn non_alphanumeric_chars() {
101 let bm = vec![("key", "https://1.3.2.3:234/a?b=c")]
102 .into_iter()
103 .map(|(a, b)| (a.into(), b.into()))
104 .collect();
105 assert_eq!(
106 parse_dotenv("key=https://1.3.2.3:234/a?b=c\n").unwrap(),
107 bm
108 );
109 }
110
111 #[test]
112 fn export() {
113 let bm = vec![("key", "value"), ("key2", "value2")]
114 .into_iter()
115 .map(|(a, b)| (a.into(), b.into()))
116 .collect();
117 assert_eq!(
118 parse_dotenv("key = value\nexport key2 = value2").unwrap(),
119 bm
120 );
121 }
122
123 #[test]
124 fn string_single_quotes() {
125 let bm = vec![("key", "value"), ("key2", "val ue2")]
126 .into_iter()
127 .map(|(a, b)| (a.into(), b.into()))
128 .collect();
129 assert_eq!(parse_dotenv("key = value\nkey2 = 'val ue2'").unwrap(), bm);
130 }
131
132 #[test]
133 fn string_double_quotes() {
134 let bm = vec![("key", "value"), ("key2", "val ue2")]
135 .into_iter()
136 .map(|(a, b)| (a.into(), b.into()))
137 .collect();
138 assert_eq!(
139 parse_dotenv("key = value\nkey2 = \"val ue2\"").unwrap(),
140 bm
141 );
142 }
143
144 #[test]
145 fn empty_value_single_quotes() {
146 let bm = vec![("key", "value"), ("key2", "")]
147 .into_iter()
148 .map(|(a, b)| (a.into(), b.into()))
149 .collect();
150 assert_eq!(parse_dotenv("key = value\nkey2 = ''").unwrap(), bm);
151 }
152
153 #[test]
154 fn empty_value_double_quotes() {
155 let bm = vec![("key", "value"), ("key2", "")]
156 .into_iter()
157 .map(|(a, b)| (a.into(), b.into()))
158 .collect();
159 assert_eq!(parse_dotenv("key = value\nkey2 = \"\"").unwrap(), bm);
160 }
161
162 #[test]
163 fn comments() {
164 let source = r#"
165 # one here
166 ENV_FOR_HYDRO=production # another one here
167 "#;
168 let bm = vec![("ENV_FOR_HYDRO", "production")]
169 .into_iter()
170 .map(|(a, b)| (a.into(), b.into()))
171 .collect();
172 assert_eq!(parse_dotenv(source).unwrap(), bm);
173 }
174
175 #[test]
176 fn complete_dotenv() {
177 let source = r#"
178 # main comment
179
180 ENV_FOR_HYDRO='testing 2' # another one here
181 export USER_ID=5gpPN5rcv5G41U_S
182 API_TOKEN=30af563ccc668bc8ced9e24e # relax! these values are fake
183 APP_SITE_URL=https://my.example.com
184 "#;
185 let bm = vec![
186 ("ENV_FOR_HYDRO", "testing 2"),
187 ("USER_ID", "5gpPN5rcv5G41U_S"),
188 ("API_TOKEN", "30af563ccc668bc8ced9e24e"),
189 ("APP_SITE_URL", "https://my.example.com"),
190 ]
191 .into_iter()
192 .map(|(a, b)| (a.into(), b.into()))
193 .collect();
194 assert_eq!(parse_dotenv(source).unwrap(), bm);
195 }
196}