ordinary-config 0.7.1

Config for Ordinary
Documentation
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

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)
}