toml_cfg/lib.rs
1//! # `toml-cfg`
2//!
3//! ## Summary
4//!
5//! * Crates can declare variables that can be overridden
6//! * Anything const, e.g. usize, strings, etc.
7//! * (Only) The "root crate" can override these variables by including a `cfg.toml` file
8//!
9//! ## Config file
10//!
11//! This is defined ONLY in the final application or "root crate"
12//!
13//! ```toml
14//! # a toml-cfg file
15//!
16//! [lib-one]
17//! buffer_size = 4096
18//!
19//! [lib-two]
20//! greeting = "Guten tag!"
21//! ```
22//!
23//! ## In the library
24//!
25//! ```rust
26//! // lib-one
27//! #[toml_cfg::toml_config]
28//! pub struct Config {
29//! #[default(32)]
30//! buffer_size: usize,
31//! }
32//! ```
33//!
34//!```rust
35//! // lib-two
36//! #[toml_cfg::toml_config]
37//! pub struct Config {
38//! #[default("hello")]
39//! greeting: &'static str,
40//! }
41//!
42//! ```
43//!
44//! ## Configuration
45//!
46//! With the `TOML_CFG` environment variable is set with a value containing
47//! "require_cfg_present", the `toml-cfg` proc macro will panic if no valid config
48//! file is found. This is indicative of either no `cfg.toml` file existing in the
49//! "root project" path, or a failure to find the correct "root project" path.
50//!
51//! This failure could occur when NOT building with a typical `cargo build`
52//! environment, including with `rust-analyzer`. This is *mostly* okay, as
53//! it doesn't seem that Rust Analyzer presents this in some misleading way.
54//!
55//! If you *do* find a case where this occurs, please open an issue!
56//!
57//! ## Look at what we get!
58//!
59//! ```shell
60//! # Print the "buffer_size" value from the `lib-one` crate.
61//! # Since it has no cfg.toml, we just get the default value.
62//! $ cd pkg-example/lib-one
63//! $ cargo run
64//! Finished dev [unoptimized + debuginfo] target(s) in 0.01s
65//! Running `target/debug/lib-one`
66//! 32
67//!
68//! # Print the "greeting" value from the `lib-two` crate.
69//! # Since it has no cfg.toml, we just get the default value.
70//! $ cd ../lib-two
71//! $ cargo run
72//! Compiling lib-two v0.1.0 (/home/james/personal/toml-cfg/pkg-example/lib-two)
73//! Finished dev [unoptimized + debuginfo] target(s) in 0.32s
74//! Running `target/debug/lib-two`
75//! hello
76//!
77//! # Print the "buffer_size" value from `lib-one`, and "greeting"
78//! # from `lib-two`. Since we HAVE defined a `cfg.toml` file, the
79//! # values defined there are used instead.
80//! $ cd ../application
81//! $ cargo run
82//! Compiling lib-two v0.1.0 (/home/james/personal/toml-cfg/pkg-example/lib-two)
83//! Compiling application v0.1.0 (/home/james/personal/toml-cfg/pkg-example/application)
84//! Finished dev [unoptimized + debuginfo] target(s) in 0.30s
85//! Running `target/debug/application`
86//! 4096
87//! Guten tag!
88//! ```
89//!
90
91use heck::ToShoutySnekCase;
92use proc_macro::TokenStream;
93use proc_macro2::TokenStream as TokenStream2;
94use quote::{quote, ToTokens};
95use serde::Deserialize;
96use std::collections::HashMap;
97use std::env;
98use std::path::{Path, PathBuf};
99use syn::Expr;
100
101#[derive(Deserialize, Clone, Debug)]
102struct Config {
103 #[serde(flatten)]
104 crates: HashMap<String, Defn>,
105}
106
107#[derive(Deserialize, Clone, Debug, Default)]
108struct Defn {
109 #[serde(flatten)]
110 vals: HashMap<String, toml::Value>,
111}
112
113#[proc_macro_attribute]
114pub fn toml_config(_attr: TokenStream, item: TokenStream) -> TokenStream {
115 let struct_defn =
116 syn::parse::<syn::ItemStruct>(item).expect("Failed to parse configuration structure!");
117
118 let require_cfg_present = if let Ok(val) = env::var("TOML_CFG") {
119 val.contains("require_cfg_present")
120 } else {
121 false
122 };
123
124 let root_path = find_root_path();
125 let cfg_path = root_path.clone();
126 let cfg_path = cfg_path.as_ref().and_then(|c| {
127 let mut x = c.to_owned();
128 x.push("cfg.toml");
129 Some(x)
130 });
131
132 let maybe_cfg = cfg_path.as_ref().and_then(|c| load_crate_cfg(&c));
133 let got_cfg = maybe_cfg.is_some();
134 if require_cfg_present {
135 assert!(
136 got_cfg,
137 "TOML_CFG=require_cfg_present set, but valid config not found!"
138 )
139 }
140 let cfg = maybe_cfg.unwrap_or_else(|| Defn::default());
141
142 let mut struct_defn_fields = TokenStream2::new();
143 let mut struct_inst_fields = TokenStream2::new();
144
145 for field in struct_defn.fields {
146 let ident = field
147 .ident
148 .expect("Failed to find field identifier. Don't use this on a tuple struct.");
149
150 // Determine the default value, declared using the `#[default(...)]` syntax
151 let default = field
152 .attrs
153 .iter()
154 .find_map(|a| {
155 a.path()
156 .is_ident("default")
157 .then(|| a.parse_args::<Expr>().ok())
158 })
159 .flatten()
160 .expect(&format!(
161 "Failed to find `#[default(...)]` attribute for field `{}`.",
162 ident.to_string(),
163 ));
164
165 let ty = field.ty;
166
167 // Is this field overridden?
168 let val = match cfg.vals.get(&ident.to_string()) {
169 Some(t) => {
170 let t_string = t.to_string();
171 syn::parse_str::<Expr>(&t_string).expect(&format!(
172 "Failed to parse `{}` as a valid token!",
173 &t_string
174 ))
175 }
176 None => default,
177 };
178
179 quote! {
180 pub #ident: #ty,
181 }
182 .to_tokens(&mut struct_defn_fields);
183
184 quote! {
185 #ident: #val,
186 }
187 .to_tokens(&mut struct_inst_fields);
188 }
189
190 let struct_ident = struct_defn.ident;
191 let shouty_snek: TokenStream2 = struct_ident
192 .to_string()
193 .TO_SHOUTY_SNEK_CASE()
194 .parse()
195 .expect("NO NOT THE SHOUTY SNAKE");
196
197 let hack_retrigger = match (got_cfg, cfg_path) {
198 (false, _) | (true, None) => quote! {},
199 (true, Some(cfg_path)) => {
200 let cfg_path = format!("{}", cfg_path.display());
201 quote! {
202 const _: &[u8] = include_bytes!(#cfg_path);
203 }
204 }
205 };
206
207 quote! {
208 pub struct #struct_ident {
209 #struct_defn_fields
210 }
211
212 pub const #shouty_snek: #struct_ident = #struct_ident {
213 #struct_inst_fields
214 };
215
216 mod toml_cfg_hack {
217 #hack_retrigger
218 }
219 }
220 .into()
221}
222
223fn load_crate_cfg(path: &Path) -> Option<Defn> {
224 let contents = std::fs::read_to_string(&path).ok()?;
225 let parsed = toml::from_str::<Config>(&contents).ok()?;
226 let name = env::var("CARGO_PKG_NAME").ok()?;
227 parsed.crates.get(&name).cloned()
228}
229
230// From https://stackoverflow.com/q/60264534
231fn find_root_path() -> Option<PathBuf> {
232 // First we get the arguments for the rustc invocation
233 let mut args = std::env::args();
234
235 // Then we loop through them all, and find the value of "out-dir"
236 let mut out_dir = None;
237 while let Some(arg) = args.next() {
238 if arg == "--out-dir" {
239 out_dir = args.next();
240 }
241 }
242
243 // Finally we clean out_dir by removing all trailing directories, until it ends with target
244 let mut out_dir = PathBuf::from(out_dir?);
245 while !out_dir.ends_with("target") {
246 if !out_dir.pop() {
247 // We ran out of directories...
248 return None;
249 }
250 }
251
252 out_dir.pop();
253
254 Some(out_dir)
255}