vne_macros/
lib.rs

1//! The macros for [vne](https://docs.rs/vne) environment variable validation library.
2//!
3//! This should not be used directly!
4
5use std::{
6    collections::HashSet,
7    fs::OpenOptions,
8    io::{ErrorKind, Write},
9    path::PathBuf,
10    sync::OnceLock,
11    time::{SystemTime, UNIX_EPOCH},
12};
13
14use proc_macro::TokenStream;
15use quote::quote;
16use serde::Deserialize;
17
18static COMPILE_TIME: OnceLock<u128> = OnceLock::new();
19
20fn state_path() -> PathBuf {
21    let compile_time = COMPILE_TIME.get_or_init(|| {
22        SystemTime::now()
23            .duration_since(UNIX_EPOCH)
24            .expect("your system time is before epoch")
25            .as_nanos()
26    });
27
28    // `VNE_MACRO_STATE_DIR` comes from the `build.rs` script in the macros crate
29    let mut buf = PathBuf::from(env!("VNE_MACRO_STATE_DIR"));
30    buf.push(&format!("macro_state_{}", compile_time));
31    buf
32}
33
34// Taken from: https://github.com/mitsuhiko/insta/blob/b113499249584cb650150d2d01ed96ee66db6b30/src/runtime.rs#L67-L88
35// Refer to: https://github.com/rust-lang/cargo/issues/3946#issuecomment-454336188
36fn get_cargo_workspace() -> PathBuf {
37    #[derive(Deserialize)]
38    struct Manifest {
39        target_directory: String,
40    }
41    let output = std::process::Command::new(env!("CARGO"))
42        .arg("metadata")
43        .arg("--format-version=1")
44        .current_dir(std::env::var("CARGO_MANIFEST_DIR").expect("'CARGO_MANIFEST_DIR' is not set"))
45        .output()
46        .unwrap();
47    // TODO: We should *really* try and remove Serde cause it will make the macro heavy
48    let manifest: Manifest = serde_json::from_slice(&output.stdout).unwrap();
49    PathBuf::from(manifest.target_directory)
50}
51
52#[proc_macro]
53pub fn build(_item: TokenStream) -> TokenStream {
54    let crate_ref = quote!(vne);
55
56    let vars = match std::fs::read_to_string(state_path()) {
57        Ok(file) => {
58            let schema_file_path = PathBuf::from(get_cargo_workspace()).join("vne");
59            let pkg_name = std::env::var("CARGO_PKG_NAME").expect("'CARGO_PKG_NAME' is not set");
60            let bin_name = std::env::var("CARGO_BIN_NAME").expect("'CARGO_BIN_NAME' is not set");
61
62            let mut lines = if schema_file_path.exists() {
63                let schema_file = std::fs::read_to_string(&schema_file_path).unwrap();
64                let mut lines = schema_file.split("\n");
65                lines.next().expect("malformed vne schema file");
66                lines
67                    .filter(|v| v.starts_with(&format!("{pkg_name}/t{bin_name}")))
68                    .collect::<Vec<&str>>()
69                    .join("\n")
70            } else {
71                String::new()
72            };
73
74            lines.push_str(&format!(
75                "{pkg_name}\t{bin_name}\t{}",
76                file.replace("\n", "\t")
77            ));
78
79            std::fs::write(
80                schema_file_path,
81                format!("{}\n{}", COMPILE_TIME.get().unwrap(), lines),
82            )
83            .unwrap();
84
85            file.split("\n")
86                .map(|v| v.to_string())
87                .filter(|v| !v.is_empty())
88                .collect()
89        }
90        Err(err) if err.kind() == ErrorKind::NotFound => HashSet::new(),
91        Err(err) => panic!("{err}"),
92    }
93    .into_iter();
94
95    quote! {
96        #crate_ref::internal::construct_env(&[#( #vars ),*])
97    }
98    .into()
99}
100
101#[proc_macro]
102pub fn vne(item: TokenStream) -> TokenStream {
103    let crate_ref = quote!(vne);
104    let item: proc_macro2::TokenStream = item.into();
105
106    // TODO: Confirm `$name` is a string literal
107    let value = item.to_string();
108    let value = value[1..value.len() - 1].to_string();
109
110    let path = state_path();
111    std::fs::create_dir_all(&path.parent().unwrap()).unwrap();
112    let mut file = OpenOptions::new()
113        .write(true)
114        .append(true)
115        .create(true)
116        .open(path)
117        .unwrap(); // TODO: Compile time error
118
119    // TODO: We should escape `\n` and `\t` from `value` and fix it on the other side
120
121    if let Err(e) = writeln!(file, "{value}") {
122        eprintln!("vne couldn't write to file: {}", e);
123    }
124
125    quote! {
126        #crate_ref::internal::from_env(#item)
127    }
128    .into()
129}