Skip to content

feat: skip for derive #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 30, 2025
Merged
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name = "init4-bin-base"
description = "Internal utilities for binaries produced by the init4 team"
keywords = ["init4", "bin", "base"]

version = "0.3.0"
version = "0.3.1"
edition = "2021"
rust-version = "1.81"
authors = ["init4", "James Prestwich"]
Expand Down
2 changes: 1 addition & 1 deletion from-env-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ syn = { version = "2.0.100", features = ["full", "parsing"] }
proc-macro = true

[dev-dependencies]
init4-bin-base = "0.2"
init4-bin-base = "0.3"
31 changes: 27 additions & 4 deletions from-env-derive/src/field.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use heck::ToPascalCase;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Ident, LitStr, spanned::Spanned};
use syn::{spanned::Spanned, Ident, LitStr};

/// A parsed Field of a struct
pub(crate) struct Field {
Expand All @@ -11,6 +11,7 @@ pub(crate) struct Field {

optional: bool,
infallible: bool,
skip: bool,
desc: Option<String>,

_attrs: Vec<syn::Attribute>,
Expand All @@ -26,13 +27,18 @@ impl TryFrom<&syn::Field> for Field {
let mut env_var = None;
let mut infallible = false;
let mut desc = None;
let mut skip = false;

field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("from_env"))
.for_each(|attr| {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip") {
skip = true;
return Ok(());
}
if meta.path.is_ident("optional") {
optional = true;
return Ok(());
Expand Down Expand Up @@ -68,6 +74,7 @@ impl TryFrom<&syn::Field> for Field {
field_name,
field_type,
optional,
skip,
infallible,
desc,
_attrs: field
Expand Down Expand Up @@ -115,7 +122,7 @@ impl Field {

/// Produces the name of the enum variant for the field
pub(crate) fn enum_variant_name(&self, idx: usize) -> Option<TokenStream> {
if self.infallible {
if self.skip || self.infallible {
return None;
}

Expand Down Expand Up @@ -143,10 +150,16 @@ impl Field {

/// Produces the a line for the `inventory` function
/// of the form
/// items.push(...);
/// items.push(...); // (if this is a FromEnvVar)
/// or
/// items.extend(...); // (if this is a FromEnv)
/// or
/// items.extend(...);
/// // nothing if this is a skip
pub(crate) fn expand_env_item_info(&self) -> TokenStream {
if self.skip {
return quote! {};
}

let description = self.desc.clone().unwrap_or_default();
let optional = self.optional;

Expand Down Expand Up @@ -197,10 +210,20 @@ impl Field {

// // OR
// let field_name = FromEnv::from_env().map_err()?;

// // OR
// let field_name = Default::default();

//```
let variant = self.enum_variant_name(idx);
let field_name = self.field_name(idx);

if self.skip {
return quote! {
let #field_name = Default::default();
};
}

let fn_invoc = if let Some(ref env_var) = self.env_var {
quote! { FromEnvVar::from_env_var(#env_var) }
} else {
Expand Down
2 changes: 1 addition & 1 deletion from-env-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use proc_macro::TokenStream as Ts;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};
use syn::{parse_macro_input, DeriveInput};

mod field;
use field::Field;
Expand Down
10 changes: 10 additions & 0 deletions from-env-derive/tests/macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub struct FromEnvTest {
desc = "Oliver is an Option<String>"
)]
pub oliver: Option<String>,

#[from_env(skip)]
memo: std::sync::OnceLock<String>,
}

#[derive(Debug, FromEnv)]
Expand All @@ -36,6 +39,13 @@ pub struct Nested {
pub from_env_test: FromEnvTest,
}

impl FromEnvTest {
/// Get the memoized value
pub fn get_memo(&self) -> &str {
self.memo.get_or_init(|| "hello world".to_string())
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
173 changes: 173 additions & 0 deletions src/utils/from_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ use std::{convert::Infallible, env::VarError, num::ParseIntError, str::FromStr};
/// occur when trying to create an instance of the struct from environment
/// variables. This error type is used in the `FromEnv` trait implementation.
///
/// ## Attributes
///
/// The macro supports the following attributes:
/// - `var = ""`: The name of the environment variable. This is required if the
/// prop implements [`FromEnvVar`].
/// - `desc = ""`: A description of the environment variable. This is required
/// if the prop implements [`FromEnvVar`].
/// - `optional`: Marks the prop as optional. This is currently only used in the
/// generated `fn inventory`, and is informational.
/// - `infallible`: Marks the prop as infallible. This means that the prop
/// cannot fail to be parsed after the environment variable is loaded.
/// - `skip`: Marks the prop as skipped. This means that the prop will not be
/// loaded from the environment, and will be generated via
/// `Default::default()` instead.
///
/// ## Conditions of use
///
/// There are a few usage requirements:
Expand Down Expand Up @@ -266,6 +281,87 @@ pub trait FromEnv: core::fmt::Debug + Sized + 'static {
fn from_env() -> Result<Self, FromEnvErr<Self::Error>>;
}

impl<T> FromEnv for Option<T>
where
T: FromEnv,
{
type Error = T::Error;

fn inventory() -> Vec<&'static EnvItemInfo> {
T::inventory()
}

fn check_inventory() -> Result<(), Vec<&'static EnvItemInfo>> {
T::check_inventory()
}

fn from_env() -> Result<Self, FromEnvErr<Self::Error>> {
match T::from_env() {
Ok(v) => Ok(Some(v)),
Err(FromEnvErr::Empty(_)) | Err(FromEnvErr::EnvError(_, _)) => Ok(None),
Err(e) => Err(e),
}
}
}

impl<T> FromEnv for Box<T>
where
T: FromEnv,
{
type Error = T::Error;

fn inventory() -> Vec<&'static EnvItemInfo> {
T::inventory()
}

fn check_inventory() -> Result<(), Vec<&'static EnvItemInfo>> {
T::check_inventory()
}

fn from_env() -> Result<Self, FromEnvErr<Self::Error>> {
T::from_env().map(Box::new)
}
}

impl<T> FromEnv for std::sync::Arc<T>
where
T: FromEnv,
{
type Error = T::Error;

fn inventory() -> Vec<&'static EnvItemInfo> {
T::inventory()
}

fn check_inventory() -> Result<(), Vec<&'static EnvItemInfo>> {
T::check_inventory()
}

fn from_env() -> Result<Self, FromEnvErr<Self::Error>> {
T::from_env().map(std::sync::Arc::new)
}
}

impl<T, U> FromEnv for std::borrow::Cow<'static, U>
where
T: FromEnv,
U: std::borrow::ToOwned<Owned = T> + core::fmt::Debug,
{
type Error = T::Error;

fn inventory() -> Vec<&'static EnvItemInfo> {
T::inventory()
}

fn check_inventory() -> Result<(), Vec<&'static EnvItemInfo>> {
T::check_inventory()
}

fn from_env() -> Result<Self, FromEnvErr<Self::Error>> {
T::from_env().map(std::borrow::Cow::Owned)
}
}

/// Trait for loading primitives from the environment. These are simple types
/// that should correspond to a single environment variable. It has been
/// implemented for common integer types, [`String`], [`url::Url`],
Expand Down Expand Up @@ -331,6 +427,49 @@ pub trait FromEnvVar: core::fmt::Debug + Sized + 'static {

/// Load the primitive from the environment at the given variable.
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>>;

/// Load the primitive from the environment at the given variable. If the
/// variable is unset or empty, return the default value.
///
/// This function will return an error if the environment variable is set
/// but cannot be parsed.
fn from_env_var_or(env_var: &str, default: Self) -> Result<Self, FromEnvErr<Self::Error>> {
match Self::from_env_var(env_var) {
Ok(v) => Ok(v),
Err(FromEnvErr::Empty(_)) | Err(FromEnvErr::EnvError(_, _)) => Ok(default),
Err(e) => Err(e),
}
}

/// Load the primitive from the environment at the given variable. If the
/// variable is unset or empty, call the provided function to get the
/// default value.
///
/// This function will return an error if the environment variable is set
/// but cannot be parsed.
fn from_env_var_or_else(
env_var: &str,
default: impl FnOnce() -> Self,
) -> Result<Self, FromEnvErr<Self::Error>> {
match Self::from_env_var(env_var) {
Ok(v) => Ok(v),
Err(FromEnvErr::Empty(_)) | Err(FromEnvErr::EnvError(_, _)) => Ok(default()),
Err(e) => Err(e),
}
}

/// Load the primitive from the environment at the given variable. If the
/// variable is unset or empty, return the value generated by
/// [`Default::default`].
///
/// This function will return an error if the environment variable is set
/// but cannot be parsed.
fn from_env_var_or_default(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>>
where
Self: Default,
{
Self::from_env_var_or_else(env_var, Self::default)
}
}

impl<T> FromEnvVar for Option<T>
Expand All @@ -348,6 +487,40 @@ where
}
}

impl<T> FromEnvVar for Box<T>
where
T: FromEnvVar,
{
type Error = T::Error;

fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
T::from_env_var(env_var).map(Box::new)
}
}

impl<T> FromEnvVar for std::sync::Arc<T>
where
T: FromEnvVar,
{
type Error = T::Error;

fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
T::from_env_var(env_var).map(std::sync::Arc::new)
}
}

impl<T, U> FromEnvVar for std::borrow::Cow<'static, U>
where
T: FromEnvVar,
U: std::borrow::ToOwned<Owned = T> + core::fmt::Debug,
{
type Error = T::Error;

fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr<Self::Error>> {
T::from_env_var(env_var).map(std::borrow::Cow::Owned)
}
}

impl FromEnvVar for String {
type Error = std::convert::Infallible;

Expand Down