Skip to main content

ax_config_macros/
lib.rs

1#![cfg_attr(feature = "nightly", feature(proc_macro_expand))]
2#![doc = include_str!("../README.md")]
3
4use ax_config_gen::{Config, OutputFormat};
5use proc_macro::{LexError, TokenStream};
6use quote::{ToTokens, quote};
7use syn::{
8    Error, Ident, LitStr, Result, Token,
9    parse::{Parse, ParseStream},
10    parse_macro_input,
11};
12
13fn compiler_error<T: ToTokens>(tokens: T, msg: String) -> TokenStream {
14    Error::new_spanned(tokens, msg).to_compile_error().into()
15}
16
17/// Parses TOML config content and expands it into Rust code.
18///
19/// # Example
20///
21/// See the [crate-level documentation][crate].
22#[proc_macro]
23pub fn parse_configs(config_toml: TokenStream) -> TokenStream {
24    #[cfg(feature = "nightly")]
25    let config_toml = match config_toml.expand_expr() {
26        Ok(s) => s,
27        Err(e) => {
28            return Error::new(proc_macro2::Span::call_site(), e.to_string())
29                .to_compile_error()
30                .into();
31        }
32    };
33
34    let config_toml = parse_macro_input!(config_toml as LitStr).value();
35    let code = Config::from_toml(&config_toml).and_then(|cfg| cfg.dump(OutputFormat::Rust));
36    match code {
37        Ok(code) => code
38            .parse()
39            .unwrap_or_else(|e: LexError| compiler_error(config_toml, e.to_string())),
40        Err(e) => compiler_error(config_toml, e.to_string()),
41    }
42}
43
44/// Includes a TOML format config file and expands it into Rust code.
45///
46/// There a three ways to specify the path to the config file, either through the
47/// path itself or through an environment variable.
48///
49/// ```rust,ignore
50/// include_configs!("path/to/config.toml");
51/// // or specify the config file path via an environment variable
52/// include_configs!(path_env = "AX_CONFIG_PATH");
53/// // or with a fallback path if the environment variable is not set
54/// include_configs!(path_env = "AX_CONFIG_PATH", fallback = "path/to/defconfig.toml");
55/// ```
56///
57/// See the [crate-level documentation][crate] for more details.
58#[proc_macro]
59pub fn include_configs(args: TokenStream) -> TokenStream {
60    let args = parse_macro_input!(args as IncludeConfigsArgs);
61    let path = match args {
62        IncludeConfigsArgs::Path(p) => p.value(),
63        IncludeConfigsArgs::PathEnv(env) => {
64            let Ok(path) = std::env::var(env.value()) else {
65                return compiler_error(
66                    &env,
67                    format!("environment variable `{}` not set", env.value()),
68                );
69            };
70            path
71        }
72        IncludeConfigsArgs::PathEnvFallback(env, fallback) => {
73            std::env::var(env.value()).unwrap_or_else(|_| fallback.value())
74        }
75    };
76
77    let root = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
78    let cfg_path = std::path::Path::new(&root).join(&path);
79
80    let Ok(config_toml) = std::fs::read_to_string(&cfg_path) else {
81        return compiler_error(path, format!("failed to read config file: {:?}", cfg_path));
82    };
83
84    quote! {
85        ::ax_config_macros::parse_configs!(#config_toml);
86    }
87    .into()
88}
89
90enum IncludeConfigsArgs {
91    Path(LitStr),
92    PathEnv(LitStr),
93    PathEnvFallback(LitStr, LitStr),
94}
95
96impl Parse for IncludeConfigsArgs {
97    fn parse(input: ParseStream) -> Result<Self> {
98        if input.peek(LitStr) {
99            return Ok(IncludeConfigsArgs::Path(input.parse()?));
100        }
101
102        let mut env = None;
103        let mut fallback = None;
104        while !input.is_empty() {
105            let ident: Ident = input.parse()?;
106            input.parse::<Token![=]>()?;
107            let str: LitStr = input.parse()?;
108
109            match ident.to_string().as_str() {
110                "path_env" => {
111                    if env.is_some() {
112                        return Err(Error::new(ident.span(), "duplicate parameter `path_env`"));
113                    }
114                    env = Some(str);
115                }
116                "fallback" => {
117                    if fallback.is_some() {
118                        return Err(Error::new(ident.span(), "duplicate parameter `fallback`"));
119                    }
120                    fallback = Some(str);
121                }
122                _ => {
123                    return Err(Error::new(
124                        ident.span(),
125                        format!("unexpected parameter `{}`", ident),
126                    ));
127                }
128            }
129
130            if input.peek(Token![,]) {
131                input.parse::<Token![,]>()?;
132            }
133        }
134
135        match (env, fallback) {
136            (Some(env), None) => Ok(IncludeConfigsArgs::PathEnv(env)),
137            (Some(env), Some(fallback)) => Ok(IncludeConfigsArgs::PathEnvFallback(env, fallback)),
138            _ => Err(Error::new(
139                input.span(),
140                "missing required parameter `path_env`",
141            )),
142        }
143    }
144}