Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.alternative
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CODEGEN_TEST_VAR1="bye!"
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on:
branches:
- master

env:
CODEGEN_TEST_VAR1: goodbye!

jobs:
tests:
strategy:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Added

- added `path` and `override_` to `dotenvy_macro::dotenv` ([PR #159](https://github.com/allan2/dotenvy/pull/159))
- added `dotenvy_macro::option_dotenv` that evaluates to an `Option<&'static str>` ([PR #159](https://github.com/allan2/dotenvy/pull/159))

### Changed
- update to 2021 edition
- update MSRV to 1.74.0
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ For more advanced usage, `EnvLoader::load_and_modify` can be used.

## Compile-time loading

The `dotenv!` macro provided by `dotenvy_macro` crate can be used.
The `dotenv!` and `option_dotenv!` macros' provided by `dotenvy_macro` crate can be used.

## Minimum Supported Rust Version

Expand Down
3 changes: 3 additions & 0 deletions ci-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ set -e

MSRV="1.74.0"

# For dotenvy_macro tests
export CODEGEN_TEST_VAR1="goodbye!"

echo "MSRV set to $MSRV"

echo "cargo fmt"
Expand Down
4 changes: 2 additions & 2 deletions dotenv_codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ proc-macro = true
dotenvy = { path = "../dotenvy" }
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true
syn = { version = "2", features = ["full"] }

[lints]
workspace = true
workspace = true
282 changes: 235 additions & 47 deletions dotenv_codegen/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,68 +1,256 @@
use dotenvy::EnvLoader;
use dotenvy::{EnvLoader, EnvSequence};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use std::env::{self, VarError};
use syn::{parse::Parser, punctuated::Punctuated, spanned::Spanned, LitStr, Token};
use std::io;
use syn::{parse::Parse, Ident, LitBool, LitStr, Token};

struct DotenvInput {
path: Option<LitStr>,
override_: bool,
var_name: LitStr,
}

impl Parse for DotenvInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut path = None;
let mut var_name = None;
let mut override_ = None;

while !input.is_empty() {
if let Ok(ident) = input.parse::<Ident>() {
input.parse::<Token![=]>()?;
match ident.to_string().as_str() {
"path" => {
if path.is_some() {
return Err(syn::Error::new(
ident.span(),
"each attribute must be set only once",
));
}
path = Some(input.parse::<LitStr>()?);
}
"override_" => {
if override_.is_some() {
return Err(syn::Error::new(
ident.span(),
"each attribute must be set only once",
));
}
override_ = Some(input.parse::<LitBool>()?.value);
}
"var" => {
if var_name.is_some() {
return Err(syn::Error::new(
ident.span(),
"variable name must be set only once",
));
}
var_name = Some(input.parse::<LitStr>()?);
}
_ => {
return Err(syn::Error::new(
ident.span(),
format!("unkown attribute: {ident}"),
))
}
}
} else if let Ok(s) = input.parse::<LitStr>() {
if var_name.is_some() {
return Err(syn::Error::new(
s.span(),
"unexpected token in macro input (variable name must be set only once)",
));
}
var_name = Some(s);
} else {
return Err(syn::Error::new(
input.span(),
"unexpected token in macro input",
));
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}

let Some(var_name) = var_name else {
return Err(syn::Error::new(
input.span(),
"environment variable name not defined",
));
};

Ok(Self {
path,
override_: override_.unwrap_or(true),
var_name,
})
}
}

/// Loads an environment variable from a file at compile time.
///
/// This macro will expand to the value of the named environment variable at compile
/// time, yielding an expression of type `&'static str`. Use
/// [`dotenvy::var`] instead if you want to read the value at runtime.
///
/// If the environment variable is not defined, then a compilation error will be
/// emitted. To not emit a compile error, use the [`option_dotenv!`](option_dotenv)
/// macro instead. A compilation error will also be emitted if the environment
/// variable is not a valid Unicode string or if reading the .env file failed.
///
/// # Examples
///
/// Basic usage:
///
/// ```rust ignore
/// # use dotenvy_macro::dotenv;
/// assert_eq!(dotenv!("VARIABLE_1"), "value");
/// ```
///
/// ```rust compile_fail
/// # use dotenvy_macro::dotenv;
/// const UNSET_VAR: &str = dotenv!("UNSET_VAR");
/// ```
///
/// Custom attributes:
///
/// ```rust ignore
/// # use dotenvy_macro::dotenv;
/// // Does not override current env with .env contents
/// const NOT_OVERRIDEN: &str = dotenv!("VARIABLE_1", override_ = false);
/// // Reads from custom file path
/// const CUSTOM_PATH: &str = dotenv!("VARIABLE_PROD", path = ".env.prod");
/// // Specifying the `var` attribute
/// const CUSTOM_PATH: &str = dotenv!(var = "VARIABLE_1");
/// ```
#[proc_macro]
/// TODO: add safety warning
pub fn dotenv(input: TokenStream) -> TokenStream {
let input = input.into();
unsafe { dotenv_inner(input) }.into()
dotenv_inner(input).into()
}

unsafe fn dotenv_inner(input: TokenStream2) -> TokenStream2 {
let loader = EnvLoader::new();
if let Err(e) = unsafe { loader.load_and_modify() } {
let msg = e.to_string();
return quote! {
compile_error!(#msg);
};
}
fn dotenv_inner(input: TokenStream2) -> TokenStream2 {
let args = match syn::parse2::<DotenvInput>(input) {
Ok(a) => a,
Err(e) => return e.into_compile_error(),
};

let sequence = if args.override_ {
EnvSequence::EnvThenInput
} else {
EnvSequence::InputThenEnv
};

let path = args.path.map_or_else(|| "./.env".to_owned(), |p| p.value());
let var_name = args.var_name.value();

let loader = EnvLoader::new().path(&path).sequence(sequence).load();

match expand_env(input) {
Ok(stream) => stream,
Err(e) => e.to_compile_error(),
match loader.as_ref().map(|l| l.get(&var_name)) {
Ok(Some(v)) => quote!(#v),
Ok(None) => {
let msg = format!("environment variable `{var_name}` not set");
quote! {
compile_error!(#msg)
}
}
Err(e) => {
if let dotenvy::Error::Io(ioe, _) = e {
if ioe.kind() == io::ErrorKind::NotFound {
if let Ok(var) = std::env::var(&var_name) {
return quote!(#var);
} else {
return quote! {
compile_error!("environment variable not set and env file missing")
};
}
}
}
let msg = e.to_string();
quote! {
compile_error!(#msg)
}
}
}
}

fn expand_env(input_raw: TokenStream2) -> syn::Result<TokenStream2> {
let args = <Punctuated<syn::LitStr, Token![,]>>::parse_terminated
.parse(input_raw.into())
.expect("expected macro to be called with a comma-separated list of string literals");
/// Optionally loads an environment variable from a file at compile time.
///
/// If the named environment variable is present at compile time, this will expand
/// into an expression of type `Option<&'static str>` whose value is `Some` of the
/// value of the environment variable (a compilation error will be emitted if the
/// environment variable is not a valid Unicode string or if reading the .env file
/// failed). If either the environment variable or the .env file is not present,
/// then this will expand to `None`. Use [`dotenvy::var`] instead if
/// you want to read the value at runtime.
///
/// A compile time error is only emitted when using this macro if the environment
/// variable exists and is not a valid Unicode string or if reading the .env file
/// failed. To also emit a compile error if the environment variable is not present,
/// use the [`dotenv!`](dotenv) macro instead.
///
/// # Examples
///
/// Basic usage:
///
/// ```rust no_run
/// # use dotenvy_macro::option_dotenv;
/// assert_eq!(option_dotenv!("UNSET_VAR"), None);
/// assert_eq!(option_dotenv!("SET_VAR"), Some("value"));
/// ```
///
/// Custom attributes:
///
/// ```rust no_run
/// # use dotenvy_macro::option_dotenv;
/// // Does not override current env with .env contents
/// const NOT_OVERRIDEN: Option<&str> = option_dotenv!("VARIABLE_1", override_ = false);
/// // Reads from custom file path
/// const CUSTOM_PATH: Option<&str> = option_dotenv!("VARIABLE_PROD", path = ".env.prod");
/// // Specifying the `var` attribute
/// const VAR_ATTR: Option<&str> = option_dotenv!(var = "VARIABLE_1");
/// ```
#[proc_macro]
pub fn option_dotenv(input: TokenStream) -> TokenStream {
let input = input.into();
option_dotenv_inner(input).into()
}

let mut iter = args.iter();
fn option_dotenv_inner(input: TokenStream2) -> TokenStream2 {
let args = match syn::parse2::<DotenvInput>(input) {
Ok(a) => a,
Err(e) => return e.into_compile_error(),
};

let var_name = iter
.next()
.ok_or_else(|| syn::Error::new(args.span(), "dotenv! takes 1 or 2 arguments"))?
.value();
let err_msg = iter.next();
let sequence = if args.override_ {
EnvSequence::EnvThenInput
} else {
EnvSequence::InputThenEnv
};

if iter.next().is_some() {
return Err(syn::Error::new(
args.span(),
"dotenv! takes 1 or 2 arguments",
));
}
let path = args.path.map_or_else(|| "./.env".to_owned(), |p| p.value());
let var_name = args.var_name.value();

match env::var(&var_name) {
Ok(val) => Ok(quote!(#val)),
Err(e) => Err(syn::Error::new(
var_name.span(),
err_msg.map_or_else(
|| match e {
VarError::NotPresent => {
format!("environment variable `{var_name}` not defined")
}
let loader = EnvLoader::new().path(&path).sequence(sequence).load();

VarError::NotUnicode(s) => {
format!("environment variable `{var_name}` was not valid Unicode: {s:?}",)
match loader.as_ref().map(|l| l.get(&var_name)) {
Ok(Some(v)) => quote!(Some(#v)),
Ok(None) => quote!(None::<&str>),
Err(e) => {
if let dotenvy::Error::Io(ioe, _) = e {
if ioe.kind() == io::ErrorKind::NotFound {
if let Ok(var) = std::env::var(&var_name) {
return quote!(Some(#var));
}
},
LitStr::value,
),
)),
return quote!(None::<&str>);
}
}
let msg = e.to_string();
quote! {
compile_error!(#msg)
}
}
}
}
Loading