vne/
lib.rs

1//! # vne
2//!
3//!> [!WARNING]
4//!> This is just an proof of concept of a stupid idea I had. It is probably not ready for use.
5//!
6//!Environment variables.
7//!
8//!## Usage
9//!
10//!You can setup `vne` in your `main.rs` like the following:
11//!
12//!```rust
13//!fn main() {
14//!  // This will ensure all variables declared by a usage of `vne!` exist.
15//!  ENV.validate().unwrap();
16//!
17//!  // The rest of your code
18//!  let testing: String = vne::vne!("TESTING");
19//!}
20//!
21//!// It is *really* important this is in `main.rs` and at the bottom of the file!
22//!const ENV: vne::Environment = vne::build!();
23//!```
24//!
25//!## Framework integration
26//!
27//!You can statically analyze the environment variables that exist within your project. This could be used by a framework to show warning at deploy time for example. You can read the `target/vne` file and parse it with in the following format:
28//!
29//!```text
30//!{time}\n
31//!# The following repeats
32//!{crate_name}\t{bin_name}\t{env_1}\t{env_2}\n
33//!```
34//!
35//!## Inspiration
36//!
37//!We make use of the pattern which I originally came across in [macro_state](https://github.com/sam0x17/macro_state).
38#![cfg_attr(docsrs, feature(doc_cfg))]
39
40use std::{path::PathBuf, sync::atomic::AtomicBool};
41
42pub use vne_macros::{build, vne};
43
44static HAS_VALIDATED: AtomicBool = AtomicBool::new(false);
45
46pub struct Environment {
47    envs: &'static [&'static str],
48}
49
50impl Environment {
51    // TODO: Error handling
52    pub fn load(self, path: impl Into<PathBuf>) -> Self {
53        let path = path.into();
54
55        // TODO: Handle `.env.example`, `.env.local`, etc
56        // TODO: Allow `path` to be to directory or direct file
57
58        if path.join(".env").exists() {
59            let raw = std::fs::read_to_string(path.join(".env"))
60                .expect("vne error: failed to read environment file");
61
62            let envs = raw
63                .lines()
64                .map(|v| v.trim())
65                .filter(|v| !v.is_empty())
66                .filter(|v| !v.starts_with('#'))
67                .map(|v| v.splitn(2, "="))
68                .map(|mut v| (v.next().unwrap(), v.next().unwrap().trim_matches('"')));
69
70            for (key, value) in envs {
71                if std::env::var(key).is_err() {
72                    std::env::set_var(key, value);
73                }
74            }
75        } else {
76            panic!("vne error: failed to find .env file")
77        }
78
79        self
80    }
81
82    // TODO: Proper error type
83    pub fn validate(&self) -> Result<(), String> {
84        for env in self.envs {
85            if std::env::var(env).is_err() {
86                return Err(format!("vne error: missing environment variable {env:?}"));
87            }
88        }
89
90        HAS_VALIDATED.store(true, std::sync::atomic::Ordering::SeqCst);
91        Ok(())
92    }
93}
94
95#[doc(hidden)]
96pub mod internal {
97    use super::*;
98
99    pub const fn construct_env(envs: &'static [&'static str]) -> Environment {
100        Environment { envs }
101    }
102
103    pub fn from_env(key: &'static str) -> String {
104        if !HAS_VALIDATED.load(std::sync::atomic::Ordering::SeqCst) {
105            panic!("vne error: attempted to access environment variable before validation was run");
106        }
107
108        std::env::var(key).expect("vne error: unreachable failed to get env")
109    }
110}