axconfig_macros/
lib.rs

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