#[derive(Debug, Clone)]
pub struct FieldDoc {
pub var_seg: &'static str,
pub name: &'static str,
pub doc: Option<&'static str>,
pub default: Option<String>,
pub example: Option<String>,
pub required: bool,
pub children: Option<Vec<FieldDoc>>,
}
pub trait Documented {
fn doc_name() -> &'static str {
""
}
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>> {
T::doc_fields()
}
}
impl<T> Documented for Vec<T> {
fn doc_fields() -> Option<Vec<FieldDoc>> {
None
}
}
impl<K, V: Documented, S> Documented for std::collections::HashMap<K, V, S> {
fn doc_fields() -> Option<Vec<FieldDoc>> {
Some(vec![FieldDoc {
var_seg: "<key>",
name: "",
doc: None,
default: None,
example: None,
required: true,
children: V::doc_fields(),
}])
}
}
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}")
}
}
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
}
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,
}
}
}
}
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(),
}),
}
}
}