use std::collections::{HashMap, HashSet, VecDeque};
use crate::{
ElicitationPattern, PatternDetails,
type_graph::registry::{all_graphable_types, lookup_type_graph},
};
#[derive(Debug, Clone)]
pub struct GraphNode {
pub name: String,
pub kind: NodeKind,
pub prompt: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeKind {
Survey,
Select,
Affirm,
Primitive,
Generic,
}
#[derive(Debug, Clone)]
pub struct GraphEdge {
pub from: String,
pub label: String,
pub to: String,
pub prompt: Option<String>,
}
#[derive(Debug, Clone, derive_more::Display, derive_more::Error)]
pub enum TypeGraphError {
#[display("root type '{}' is not registered in the TypeGraph inventory", _0)]
UnknownRoot(#[error(not(source))] String),
}
#[derive(Debug, Default)]
pub struct TypeGraph {
pub nodes: HashMap<String, GraphNode>,
pub edges: Vec<GraphEdge>,
pub roots: Vec<String>,
}
impl TypeGraph {
pub fn from_root(root: &str) -> Result<Self, TypeGraphError> {
Self::from_roots(&[root])
}
pub fn from_roots(roots: &[&str]) -> Result<Self, TypeGraphError> {
let mut graph = Self::default();
let mut visited: HashSet<String> = HashSet::new();
let mut queue: VecDeque<String> = VecDeque::new();
for &root in roots {
if lookup_type_graph(root).is_none() {
return Err(TypeGraphError::UnknownRoot(root.to_string()));
}
graph.roots.push(root.to_string());
queue.push_back(root.to_string());
}
while let Some(name) = queue.pop_front() {
if !visited.insert(name.clone()) {
continue;
}
match lookup_type_graph(&name) {
None => {
let kind = classify_leaf(&name);
graph.nodes.insert(
name.clone(),
GraphNode {
name,
kind,
prompt: None,
},
);
}
Some(meta) => {
let kind = match meta.pattern() {
ElicitationPattern::Survey => NodeKind::Survey,
ElicitationPattern::Select => NodeKind::Select,
ElicitationPattern::Affirm => NodeKind::Affirm,
ElicitationPattern::Primitive => NodeKind::Primitive,
};
graph.nodes.insert(
name.clone(),
GraphNode {
name: name.clone(),
kind,
prompt: meta.description.map(str::to_string),
},
);
match meta.details {
PatternDetails::Survey { fields } => {
for field in fields {
graph.edges.push(GraphEdge {
from: name.clone(),
label: field.name.to_string(),
to: field.type_name.to_string(),
prompt: field.prompt.map(str::to_string),
});
if !visited.contains(field.type_name) {
queue.push_back(field.type_name.to_string());
}
}
}
PatternDetails::Select { variants } => {
for variant in variants {
let variant_node = format!("{}::{}", name, variant.label);
if variant.fields.is_empty() {
graph.edges.push(GraphEdge {
from: name.clone(),
label: variant.label.clone(),
to: variant_node.clone(),
prompt: None,
});
if !visited.contains(&variant_node) {
visited.insert(variant_node.clone());
graph.nodes.insert(
variant_node.clone(),
GraphNode {
name: variant_node,
kind: NodeKind::Primitive,
prompt: None,
},
);
}
} else {
graph.edges.push(GraphEdge {
from: name.clone(),
label: variant.label.clone(),
to: variant_node.clone(),
prompt: None,
});
if !visited.contains(&variant_node) {
visited.insert(variant_node.clone());
graph.nodes.insert(
variant_node.clone(),
GraphNode {
name: variant_node.clone(),
kind: NodeKind::Select,
prompt: None,
},
);
for field in &variant.fields {
graph.edges.push(GraphEdge {
from: variant_node.clone(),
label: field.name.to_string(),
to: field.type_name.to_string(),
prompt: field.prompt.map(str::to_string),
});
if !visited.contains(field.type_name) {
queue.push_back(field.type_name.to_string());
}
}
}
}
}
}
PatternDetails::Affirm | PatternDetails::Primitive => {
}
}
}
}
}
Ok(graph)
}
pub fn registered_types() -> Vec<&'static str> {
all_graphable_types()
}
}
fn classify_leaf(name: &str) -> NodeKind {
let trimmed = name.trim();
let is_generic = !trimmed.contains("::")
&& !trimmed.contains('<')
&& trimmed.len() <= 2
&& trimmed.chars().all(|c| c.is_ascii_uppercase());
if is_generic {
NodeKind::Generic
} else {
NodeKind::Primitive
}
}