use std::fmt::Write as _;
use std::{env, fs, path::Path};
const SUPPORTED_TYPES: [&str; 1] = ["list<string>"];
fn main() {
println!("cargo:rerun-if-changed=settings.toml");
let text = fs::read_to_string("settings.toml").expect("could not read settings.toml");
let table: toml::Table = text.parse().expect("settings.toml is not valid TOML");
let mut fields = String::new();
let mut defaults = String::new();
let mut metas = String::new();
for (name, value) in &table {
let entry = value
.as_table()
.unwrap_or_else(|| panic!("setting `{name}` must be a table"));
let ty = string_at(entry, name, "type");
assert!(
SUPPORTED_TYPES.contains(&ty.as_str()),
"setting `{name}` has type `{ty}`, which build.rs cannot emit \
(supported: {SUPPORTED_TYPES:?}). Teach the generator the type \
rather than shipping a setting that reads as supported and is not."
);
let docs = string_at(entry, name, "docs");
let since = string_at(entry, name, "since");
let default = strings_at(entry, name, "default");
let sources = entry
.get("sources")
.and_then(toml::Value::as_table)
.unwrap_or_else(|| panic!("setting `{name}` has no [sources]"));
let cli = strings_at(sources, name, "cli");
let envs = strings_at(sources, name, "env");
let config = strings_at(sources, name, "config");
assert!(
!cli.is_empty() || !envs.is_empty() || !config.is_empty(),
"setting `{name}` declares no sources, so nothing can ever set it"
);
let examples = entry
.get("examples")
.map(|_| strings_at(entry, name, "examples"))
.unwrap_or_default();
writeln!(fields, " pub {name}: Vec<String>,").unwrap();
writeln!(defaults, " {name}: {},", vec_expr(&default)).unwrap();
writeln!(
metas,
" SettingMeta {{\n \
name: {name:?},\n \
type_: {ty:?},\n \
default: {:?},\n \
cli_flags: {},\n \
env_vars: {},\n \
config_keys: {},\n \
examples: {},\n \
since: {since:?},\n \
docs: {docs:?},\n }},",
toml_list(&default),
slice_expr(&cli),
slice_expr(&envs),
slice_expr(&config),
slice_expr(&examples),
)
.unwrap();
}
let out = format!(
"// @generated by build.rs from settings.toml — do not edit.\n\
\n\
/// Every setting, resolved.\n\
#[derive(Debug, Clone, PartialEq, Eq)]\n\
pub struct Settings {{\n{fields}}}\n\
\n\
impl Default for Settings {{\n \
fn default() -> Self {{\n \
Self {{\n{defaults} }}\n }}\n}}\n\
\n\
/// Static description of one setting, for `tak settings` and docs.\n\
#[derive(Debug, Clone, Copy)]\n\
pub struct SettingMeta {{\n \
pub name: &'static str,\n \
pub type_: &'static str,\n \
/// The default, rendered as the TOML literal from the registry.\n \
pub default: &'static str,\n \
pub cli_flags: &'static [&'static str],\n \
pub env_vars: &'static [&'static str],\n \
/// Dotted keys in `tak.toml`.\n \
pub config_keys: &'static [&'static str],\n \
pub examples: &'static [&'static str],\n \
pub since: &'static str,\n \
pub docs: &'static str,\n}}\n\
\n\
/// Every setting tak supports, in registry order.\n\
pub const SETTINGS: &[SettingMeta] = &[\n{metas}];\n"
);
let path = Path::new(&env::var("OUT_DIR").expect("OUT_DIR")).join("settings_generated.rs");
fs::write(&path, out).expect("could not write generated settings");
}
fn string_at(table: &toml::Table, setting: &str, key: &str) -> String {
table
.get(key)
.and_then(toml::Value::as_str)
.unwrap_or_else(|| panic!("setting `{setting}` needs a string `{key}`"))
.to_string()
}
fn strings_at(table: &toml::Table, setting: &str, key: &str) -> Vec<String> {
let Some(value) = table.get(key) else {
return Vec::new();
};
value
.as_array()
.unwrap_or_else(|| panic!("setting `{setting}`: `{key}` must be an array"))
.iter()
.map(|v| {
v.as_str()
.unwrap_or_else(|| panic!("setting `{setting}`: `{key}` must hold strings"))
.to_string()
})
.collect()
}
fn vec_expr(items: &[String]) -> String {
if items.is_empty() {
return "Vec::new()".to_string();
}
let inner: Vec<String> = items.iter().map(|s| format!("{s:?}.to_string()")).collect();
format!("vec![{}]", inner.join(", "))
}
fn slice_expr(items: &[String]) -> String {
let inner: Vec<String> = items.iter().map(|s| format!("{s:?}")).collect();
format!("&[{}]", inner.join(", "))
}
fn toml_list(items: &[String]) -> String {
let inner: Vec<String> = items.iter().map(|s| format!("{s:?}")).collect();
format!("[{}]", inner.join(", "))
}