use crate::node::NodeCodec;
use gantz_ca::{Datum, GraphAddr, Name, NodeData, Registry, Timestamp};
use gantz_format::sexpr;
use gantz_format::{Addr, Form, GraphLabels, Loaded};
pub use gantz_format::FormatError;
const CLAIMED: &[&str] = &[
crate::section::DESCRIPTIONS_ID,
crate::section::VIEWS_ID,
crate::section::DEMOS_ID,
];
fn normalize_datum(codec: &NodeCodec, datum: Datum) -> Result<NodeData, FormatError> {
let Datum::Map(mut entries) = datum else {
return Err(FormatError::malformed("node datum is not a map"));
};
let Some(ix) = entries.iter().position(|(k, _)| k == "type") else {
return Err(FormatError::malformed("node datum has no `type` tag"));
};
let (_, tag) = entries.remove(ix);
let Datum::Str(tag) = tag else {
return Err(FormatError::malformed("node `type` tag is not a string"));
};
codec
.normalize(&NodeData::new(tag.clone(), Datum::Map(entries)))
.map_err(|e| FormatError::node_deserialize(tag, e.to_string()))
}
pub fn from_str(text: &str, now: Timestamp, codec: &NodeCodec) -> Result<Registry, FormatError> {
from_str_seeded(text, now, &std::collections::BTreeMap::new(), codec)
}
pub fn from_str_seeded(
text: &str,
now: Timestamp,
seed: &std::collections::BTreeMap<String, GraphAddr>,
codec: &NodeCodec,
) -> Result<Registry, FormatError> {
let loaded = gantz_format::from_str_normalized(text, now, &codec.sugars(), seed, &|datum| {
normalize_datum(codec, datum)
})?;
Ok(registry_from_loaded(loaded))
}
fn registry_from_loaded(mut loaded: Loaded) -> Registry {
let extra = std::mem::take(&mut loaded.extra);
for form in &extra {
match form.head.as_str() {
"descriptions" => apply_descriptions(form, &mut loaded),
"layout" => apply_layout(form, &mut loaded),
"demo" => apply_demo(form, &mut loaded),
other => log::warn!("ignoring unrecognised `.gantz` form `{other}`"),
}
}
loaded.registry
}
pub fn to_string(registry: &Registry, codec: &NodeCodec) -> Result<String, FormatError> {
let dumped = gantz_format::to_string_with(registry, &codec.sugars(), CLAIMED)?;
let mut sections = vec![dumped.text.trim_end().to_string()];
sections.extend(descriptions_text(registry));
for (commit_ca, view) in crate::section::views(registry) {
let Some(commit) = registry.commits().get(&commit_ca) else {
continue;
};
if let Some(labels) = dumped.graphs.get(&commit.graph) {
sections.push(layout_text(labels, &view, false));
}
}
for (name, demo) in crate::section::demos(registry) {
sections.push(format!("(demo {} {})", name, sexpr::quote(&demo)));
}
let mut result = sections.join("\n\n");
result.push('\n');
Ok(result)
}
pub fn to_string_named(registry: &Registry, codec: &NodeCodec) -> Result<String, FormatError> {
let dumped = gantz_format::to_string_named_with(registry, &codec.sugars(), CLAIMED)?;
let mut sections = vec![dumped.text.trim_end().to_string()];
sections.extend(descriptions_text(registry));
for (_name, commit_ca) in registry.heads() {
let (Some(view), Some(commit)) = (
crate::section::view(registry, &commit_ca),
registry.commits().get(&commit_ca),
) else {
continue;
};
if let Some(labels) = dumped.graphs.get(&commit.graph) {
sections.push(layout_text(labels, &view, true));
}
}
for (name, demo) in crate::section::demos(registry) {
sections.push(format!("(demo {} {})", name, sexpr::quote(&demo)));
}
let mut result = sections.join("\n\n");
result.push('\n');
Ok(result)
}
fn apply_descriptions(form: &Form, loaded: &mut Loaded) {
let src = &form.raw;
let Ok(forms) = sexpr::read(src) else { return };
let Some(args) = forms.first().and_then(sexpr::list_args) else {
return;
};
for entry in &args[1..] {
let Some(eargs) = sexpr::list_args(entry) else {
continue;
};
let (Some(name), Some(text)) = (
eargs.first().and_then(sexpr::as_symbol),
eargs.get(1).and_then(sexpr::as_string),
) else {
continue;
};
if loaded.names.contains_key(&name) {
let name: Name = name.parse().expect("infallible");
crate::section::set_description(&mut loaded.registry, name, text);
}
}
}
fn descriptions_text(registry: &Registry) -> Option<String> {
let mut entries = crate::section::descriptions(registry).peekable();
entries.peek()?;
let mut s = "(descriptions".to_string();
for (name, text) in entries {
s.push_str(&format!("\n ({} {})", name, sexpr::quote(&text)));
}
s.push(')');
Some(s)
}
fn apply_layout(form: &Form, loaded: &mut Loaded) {
let src = &form.raw;
let Ok(forms) = sexpr::read(src) else { return };
let Some(args) = forms.first().and_then(sexpr::list_args) else {
return;
};
let Some(graph_id) = args.get(1).and_then(addr_of) else {
return;
};
let (Some(&head), Some(index)) = (
loaded.graph_head.get(&graph_id),
loaded.index.get(&graph_id),
) else {
return;
};
let mut layout = egui_graph::Layout::default();
let mut camera = crate::Camera::default();
for entry in &args[2..] {
let Some(eargs) = sexpr::list_args(entry) else {
continue;
};
let Some(head_sym) = eargs.first().and_then(sexpr::as_symbol) else {
continue;
};
if head_sym == "camera" {
let f: Vec<f32> = eargs[1..]
.iter()
.filter_map(|n| sexpr::as_f32(n, src))
.collect();
if f.len() == 3 {
camera = crate::Camera {
center: egui::pos2(f[0], f[1]),
zoom: f[2],
};
}
} else if head_sym == "scene" {
let f: Vec<f32> = eargs[1..]
.iter()
.filter_map(|n| sexpr::as_f32(n, src))
.collect();
if f.len() == 4 {
let rect = egui::Rect::from_min_max(egui::pos2(f[0], f[1]), egui::pos2(f[2], f[3]));
camera = crate::Camera {
center: rect.center(),
zoom: 1.0,
};
}
} else if let (Some(x), Some(y)) = (
eargs.get(1).and_then(|n| sexpr::as_f32(n, src)),
eargs.get(2).and_then(|n| sexpr::as_f32(n, src)),
) {
if let Some(&ix) = index.get(&head_sym) {
layout.insert(egui_graph::NodeId(ix as u64), egui::pos2(x, y));
}
}
}
let view = crate::SceneView { camera, layout };
crate::section::set_view(&mut loaded.registry, head, &view);
}
fn layout_text(labels: &GraphLabels, view: &crate::SceneView, bare_id: bool) -> String {
let mut positions: Vec<(String, f32, f32)> = view
.layout
.iter()
.filter_map(|(nid, pos)| {
labels
.labels
.get(&(nid.0 as usize))
.map(|l| (l.clone(), pos.x, pos.y))
})
.collect();
positions.sort_by(|a, b| a.0.cmp(&b.0));
let id = if bare_id {
labels.id.clone()
} else {
sexpr::quote(&labels.id)
};
let mut s = format!("(layout {id}");
for (label, x, y) in positions {
s.push_str(&format!(
"\n ({label} {} {})",
sexpr::num(x),
sexpr::num(y)
));
}
let c = view.camera;
s.push_str(&format!(
"\n (camera {} {} {}))",
sexpr::num(c.center.x),
sexpr::num(c.center.y),
sexpr::num(c.zoom),
));
s
}
fn apply_demo(form: &Form, loaded: &mut Loaded) {
let src = &form.raw;
let Ok(forms) = sexpr::read(src) else { return };
let Some(args) = forms.first().and_then(sexpr::list_args) else {
return;
};
let (Some(name), Some(demo)) = (
args.get(1).and_then(sexpr::as_symbol),
args.get(2).and_then(sexpr::as_string),
) else {
return;
};
if loaded.names.contains_key(&name) {
let name: Name = name.parse().expect("infallible");
crate::section::set_demo(&mut loaded.registry, name, demo);
}
}
fn addr_of(e: &sexpr::ExprKind) -> Option<Addr> {
sexpr::as_string(e)
.map(Addr::Concrete)
.or_else(|| sexpr::as_symbol(e).map(Addr::Label))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_node::{TestGraph, codec, commit_named, expr, named_ref};
use gantz_ca::CommitAddr;
use std::time::Duration;
fn name(s: &str) -> Name {
s.parse().unwrap()
}
fn test_registry() -> (Registry, CommitAddr) {
let mut reg = Registry::default();
let mut leaf_g = TestGraph::default();
leaf_g.add_node(expr("(+ 1 1)"));
let (_, leaf_ga) = commit_named(&mut reg, Duration::from_secs(1), &leaf_g, &name("leaf"));
let mut root_g = TestGraph::default();
root_g.add_node(named_ref("leaf", leaf_ga));
let (root_ca, _) = commit_named(&mut reg, Duration::from_secs(2), &root_g, &name("root"));
crate::section::set_description(&mut reg, name("root"), "the root".to_string());
crate::section::set_demo(&mut reg, name("root"), "demo-root".to_string());
let view = crate::SceneView {
camera: crate::Camera {
center: egui::pos2(3.0, 4.0),
zoom: 2.0,
},
layout: [(egui_graph::NodeId(0), egui::pos2(10.0, 20.0))]
.into_iter()
.collect(),
};
crate::section::set_view(&mut reg, root_ca, &view);
(reg, root_ca)
}
fn assert_sections_survive(parsed: &Registry) {
assert!(parsed.head(&name("leaf")).is_some());
let root_ca = parsed.head(&name("root")).expect("root head survives");
assert_eq!(
crate::section::description(parsed, &name("root")).as_deref(),
Some("the root"),
);
assert_eq!(
crate::section::demo(parsed, &name("root")).as_deref(),
Some("demo-root"),
);
let view = crate::section::view(parsed, &root_ca).expect("view survives");
assert_eq!(
view.layout.get(&egui_graph::NodeId(0)).copied(),
Some(egui::pos2(10.0, 20.0)),
);
assert_eq!(view.camera.center, egui::pos2(3.0, 4.0));
assert_eq!(view.camera.zoom, 2.0);
}
#[test]
fn sections_round_trip_through_text() {
let (reg, root_ca) = test_registry();
let text = to_string(®, &codec()).unwrap();
assert!(!text.contains("(section"));
assert!(text.contains("(descriptions"));
assert!(text.contains("(layout"));
assert!(text.contains("(demo root"));
let parsed: Registry = from_str(&text, Duration::from_secs(9), &codec()).unwrap();
assert_eq!(parsed.head(&name("root")), Some(root_ca));
assert_sections_survive(&parsed);
}
#[test]
fn sections_round_trip_through_named_text() {
let (reg, _root_ca) = test_registry();
let text = to_string_named(®, &codec()).unwrap();
assert!(!text.contains("(section"));
let parsed: Registry = from_str(&text, Duration::from_secs(9), &codec()).unwrap();
assert_sections_survive(&parsed);
}
#[test]
fn empty_sections_emit_no_forms() {
let mut reg = Registry::default();
let mut g = TestGraph::default();
g.add_node(expr("(+ 1 1)"));
commit_named(&mut reg, Duration::from_secs(1), &g, &name("only"));
let text = to_string(®, &codec()).unwrap();
assert!(!text.contains("(descriptions"));
assert!(!text.contains("(layout"));
assert!(!text.contains("(demo"));
}
}