diff --git a/cove-config/src/doc.rs b/cove-config/src/doc.rs new file mode 100644 index 0000000..dc232f4 --- /dev/null +++ b/cove-config/src/doc.rs @@ -0,0 +1,190 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Clone, Default)] +pub struct ValueInfo { + pub required: Option, + pub r#type: Option, + pub values: Option>, + pub default: Option, +} + +impl ValueInfo { + fn as_markdown(&self) -> String { + let mut lines = vec![]; + + if let Some(required) = self.required { + let yesno = if required { "yes" } else { "no" }; + lines.push(format!("**Required:** {yesno}")); + } + + if let Some(r#type) = &self.r#type { + lines.push(format!("**Type:** {type}")); + } + + if let Some(values) = &self.values { + let values = values.join(", "); + lines.push(format!("**Values:** {values}")); + } + + if let Some(default) = &self.default { + lines.push(format!("**Default:** {default}")); + } + + lines.join(" \n") + } +} + +#[derive(Clone, Default)] +pub struct StructInfo { + pub fields: HashMap>, +} + +#[derive(Clone, Default)] +pub struct WrapInfo { + pub inner: Option>, + pub metavar: Option, +} + +#[derive(Clone, Default)] +pub struct Doc { + pub description: Option, + + pub value_info: ValueInfo, + pub struct_info: StructInfo, + pub wrap_info: WrapInfo, +} + +struct Entry { + path: String, + description: String, + value_info: ValueInfo, +} + +impl Entry { + fn new(description: String, value_info: ValueInfo) -> Self { + Self { + path: String::new(), + description, + value_info, + } + } + + fn with_parent(mut self, segment: String) -> Self { + if self.path.is_empty() { + self.path = segment; + } else { + self.path = format!("{segment}.{}", self.path); + } + self + } +} + +impl Doc { + fn entries(&self) -> Vec { + let mut entries = vec![]; + + if let Some(description) = &self.description { + entries.push(Entry::new(description.clone(), self.value_info.clone())); + } + + for (segment, field) in &self.struct_info.fields { + entries.extend( + field + .entries() + .into_iter() + .map(|entry| entry.with_parent(segment.clone())), + ); + } + + if let Some(inner) = &self.wrap_info.inner { + let segment = match &self.wrap_info.metavar { + Some(metavar) => format!("<{metavar}>"), + None => "<...>".to_string(), + }; + entries.extend( + inner + .entries() + .into_iter() + .map(|entry| entry.with_parent(segment.clone())), + ); + } + + entries + } + + pub fn format_as_markdown(&self) -> String { + // Print entries in alphabetical order to make generated documentation + // format more stable. + let mut entries = self.entries(); + entries.sort_unstable_by(|a, b| a.path.cmp(&b.path)); + + let mut result = String::new(); + + result.push_str("# Configuration options\n\n"); + result.push_str("Cove's config file uses the [TOML](https://toml.io/) format.\n"); + + for entry in entries { + result.push_str(&format!("\n## `{}`\n", entry.path)); + + let value_info = entry.value_info.as_markdown(); + if !value_info.is_empty() { + result.push_str(&format!("\n{value_info}\n")); + } + + if !entry.description.is_empty() { + result.push_str(&format!("\n{}\n", entry.description)); + } + } + + result + } +} + +pub trait Document { + fn doc() -> Doc; +} + +impl Document for String { + fn doc() -> Doc { + let mut doc = Doc::default(); + doc.value_info.required = Some(true); + doc.value_info.r#type = Some("string".to_string()); + doc + } +} + +impl Document for bool { + fn doc() -> Doc { + let mut doc = Doc::default(); + doc.value_info.required = Some(true); + doc.value_info.r#type = Some("boolean".to_string()); + doc + } +} + +impl Document for PathBuf { + fn doc() -> Doc { + let mut doc = Doc::default(); + doc.value_info.required = Some(true); + doc.value_info.r#type = Some("path".to_string()); + doc + } +} + +impl Document for Option { + fn doc() -> Doc { + let mut doc = I::doc(); + assert_eq!(doc.value_info.required, Some(true)); + doc.value_info.required = Some(false); + doc + } +} + +impl Document for HashMap { + fn doc() -> Doc { + let mut doc = Doc::default(); + doc.wrap_info.inner = Some(Box::new(I::doc())); + doc + } +} diff --git a/cove-config/src/lib.rs b/cove-config/src/lib.rs index 2beb8db..561bd13 100644 --- a/cove-config/src/lib.rs +++ b/cove-config/src/lib.rs @@ -9,6 +9,8 @@ // Clippy lints #![warn(clippy::use_self)] +pub mod doc; + use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf};