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