env_config/
lib.rs

1//! Adds proc macro that adds methods to parse environment variables and write documentation using [doc-writer].
2//!
3//! # Examples
4//! ```
5#![doc = include_str!("../examples/http.rs")]
6//! ```
7//!
8//! Would yield the following documentation:
9#![doc = include_str!("../examples/http.md")]
10//!
11//! [doc-writer]: https://docs.rs/doc-writer/
12
13use convert_case::{Case, Casing};
14use proc_macro::TokenStream as CompilerTokenStream;
15use proc_macro2::{Ident, TokenStream};
16use quote::quote;
17use syn::{
18    parse_macro_input, Attribute, Data, DataEnum, DataStruct, DeriveInput, Lit, LitStr, Meta,
19};
20
21#[macro_use]
22extern crate proc_macro_error;
23
24mod struct_attributes {
25    use bae::FromAttributes;
26    use syn::LitStr;
27
28    #[derive(Debug, Default, FromAttributes)]
29    pub struct Env {
30        pub prefix: Option<LitStr>,
31    }
32}
33
34use struct_attributes::Env as StructAttributes;
35
36mod field_attributes {
37    use bae::FromAttributes;
38    use syn::{Expr, LitStr};
39
40    #[derive(Debug, Default, FromAttributes)]
41    pub struct Env {
42        pub rename: Option<LitStr>,
43        pub no_prefix: Option<()>,
44        pub skip: Option<()>,
45
46        pub default: Option<Expr>,
47        pub flatten: Option<()>,
48    }
49}
50
51use field_attributes::Env as FieldAttributes;
52
53mod enum_attributes {
54    use bae::FromAttributes;
55    use syn::LitStr;
56
57    #[derive(Debug, Default, FromAttributes)]
58    pub struct Env {
59        pub rename_all: Option<LitStr>,
60    }
61}
62
63use enum_attributes::Env as EnumAttributes;
64
65mod variant_attributes {
66    use bae::FromAttributes;
67    use syn::LitStr;
68
69    #[derive(Debug, Default, FromAttributes)]
70    pub struct Env {
71        pub rename: Option<LitStr>,
72        pub skip: Option<()>,
73
74        pub default: Option<()>,
75    }
76}
77
78use variant_attributes::Env as VariantAttributes;
79
80/// Derives `EnvConfig` as described in the [module description][self].
81#[proc_macro_derive(EnvConfig, attributes(env))]
82#[proc_macro_error]
83pub fn derive_config(input: CompilerTokenStream) -> CompilerTokenStream {
84    let input = parse_macro_input!(input as DeriveInput);
85    match input.data {
86        Data::Struct(data) => {
87            let container_attrs = match StructAttributes::try_from_attributes(&input.attrs) {
88                Ok(attrs) => attrs.unwrap_or_default(),
89                Err(e) => {
90                    emit_error!(input.ident, format!("{}: {}", input.ident, e));
91                    return CompilerTokenStream::new();
92                }
93            };
94            derive_config_struct(input.ident, container_attrs, data)
95        }
96        Data::Enum(data) => {
97            let container_attrs = match EnumAttributes::try_from_attributes(&input.attrs) {
98                Ok(attrs) => attrs.unwrap_or_default(),
99                Err(e) => {
100                    emit_error!(input.ident, format!("{}: {}", input.ident, e));
101                    return CompilerTokenStream::new();
102                }
103            };
104            derive_config_enum(input.ident, container_attrs, data)
105        }
106        Data::Union(data) => {
107            emit_error!(
108                data.union_token,
109                "deriving EnvConfig only works on structs and enums"
110            );
111            TokenStream::new()
112        }
113    }
114    .into()
115}
116
117#[allow(unused_parens)]
118fn derive_config_struct(
119    struct_name: Ident,
120    container_attrs: StructAttributes,
121    data: DataStruct,
122) -> TokenStream {
123    let prefix = container_attrs
124        .prefix
125        .map(|s| s.value())
126        .unwrap_or_else(|| "".to_owned());
127
128    let mut default_code = TokenStream::new();
129    let mut from_env_code = TokenStream::new();
130    let mut doc_code = TokenStream::new();
131
132    for field in data.fields {
133        let field_ident = match field.ident {
134            Some(ident) => ident,
135            None => {
136                emit_error!(
137                    field.ty,
138                    "deriving EnvConfig only works on structs with named fields"
139                );
140                return TokenStream::new();
141            }
142        };
143
144        let field_attrs: FieldAttributes = match FieldAttributes::try_from_attributes(&field.attrs)
145        {
146            Ok(attrs) => attrs.unwrap_or_default(),
147            Err(e) => {
148                emit_error!(field_ident, format!("{}: {}", field_ident, e));
149                return TokenStream::new();
150            }
151        };
152
153        if let Some(default) = field_attrs.default {
154            default_code.extend(quote! { #field_ident: #default, });
155        } else {
156            default_code.extend(quote! { #field_ident: ::std::default::Default::default(), });
157        }
158
159        if field_attrs.skip.is_some() {
160            continue;
161        }
162        if field_attrs.flatten.is_some() {
163            emit_error!(
164                field_ident,
165                format!(
166                    "{}: {}",
167                    field_ident,
168                    "#[env(flatten)] is not yet implemented" // TODO
169                )
170            );
171            continue;
172        }
173
174        let mut name = String::new();
175        if field_attrs.no_prefix.is_none() {
176            name.push_str(&prefix);
177        }
178        name.push_str(
179            &field_attrs
180                .rename
181                .map(|s| s.value())
182                .unwrap_or_else(|| field_ident.to_string())
183                .to_uppercase(),
184        );
185
186        let field_doc = match doc(&field.attrs[..]) {
187            Ok(doc) => doc,
188            Err(_) => return TokenStream::new(),
189        };
190
191        from_env_code.extend(quote! {
192            #name => parsed.#field_ident = ::std::str::FromStr::from_str(&val)?,
193        });
194        doc_code.extend(quote! {
195            doc.variable(#name, &self.#field_ident.to_string())?;
196            doc.plain(#field_doc)?;
197        });
198    }
199
200    let ts = quote! {
201        impl ::std::default::Default for #struct_name {
202            fn default() -> Self {
203                Self {
204                    #default_code
205                }
206            }
207        }
208
209        impl #struct_name {
210            pub fn from_env(vars: impl ::std::iter::Iterator<Item = (::std::string::String, ::std::string::String)>) -> Result<Self, Box<dyn ::std::error::Error>> {
211                let mut parsed = Self::default();
212
213                for (key, val) in vars {
214                    match key.as_str() {
215                        #from_env_code
216                        _ => { /* skip unknown keys */ },
217                    }
218                }
219                Ok(parsed)
220            }
221
222            pub fn document_env<D: ::doc_writer::DocumentationWriter>(&self, doc: &mut D) -> Result<(), D::Error> {
223                #doc_code
224                Ok(())
225            }
226        }
227    };
228    ts
229}
230
231fn derive_config_enum(
232    enum_name: Ident,
233    container_attrs: EnumAttributes,
234    data: DataEnum,
235) -> TokenStream {
236    let case = match parse_case(container_attrs.rename_all) {
237        Ok(case) => case,
238        Err(_) => return TokenStream::new(),
239    };
240
241    let mut default_code = TokenStream::new();
242    let mut doc_code = TokenStream::new();
243    let mut parse_code = TokenStream::new();
244    let mut display_code = TokenStream::new();
245    let mut valid_list = String::new();
246
247    for variant in data.variants {
248        let variant_ident = variant.ident;
249
250        let variant_attrs: VariantAttributes =
251            match VariantAttributes::try_from_attributes(&variant.attrs) {
252                Ok(attrs) => attrs.unwrap_or_default(),
253                Err(e) => {
254                    emit_error!(variant_ident, format!("{}: {}", variant_ident, e));
255                    return TokenStream::new();
256                }
257            };
258
259        let name = &variant_attrs
260            .rename
261            .map(|s| s.value())
262            .unwrap_or_else(|| variant_ident.to_string())
263            .to_case(case);
264        let lower_name = name.to_ascii_lowercase();
265
266        if variant_attrs.default.is_some() {
267            default_code = quote! {
268                impl ::std::default::Default for #enum_name {
269                    fn default() -> Self {
270                        Self::#variant_ident
271                    }
272                }
273            }
274        }
275
276        display_code.extend(quote! {
277            Self::#variant_ident => write!(f, #name),
278        });
279
280        if variant_attrs.skip.is_some() {
281            continue;
282        }
283
284        valid_list.push_str(&format!("{:?}, ", name));
285
286        let variant_doc = match doc(&variant.attrs[..]) {
287            Ok(attrs) => attrs,
288            Err(_) => return TokenStream::new(),
289        };
290
291        parse_code.extend(quote! {
292            #lower_name => Self::#variant_ident,
293        });
294        doc_code.extend(quote! {
295            doc.variant(#name)?;
296            doc.plain(#variant_doc)?;
297        });
298    }
299
300    let error_name = syn::Ident::new(
301        &format!("Parse{}Error", enum_name.to_string()),
302        enum_name.span(),
303    );
304    let valid_list = valid_list.trim_end_matches(&[',', ' '][..]);
305
306    let ts = quote! {
307        #default_code
308
309        #[doc(hidden)]
310        #[derive(Debug)]
311        pub struct #error_name {
312            got: String
313        }
314
315        impl ::std::fmt::Display for #error_name {
316            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
317                write!(f, "expected one of {}, got {:?}", #valid_list, self.got)
318            }
319        }
320
321        impl ::std::error::Error for #error_name {}
322
323        impl ::std::str::FromStr for #enum_name {
324            type Err = #error_name;
325
326            fn from_str(s: &str) -> Result<Self, Self::Err> {
327                let lower_s = s.to_ascii_lowercase();
328                Ok(match lower_s.as_str() {
329                    #parse_code
330                    _ => return Err(#error_name { got: s.to_string() })
331                })
332            }
333        }
334
335        impl ::std::fmt::Display for #enum_name {
336            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
337                match self {
338                    #display_code
339                }
340            }
341        }
342
343        impl #enum_name {
344            pub fn document_enum<D: ::doc_writer::DocumentationWriter>(doc: &mut D) -> Result<(), D::Error> {
345                #doc_code
346                Ok(())
347            }
348        }
349    };
350    ts
351}
352
353fn parse_case(pattern: Option<LitStr>) -> Result<Case, ()> {
354    if let Some(pattern) = pattern {
355        Ok(match pattern.value().as_str() {
356            "lowercase" => Case::Flat,
357            "UPPERCASE" => Case::UpperFlat,
358            "PascalCase" => Case::Pascal,
359            "camelCase" => Case::Camel,
360            "snake_case" => Case::Snake,
361            "SCREAMING_SNAKE_CASE" => Case::ScreamingSnake,
362            "kebab-case" => Case::Kebab,
363            "SCREAMING-KEBAB-CASE" => Case::Cobol,
364            _ => {
365                emit_error!(
366                    pattern,
367                    r#"#[env(rename_all)] only accepts "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", and "SCREAMING-KEBAB-CASE"#
368                );
369                return Err(());
370            }
371        })
372    } else {
373        Ok(Case::Pascal)
374    }
375}
376
377fn doc(attrs: &[Attribute]) -> Result<String, ()> {
378    let mut doc = String::new();
379    for attr in attrs {
380        if !attr.path.is_ident("doc") {
381            continue;
382        }
383        let doc_attr = match attr.parse_meta() {
384            Ok(attr) => attr,
385            Err(e) => {
386                emit_error!(attr.tokens, e.to_string());
387                return Err(());
388            }
389        };
390        match doc_attr {
391            Meta::NameValue(kv) => match kv.lit {
392                Lit::Str(s) => {
393                    doc.push_str(&s.value());
394                    doc.push('\n')
395                }
396                _ => {
397                    emit_error!(
398                        attr.tokens,
399                        "#[doc] attributes must consist of literal strings, like #[doc = \"Info\"]"
400                    );
401                    return Err(());
402                }
403            },
404            _ => {
405                emit_error!(
406                    attr.tokens,
407                    "#[doc] attributes must be assignments, like #[doc = \"Info\"]"
408                );
409                return Err(());
410            }
411        }
412    }
413    Ok(doc)
414}