use serde::{Deserialize, Serialize};
use crate::registry::component::{ComponentKind, ComponentMetadata};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegistrySnapshot {
pub components: Vec<ComponentMetadata>,
#[serde(default)]
pub aliases: Vec<AliasBinding>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AliasBinding {
pub kind: ComponentKind,
pub alias: String,
pub canonical: String,
}
impl RegistrySnapshot {
pub fn len(&self) -> usize {
self.components.len()
}
pub fn is_empty(&self) -> bool {
self.components.is_empty()
}
pub fn by_kind(&self, kind: ComponentKind) -> Vec<&ComponentMetadata> {
self.components
.iter()
.filter(|meta| meta.kind == kind)
.collect()
}
pub fn count(&self, kind: ComponentKind) -> usize {
self.components.iter().filter(|m| m.kind == kind).count()
}
pub fn to_dot(&self) -> String {
let mut out = String::from("digraph registry {\n rankdir=LR;\n");
for kind in ComponentKind::ALL {
let members = self.by_kind(kind);
if members.is_empty() {
continue;
}
out.push_str(&format!(
" subgraph cluster_{} {{\n label=\"{}\";\n",
kind_label(kind),
kind_label(kind)
));
for meta in members {
let escaped_id = escape_dot_string(&meta.id.0);
out.push_str(&format!(
" \"{}:{}\" [label=\"{}\"];\n",
kind_label(kind),
escaped_id,
escaped_id
));
}
out.push_str(" }\n");
}
out.push_str("}\n");
out
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticSeverity {
Warning,
Error,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegistryDiagnostic {
pub severity: DiagnosticSeverity,
pub kind: ComponentKind,
pub name: String,
pub message: String,
}
fn kind_label(kind: ComponentKind) -> &'static str {
kind.as_str()
}
fn escape_dot_string(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}
pub(crate) fn alias_shadows_component(kind: ComponentKind, alias: &str) -> RegistryDiagnostic {
RegistryDiagnostic {
severity: DiagnosticSeverity::Warning,
kind,
name: alias.to_string(),
message: format!(
"alias `{alias}` shadows a registered {} of the same name; \
the component takes precedence and the alias is unreachable",
kind_label(kind)
),
}
}
pub(crate) fn name_reused_across_kinds(name: &str, kinds: &[ComponentKind]) -> RegistryDiagnostic {
let rendered = kinds
.iter()
.map(|k| k.as_str())
.collect::<Vec<_>>()
.join(", ");
RegistryDiagnostic {
severity: DiagnosticSeverity::Warning,
kind: kinds[0],
name: name.to_string(),
message: format!(
"name `{name}` is registered under multiple kinds ({rendered}); \
ensure callers disambiguate by kind"
),
}
}
pub(crate) fn dangling_alias(
kind: ComponentKind,
alias: &str,
canonical: &str,
) -> RegistryDiagnostic {
RegistryDiagnostic {
severity: DiagnosticSeverity::Error,
kind,
name: alias.to_string(),
message: format!(
"alias `{alias}` resolves to `{canonical}`, which is not a registered {}",
kind_label(kind)
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry::component::ComponentId;
#[test]
fn to_dot_escapes_quotes_and_backslashes_in_component_ids() {
let snapshot = RegistrySnapshot {
components: vec![ComponentMetadata {
id: ComponentId(r#"weird"name\with\backslashes"#.to_string()),
kind: ComponentKind::Tool,
description: None,
tags: Vec::new(),
aliases: Vec::new(),
}],
aliases: Vec::new(),
};
let dot = snapshot.to_dot();
assert!(!dot.contains("\"weird\"name"));
assert!(dot.contains(r#"weird\"name\\with\\backslashes"#));
}
}