use serde::de::Error as DeError;
use serde::ser::Error as SerError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value as Json;
use crate::extras::button::Button;
use crate::extras::code::Code;
use crate::extras::diff::Diff;
use crate::extras::empty::Empty;
use crate::extras::image::Image;
use crate::extras::input::Input;
use crate::extras::key_hints::KeyHints;
use crate::extras::log::Log;
use crate::extras::markdown::Markdown;
use crate::extras::select::Select;
use crate::extras::spinner::Spinner;
use crate::extras::toggle::Toggle;
use crate::extras::tree::Tree;
use crate::layout::Layout;
use crate::style::Style;
use crate::widgets::barchart::BarChart;
use crate::widgets::block::{Block, BlockProps};
use crate::widgets::chart::Chart;
use crate::widgets::gauge::{Gauge, LineGauge};
use crate::widgets::list::List;
use crate::widgets::paragraph::Paragraph;
use crate::widgets::scrollbar::Scrollbar;
use crate::widgets::sparkline::Sparkline;
use crate::widgets::table::Table;
use crate::widgets::tabs::Tabs;
pub type Key = String;
pub type ActionId = String;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Common {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<Key>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style: Option<Style>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block: Option<BlockProps>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fallback: Option<Box<Node>>,
}
impl Common {
#[must_use]
pub fn new() -> Self {
Self {
key: None,
style: None,
block: None,
fallback: None,
}
}
#[must_use]
pub fn key(mut self, key: impl Into<Key>) -> Self {
self.key = Some(key.into());
self
}
#[must_use]
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
#[must_use]
pub fn block(mut self, block: BlockProps) -> Self {
self.block = Some(block);
self
}
#[must_use]
pub fn fallback(mut self, node: impl Into<Node>) -> Self {
self.fallback = Some(Box::new(node.into()));
self
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Unknown {
pub t: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<Key>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style: Option<Style>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block: Option<BlockProps>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fallback: Option<Box<Node>>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Node {
Layout(Layout),
Block(Block),
Paragraph(Paragraph),
List(List),
Table(Table),
Tabs(Tabs),
Gauge(Gauge),
LineGauge(LineGauge),
Sparkline(Sparkline),
BarChart(BarChart),
Chart(Chart),
Scrollbar(Scrollbar),
Markdown(Markdown),
Code(Code),
Diff(Diff),
Image(Image),
Log(Log),
Tree(Tree),
KeyHints(KeyHints),
Input(Input),
Toggle(Toggle),
Select(Select),
Button(Button),
Spinner(Spinner),
Empty(Empty),
Unknown(Unknown),
}
macro_rules! for_each_kind {
($macro:ident) => {
$macro! {
(Layout, "Layout", Layout),
(Block, "Block", Block),
(Paragraph, "Paragraph", Paragraph),
(List, "List", List),
(Table, "Table", Table),
(Tabs, "Tabs", Tabs),
(Gauge, "Gauge", Gauge),
(LineGauge, "LineGauge", LineGauge),
(Sparkline, "Sparkline", Sparkline),
(BarChart, "BarChart", BarChart),
(Chart, "Chart", Chart),
(Scrollbar, "Scrollbar", Scrollbar),
(Markdown, "Markdown", Markdown),
(Code, "Code", Code),
(Diff, "Diff", Diff),
(Image, "Image", Image),
(Log, "Log", Log),
(Tree, "Tree", Tree),
(KeyHints, "KeyHints", KeyHints),
(Input, "Input", Input),
(Toggle, "Toggle", Toggle),
(Select, "Select", Select),
(Button, "Button", Button),
(Spinner, "Spinner", Spinner),
(Empty, "Empty", Empty)
}
};
}
macro_rules! impl_node_kind_table {
($(($variant:ident, $tag:literal, $ty:ty)),* $(,)?) => {
pub const NODE_KINDS: [&str; 25] = [$($tag),*];
impl Node {
#[must_use]
pub fn tag(&self) -> Option<&'static str> {
match self {
$(Self::$variant(_) => Some($tag),)*
Self::Unknown(_) => None,
}
}
#[must_use]
pub fn kind(&self) -> &str {
match self {
$(Self::$variant(_) => $tag,)*
Self::Unknown(u) => &u.t,
}
}
#[must_use]
pub fn common(&self) -> CommonRef<'_> {
match self {
$(Self::$variant(inner) => CommonRef {
key: inner.common.key.as_deref(),
style: inner.common.style.as_ref(),
block: inner.common.block.as_ref(),
fallback: inner.common.fallback.as_deref(),
},)*
Self::Unknown(u) => CommonRef {
key: u.key.as_deref(),
style: u.style.as_ref(),
block: u.block.as_ref(),
fallback: u.fallback.as_deref(),
},
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct CommonRef<'a> {
pub key: Option<&'a str>,
pub style: Option<&'a Style>,
pub block: Option<&'a BlockProps>,
pub fallback: Option<&'a Node>,
}
impl Serialize for Node {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
fn tagged<S: Serializer>(
serializer: S,
tag: &str,
value: &impl Serialize,
) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let inner = serde_json::to_value(value)
.map_err(|e| S::Error::custom(e.to_string()))?;
let Json::Object(map) = inner else {
return Err(S::Error::custom("node props must serialize to an object"));
};
let mut out = serializer.serialize_map(Some(map.len() + 1))?;
out.serialize_entry("t", tag)?;
for (k, v) in map {
out.serialize_entry(&k, &v)?;
}
out.end()
}
match self {
$(Self::$variant(inner) => tagged(serializer, $tag, inner),)*
Self::Unknown(u) => u.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for Node {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = Json::deserialize(deserializer)?;
let tag = match value.get("t") {
Some(Json::String(tag)) => tag.clone(),
_ if !value.is_object() => {
return Err(D::Error::custom("a node must be a JSON object"));
}
_ => {
return Err(D::Error::custom("a node needs a string \"t\" tag"));
}
};
match tag.as_str() {
$($tag => serde_json::from_value::<$ty>(value)
.map(Self::$variant)
.map_err(|e| D::Error::custom(format!("{tag}: {}", e))),)*
_ => serde_json::from_value::<Unknown>(value)
.map(Self::Unknown)
.map_err(|e| D::Error::custom(format!("{tag}: {}", e))),
}
}
}
};
}
for_each_kind!(impl_node_kind_table);
pub trait IntoNode: Into<Node> {
fn into_node(self) -> Node {
self.into()
}
}
impl IntoNode for Node {}
pub trait WithCommon: Sized {
fn common_mut(&mut self) -> &mut Common;
#[must_use]
fn key(mut self, key: impl Into<Key>) -> Self {
self.common_mut().key = Some(key.into());
self
}
#[must_use]
fn style(mut self, style: Style) -> Self {
self.common_mut().style = Some(style);
self
}
#[must_use]
fn block(mut self, block: impl Into<BlockProps>) -> Self {
self.common_mut().block = Some(block.into());
self
}
#[must_use]
fn fallback(mut self, node: impl Into<Node>) -> Self {
self.common_mut().fallback = Some(Box::new(node.into()));
self
}
}
macro_rules! impl_with_common {
($(($variant:ident, $tag:literal, $ty:ty)),* $(,)?) => {
$(impl WithCommon for $ty {
fn common_mut(&mut self) -> &mut Common {
&mut self.common
}
})*
};
}
for_each_kind!(impl_with_common);
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "tests assert by unwrapping"
)]
use super::*;
#[test]
fn unknown_kind_is_kept_with_fallback() {
let raw = r#"{"t":"Holgram","style":{"fg":"accent"},"fallback":{"t":"Spinner"}}"#;
let node: Node = serde_json::from_str(raw).expect("unknown node parses");
assert_eq!(node.kind(), "Holgram");
let fallback = node.common().fallback.expect("fallback kept");
assert_eq!(fallback.tag(), Some("Spinner"));
let round: Node = serde_json::from_str(&serde_json::to_string(&node).expect("serializes"))
.expect("re-parses");
assert_eq!(node, round);
}
#[test]
fn missing_tag_is_an_error() {
assert!(
serde_json::from_str::<Node>(r#"{"text":"hi"}"#).is_err(),
"missing t must not parse"
);
}
#[test]
fn all_tags_round_trip() {
let samples: Vec<Node> = vec![
Layout::vertical([crate::layout::Constraint::Fill(1)]).into(),
Block::bordered().into(),
Paragraph::new("x").into(),
List::new(["a"]).into(),
Table::new(
[crate::widgets::table::Row::new(["a"])],
[crate::layout::Constraint::Length(4)],
)
.into(),
Tabs::new([crate::widgets::tabs::TabTitle::new("a", "A")]).into(),
Gauge::new().into(),
LineGauge::new(0.5).into(),
Sparkline::new([Some(1), None]).into(),
BarChart::new([crate::widgets::barchart::BarGroup::new([
crate::widgets::barchart::Bar::new(1),
])])
.into(),
Chart::new([crate::widgets::chart::Dataset::new([[0.0, 1.0]])]).into(),
Scrollbar::new(crate::widgets::scrollbar::ScrollbarState::new(10)).into(),
Markdown::new("# x").into(),
Code::new("let x = 1;").into(),
Diff::patch("@@ -1 +1 @@\n-a\n+b").into(),
Image::new(crate::extras::image::ImageSource::path("a.png"), "an image").into(),
Log::new(["line"]).into(),
Tree::new([crate::extras::tree::TreeNode::new("a", "A")]).into(),
KeyHints::new([("enter", "open")]).into(),
Input::new("name").into(),
Toggle::new("on", "Enabled", true).into(),
Select::new("mode", [("a", "A")]).into(),
Button::new("Go", "go").into(),
Spinner::new("working").into(),
Empty::new().title("Nothing").into(),
];
assert_eq!(samples.len(), NODE_KINDS.len());
for node in samples {
let json = serde_json::to_value(&node).unwrap_or(Json::Null);
let back: Node = serde_json::from_value(json).unwrap_or(Node::Unknown(Unknown {
t: "boom".into(),
..Unknown::default()
}));
assert_eq!(node, back, "round trip for {}", node.kind());
}
}
}