tak-cli 0.0.3

Benchmark command-line programs and track their performance over time. Experimental; do not use.
Documentation
//! Generates settings code from `settings.toml`.
//!
//! Same arrangement as mise, fnox and aube: one declarative registry, and the
//! struct, the defaults and the introspection metadata are all derived from it.
//! Adding a setting is a block of TOML; nothing has to be kept in step by hand.
//!
//! Emits `$OUT_DIR/settings_generated.rs`, which `src/settings.rs` includes.

use std::fmt::Write as _;
use std::{env, fs, path::Path};

/// Types the generator knows how to emit.
///
/// Deliberately one entry long. A generator that silently accepted an unknown
/// type would emit a field nothing could set, which is worse than not building
/// — the setting would appear in `tak settings`, appear in the docs, and do
/// nothing. Widening this is a few lines, and should happen when a setting
/// needs it rather than in advance.
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();

    // `toml::Table` iterates in sorted key order, so the generated file is
    // byte-stable across builds and shows up in diffs only when it changes.
    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()
}

/// `vec!["a".to_string(), "b".to_string()]`
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(", "))
}

/// `&["a", "b"]`
fn slice_expr(items: &[String]) -> String {
    let inner: Vec<String> = items.iter().map(|s| format!("{s:?}")).collect();
    format!("&[{}]", inner.join(", "))
}

/// The TOML literal a reader would write to produce this value.
fn toml_list(items: &[String]) -> String {
    let inner: Vec<String> = items.iter().map(|s| format!("{s:?}")).collect();
    format!("[{}]", inner.join(", "))
}