env_parser/
lib.rs

1#[cfg(feature = "to_lazy_static")]
2pub mod to_lazy_static;
3
4/// Reads and transformers the env file.
5/// Note: when parsing a value, it will do in this order: Try to convert it into...
6///     1. i32
7///     2. f32
8///     3. String (always succeeds)
9/// If you want a different type to be parsed, do that in the transformer trait.
10pub fn read_env<T: Transform>(env_reader: &mut EnvReader<T>) {
11    let raw = String::from_utf8(env_reader.env.clone()).unwrap();
12    let env = raw.split('\n').collect::<Vec<_>>();
13
14    let mut comments_above_key = vec![];
15
16    for line in env {
17        let trimmed = line.trim();
18
19        if trimmed.is_empty() {
20            if env_reader
21                .transformer
22                .remove_comments_if_blank_line_occurs()
23            {
24                // Remove all comments
25                comments_above_key = vec![];
26            }
27
28            continue;
29        }
30
31        // Check if the line is a comment
32        if trimmed.starts_with('#') {
33            comments_above_key.push(trimmed.replace('#', "//"));
34            continue;
35        }
36
37        // This is the actual key, split it by checking the first '='
38        // Note: when split_once is stabelized, replace it with the actual code
39        //let (key, value) = trimmed.split_once('=').unwrap();
40
41        let (key, value) = {
42            let split = trimmed
43                .find('=')
44                .unwrap_or_else(|| panic!("No '=' found in: '{}'", trimmed));
45
46            (&trimmed[0..split], &trimmed[split + 1..])
47        };
48
49        let env_type = if let Ok(o) = value.parse::<i32>() {
50            EnvType::I32(o)
51        } else if let Ok(o) = value.parse::<f32>() {
52            EnvType::F32(o)
53        } else if let Ok(o) = value.parse::<bool>() {
54            EnvType::Bool(o)
55        } else {
56            let string_value = value.to_string();
57
58            // Empty value is ignored and saved as a string type
59
60            EnvType::StaticStr(string_value)
61        };
62
63        env_reader
64            .transformer
65            .write(comments_above_key.clone(), key, env_type);
66
67        // Prepare for a new loop
68        comments_above_key = vec![];
69    }
70}
71
72pub struct EnvReader<'a, T: Transform> {
73    pub env: Vec<u8>,
74    pub transformer: &'a mut T,
75}
76
77impl<'a, T: Transform> EnvReader<'a, T> {
78    pub fn new(env: Vec<u8>, transformer: &'a mut T) -> EnvReader<'a, T> {
79        EnvReader { env, transformer }
80    }
81}
82
83pub trait CustomMap {
84    fn rust_type(&self) -> String;
85    fn raw_value(&self) -> String;
86    fn value(&self) -> String;
87    #[cfg(feature = "to_lazy_static")]
88    fn transform(&self) -> String;
89}
90
91/// The different values an env file can hold
92pub enum EnvType {
93    Bool(bool),
94    I32(i32),
95    I64(i64),
96    I128(i128),
97    U8(u8),
98    U32(u32),
99    U128(u128),
100    F32(f32),
101    F64(f64),
102    USize(usize),
103    StaticStr(String),
104    // Implement this type if one of the defaults is not sufficient
105    Custom(Box<dyn CustomMap>),
106}
107
108impl EnvType {
109    /// The Rust type
110    pub fn rust_type(&self) -> String {
111        match self {
112            EnvType::Bool(_) => "bool".to_string(),
113            EnvType::I32(_) => "i32".to_string(),
114            EnvType::I64(_) => "i64".to_string(),
115            EnvType::I128(_) => "i128".to_string(),
116            EnvType::U8(_) => "u8".to_string(),
117            EnvType::U32(_) => "u32".to_string(),
118            EnvType::U128(_) => "u128".to_string(),
119            EnvType::F32(_) => "f32".to_string(),
120            EnvType::F64(_) => "f64".to_string(),
121            EnvType::USize(_) => "usize".to_string(),
122            EnvType::StaticStr(_) => "&'static str".to_string(),
123            EnvType::Custom(c) => c.rust_type(),
124        }
125        .replace('\"', "")
126    }
127
128    /// The actual value the env property holds
129    pub fn raw_value(&self) -> String {
130        match self {
131            EnvType::Bool(val) => val.to_string(),
132            EnvType::I32(val) => val.to_string(),
133            EnvType::I64(val) => val.to_string(),
134            EnvType::I128(val) => val.to_string(),
135            EnvType::U8(val) => val.to_string(),
136            EnvType::U32(val) => val.to_string(),
137            EnvType::U128(val) => val.to_string(),
138            EnvType::F32(val) => val.to_string(),
139            EnvType::F64(val) => val.to_string(),
140            EnvType::USize(val) => val.to_string(),
141            EnvType::StaticStr(val) => format!("\"{}\"", val),
142            EnvType::Custom(c) => c.raw_value(),
143        }
144    }
145
146    /// Adds the type if needed behind the raw value
147    /// This is needed if the user wants the value 1 to be an f32. If you only type:
148    /// ```compile_fail
149    /// const MY_VARIABLE: f32 = 1;
150    /// ```
151    /// The following compile error occurs: mismatched types [E0308] expected `f32`, found `i32`
152    /// That's why the type is needed behind the value:
153    /// ```
154    /// const MY_VARIABLE: f32 = 1f32;
155    /// ```
156    pub fn value(&self) -> String {
157        let ty = self.raw_value();
158
159        match self {
160            EnvType::StaticStr(_) | EnvType::Bool(_) => ty,
161            EnvType::Custom(c) => c.value(),
162            _ => ty + &self.rust_type(),
163        }
164    }
165}
166
167/// Customize transformation by implementing this trait
168pub trait Transform {
169    /// If two comments appear but with a blank line in between them, it may mean that the above comment
170    /// should be skipped, e.g.:
171    /// `
172    /// # This is some comment
173    /// - blank line-
174    /// # This is another comment
175    /// `
176    /// If this method returns true, the first comment will not be included when calling env_type
177    /// in the comments parameter
178    fn remove_comments_if_blank_line_occurs(&self) -> bool {
179        false
180    }
181
182    /// Writes the output
183    fn write(&mut self, comments: Vec<String>, key: &str, inferred_type: EnvType);
184}
185
186#[cfg(test)]
187mod locations {
188    use std::fs::File;
189    use std::io::Read;
190    use std::path::PathBuf;
191
192    pub fn src() -> PathBuf {
193        std::env::current_dir().unwrap().join("src")
194    }
195
196    pub fn env() -> Vec<u8> {
197        include_bytes!("../.env").to_vec()
198    }
199
200    pub fn temp_rs() -> PathBuf {
201        src().join("temp_rs.rs")
202    }
203
204    pub fn check_equals(to_check: &str) {
205        // Check if the generated file is equal to what is expected
206        let mut assert_test = String::new();
207        File::open(src().join(to_check))
208            .unwrap()
209            .read_to_string(&mut assert_test)
210            .unwrap();
211
212        let mut temp_rs_string = String::new();
213        File::open(temp_rs())
214            .unwrap()
215            .read_to_string(&mut temp_rs_string)
216            .unwrap();
217
218        // On windows, the left file somehow has \r inside the file but the right file doesn't
219        assert_eq!(assert_test.replace('\r', ""), temp_rs_string);
220
221        std::fs::remove_file(temp_rs()).unwrap();
222    }
223}
224
225#[test]
226fn test_write() {
227    use crate::locations::{check_equals, env, temp_rs};
228    use std::fs::File;
229    use std::io::Write;
230
231    // Create a transformer that writes the output to a Rust file
232    struct TransformerImpl {
233        file: File,
234    }
235
236    impl Transform for TransformerImpl {
237        fn write(&mut self, comments: Vec<String>, key: &str, inferred_type: EnvType) {
238            for comment in comments {
239                writeln!(&self.file, "{}", comment).unwrap();
240            }
241
242            let inferred_type = if key == "SOME_I64_VAL" {
243                EnvType::I64(inferred_type.raw_value().parse().unwrap())
244            } else {
245                inferred_type
246            };
247
248            let declaration = format!(
249                "pub const {}: {} = {};",
250                key,
251                inferred_type.rust_type(),
252                inferred_type.value()
253            );
254
255            writeln!(&self.file, "{}", declaration).unwrap();
256        }
257    }
258
259    read_env(&mut EnvReader::new(
260        env(),
261        &mut TransformerImpl {
262            file: File::create(&temp_rs()).unwrap(),
263        },
264    ));
265
266    check_equals("assert_test.rs");
267}