1
0
Fork 0
forked from wry/wry
wry/toml-spec/src/markdown.rs
2025-02-21 10:44:29 +01:00

262 lines
8.5 KiB
Rust

use {
crate::types::{
ArraySpec, Described, NestableTypesSpec, NumberSpec, RefOrSpec, SingleTableSpec,
StringSpec, TableSpec, TopLevelTypeSpec, VariantSpec,
},
anyhow::Result,
std::io::Write,
};
pub fn generate_markdown(types: &[(&String, &Described<TopLevelTypeSpec>)]) -> Result<()> {
const TEMPLATE: &str = include_str!("../spec/template.md");
let mut buf = vec![];
buf.extend_from_slice(TEMPLATE.as_bytes());
for (name, ty) in types {
write_top_level_type_spec(&mut buf, name, ty)?;
}
std::fs::write(
concat!(env!("CARGO_MANIFEST_DIR"), "/spec/spec.generated.md"),
&buf,
)?;
Ok(())
}
fn write_top_level_type_spec(
buf: &mut Vec<u8>,
name: &str,
spec: &Described<TopLevelTypeSpec>,
) -> Result<()> {
writeln!(buf, "<a name=\"types-{name}\"></a>")?;
writeln!(buf, "### `{name}`")?;
writeln!(buf)?;
writeln!(buf, "{}", spec.description.trim())?;
writeln!(buf)?;
match &spec.value {
TopLevelTypeSpec::Variable { variants } => {
writeln!(
buf,
"Values of this type should have one of the following forms:"
)?;
writeln!(buf)?;
for variant in variants {
write!(buf, "#### ")?;
let name = match &variant.value {
VariantSpec::String(_) => "A string",
VariantSpec::Number(_) => "A number",
VariantSpec::Boolean => "A boolean",
VariantSpec::Array(_) => "An array",
VariantSpec::Table(_) => "A table",
};
writeln!(buf, "{name}")?;
writeln!(buf)?;
writeln!(buf, "{}", variant.description.trim())?;
writeln!(buf)?;
write_variant_spec(buf, &variant.value)?;
}
}
TopLevelTypeSpec::Single(variant) => {
let name = match &variant {
VariantSpec::String(_) => "strings",
VariantSpec::Number(_) => "numbers",
VariantSpec::Boolean => "booleans",
VariantSpec::Array(_) => "arrays",
VariantSpec::Table(_) => "tables",
};
writeln!(buf, "Values of this type should be {name}.")?;
writeln!(buf)?;
write_variant_spec(buf, variant)?;
}
}
writeln!(buf)?;
Ok(())
}
fn write_variant_spec(buf: &mut Vec<u8>, spec: &VariantSpec) -> Result<()> {
macro_rules! spec {
($v:expr) => {
match $v {
RefOrSpec::Ref { name } => {
writeln!(buf, "The value should be a [{name}](#types-{name}).")?;
writeln!(buf)?;
return Ok(());
}
RefOrSpec::Spec(s) => s,
}
};
}
match spec {
VariantSpec::String(ss) => {
let ss = spec!(ss);
write_string_spec(buf, ss, "")?;
}
VariantSpec::Number(ns) => {
let ns = spec!(ns);
write_number_spec(buf, ns, "")?;
}
VariantSpec::Boolean => {}
VariantSpec::Array(s) => {
let s = spec!(s);
write_array_spec(buf, s, "")?;
}
VariantSpec::Table(ts) => {
let ts = spec!(ts);
match ts {
TableSpec::Tagged { types } => {
writeln!(
buf,
"This table is a tagged union. The variant is determined by the `type` field. It takes one of the following values:"
)?;
writeln!(buf)?;
for (name, spec) in types {
writeln!(buf, "- `{name}`:")?;
writeln!(buf)?;
for line in spec.description.trim().lines() {
writeln!(buf, " {line}")?;
}
writeln!(buf)?;
write_single_table_spec(buf, &spec.value, " ")?;
}
}
TableSpec::Single(s) => {
write_single_table_spec(buf, s, "")?;
}
}
}
}
Ok(())
}
fn write_single_table_spec(buf: &mut Vec<u8>, spec: &SingleTableSpec, pad: &str) -> Result<()> {
writeln!(buf, "{pad}The table has the following fields:")?;
writeln!(buf)?;
for (name, fs) in &spec.fields {
let optional = match fs.value.required {
true => "required",
false => "optional",
};
writeln!(buf, "{pad}- `{name}` ({optional}):")?;
writeln!(buf)?;
for line in fs.description.trim().lines() {
writeln!(buf, "{pad} {line}")?;
}
writeln!(buf)?;
write!(buf, "{pad} The value of this field should be ")?;
let spec = write_nestable_type_spec(buf, &fs.value.kind, false)?;
writeln!(buf, ".")?;
writeln!(buf)?;
if let Some(spec) = spec {
let pad = format!("{pad} ");
write_nestable_type_restrictions(buf, spec, &pad)?;
}
}
Ok(())
}
fn write_nestable_type_spec<'a>(
buf: &mut Vec<u8>,
spec: &'a RefOrSpec<NestableTypesSpec>,
plural: bool,
) -> Result<Option<&'a NestableTypesSpec>> {
let spec = match spec {
RefOrSpec::Ref { name } => {
if plural {
write!(buf, "[{name}s](#types-{name})")?;
} else {
write!(buf, "a [{name}](#types-{name})")?;
}
return Ok(None);
}
RefOrSpec::Spec(s) => s,
};
let name = match (spec, plural) {
(NestableTypesSpec::String(_), false) => "a string",
(NestableTypesSpec::String(_), true) => "strings",
(NestableTypesSpec::Number(_), false) => "a number",
(NestableTypesSpec::Number(_), true) => "numbers",
(NestableTypesSpec::Boolean, false) => "a boolean",
(NestableTypesSpec::Boolean, true) => "booleans",
(NestableTypesSpec::Map(s), _) => {
let name = match plural {
true => "tables",
false => "a table",
};
write!(buf, "{name} whose values are ")?;
return write_nestable_type_spec(buf, &s.values, true);
}
(NestableTypesSpec::Array(s), _) => {
let name = match plural {
true => "arrays",
false => "an array",
};
write!(buf, "{name} of ")?;
return write_nestable_type_spec(buf, &s.items, true);
}
};
write!(buf, "{name}")?;
Ok(Some(spec))
}
fn write_nestable_type_restrictions(
buf: &mut Vec<u8>,
spec: &NestableTypesSpec,
pad: &str,
) -> Result<()> {
match spec {
NestableTypesSpec::String(s) => write_string_spec(buf, s, pad),
NestableTypesSpec::Number(s) => write_number_spec(buf, s, pad),
NestableTypesSpec::Boolean => Ok(()),
NestableTypesSpec::Array(_) => Ok(()),
NestableTypesSpec::Map(_) => Ok(()),
}
}
fn write_string_spec(buf: &mut Vec<u8>, spec: &StringSpec, pad: &str) -> Result<()> {
if let Some(values) = &spec.values {
writeln!(
buf,
"{pad}The string should have one of the following values:"
)?;
writeln!(buf)?;
for value in values {
writeln!(buf, "{pad}- `{}`:", value.value.value)?;
writeln!(buf)?;
for line in value.description.lines() {
writeln!(buf, "{pad} {line}")?;
}
writeln!(buf)?;
}
writeln!(buf)?;
}
Ok(())
}
fn write_array_spec(buf: &mut Vec<u8>, spec: &ArraySpec, pad: &str) -> Result<()> {
write!(buf, "{pad}Each element of this array should be ")?;
let spec = write_nestable_type_spec(buf, &spec.items, false)?;
writeln!(buf, ".")?;
writeln!(buf)?;
if let Some(spec) = spec {
write_nestable_type_restrictions(buf, spec, pad)?;
}
Ok(())
}
fn write_number_spec(buf: &mut Vec<u8>, spec: &NumberSpec, pad: &str) -> Result<()> {
if spec.integer_only {
writeln!(buf, "{pad}The numbers should be integers.")?;
writeln!(buf)?;
}
if let Some(minimum) = spec.minimum {
let greater = match spec.exclusive_minimum {
true => "strictly greater than",
false => "greater than or equal to",
};
writeln!(buf, "{pad}The numbers should be {greater} {minimum}.")?;
writeln!(buf)?;
}
Ok(())
}