use crate::Result;
use crate::chunks::ChunkFile;
use crate::hash::fnv1a64;
use crate::read::Bytes;
use crate::structs::Structs;
const FILE: &str = "agraph.bin";
use crate::property::read_properties;
pub use crate::property::{Property, PropertyValue, property};
#[derive(Debug, Clone, PartialEq)]
pub struct Node {
pub id_hash: u64,
pub id: String,
pub label: String,
pub x: f32,
pub y: f32,
pub color: [u8; 4],
pub properties: Vec<Property>,
}
impl Node {
pub fn property(&self, key: &str) -> Option<&PropertyValue> {
property(&self.properties, key)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Edge {
pub from: u32,
pub to: u32,
pub weight: i32,
pub label: String,
pub properties: Vec<Property>,
}
impl Edge {
pub fn property(&self, key: &str) -> Option<&PropertyValue> {
property(&self.properties, key)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Graph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
pub structs: Structs,
}
impl Graph {
pub fn decode(bytes: &[u8]) -> Result<Self> {
let file = ChunkFile::open(bytes, "AGRAPH", &[1], FILE)?;
let desc = Bytes::new(file.require("DESC", FILE)?, FILE);
let node_count = desc.u32(0)? as usize;
let edge_count = desc.u32(4)? as usize;
let strings = Bytes::new(file.require("STRINGS", FILE)?, FILE);
let props = file.chunk("PROPS").map(|p| Bytes::new(p, FILE));
let nodes = Bytes::new(file.require("NODES", FILE)?, FILE)
.records_n(48, node_count)?
.map(|r| {
Ok(Node {
id_hash: r.u64(0)?,
id: r.text_ref(8, strings)?,
label: r.text_ref(16, strings)?,
x: r.f32(24)?,
y: r.f32(28)?,
color: r.array(32)?,
properties: read_properties(props, strings, r.u32(36)?, r.u32(40)?)?,
})
})
.collect::<Result<Vec<_>>>()?;
let edges = Bytes::new(file.require("EDGES", FILE)?, FILE)
.records_n(32, edge_count)?
.map(|r| {
let (from, to) = (r.u32(0)?, r.u32(4)?);
if from as usize >= node_count || to as usize >= node_count {
return Err(r.err(format!("edge {from} → {to} names a missing node")));
}
Ok(Edge {
from,
to,
weight: r.i32(8)?,
label: r.text_ref(12, strings)?,
properties: read_properties(props, strings, r.u32(20)?, r.u32(24)?)?,
})
})
.collect::<Result<Vec<_>>>()?;
Ok(Self {
nodes,
edges,
structs: Structs::from_chunks(&file, FILE)?,
})
}
pub fn node_index(&self, id: &str) -> Option<usize> {
let hash = fnv1a64(id.as_bytes());
self.nodes
.iter()
.position(|n| n.id_hash == hash && n.id == id)
}
pub fn node(&self, id: &str) -> Option<&Node> {
self.node_index(id).map(|i| &self.nodes[i])
}
pub fn edges_from(&self, index: usize) -> impl Iterator<Item = &Edge> {
self.edges.iter().filter(move |e| e.from as usize == index)
}
pub fn edges_to(&self, index: usize) -> impl Iterator<Item = &Edge> {
self.edges.iter().filter(move |e| e.to as usize == index)
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::chunks::tests::file;
pub fn text(strings: &mut Vec<u8>, text: &str) -> Vec<u8> {
let at = strings.len() as u32;
strings.extend_from_slice(text.as_bytes());
[at.to_le_bytes(), (text.len() as u32).to_le_bytes()].concat()
}
pub fn prop(strings: &mut Vec<u8>, key: &str, ty: u32, value: [u8; 8]) -> Vec<u8> {
let mut out = fnv1a64(key.as_bytes()).to_le_bytes().to_vec();
out.extend(text(strings, key));
out.extend(ty.to_le_bytes());
out.extend(0u32.to_le_bytes());
out.extend(value);
out
}
fn node(strings: &mut Vec<u8>, id: &str, x: f32, first: u32, count: u32) -> Vec<u8> {
let mut out = fnv1a64(id.as_bytes()).to_le_bytes().to_vec();
out.extend(text(strings, id));
out.extend(text(strings, &id.to_uppercase()));
out.extend(x.to_le_bytes());
out.extend(2.0f32.to_le_bytes());
out.extend([255, 0, 0, 255]);
for v in [first, count, 0] {
out.extend(v.to_le_bytes());
}
out
}
fn sample(drop: Option<&str>) -> Vec<u8> {
sample_with(drop, &[])
}
fn sample_with(drop: Option<&str>, extra: &[(&str, Vec<u8>)]) -> Vec<u8> {
let mut strings = Vec::new();
let mut nodes = node(&mut strings, "a", 1.0, 0, 1);
nodes.extend(node(&mut strings, "b", 3.0, 1, 0));
let mut props = prop(&mut strings, "hp", 1, (-5i64).to_le_bytes());
let mut edges = Vec::new();
for v in [0u32, 1] {
edges.extend(v.to_le_bytes());
}
edges.extend(7i32.to_le_bytes());
edges.extend(text(&mut strings, "go"));
for v in [1u32, 1, 0] {
edges.extend(v.to_le_bytes());
}
let (at, len) = (strings.len() as u32, 3u32);
strings.extend_from_slice(b"key");
let mut value = [0u8; 8];
value[..4].copy_from_slice(&at.to_le_bytes());
value[4..].copy_from_slice(&len.to_le_bytes());
props.extend(prop(&mut strings, "door", 3, value));
let mut desc = Vec::new();
for v in [2u32, 1, 2, 0] {
desc.extend(v.to_le_bytes());
}
let chunks: Vec<(&str, &[u8])> = [
("DESC", &desc[..]),
("NODES", &nodes[..]),
("EDGES", &edges[..]),
("PROPS", &props[..]),
("STRINGS", &strings[..]),
]
.into_iter()
.chain(extra.iter().map(|(name, payload)| (*name, &payload[..])))
.filter(|(name, _)| Some(*name) != drop)
.collect();
file("AGRAPH", 1, &chunks)
}
#[test]
fn decodes_nodes_edges_and_properties() {
let graph = Graph::decode(&sample(None)).unwrap();
assert_eq!(graph.nodes.len(), 2);
let a = &graph.nodes[0];
assert_eq!((a.id.as_str(), a.label.as_str()), ("a", "A"));
assert_eq!((a.x, a.y, a.color), (1.0, 2.0, [255, 0, 0, 255]));
assert_eq!(a.property("hp"), Some(&PropertyValue::Int(-5)));
assert!(graph.nodes[1].properties.is_empty());
let edge = &graph.edges[0];
assert_eq!((edge.from, edge.to, edge.weight), (0, 1, 7));
assert_eq!(edge.label, "go");
assert_eq!(
edge.property("door"),
Some(&PropertyValue::Text("key".into()))
);
assert_eq!(graph.node_index("b"), Some(1));
assert_eq!(graph.edges_from(0).count(), 1);
assert_eq!(graph.edges_to(0).count(), 0);
}
#[test]
fn reads_attached_structs() {
use crate::data::Value;
use crate::structs::Scope;
assert!(Graph::decode(&sample(None)).unwrap().structs.is_empty());
let extra = crate::structs::tests::chunks(2);
let graph = Graph::decode(&sample_with(None, &extra)).unwrap();
assert_eq!(graph.structs.all().len(), 2);
let meta = graph.structs.get(Scope::Asset, 0, "Meta").unwrap();
assert_eq!(meta.get("tier"), Some(&Value::Int(3)));
let step = graph.structs.of(Scope::Edge, 1).next().unwrap();
assert_eq!(step.type_name, "Step");
assert_eq!(step.get("speed").and_then(Value::as_float), Some(2.5));
assert!(Graph::decode(&sample_with(Some("SREFS"), &extra)).is_err());
}
#[test]
fn rejects_missing_chunks() {
assert!(Graph::decode(&sample(Some("EDGES"))).is_err());
assert!(Graph::decode(&sample(Some("PROPS"))).is_err());
}
}