vne 0.0.1

Environment variables.
Documentation
//! # vne
//!
//!> [!WARNING]
//!> This is just an proof of concept of a stupid idea I had. It is probably not ready for use.
//!
//!Environment variables.
//!
//!## Usage
//!
//!You can setup `vne` in your `main.rs` like the following:
//!
//!```rust
//!fn main() {
//!  // This will ensure all variables declared by a usage of `vne!` exist.
//!  ENV.validate().unwrap();
//!
//!  // The rest of your code
//!  let testing: String = vne::vne!("TESTING");
//!}
//!
//!// It is *really* important this is in `main.rs` and at the bottom of the file!
//!const ENV: vne::Environment = vne::build!();
//!```
//!
//!## Framework integration
//!
//!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:
//!
//!```text
//!{time}\n
//!# The following repeats
//!{crate_name}\t{bin_name}\t{env_1}\t{env_2}\n
//!```
//!
//!## Inspiration
//!
//!We make use of the pattern which I originally came across in [macro_state](https://github.com/sam0x17/macro_state).
#![cfg_attr(docsrs, feature(doc_cfg))]

use std::{path::PathBuf, sync::atomic::AtomicBool};

pub use vne_macros::{build, vne};

static HAS_VALIDATED: AtomicBool = AtomicBool::new(false);

pub struct Environment {
    envs: &'static [&'static str],
}

impl Environment {
    // TODO: Error handling
    pub fn load(self, path: impl Into<PathBuf>) -> Self {
        let path = path.into();

        // TODO: Handle `.env.example`, `.env.local`, etc
        // TODO: Allow `path` to be to directory or direct file

        if path.join(".env").exists() {
            let raw = std::fs::read_to_string(path.join(".env"))
                .expect("vne error: failed to read environment file");

            let envs = raw
                .lines()
                .map(|v| v.trim())
                .filter(|v| !v.is_empty())
                .filter(|v| !v.starts_with('#'))
                .map(|v| v.splitn(2, "="))
                .map(|mut v| (v.next().unwrap(), v.next().unwrap().trim_matches('"')));

            for (key, value) in envs {
                if std::env::var(key).is_err() {
                    std::env::set_var(key, value);
                }
            }
        } else {
            panic!("vne error: failed to find .env file")
        }

        self
    }

    // TODO: Proper error type
    pub fn validate(&self) -> Result<(), String> {
        for env in self.envs {
            if std::env::var(env).is_err() {
                return Err(format!("vne error: missing environment variable {env:?}"));
            }
        }

        HAS_VALIDATED.store(true, std::sync::atomic::Ordering::SeqCst);
        Ok(())
    }
}

#[doc(hidden)]
pub mod internal {
    use super::*;

    pub const fn construct_env(envs: &'static [&'static str]) -> Environment {
        Environment { envs }
    }

    pub fn from_env(key: &'static str) -> String {
        if !HAS_VALIDATED.load(std::sync::atomic::Ordering::SeqCst) {
            panic!("vne error: attempted to access environment variable before validation was run");
        }

        std::env::var(key).expect("vne error: unreachable failed to get env")
    }
}