use std::error::Error;
use std::fmt;
use serde::de::{self, IgnoredAny, MapAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use crate::Action;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NodeId(pub String);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Role {
App,
Pane,
List,
ListItem,
Tree,
TreeItem,
Table,
Row,
Cell,
TextInput,
Button,
Checkbox,
Select,
Option,
Tabs,
Tab,
Link,
Text,
Log,
Terminal,
Image,
Chart,
ProgressBar,
Status,
Scrollbar,
Dialog,
Menu,
MenuItem,
Other,
}
impl Role {
fn from_wire(name: &str) -> Self {
match name {
"app" => Role::App,
"pane" => Role::Pane,
"list" => Role::List,
"list_item" => Role::ListItem,
"tree" => Role::Tree,
"tree_item" => Role::TreeItem,
"table" => Role::Table,
"row" => Role::Row,
"cell" => Role::Cell,
"text_input" => Role::TextInput,
"button" => Role::Button,
"checkbox" => Role::Checkbox,
"select" => Role::Select,
"option" => Role::Option,
"tabs" => Role::Tabs,
"tab" => Role::Tab,
"link" => Role::Link,
"text" => Role::Text,
"log" => Role::Log,
"terminal" => Role::Terminal,
"image" => Role::Image,
"chart" => Role::Chart,
"progress_bar" => Role::ProgressBar,
"status" => Role::Status,
"scrollbar" => Role::Scrollbar,
"dialog" => Role::Dialog,
"menu" => Role::Menu,
"menu_item" => Role::MenuItem,
_ => Role::Other,
}
}
}
impl<'de> Deserialize<'de> for Role {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct RoleVisitor;
impl<'de> Visitor<'de> for RoleVisitor {
type Value = Role;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a role name")
}
fn visit_str<E>(self, name: &str) -> Result<Role, E>
where
E: de::Error,
{
Ok(Role::from_wire(name))
}
fn visit_map<A>(self, mut map: A) -> Result<Role, A::Error>
where
A: MapAccess<'de>,
{
let Some(name) = map.next_key::<String>()? else {
return Err(de::Error::invalid_length(0, &self));
};
map.next_value::<IgnoredAny>()?;
while map.next_entry::<IgnoredAny, IgnoredAny>()?.is_some() {}
Ok(Role::from_wire(&name))
}
}
deserializer.deserialize_any(RoleVisitor)
}
}
pub const MAX_NODE_DEPTH: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeTooDeep {
depth: usize,
deepest: NodeId,
}
impl TreeTooDeep {
pub fn depth(&self) -> usize {
self.depth
}
pub fn deepest(&self) -> &NodeId {
&self.deepest
}
}
impl fmt::Display for TreeTooDeep {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"node tree is {} levels deep at node `{}`, over the {MAX_NODE_DEPTH} level limit for a taria snapshot",
self.depth, self.deepest.0
)
}
}
impl Error for TreeTooDeep {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Node {
pub id: NodeId,
pub role: Role,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(default)]
pub focused: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<Action>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<Node>,
}
impl Node {
pub fn new(id: impl Into<String>, role: Role) -> Self {
Self {
id: NodeId(id.into()),
role,
label: None,
value: None,
focused: false,
actions: Vec::new(),
children: Vec::new(),
}
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = Some(value.into());
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
pub fn action(mut self, action: Action) -> Self {
self.actions.push(action);
self
}
pub fn actions(mut self, actions: impl IntoIterator<Item = Action>) -> Self {
self.actions.extend(actions);
self
}
pub fn child(mut self, child: Node) -> Self {
self.children.push(child);
self
}
pub fn children(mut self, children: impl IntoIterator<Item = Node>) -> Self {
self.children.extend(children);
self
}
pub fn depth(&self) -> usize {
self.deepest().0
}
pub fn check_depth(&self) -> Result<(), TreeTooDeep> {
let (depth, deepest) = self.deepest();
if depth > MAX_NODE_DEPTH {
return Err(TreeTooDeep {
depth,
deepest: deepest.clone(),
});
}
Ok(())
}
fn deepest(&self) -> (usize, &NodeId) {
let mut deepest = (1, &self.id);
let mut pending = vec![(self, 1usize)];
while let Some((node, level)) = pending.pop() {
if level > deepest.0 {
deepest = (level, &node.id);
}
for child in &node.children {
pending.push((child, level + 1));
}
}
deepest
}
}
#[cfg(test)]
mod tests {
use super::*;
fn role_json() -> Vec<(Role, &'static str)> {
let mut table: Vec<(Role, &'static str)> = Vec::new();
let mut role = Some(Role::App);
while let Some(current) = role {
if table.iter().any(|(seen, _)| *seen == current) {
break;
}
let (json, next) = match current {
Role::App => (r#""app""#, Some(Role::Pane)),
Role::Pane => (r#""pane""#, Some(Role::List)),
Role::List => (r#""list""#, Some(Role::ListItem)),
Role::ListItem => (r#""list_item""#, Some(Role::Tree)),
Role::Tree => (r#""tree""#, Some(Role::TreeItem)),
Role::TreeItem => (r#""tree_item""#, Some(Role::Table)),
Role::Table => (r#""table""#, Some(Role::Row)),
Role::Row => (r#""row""#, Some(Role::Cell)),
Role::Cell => (r#""cell""#, Some(Role::TextInput)),
Role::TextInput => (r#""text_input""#, Some(Role::Button)),
Role::Button => (r#""button""#, Some(Role::Checkbox)),
Role::Checkbox => (r#""checkbox""#, Some(Role::Select)),
Role::Select => (r#""select""#, Some(Role::Option)),
Role::Option => (r#""option""#, Some(Role::Tabs)),
Role::Tabs => (r#""tabs""#, Some(Role::Tab)),
Role::Tab => (r#""tab""#, Some(Role::Link)),
Role::Link => (r#""link""#, Some(Role::Text)),
Role::Text => (r#""text""#, Some(Role::Log)),
Role::Log => (r#""log""#, Some(Role::Terminal)),
Role::Terminal => (r#""terminal""#, Some(Role::Image)),
Role::Image => (r#""image""#, Some(Role::Chart)),
Role::Chart => (r#""chart""#, Some(Role::ProgressBar)),
Role::ProgressBar => (r#""progress_bar""#, Some(Role::Status)),
Role::Status => (r#""status""#, Some(Role::Scrollbar)),
Role::Scrollbar => (r#""scrollbar""#, Some(Role::Dialog)),
Role::Dialog => (r#""dialog""#, Some(Role::Menu)),
Role::Menu => (r#""menu""#, Some(Role::MenuItem)),
Role::MenuItem => (r#""menu_item""#, Some(Role::Other)),
Role::Other => (r#""other""#, None),
};
table.push((current, json));
role = next;
}
table
}
#[test]
fn the_role_table_walks_the_whole_vocabulary() {
let table = role_json();
assert_eq!(
table.last().map(|(role, _)| *role),
Some(Role::Other),
"the walk must end at the last role, not partway: {table:?}"
);
}
#[test]
fn every_role_serializes_to_its_frozen_json() {
for (role, expected) in role_json() {
assert_eq!(
serde_json::to_string(&role).unwrap(),
expected,
"role {role:?}"
);
}
}
#[test]
fn every_role_roundtrips() {
for (role, json) in role_json() {
let back: Role = serde_json::from_str(json).unwrap();
assert_eq!(back, role, "role {role:?} via {json}");
}
}
#[test]
fn unknown_role_degrades_to_other() {
let node: Node =
serde_json::from_str(r#"{"id":"n","role":"sparkline","focused":false,"label":"cpu"}"#)
.unwrap();
assert_eq!(node.role, Role::Other);
assert_eq!(node.label.as_deref(), Some("cpu"));
}
#[test]
fn focused_may_be_absent_and_is_still_serialized() {
let node: Node = serde_json::from_str(r#"{"id":"n","role":"text"}"#).unwrap();
assert!(!node.focused);
let json = serde_json::to_string(&Node::new("n", Role::Text)).unwrap();
assert!(
json.contains(r#""focused":false"#),
"the field still goes on the wire: {json}"
);
}
#[test]
fn role_object_form_still_parses() {
assert_eq!(
serde_json::from_str::<Role>(r#"{"button":null}"#).unwrap(),
Role::Button
);
assert_eq!(
serde_json::from_str::<Role>(r#"{"sparkline":null}"#).unwrap(),
Role::Other
);
}
#[test]
fn malformed_role_is_still_an_error() {
assert!(serde_json::from_str::<Role>("7").is_err());
assert!(serde_json::from_str::<Role>("{}").is_err());
}
#[test]
fn builder_fills_all_fields() {
let node = Node::new("list", Role::List)
.label("Tasks")
.value("2 of 5 done")
.focused(true)
.action(Action::Select)
.actions([Action::Scroll, Action::Custom("archive".into())])
.child(Node::new("item-1", Role::ListItem).label("Buy milk"))
.children([
Node::new("item-2", Role::ListItem),
Node::new("item-3", Role::ListItem),
]);
assert_eq!(node.id, NodeId("list".into()));
assert_eq!(node.role, Role::List);
assert_eq!(node.label.as_deref(), Some("Tasks"));
assert_eq!(node.value.as_deref(), Some("2 of 5 done"));
assert!(node.focused);
assert_eq!(
node.actions,
vec![
Action::Select,
Action::Scroll,
Action::Custom("archive".into())
]
);
assert_eq!(node.children.len(), 3);
assert_eq!(node.children[0].label.as_deref(), Some("Buy milk"));
assert!(!node.children[1].focused);
}
#[test]
fn nested_node_roundtrips() {
let node = Node::new("root", Role::App).child(
Node::new("pane", Role::Pane).child(Node::new("input", Role::TextInput).focused(true)),
);
let json = serde_json::to_string(&node).unwrap();
let back: Node = serde_json::from_str(&json).unwrap();
assert_eq!(back, node);
}
#[test]
fn empty_optional_fields_are_omitted() {
let json = serde_json::to_string(&Node::new("n", Role::Text)).unwrap();
assert!(!json.contains("label"), "json: {json}");
assert!(!json.contains("value"), "json: {json}");
assert!(!json.contains("actions"), "json: {json}");
assert!(!json.contains("children"), "json: {json}");
}
fn chain(depth: usize) -> Node {
let mut node = Node::new(format!("n{depth}"), Role::TreeItem);
for level in (1..depth).rev() {
node = Node::new(format!("n{level}"), Role::TreeItem).child(node);
}
node
}
fn drop_iteratively(root: Node) {
let mut pending = vec![root];
while let Some(mut node) = pending.pop() {
pending.append(&mut node.children);
}
}
#[test]
fn depth_counts_the_root_and_follows_the_longest_branch() {
assert_eq!(Node::new("leaf", Role::Text).depth(), 1);
assert_eq!(chain(9).depth(), 9);
let root = Node::new("root", Role::App)
.child(Node::new("shallow", Role::Text))
.child(chain(3));
assert_eq!(root.depth(), 4);
}
#[test]
fn check_depth_passes_at_the_limit_and_names_the_node_past_it() {
assert!(chain(MAX_NODE_DEPTH).check_depth().is_ok());
let err = chain(MAX_NODE_DEPTH + 1).check_depth().unwrap_err();
assert_eq!(err.depth(), MAX_NODE_DEPTH + 1);
assert_eq!(err.deepest(), &NodeId(format!("n{}", MAX_NODE_DEPTH + 1)));
let message = err.to_string();
assert!(message.contains(&MAX_NODE_DEPTH.to_string()), "{message}");
assert!(
message.contains(&format!("n{}", MAX_NODE_DEPTH + 1)),
"{message}"
);
}
#[test]
fn a_tree_deeper_than_the_call_stack_is_still_measurable() {
let deep = chain(100_000);
assert_eq!(deep.depth(), 100_000);
assert!(deep.check_depth().is_err());
drop_iteratively(deep);
}
#[test]
fn the_depth_limit_has_headroom_under_the_parser() {
use crate::Snapshot;
use crate::wire::AppToBridge;
let parses = |depth: usize| {
let msg = AppToBridge::Snapshot(Snapshot::new(1, chain(depth)));
let line = serde_json::to_string(&msg).unwrap();
serde_json::from_str::<AppToBridge>(&line).is_ok()
};
assert!(parses(MAX_NODE_DEPTH), "a tree at the limit must parse");
if let Some(ceiling) = (MAX_NODE_DEPTH..=256).find(|&depth| !parses(depth)) {
assert!(
ceiling >= MAX_NODE_DEPTH + 16,
"only {} levels between the limit and the parser, which stops at {ceiling}",
ceiling - MAX_NODE_DEPTH
);
}
}
}