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#[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#[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}