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}