use anyhow::bail;
use schemars::Schema;
use serde_json::{Map, Value};
pub fn to_markdown(schema: &Schema) -> anyhow::Result<String> {
let mut markdown = "<!--
Copyright (C) 2026 Ordinary Labs, LLC.
SPDX-License-Identifier: AGPL-3.0-only
-->
# `ordinary.json` Reference\n\n"
.to_owned();
let Some(root) = schema.as_object() else {
bail!("schema is not object");
};
let title = get_as_string(root, "title")?;
markdown.push_str(&format!("## `{title}`\n\n"));
let desc = get_as_string(root, "description")?;
markdown.push_str(&format!("{desc}\n\n"));
let props = get_properties_as_string(root)?;
markdown.push_str(&props);
let Some(defs) = root.get("$defs") else {
bail!("no defs");
};
if let Some(defs) = defs.as_object() {
for (k, def) in defs {
markdown.push_str(&format!("## `{k}`\n\n"));
let Some(def) = def.as_object() else {
bail!("def {k} is not an object");
};
if let Ok(props) = get_properties_as_string(def) {
if let Ok(desc) = get_as_string(def, "description") {
markdown.push_str(&format!("{desc}\n\n"));
}
markdown.push_str(&props);
} else if let Ok(t) = get_type_as_string(def) {
markdown.push_str(&t);
if let Ok(desc) = get_as_string(def, "description") {
markdown.push_str(&format!("{desc}\n\n"));
}
}
}
};
markdown.push_str("<hr/>\n\n_This document was generated automatically for structs defined in the [`ordinary-config`](https://docs.rs/ordinary-config/latest/ordinary_config/) crate._");
Ok(markdown)
}
fn get_as_string(map: &Map<String, Value>, key: &str) -> anyhow::Result<String> {
let Some(value) = map.get(key) else {
bail!("no {key}");
};
let Some(as_str) = value.as_str() else {
bail!("{key} not a string");
};
Ok(as_str.to_owned())
}
fn get_type_as_string(map: &Map<String, Value>) -> anyhow::Result<String> {
let mut out = String::new();
if let Some(t) = map.get("type") {
out.push_str("\n\n");
match t {
Value::String(t) => {
out.push_str(&format!("> {}\n\n", get_type(map, t)));
}
Value::Array(arr) => {
if !arr.is_empty() {
out.push_str("> ");
}
for (i, t) in arr.iter().enumerate() {
if let Some(t) = t.as_str() {
out.push_str(&format!(
"{}{}",
if i == 0 { "" } else { " | " },
get_type(map, t)
))
}
}
out.push_str("\n\n");
}
_ => bail!("type is neither string nor array"),
}
} else if let Some(any_of) = map.get("anyOf")
&& let Value::Array(any_of) = any_of
{
if !any_of.is_empty() {
out.push_str("> (any of) ");
}
for (i, opt) in any_of.iter().enumerate() {
any_of_or_items(&mut out, i, opt);
}
out.push_str("\n\n");
} else if let Some(one_of) = map.get("oneOf")
&& let Value::Array(one_of) = one_of
{
if !one_of.is_empty() {
out.push_str("> (one of)\n\n");
}
for opt in one_of.iter() {
if let Some(opt) = opt.as_object() {
let t = get_as_string(opt, "type")?;
if let Ok(c) = get_as_string(opt, "const") {
if let Ok(desc) = get_as_string(opt, "description") {
out.push_str(&format!("{desc}\n\n"));
}
out.push_str(&format!("> {c} ({t})\n\n"));
} else if let Some(e) = opt.get("enum")
&& let Value::Array(e) = e
{
out.push_str("> (enum) ");
for (i, member) in e.iter().enumerate() {
if let Value::String(member) = member {
out.push_str(&format!("{}{member}", if i == 0 { "" } else { " | " }))
}
}
out.push_str("\n\n");
} else {
out.push_str(&format!("> {t}\n\n"));
if let Ok(desc) = get_as_string(opt, "description") {
out.push_str(&format!("{desc}\n\n"));
}
if let Ok(props) = get_properties_as_string(opt) {
out.push_str(&format!("{props}\n\n"))
}
}
}
}
} else {
out.push_str("\n\n");
}
Ok(out)
}
fn get_type(map: &Map<String, Value>, t: &str) -> String {
if t == "array"
&& let Some(items) = map.get("items")
&& let Some(items) = items.as_object()
{
if let Some(r) = items.get("$ref")
&& let Some(r) = r.as_str()
&& let Some(r) = r.strip_prefix("#/$defs/")
{
return format!("{t}<[{r}](#{})>", r.to_lowercase());
} else if let Some(tt) = items.get("type")
&& let Some(tt) = tt.as_str()
{
return format!("{t}<{tt}>");
}
}
format!("{t}")
}
fn any_of_or_items(out: &mut String, i: usize, opt: &Value) {
if let Some(opt) = opt.as_object() {
if let Ok(t) = get_as_string(opt, "type") {
out.push_str(&format!("{}{t}", if i == 0 { "" } else { " | " }))
} else if let Ok(r) = get_as_string(opt, "$ref")
&& let Some(r) = r.strip_prefix("#/$defs/")
{
out.push_str(&format!(
"{}[{r}](#{})",
if i == 0 { "" } else { " | " },
r.to_lowercase()
))
}
}
}
fn get_properties_as_string(map: &Map<String, Value>) -> anyhow::Result<String> {
let mut out = String::new();
let Some(props) = map.get("properties") else {
bail!("no properties");
};
let Some(map) = props.as_object() else {
bail!("props is not an object");
};
for (k, prop) in map {
out.push_str(&format!("### `{k}`\n\n"));
let Some(prop) = prop.as_object() else {
bail!("prop is not an object")
};
let t = get_type_as_string(prop)?;
out.push_str(&t);
if let Ok(desc) = get_as_string(prop, "description") {
out.push_str(&format!("{desc}\n\n"));
}
if let Ok(props) = get_properties_as_string(prop) {
out.push_str(&format!("{props}\n\n"));
}
if let Some(items) = map.get("items") {
any_of_or_items(&mut out, 0, items);
out.push_str("\n\n");
};
}
Ok(out)
}