confroid 0.0.4

The n+1-st config reader for your environment-based configs.
Documentation
//! Documentation generation for config structs (`docs` feature).
//!
//! [`Documented`] is implemented by `#[derive(Config)]` (and for the built-in
//! scalar/collection types) so that [`env_example`] and [`markdown_table`] can
//! walk a config's shape and render it.

/// A single field in a config's documentation tree.
#[derive(Debug, Clone)]
pub struct FieldDoc {
    /// The environment-variable segment for this field, e.g. `PORT`.
    pub var_seg: &'static str,
    /// The Rust field name, e.g. `port`.
    pub name: &'static str,
    /// The field's doc comment, if any.
    pub doc: Option<&'static str>,
    /// The rendered default value, if the field has a `default`.
    pub default: Option<String>,
    /// The rendered example value, if the field has an `example`.
    pub example: Option<String>,
    /// Whether the field must be explicitly configured.
    ///
    /// Optional fields and fields with defaults are not required.
    pub required: bool,
    /// The sub-fields, if this field is a nested struct; `None` for leaves.
    pub children: Option<Vec<FieldDoc>>,
}

/// A type that can describe its configuration shape for documentation.
///
/// Implemented automatically by `#[derive(Config)]` when the `docs` feature is
/// enabled, and for the built-in scalar and collection types.
pub trait Documented {
    /// A human-readable name for the config (the struct name); empty for
    /// non-struct types.
    fn doc_name() -> &'static str {
        ""
    }

    /// The sub-fields of this type, or `None` if it is a scalar leaf.
    fn doc_fields() -> Option<Vec<FieldDoc>>;
}

macro_rules! impl_documented_leaf {
    ($($ty:ty),+ $(,)?) => {
        $(
            impl Documented for $ty {
                fn doc_fields() -> Option<Vec<FieldDoc>> {
                    None
                }
            }
        )+
    };
}

impl_documented_leaf!(
    u8,
    u16,
    u32,
    u64,
    u128,
    usize,
    i8,
    i16,
    i32,
    i64,
    i128,
    isize,
    f32,
    f64,
    bool,
    char,
    String,
    Box<str>,
    std::sync::Arc<str>,
    std::num::NonZeroU16,
    std::net::IpAddr,
    std::net::Ipv4Addr,
    std::net::Ipv6Addr,
    std::net::SocketAddr,
    std::net::SocketAddrV4,
    std::net::SocketAddrV6,
    std::path::PathBuf,
);

#[cfg(feature = "humantime")]
impl_documented_leaf!(std::time::Duration);

#[cfg(feature = "url")]
impl_documented_leaf!(url::Url);

impl<T: Documented> Documented for Option<T> {
    fn doc_fields() -> Option<Vec<FieldDoc>> {
        // An optional struct documents like the struct it wraps.
        T::doc_fields()
    }
}

impl<T> Documented for Vec<T> {
    fn doc_fields() -> Option<Vec<FieldDoc>> {
        // Rendered as a single indexed/delimited leaf.
        None
    }
}

impl<K, V: Documented, S> Documented for std::collections::HashMap<K, V, S> {
    fn doc_fields() -> Option<Vec<FieldDoc>> {
        // A map contributes one representative entry so the generated
        // documentation can show the keyed variable shape. In particular,
        // maps of structs must retain the value type's nested fields.
        Some(vec![FieldDoc {
            var_seg: "<key>",
            name: "",
            doc: None,
            default: None,
            example: None,
            required: true,
            children: V::doc_fields(),
        }])
    }
}

/// The `(default: …, example: …)` annotation for a leaf field, if any.
fn annotation(field: &FieldDoc) -> Option<String> {
    match (&field.default, &field.example) {
        (Some(d), Some(e)) => Some(format!("(default: {d}, example: {e})")),
        (Some(d), None) => Some(format!("(default: {d})")),
        (None, Some(e)) => Some(format!("(example: {e})")),
        (None, None) => None,
    }
}

fn join_var(prefix: &str, seg: &str) -> String {
    if prefix.is_empty() {
        seg.to_string()
    } else if seg.is_empty() {
        prefix.to_string()
    } else {
        format!("{prefix}__{seg}")
    }
}

/// Render `T` as a `.env.example`-style file.
///
/// Fields with a default show that default as the value; required fields are
/// left empty. Nested structs contribute a comment header followed by their
/// prefixed variables. Top-level entries are separated by blank lines;
/// variables within a nested section are kept together.
pub fn env_example<T: Documented>() -> String {
    let name = T::doc_name();
    let header = if name.is_empty() {
        "# configuration".to_string()
    } else {
        format!("# {name} configuration")
    };

    let blocks: Vec<String> = T::doc_fields()
        .unwrap_or_default()
        .iter()
        .map(|field| render_block(field, "", false))
        .collect();

    let mut result = header;
    if !blocks.is_empty() {
        result.push_str("\n\n");
        result.push_str(&blocks.join("\n\n"));
    }
    result.push('\n');
    result
}

/// Render one field (and, for nested structs, its subtree) as a text block
/// with no leading or trailing blank lines.
fn render_block(field: &FieldDoc, prefix: &str, parent_commented_out: bool) -> String {
    let var = join_var(prefix, field.var_seg);
    let commented_out = parent_commented_out || !field.required;
    match &field.children {
        Some(children) => {
            let inner = children
                .iter()
                .map(|child| render_block(child, &var, commented_out))
                .collect::<Vec<_>>()
                .join("\n");
            match field.doc {
                Some(doc) => format!("# {doc}\n\n{inner}"),
                None => inner,
            }
        }
        None => {
            let value = field.default.clone().unwrap_or_default();
            let assignment = if commented_out {
                format!("# {var}={value}")
            } else {
                format!("{var}={value}")
            };
            match (field.doc, annotation(field)) {
                (Some(doc), Some(ann)) => format!("# {doc} {ann}\n{assignment}"),
                (Some(doc), None) => format!("# {doc}\n{assignment}"),
                (None, Some(ann)) => format!("# {ann}\n{assignment}"),
                (None, None) => assignment,
            }
        }
    }
}

/// Render `T` as a Markdown table of every (leaf) variable.
pub fn markdown_table<T: Documented>() -> String {
    let mut rows = Vec::new();
    if let Some(fields) = T::doc_fields() {
        collect_rows(&fields, "", &mut rows);
    }

    let mut out = String::new();
    out.push_str("| Variable | Description | Default | Example |\n");
    out.push_str("| --- | --- | --- | --- |\n");
    for row in rows {
        out.push_str(&format!(
            "| `{}` | {} | {} | {} |\n",
            row.var,
            row.doc,
            code_or_blank(&row.default),
            code_or_blank(&row.example),
        ));
    }
    out
}

struct Row {
    var: String,
    doc: String,
    default: Option<String>,
    example: Option<String>,
}

fn code_or_blank(value: &Option<String>) -> String {
    match value {
        Some(v) => format!("`{v}`"),
        None => String::new(),
    }
}

fn collect_rows(fields: &[FieldDoc], prefix: &str, rows: &mut Vec<Row>) {
    for field in fields {
        let var = join_var(prefix, field.var_seg);
        match &field.children {
            Some(children) => collect_rows(children, &var, rows),
            None => rows.push(Row {
                var,
                doc: field.doc.unwrap_or("").to_string(),
                default: field.default.clone(),
                example: field.example.clone(),
            }),
        }
    }
}