envcrypt_macro/
lib.rs

1#![deny(missing_docs, clippy::missing_docs_in_private_items)]
2#![cfg_attr(windows, doc = include_str!("..\\README.md"))]
3#![cfg_attr(not(windows), doc = include_str!("../README.md"))]
4
5use std::env::{self, VarError};
6
7use proc_macro::{Literal, TokenStream, TokenTree};
8use proc_macro_error::{abort_call_site, proc_macro_error};
9
10mod encrypt;
11use encrypt::encrypt;
12
13/// Shortcut for aborting due to a syntax error.
14macro_rules! syntax_error {
15    ($macro_name:ident) => {
16        match $macro_name {
17            "envc" => abort_call_site!("Invalid syntax. Valid forms are `envc!(\"VAR_NAME\")` and `envc!(\"VAR_NAME\", \"custom error message\")`."),
18
19            "option_envc" => abort_call_site!("Invalid syntax. Expected input of the form `option_envc!(\"VAR_NAME\")`"),
20
21            _ => panic!("Unknown macro name")
22        }
23    };
24}
25
26#[allow(missing_docs)]
27#[proc_macro_error]
28#[proc_macro]
29pub fn envc(tokens: TokenStream) -> TokenStream {
30    let (env_var_key, abort_message) = match parse(tokens, "envc") {
31        Input::VariableName(variable_name) => (
32            variable_name.clone(),
33            format!("environment variable `{}` not defined", &variable_name),
34        ),
35        Input::VariableNameAndAbortMessage {
36            variable_name,
37            abort_message,
38        } => (variable_name, abort_message),
39    };
40
41    match env::var(&env_var_key) {
42        Ok(unencrypted_variable) => encrypt(unencrypted_variable),
43
44        Err(VarError::NotUnicode(_)) => {
45            abort_call_site!(
46                "Environment variable ${} contains non-unicode value",
47                &env_var_key
48            )
49        }
50
51        Err(VarError::NotPresent) => abort_call_site!("{}", abort_message),
52    }
53}
54
55#[allow(missing_docs)]
56#[proc_macro_error]
57#[proc_macro]
58pub fn option_envc(tokens: TokenStream) -> TokenStream {
59    let env_var_key = match parse(tokens, "option_envc") {
60        Input::VariableName(variable_name) => variable_name,
61        Input::VariableNameAndAbortMessage { .. } => abort_call_site!(
62            "Invalid syntax. Expected input of the form `option_envc!(\"VAR_NAME\")`"
63        ),
64    };
65
66    match env::var(&env_var_key) {
67        Err(VarError::NotUnicode(_)) => {
68            abort_call_site!(
69                "Environment variable ${} contains non-unicode value",
70                &env_var_key
71            )
72        }
73        maybe_missing => encrypt(maybe_missing.ok()),
74    }
75}
76
77/// Returns `Some(value)` if the provided literal was a string literal,
78/// or `None` otherwise.
79fn stringify(literal: &Literal) -> Option<String> {
80    let stringified = literal.to_string();
81    if stringified.starts_with('"') && stringified.ends_with('"') {
82        Some(stringified[1..stringified.len() - 1].to_owned())
83    } else {
84        None
85    }
86}
87
88/// Possible inputs to the [`envc!`] and [`option_envc!`] macros.
89enum Input {
90    /// A variable to inspect at compile time
91    VariableName(String),
92
93    /// A variable to inspect at compile time and a custom abort message if it's missing
94    VariableNameAndAbortMessage {
95        /// The variable to inspect
96        variable_name: String,
97
98        /// The message to display if the variable is missing
99        abort_message: String,
100    },
101}
102
103/// Parses a [`TokenStream`] into [`Input`]
104fn parse(tokens: TokenStream, macro_name: &str) -> Input {
105    let tokens_vec = tokens.into_iter().collect::<Vec<_>>();
106
107    match *tokens_vec.as_slice() {
108        // `envc!("MY_VAR")`
109        [TokenTree::Literal(ref variable_literal)] => {
110            if let Some(variable) = stringify(variable_literal) {
111                Input::VariableName(variable)
112            } else {
113                syntax_error!(macro_name)
114            }
115        }
116
117        // `envc!("MY_VAR", "custom error message")
118        [TokenTree::Literal(ref variable_literal), TokenTree::Punct(ref comma), TokenTree::Literal(ref message_literal)] => {
119            match (
120                stringify(variable_literal),
121                comma.as_char(),
122                stringify(message_literal),
123            ) {
124                (Some(variable_name), ',', Some(abort_message)) => {
125                    Input::VariableNameAndAbortMessage {
126                        variable_name,
127                        abort_message,
128                    }
129                }
130                _ => syntax_error!(macro_name),
131            }
132        }
133        _ => syntax_error!(macro_name),
134    }
135}