use std::collections::HashMap;
use arora_types::module::high::TypeRef;
use arora_types::value::Value;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Io {
pub id: Uuid,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ty: Option<TypeRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub predetermined_key: Option<String>,
}
impl Io {
pub fn new(id: Uuid) -> Self {
Self {
id,
ty: None,
predetermined_key: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Port {
pub node: Uuid,
pub port: Uuid,
}
impl Port {
pub fn new(node: Uuid, port: Uuid) -> Self {
Self { node, port }
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum LinkSource {
Literal(Value),
Variable(Uuid),
Port(Port),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Link {
pub target: Port,
pub source: LinkSource,
}
impl Link {
pub fn new(target: Port, source: LinkSource) -> Self {
Self { target, source }
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
pub struct Node {
pub id: Uuid,
pub function: Uuid,
#[serde(default)]
pub inputs: Vec<Io>,
#[serde(default)]
pub outputs: Vec<Io>,
#[serde(default)]
pub children: Option<Vec<Uuid>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
pub struct Graph {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root: Option<Uuid>,
#[serde(default)]
pub nodes: HashMap<Uuid, Node>,
#[serde(default)]
pub links: Vec<Link>,
#[serde(default)]
pub variables: HashMap<Uuid, String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GraphError {
pub message: String,
}
impl std::fmt::Display for GraphError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for GraphError {}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
pub struct GraphDiff {
#[serde(default)]
pub add_nodes: Vec<Node>,
#[serde(default)]
pub remove_nodes: Vec<Uuid>,
#[serde(default)]
pub add_links: Vec<Link>,
#[serde(default)]
pub remove_links: Vec<Port>,
#[serde(default)]
pub set_predetermined: Vec<(Port, Option<String>)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub set_root: Option<Uuid>,
#[serde(default)]
pub variables: HashMap<Uuid, String>,
}
impl GraphDiff {
pub fn load(graph: Graph) -> Self {
let mut add_nodes: Vec<Node> = graph.nodes.into_values().collect();
if let Some(root) = graph.root {
add_nodes.sort_by_key(|n| n.id != root);
}
Self {
add_nodes,
add_links: graph.links,
set_root: graph.root,
variables: graph.variables,
..Self::default()
}
}
}
impl Graph {
pub fn empty() -> Self {
Self::default()
}
pub fn node(&self, id: &Uuid) -> Option<&Node> {
self.nodes.get(id)
}
pub fn link_to(&self, target: &Port) -> Option<&Link> {
self.links.iter().find(|l| &l.target == target)
}
pub fn apply(&mut self, diff: GraphDiff) -> Result<(), GraphError> {
for target in &diff.remove_links {
self.links.retain(|l| &l.target != target);
}
for id in &diff.remove_nodes {
self.nodes.remove(id);
self.links
.retain(|l| &l.target.node != id && !links_source_is_node(&l.source, id));
if self.root == Some(*id) {
self.root = None;
}
}
for node in diff.add_nodes {
self.nodes.insert(node.id, node);
}
for link in diff.add_links {
self.links.retain(|l| l.target != link.target);
self.links.push(link);
}
for (port, key) in diff.set_predetermined {
let node = self.nodes.get_mut(&port.node).ok_or_else(|| GraphError {
message: format!("predetermined key targets unknown node {}", port.node),
})?;
let io = node
.inputs
.iter_mut()
.chain(node.outputs.iter_mut())
.find(|io| io.id == port.port)
.ok_or_else(|| GraphError {
message: format!(
"predetermined key targets unknown slot {} on node {}",
port.port, port.node
),
})?;
io.predetermined_key = key;
}
if let Some(root) = diff.set_root {
self.root = Some(root);
}
self.variables.extend(diff.variables);
Ok(())
}
}
fn links_source_is_node(source: &LinkSource, id: &Uuid) -> bool {
matches!(source, LinkSource::Port(p) if &p.node == id)
}
#[cfg(test)]
mod tests {
use super::*;
fn node(id: Uuid, function: Uuid) -> Node {
Node {
id,
function,
..Node::default()
}
}
#[test]
fn load_onto_empty_reproduces_the_graph() {
let a = Uuid::from_u128(0xA);
let b = Uuid::from_u128(0xB);
let mut graph = Graph::empty();
graph.nodes.insert(a, node(a, Uuid::from_u128(0xF1)));
graph.nodes.insert(b, node(b, Uuid::from_u128(0xF2)));
graph.links.push(Link::new(
Port::new(b, Uuid::from_u128(0x1)),
LinkSource::Port(Port::new(a, Uuid::from_u128(0x2))),
));
graph.root = Some(a);
graph
.variables
.insert(Uuid::from_u128(0x9), "battery".into());
let mut rebuilt = Graph::empty();
rebuilt.apply(GraphDiff::load(graph.clone())).unwrap();
assert_eq!(rebuilt, graph);
}
#[test]
fn load_diff_lists_the_root_node_first() {
let a = Uuid::from_u128(0xA);
let b = Uuid::from_u128(0xB);
let c = Uuid::from_u128(0xC);
let mut graph = Graph::empty();
for id in [a, b, c] {
graph.nodes.insert(id, node(id, Uuid::from_u128(0xF0)));
}
graph.root = Some(c);
let diff = GraphDiff::load(graph);
assert_eq!(diff.add_nodes.first().unwrap().id, c);
}
#[test]
fn removing_a_node_drops_links_touching_it() {
let a = Uuid::from_u128(0xA);
let b = Uuid::from_u128(0xB);
let mut graph = Graph::empty();
graph.nodes.insert(a, node(a, Uuid::from_u128(0xF1)));
graph.nodes.insert(b, node(b, Uuid::from_u128(0xF2)));
graph.links.push(Link::new(
Port::new(a, Uuid::from_u128(0x1)),
LinkSource::Port(Port::new(b, Uuid::from_u128(0x2))),
));
graph
.apply(GraphDiff {
remove_nodes: vec![b],
..GraphDiff::default()
})
.unwrap();
assert!(!graph.nodes.contains_key(&b));
assert!(graph.links.is_empty(), "dangling link dropped");
}
#[test]
fn adding_a_link_replaces_the_one_on_the_same_target() {
let a = Uuid::from_u128(0xA);
let target = Port::new(a, Uuid::from_u128(0x1));
let mut graph = Graph::empty();
graph.nodes.insert(a, node(a, Uuid::from_u128(0xF1)));
graph
.apply(GraphDiff {
add_links: vec![Link::new(target, LinkSource::Literal(Value::U8(1)))],
..GraphDiff::default()
})
.unwrap();
graph
.apply(GraphDiff {
add_links: vec![Link::new(target, LinkSource::Literal(Value::U8(2)))],
..GraphDiff::default()
})
.unwrap();
assert_eq!(graph.links.len(), 1);
assert_eq!(
graph.link_to(&target).unwrap().source,
LinkSource::Literal(Value::U8(2))
);
}
#[test]
fn set_predetermined_key_overrides_the_slot() {
let a = Uuid::from_u128(0xA);
let slot = Uuid::from_u128(0x1);
let mut graph = Graph::empty();
graph.nodes.insert(
a,
Node {
id: a,
function: Uuid::from_u128(0xF1),
inputs: vec![Io::new(slot)],
..Node::default()
},
);
graph
.apply(GraphDiff {
set_predetermined: vec![(Port::new(a, slot), Some("head/pitch".into()))],
..GraphDiff::default()
})
.unwrap();
assert_eq!(
graph.nodes[&a].inputs[0].predetermined_key.as_deref(),
Some("head/pitch")
);
}
#[test]
fn predetermined_key_on_unknown_slot_is_an_error() {
let a = Uuid::from_u128(0xA);
let mut graph = Graph::empty();
graph.nodes.insert(a, node(a, Uuid::from_u128(0xF1)));
let err = graph.apply(GraphDiff {
set_predetermined: vec![(Port::new(a, Uuid::from_u128(0x2)), Some("k".into()))],
..GraphDiff::default()
});
assert!(err.is_err());
}
}