use std::collections::HashMap;
use panproto_gat::Name;
use serde::{Deserialize, Serialize};
use crate::value::{FieldPresence, Value};
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum NodeShape {
#[default]
Plain,
List,
XmlElement {
tag: Name,
},
XmlTextSegment,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Node {
pub id: u32,
pub anchor: Name,
pub value: Option<FieldPresence>,
pub discriminator: Option<Name>,
pub extra_fields: HashMap<String, Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub position: Option<u32>,
#[serde(default, skip_serializing_if = "node_shape_is_default")]
pub shape: NodeShape,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub annotations: HashMap<String, Value>,
}
const fn node_shape_is_default(shape: &NodeShape) -> bool {
matches!(shape, NodeShape::Plain)
}
impl Node {
#[must_use]
pub fn new(id: u32, anchor: impl Into<Name>) -> Self {
Self {
id,
anchor: anchor.into(),
value: None,
discriminator: None,
extra_fields: HashMap::new(),
position: None,
shape: NodeShape::Plain,
annotations: HashMap::new(),
}
}
#[must_use]
pub fn with_shape(mut self, shape: NodeShape) -> Self {
self.shape = shape;
self
}
#[must_use]
pub const fn is_list(&self) -> bool {
matches!(self.shape, NodeShape::List)
}
#[must_use]
pub const fn is_xml_text_segment(&self) -> bool {
matches!(self.shape, NodeShape::XmlTextSegment)
}
#[must_use]
pub const fn xml_tag(&self) -> Option<&Name> {
match &self.shape {
NodeShape::XmlElement { tag } => Some(tag),
_ => None,
}
}
#[must_use]
pub fn with_value(mut self, value: FieldPresence) -> Self {
self.value = Some(value);
self
}
#[must_use]
pub fn with_discriminator(mut self, disc: impl Into<Name>) -> Self {
self.discriminator = Some(disc.into());
self
}
#[must_use]
pub fn with_extra_field(mut self, key: impl Into<String>, value: Value) -> Self {
self.extra_fields.insert(key.into(), value);
self
}
#[must_use]
pub fn has_value(&self) -> bool {
self.value.as_ref().is_some_and(FieldPresence::is_present)
}
#[must_use]
pub const fn is_leaf(&self) -> bool {
self.value.is_some()
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn node_builder() {
let node = Node::new(0, "post:body.text")
.with_value(FieldPresence::Present(Value::Str("hello".into())))
.with_discriminator("string")
.with_extra_field("$lang", Value::Str("en".into()));
assert_eq!(node.id, 0);
assert_eq!(node.anchor, "post:body.text");
assert!(node.has_value());
assert!(node.is_leaf());
assert_eq!(node.discriminator.as_deref(), Some("string"));
assert_eq!(
node.extra_fields.get("$lang"),
Some(&Value::Str("en".into()))
);
}
#[test]
fn node_without_value() {
let node = Node::new(1, "post:body");
assert!(!node.has_value());
assert!(!node.is_leaf());
}
#[test]
fn default_shape_is_plain() {
let node = Node::new(0, "v");
assert!(matches!(node.shape, NodeShape::Plain));
assert!(!node.is_list());
assert!(!node.is_xml_text_segment());
assert_eq!(node.xml_tag(), None);
}
#[test]
fn with_shape_list() {
let node = Node::new(0, "v").with_shape(NodeShape::List);
assert!(node.is_list());
assert!(!node.is_xml_text_segment());
assert_eq!(node.xml_tag(), None);
}
#[test]
fn with_shape_xml_element_carries_tag() {
let node = Node::new(0, "v").with_shape(NodeShape::XmlElement {
tag: Name::from("para"),
});
assert!(!node.is_list());
assert!(!node.is_xml_text_segment());
assert_eq!(node.xml_tag().map(Name::as_ref), Some("para"));
}
#[test]
fn with_shape_xml_text_segment() {
let node = Node::new(0, "v").with_shape(NodeShape::XmlTextSegment);
assert!(!node.is_list());
assert!(node.is_xml_text_segment());
assert_eq!(node.xml_tag(), None);
}
#[test]
fn shape_serialization_skips_default() {
let node = Node::new(0, "v");
let json = serde_json::to_string(&node).expect("serialize plain node");
assert!(
!json.contains("shape"),
"Plain shape must skip-serialize: {json}"
);
}
#[test]
fn shape_serialization_emits_non_default() {
let node = Node::new(0, "v").with_shape(NodeShape::List);
let json = serde_json::to_string(&node).expect("serialize list node");
assert!(json.contains("\"shape\""), "non-Plain shape must serialize");
assert!(json.contains("\"list\""), "expected list tag in: {json}");
}
}