rdom_core/node_or_string.rs
1//! `NodeOrString` — sum type for variadic tree-mutation helpers.
2//!
3//! JS's variadic `el.append(node1, "text", node2)` doesn't map
4//! cleanly to Rust (no varargs, no string coercion). The rdom
5//! shape takes a slice or iterator of [`NodeOrString`] — built
6//! with `From` impls so authors can write:
7//!
8//! ```
9//! use rdom_core::{Dom, NodeOrString};
10//!
11//! let mut dom: Dom = Dom::new();
12//! let p = dom.create_element("p");
13//! let strong = dom.create_element("strong");
14//!
15//! let items: Vec<NodeOrString> = vec![
16//! "hello, ".into(),
17//! strong.into(),
18//! "!".into(),
19//! ];
20//! # let _ = (p, items);
21//! ```
22//!
23//! The actual `append` / `prepend` / `before` / `after` /
24//! `replace_children` / `replace_with` methods on `NodeMut` come
25//! in M4b step 14. This module ships the carrier type ahead of
26//! time so step 14 has a stable substrate to build on.
27
28use crate::node_id::NodeId;
29
30/// Either an existing node (by id) or a string to be wrapped in a
31/// fresh text node. Built via `From` impls so call sites read
32/// naturally — see module docs for an example.
33///
34/// The variant is named `Text` (not `String`) to keep the carrier
35/// semantics explicit: a `Text` value becomes a `Text` node in
36/// the DOM, never inline markup.
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub enum NodeOrString {
39 /// An existing node — will be linked into the destination by
40 /// id. The caller is responsible for ensuring the node id is
41 /// valid for the target Dom.
42 Node(NodeId),
43 /// Text content — the receiver will create a fresh `Text`
44 /// node carrying this string.
45 Text(String),
46}
47
48impl From<NodeId> for NodeOrString {
49 fn from(id: NodeId) -> Self {
50 NodeOrString::Node(id)
51 }
52}
53
54impl From<String> for NodeOrString {
55 fn from(s: String) -> Self {
56 NodeOrString::Text(s)
57 }
58}
59
60impl From<&str> for NodeOrString {
61 fn from(s: &str) -> Self {
62 NodeOrString::Text(s.to_string())
63 }
64}
65
66impl From<&String> for NodeOrString {
67 fn from(s: &String) -> Self {
68 NodeOrString::Text(s.clone())
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::Dom;
76
77 #[test]
78 fn from_node_id_produces_node_variant() {
79 let mut dom: Dom = Dom::new();
80 let n = dom.create_element("p");
81 let v: NodeOrString = n.into();
82 assert_eq!(v, NodeOrString::Node(n));
83 }
84
85 #[test]
86 fn from_str_produces_text_variant() {
87 let v: NodeOrString = "hello".into();
88 assert_eq!(v, NodeOrString::Text("hello".into()));
89 }
90
91 #[test]
92 fn from_string_produces_text_variant() {
93 let v: NodeOrString = String::from("hello").into();
94 assert_eq!(v, NodeOrString::Text("hello".into()));
95 }
96
97 #[test]
98 fn from_string_ref_clones_and_produces_text_variant() {
99 let s = String::from("hello");
100 let v: NodeOrString = (&s).into();
101 assert_eq!(v, NodeOrString::Text("hello".into()));
102 // Original String unaffected — &String → NodeOrString clones.
103 assert_eq!(s, "hello");
104 }
105
106 #[test]
107 fn variadic_ergonomics_via_into_iterator() {
108 // The shape M4b step 14's `append` will take:
109 // `impl IntoIterator<Item = NodeOrString>`. Authors build
110 // a Vec<NodeOrString> via Into::into per element. This
111 // test demonstrates the ergonomic that future helpers will
112 // expose; it's also the canonical step-9 failing test
113 // before the From impls existed.
114 let mut dom: Dom = Dom::new();
115 let strong = dom.create_element("strong");
116 let em = dom.create_element("em");
117
118 let items: Vec<NodeOrString> = vec![
119 "before ".into(),
120 strong.into(),
121 " mid ".into(),
122 em.into(),
123 " after".into(),
124 ];
125
126 assert_eq!(items.len(), 5);
127 match &items[0] {
128 NodeOrString::Text(s) => assert_eq!(s, "before "),
129 _ => panic!("expected Text variant"),
130 }
131 match &items[1] {
132 NodeOrString::Node(n) => assert_eq!(*n, strong),
133 _ => panic!("expected Node variant"),
134 }
135 match &items[4] {
136 NodeOrString::Text(s) => assert_eq!(s, " after"),
137 _ => panic!("expected Text variant"),
138 }
139 }
140
141 #[test]
142 fn distinct_variants_compare_not_equal() {
143 let mut dom: Dom = Dom::new();
144 let n = dom.create_element("p");
145 let from_node = NodeOrString::Node(n);
146 let from_text = NodeOrString::Text("p".into());
147 assert_ne!(from_node, from_text);
148 }
149}