#![doc = include_str!("../examples/http.rs")]
#![doc = include_str!("../examples/http.md")]
use convert_case::{Case, Casing};
use proc_macro::TokenStream as CompilerTokenStream;
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{
parse_macro_input, Attribute, Data, DataEnum, DataStruct, DeriveInput, Lit, LitStr, Meta,
};
#[macro_use]
extern crate proc_macro_error;
mod struct_attributes {
use bae::FromAttributes;
use syn::LitStr;
#[derive(Debug, Default, FromAttributes)]
pub struct Env {
pub prefix: Option<LitStr>,
}
}
use struct_attributes::Env as StructAttributes;
mod field_attributes {
use bae::FromAttributes;
use syn::{Expr, LitStr};
#[derive(Debug, Default, FromAttributes)]
pub struct Env {
pub rename: Option<LitStr>,
pub no_prefix: Option<()>,
pub skip: Option<()>,
pub default: Option<Expr>,
pub flatten: Option<()>,
}
}
use field_attributes::Env as FieldAttributes;
mod enum_attributes {
use bae::FromAttributes;
use syn::LitStr;
#[derive(Debug, Default, FromAttributes)]
pub struct Env {
pub rename_all: Option<LitStr>,
}
}
use enum_attributes::Env as EnumAttributes;
mod variant_attributes {
use bae::FromAttributes;
use syn::LitStr;
#[derive(Debug, Default, FromAttributes)]
pub struct Env {
pub rename: Option<LitStr>,
pub skip: Option<()>,
pub default: Option<()>,
}
}
use variant_attributes::Env as VariantAttributes;
#[proc_macro_derive(EnvConfig, attributes(env))]
#[proc_macro_error]
pub fn derive_config(input: CompilerTokenStream) -> CompilerTokenStream {
let input = parse_macro_input!(input as DeriveInput);
match input.data {
Data::Struct(data) => {
let container_attrs = match StructAttributes::try_from_attributes(&input.attrs) {
Ok(attrs) => attrs.unwrap_or_default(),
Err(e) => {
emit_error!(input.ident, format!("{}: {}", input.ident, e));
return CompilerTokenStream::new();
}
};
derive_config_struct(input.ident, container_attrs, data)
}
Data::Enum(data) => {
let container_attrs = match EnumAttributes::try_from_attributes(&input.attrs) {
Ok(attrs) => attrs.unwrap_or_default(),
Err(e) => {
emit_error!(input.ident, format!("{}: {}", input.ident, e));
return CompilerTokenStream::new();
}
};
derive_config_enum(input.ident, container_attrs, data)
}
Data::Union(data) => {
emit_error!(
data.union_token,
"deriving EnvConfig only works on structs and enums"
);
TokenStream::new()
}
}
.into()
}
#[allow(unused_parens)]
fn derive_config_struct(
struct_name: Ident,
container_attrs: StructAttributes,
data: DataStruct,
) -> TokenStream {
let prefix = container_attrs
.prefix
.map(|s| s.value())
.unwrap_or_else(|| "".to_owned());
let mut default_code = TokenStream::new();
let mut from_env_code = TokenStream::new();
let mut doc_code = TokenStream::new();
for field in data.fields {
let field_ident = match field.ident {
Some(ident) => ident,
None => {
emit_error!(
field.ty,
"deriving EnvConfig only works on structs with named fields"
);
return TokenStream::new();
}
};
let field_attrs: FieldAttributes = match FieldAttributes::try_from_attributes(&field.attrs)
{
Ok(attrs) => attrs.unwrap_or_default(),
Err(e) => {
emit_error!(field_ident, format!("{}: {}", field_ident, e));
return TokenStream::new();
}
};
if let Some(default) = field_attrs.default {
default_code.extend(quote! { #field_ident: #default, });
} else {
default_code.extend(quote! { #field_ident: ::std::default::Default::default(), });
}
if field_attrs.skip.is_some() {
continue;
}
if field_attrs.flatten.is_some() {
emit_error!(
field_ident,
format!(
"{}: {}",
field_ident,
"#[env(flatten)] is not yet implemented" )
);
continue;
}
let mut name = String::new();
if field_attrs.no_prefix.is_none() {
name.push_str(&prefix);
}
name.push_str(
&field_attrs
.rename
.map(|s| s.value())
.unwrap_or_else(|| field_ident.to_string())
.to_uppercase(),
);
let field_doc = match doc(&field.attrs[..]) {
Ok(doc) => doc,
Err(_) => return TokenStream::new(),
};
from_env_code.extend(quote! {
#name => parsed.#field_ident = ::std::str::FromStr::from_str(&val)?,
});
doc_code.extend(quote! {
doc.variable(#name, &self.#field_ident.to_string())?;
doc.plain(#field_doc)?;
});
}
let ts = quote! {
impl ::std::default::Default for #struct_name {
fn default() -> Self {
Self {
#default_code
}
}
}
impl #struct_name {
pub fn from_env(vars: impl ::std::iter::Iterator<Item = (::std::string::String, ::std::string::String)>) -> Result<Self, Box<dyn ::std::error::Error>> {
let mut parsed = Self::default();
for (key, val) in vars {
match key.as_str() {
#from_env_code
_ => { },
}
}
Ok(parsed)
}
pub fn document_env<D: ::doc_writer::DocumentationWriter>(&self, doc: &mut D) -> Result<(), D::Error> {
#doc_code
Ok(())
}
}
};
ts
}
fn derive_config_enum(
enum_name: Ident,
container_attrs: EnumAttributes,
data: DataEnum,
) -> TokenStream {
let case = match parse_case(container_attrs.rename_all) {
Ok(case) => case,
Err(_) => return TokenStream::new(),
};
let mut default_code = TokenStream::new();
let mut doc_code = TokenStream::new();
let mut parse_code = TokenStream::new();
let mut display_code = TokenStream::new();
let mut valid_list = String::new();
for variant in data.variants {
let variant_ident = variant.ident;
let variant_attrs: VariantAttributes =
match VariantAttributes::try_from_attributes(&variant.attrs) {
Ok(attrs) => attrs.unwrap_or_default(),
Err(e) => {
emit_error!(variant_ident, format!("{}: {}", variant_ident, e));
return TokenStream::new();
}
};
let name = &variant_attrs
.rename
.map(|s| s.value())
.unwrap_or_else(|| variant_ident.to_string())
.to_case(case);
let lower_name = name.to_ascii_lowercase();
if variant_attrs.default.is_some() {
default_code = quote! {
impl ::std::default::Default for #enum_name {
fn default() -> Self {
Self::#variant_ident
}
}
}
}
display_code.extend(quote! {
Self::#variant_ident => write!(f, #name),
});
if variant_attrs.skip.is_some() {
continue;
}
valid_list.push_str(&format!("{:?}, ", name));
let variant_doc = match doc(&variant.attrs[..]) {
Ok(attrs) => attrs,
Err(_) => return TokenStream::new(),
};
parse_code.extend(quote! {
#lower_name => Self::#variant_ident,
});
doc_code.extend(quote! {
doc.variant(#name)?;
doc.plain(#variant_doc)?;
});
}
let error_name = syn::Ident::new(
&format!("Parse{}Error", enum_name.to_string()),
enum_name.span(),
);
let valid_list = valid_list.trim_end_matches(&[',', ' '][..]);
let ts = quote! {
#default_code
#[doc(hidden)]
#[derive(Debug)]
pub struct #error_name {
got: String
}
impl ::std::fmt::Display for #error_name {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
write!(f, "expected one of {}, got {:?}", #valid_list, self.got)
}
}
impl ::std::error::Error for #error_name {}
impl ::std::str::FromStr for #enum_name {
type Err = #error_name;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lower_s = s.to_ascii_lowercase();
Ok(match lower_s.as_str() {
#parse_code
_ => return Err(#error_name { got: s.to_string() })
})
}
}
impl ::std::fmt::Display for #enum_name {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match self {
#display_code
}
}
}
impl #enum_name {
pub fn document_enum<D: ::doc_writer::DocumentationWriter>(doc: &mut D) -> Result<(), D::Error> {
#doc_code
Ok(())
}
}
};
ts
}
fn parse_case(pattern: Option<LitStr>) -> Result<Case, ()> {
if let Some(pattern) = pattern {
Ok(match pattern.value().as_str() {
"lowercase" => Case::Flat,
"UPPERCASE" => Case::UpperFlat,
"PascalCase" => Case::Pascal,
"camelCase" => Case::Camel,
"snake_case" => Case::Snake,
"SCREAMING_SNAKE_CASE" => Case::ScreamingSnake,
"kebab-case" => Case::Kebab,
"SCREAMING-KEBAB-CASE" => Case::Cobol,
_ => {
emit_error!(
pattern,
r#"#[env(rename_all)] only accepts "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", and "SCREAMING-KEBAB-CASE"#
);
return Err(());
}
})
} else {
Ok(Case::Pascal)
}
}
fn doc(attrs: &[Attribute]) -> Result<String, ()> {
let mut doc = String::new();
for attr in attrs {
if !attr.path.is_ident("doc") {
continue;
}
let doc_attr = match attr.parse_meta() {
Ok(attr) => attr,
Err(e) => {
emit_error!(attr.tokens, e.to_string());
return Err(());
}
};
match doc_attr {
Meta::NameValue(kv) => match kv.lit {
Lit::Str(s) => {
doc.push_str(&s.value());
doc.push('\n')
}
_ => {
emit_error!(
attr.tokens,
"#[doc] attributes must consist of literal strings, like #[doc = \"Info\"]"
);
return Err(());
}
},
_ => {
emit_error!(
attr.tokens,
"#[doc] attributes must be assignments, like #[doc = \"Info\"]"
);
return Err(());
}
}
}
Ok(doc)
}