#[derive(Clone, Debug, PartialEq)]
pub enum StaticValue {
Bool(bool),
Int(i64),
Float(f64),
Str(String),
}
#[derive(Clone, Debug, PartialEq)]
pub enum PropValue {
Static(StaticValue),
Hole(usize),
}
#[derive(Clone, Debug, PartialEq)]
pub struct TemplateNode {
pub widget: String,
pub args: Vec<PropValue>,
pub props: Vec<(String, PropValue)>,
pub children: Vec<TemplateNode>,
}
impl TemplateNode {
pub fn new(widget: impl Into<String>) -> Self {
Self { widget: widget.into(), args: Vec::new(), props: Vec::new(), children: Vec::new() }
}
pub fn with_arg_static(mut self, value: StaticValue) -> Self {
self.args.push(PropValue::Static(value));
self
}
pub fn with_arg_hole(mut self, index: usize) -> Self {
self.args.push(PropValue::Hole(index));
self
}
pub fn with_static(mut self, key: impl Into<String>, value: StaticValue) -> Self {
self.props.push((key.into(), PropValue::Static(value)));
self
}
pub fn with_hole(mut self, key: impl Into<String>, index: usize) -> Self {
self.props.push((key.into(), PropValue::Hole(index)));
self
}
pub fn with_child(mut self, child: TemplateNode) -> Self {
self.children.push(child);
self
}
fn hole_extent(&self) -> usize {
let hole_idx = |v: &PropValue| match v {
PropValue::Hole(i) => Some(i + 1),
PropValue::Static(_) => None,
};
let in_args = self.args.iter().filter_map(hole_idx).max().unwrap_or(0);
let in_props = self.props.iter().filter_map(|(_, v)| hole_idx(v)).max().unwrap_or(0);
let below = self.children.iter().map(TemplateNode::hole_extent).max().unwrap_or(0);
in_args.max(in_props).max(below)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TemplateKey {
pub file: String,
pub line: u32,
pub col: u32,
}
impl TemplateKey {
pub fn new(file: impl Into<String>, line: u32, col: u32) -> Self {
Self { file: file.into(), line, col }
}
}
impl std::fmt::Display for TemplateKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.col)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Template {
pub key: TemplateKey,
pub root: TemplateNode,
pub hole_count: usize,
}
impl Template {
pub fn new(key: TemplateKey, root: TemplateNode) -> Self {
let hole_count = root.hole_extent();
Self { key, root, hole_count }
}
pub fn hole_signature_matches(&self, other: &Template) -> bool {
self.hole_count == other.hole_count
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key() -> TemplateKey {
TemplateKey::new("src/app.rs", 12, 5)
}
#[test]
fn leaf_node_has_no_props_or_children() {
let n = TemplateNode::new("Text");
assert_eq!(n.widget, "Text");
assert!(n.props.is_empty());
assert!(n.children.is_empty());
}
#[test]
fn builder_records_statics_holes_and_children_in_order() {
let n = TemplateNode::new("Button")
.with_static("label", StaticValue::Str("Save".into()))
.with_hole("on_press", 0)
.with_child(TemplateNode::new("Icon"));
assert_eq!(n.props.len(), 2);
assert_eq!(n.props[0], ("label".to_string(), PropValue::Static(StaticValue::Str("Save".into()))));
assert_eq!(n.props[1], ("on_press".to_string(), PropValue::Hole(0)));
assert_eq!(n.children.len(), 1);
assert_eq!(n.children[0].widget, "Icon");
}
#[test]
fn hole_count_is_zero_for_a_fully_static_tree() {
let root = TemplateNode::new("Column")
.with_static("spacing", StaticValue::Int(12))
.with_child(TemplateNode::new("Text").with_static("content", StaticValue::Str("Hi".into())));
let t = Template::new(key(), root);
assert_eq!(t.hole_count, 0);
}
#[test]
fn hole_count_is_max_index_plus_one_across_the_whole_tree() {
let root = TemplateNode::new("Column")
.with_child(TemplateNode::new("Button").with_hole("on_press", 0))
.with_child(TemplateNode::new("Button").with_hole("on_press", 1));
let t = Template::new(key(), root);
assert_eq!(t.hole_count, 2);
}
#[test]
fn hole_count_uses_the_highest_index_even_when_sparse() {
let root = TemplateNode::new("Text").with_hole("content", 3);
let t = Template::new(key(), root);
assert_eq!(t.hole_count, 4);
}
#[test]
fn signature_matches_only_when_hole_counts_are_equal() {
let a = Template::new(
key(),
TemplateNode::new("Text").with_static("content", StaticValue::Str("A".into())),
);
let b = Template::new(
key(),
TemplateNode::new("Text").with_static("content", StaticValue::Str("B".into())),
);
assert!(a.hole_signature_matches(&b));
let c = Template::new(key(), TemplateNode::new("Text").with_hole("content", 0));
assert!(!a.hole_signature_matches(&c));
}
#[test]
fn key_displays_as_file_line_col() {
assert_eq!(TemplateKey::new("src/app.rs", 12, 5).to_string(), "src/app.rs:12:5");
}
#[test]
fn key_equality_and_hashing_identify_a_view_site() {
use std::collections::HashSet;
let mut seen = HashSet::new();
seen.insert(TemplateKey::new("src/app.rs", 12, 5));
assert!(seen.contains(&TemplateKey::new("src/app.rs", 12, 5)));
assert!(!seen.contains(&TemplateKey::new("src/app.rs", 12, 6)));
}
}