env_smart/
lib.rs

1//! Improved version of `env!` macro from std.
2//!
3//! ## Syntax:
4//!
5//! - Standard `env!` - If plain string specified then behavior is the same as standard [env](https://doc.rust-lang.org/std/macro.env.html) macro
6//! - Simplified formatting - Allows to format string using multiple variables enveloped into `{}` brackets. Note that bracket escaping is not supported
7//!
8//!
9//!## Sources
10//!
11//!Macro fetches environment variables in following order:
12//!
13//!- Use `.env` file from root where build is run. Duplicate values are not allowed.
14//!- Use current environment where proc macro runs. It will not override `.env` variables
15//!
16//! ## Usage
17//!
18//! ```rust
19//! use env_smart::env;
20//!
21//! static USER_AGENT: &str = env!("{CARGO_PKG_NAME}-{CARGO_PKG_VERSION}");
22//!
23//! assert_eq!(USER_AGENT, "env-smart-1.0.1");
24//!
25//! static TEST: &str = env!("test-{CARGO_PKG_NAME}-{CARGO_PKG_VERSION}");
26//!
27//! assert_eq!(TEST, "test-env-smart-1.0.1");
28//!
29//! assert_eq!(env!("{CARGO_PKG_NAME}"), "env-smart");
30//!
31//! assert_eq!(env!("CARGO_PKG_NAME"), "env-smart");
32//!
33//! #[cfg(not(windows))]
34//! assert_ne!(env!("PWD"), "PWD");
35//! ```
36
37#![warn(missing_docs)]
38#![cfg_attr(feature = "cargo-clippy", allow(clippy::style))]
39
40use proc_macro::{TokenStream, TokenTree};
41
42use core::mem;
43use core::cell::UnsafeCell;
44
45use std::fs;
46use std::io::{self, BufRead};
47use std::collections::{hash_map, HashMap};
48use std::sync::Once;
49
50mod format;
51
52const QUOTE: char = '"';
53
54#[cold]
55#[inline(never)]
56fn compile_error(error: &str) -> TokenStream {
57    format!("compile_error!(\"{error}\")").parse().unwrap()
58}
59
60fn read_envs() -> Result<HashMap<String, String>, TokenStream> {
61    const QUOTES: &[char] = &['"', '\''];
62    let mut envs = HashMap::default();
63
64    match fs::File::open(".env") {
65        Ok(file) => {
66            let file = io::BufReader::new(file);
67            for line in file.lines() {
68                match line {
69                    Ok(line) => {
70                        let mut split = line.splitn(2, '=');
71                        let key = split.next().unwrap();
72                        let value = match split.next() {
73                            Some(value) => value.trim_matches(QUOTES),
74                            None => return Err(compile_error(&format!(".env file has '{key}' without value"))),
75                        };
76
77                        if envs.insert(key.to_owned(), value.to_owned()).is_some() {
78                            return Err(compile_error(&format!(".env file has multiple instances of '{key}'")))
79                        }
80                    },
81                    Err(error) => {
82                        let error = format!(".env: Read fail: {error}");
83                        return Err(compile_error(&error));
84                    }
85                }
86            }
87        }
88        Err(error) => match error.kind() {
89            io::ErrorKind::NotFound => (),
90            _ => {
91                let error = format!(".env: Cannot open: {error}");
92                return Err(compile_error(&error));
93            },
94        }
95    };
96
97    for (key, value) in std::env::vars() {
98        match envs.entry(key) {
99            hash_map::Entry::Vacant(vacant) => {
100                vacant.insert(value);
101            },
102            hash_map::Entry::Occupied(_) => (),
103        }
104    }
105
106    Ok(envs)
107}
108
109//Like imagine using lock for one time initialization
110type State = UnsafeCell<mem::MaybeUninit<Result<HashMap<String, String>, TokenStream>>>;
111struct Cache(State);
112unsafe impl Sync for Cache {}
113
114//This implementation may or may not in future, but at the current moment we can freely rely on
115//execution context to be shared between all instances of macro call
116fn read_cached_envs() -> &'static Result<HashMap<String, String>, TokenStream> {
117    static STATE: Cache = Cache(State::new(mem::MaybeUninit::uninit()));
118    static LOCK: Once = Once::new();
119
120    LOCK.call_once(|| {
121        unsafe {
122            *STATE.0.get() = mem::MaybeUninit::new(read_envs());
123        }
124    });
125
126    unsafe {
127        &*(STATE.0.get() as *const _)
128    }
129}
130
131struct Args {
132    input: String,
133}
134
135impl Args {
136    pub fn from_tokens(input: TokenStream) -> Result<Self, TokenStream> {
137        const EXPECTED_STRING: &str = "Expected string literal";
138        let mut args = input.into_iter();
139
140        let input = match args.next() {
141            Some(TokenTree::Literal(lit)) => {
142                let quoted = lit.to_string();
143                let result = quoted.trim_matches(QUOTE);
144                if result.len() + 2 != quoted.len() {
145                    return Err(compile_error(EXPECTED_STRING));
146                }
147                result.to_owned()
148            },
149            Some(unexpected) => return Err(compile_error(&format!("{EXPECTED_STRING}, got {:?}", unexpected))),
150            None => return Err(compile_error("Missing input string")),
151        };
152
153        Ok(Self {
154            input,
155        })
156    }
157}
158
159#[proc_macro]
160///Inserts env variable
161pub fn env(input: TokenStream) -> TokenStream {
162    let args = match Args::from_tokens(input) {
163        Ok(args) => args,
164        Err(error) => return error,
165    };
166    let envs = match read_cached_envs() {
167        Ok(envs) => envs,
168        Err(error) => return error.clone(),
169    };
170
171    let mut output = String::new();
172    let mut formatter = format::Format::new(args.input.as_str(), &envs);
173
174    let mut plain_len = 0;
175    let mut args_len = 0;
176
177    output.push(QUOTE);
178    while let Some(part) = formatter.next() {
179        match part {
180            Ok(part) => match part {
181                format::Part::Plain(plain) => {
182                    plain_len += 1;
183                    output.push_str(plain);
184                }
185                format::Part::Argument(plain) => {
186                    args_len += 1;
187                    output.push_str(plain);
188                }
189            },
190            Err(error) => {
191                return compile_error(&format!("Format string error {error}"));
192            }
193        }
194    }
195
196    if args_len == 0 {
197        debug_assert_eq!(plain_len, 1);
198        match std::env::var(&output[1..]) {
199            Ok(value) => {
200                output.clear();
201                output.push(QUOTE);
202                output.push_str(&value);
203            },
204            Err(_) => return compile_error(&format!("env:{}: Cannot fetch env value", &output[1..])),
205        }
206    }
207
208    output.push(QUOTE);
209
210    output.parse().expect("valid literal string syntax")
211}