use std::fmt::Write;
use armour_core::{Fields, PathSeg, RelationKind, SchemaGraph, Typ};
pub trait SchemaFormatter {
fn format(&self, graph: &SchemaGraph) -> String;
}
pub struct MermaidEr;
fn typ_label(t: &Typ) -> String {
match t {
Typ::Scalar(s) => format!("{s:?}"),
Typ::Array(n, inner) => format!("Array{}_{}", n, typ_label(inner)),
Typ::Vec(inner) => format!("Vec_{}", typ_label(inner)),
Typ::Optional(inner) => format!("Opt_{}", typ_label(inner)),
Typ::SimpleEnum(e) => e.name.to_string(),
Typ::Struct(s) if !s.name.is_empty() => s.name.to_string(),
Typ::Struct(_) => "tuple".to_string(),
Typ::Enum(e) => e.name.to_string(),
Typ::Custom(name, _) => (*name).to_string(),
}
}
fn seg_label(seg: &PathSeg) -> String {
match seg {
PathSeg::Field(n) | PathSeg::Variant(n) => (*n).to_string(),
PathSeg::Index(i) => i.to_string(),
PathSeg::Elem => "[]".to_string(),
}
}
fn ident(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' {
c
} else {
'_'
}
})
.collect()
}
impl SchemaFormatter for MermaidEr {
fn format(&self, graph: &SchemaGraph) -> String {
let mut out = String::from("erDiagram\n");
for c in &graph.collections {
let _ = writeln!(out, " {} {{", ident(&c.name));
if let Typ::Struct(s) = &c.ty
&& let Fields::Named(fields) = &s.fields
{
for (fname, fty) in fields.iter() {
let _ = writeln!(out, " {} {}", ident(&typ_label(fty)), ident(fname));
}
}
out.push_str(" }\n");
}
for r in &graph.relations {
let (arrow, label) = match &r.kind {
RelationKind::Fk {
brand, field_path, ..
} => {
let field = field_path
.iter()
.map(seg_label)
.collect::<Vec<_>>()
.join(".");
("}o--||", format!("fk {field} ({brand})"))
}
RelationKind::Index => ("||--o{", "index".to_string()),
RelationKind::Denorm => ("||--||", "denorm".to_string()),
RelationKind::Counter { field } => ("||--o{", format!("counter {field}")),
};
let _ = writeln!(
out,
" {} {arrow} {} : \"{label}\"",
ident(&r.from),
ident(&r.to)
);
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use armour_core::{CollectionKind, CollectionNode, ScalarTyp, SchemaGraph};
#[test]
fn schema_mermaid_sanitizes_identifiers() {
let g = SchemaGraph {
collections: vec![CollectionNode {
name: "my coll!".into(),
kind: CollectionKind::Standalone,
ty: Typ::Scalar(ScalarTyp::U64),
self_brand: None,
storage: None,
}],
relations: vec![],
};
let out = MermaidEr.format(&g);
assert!(out.contains("my_coll_ {"), "{out}");
}
}