1#![doc = include_str!("../examples/http.rs")]
6#![doc = include_str!("../examples/http.md")]
10use 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#[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" )
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 _ => { },
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}