1use std::{collections::HashSet, error::Error, fmt};
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(try_from = "u8", into = "u8")]
8pub enum LayoutDirection {
9 Left,
11 #[default]
13 Right,
14 Side,
16 Down,
18}
19impl TryFrom<u8> for LayoutDirection {
20 type Error = String;
21 fn try_from(value: u8) -> Result<Self, Self::Error> {
22 match value {
23 0 => Ok(Self::Left),
24 1 => Ok(Self::Right),
25 2 => Ok(Self::Side),
26 3 => Ok(Self::Down),
27 _ => Err(format!("Invalid layout direction: {value}")),
28 }
29 }
30}
31impl From<LayoutDirection> for u8 {
32 fn from(value: LayoutDirection) -> Self {
33 match value {
34 LayoutDirection::Left => 0,
35 LayoutDirection::Right => 1,
36 LayoutDirection::Side => 2,
37 LayoutDirection::Down => 3,
38 }
39 }
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(try_from = "u8", into = "u8")]
45pub enum NodeDirection {
46 Left,
48 Right,
50}
51impl TryFrom<u8> for NodeDirection {
52 type Error = String;
53 fn try_from(value: u8) -> Result<Self, Self::Error> {
54 match value {
55 0 => Ok(Self::Left),
56 1 => Ok(Self::Right),
57 _ => Err(format!("Invalid node direction: {value}")),
58 }
59 }
60}
61impl From<NodeDirection> for u8 {
62 fn from(value: NodeDirection) -> Self {
63 match value {
64 NodeDirection::Left => 0,
65 NodeDirection::Right => 1,
66 }
67 }
68}
69
70#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase", deny_unknown_fields)]
73pub struct NodeStyle {
74 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub font_size: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub font_family: Option<String>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub color: Option<String>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub background: Option<String>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub font_weight: Option<String>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub width: Option<String>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub border: Option<String>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub text_decoration: Option<String>,
98}
99
100fn present_json<'de, D: serde::Deserializer<'de>>(
101 deserializer: D,
102) -> Result<Option<serde_json::Value>, D::Error> {
103 serde_json::Value::deserialize(deserializer).map(Some)
104}
105
106fn expanded_default() -> bool {
107 true
108}
109
110#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase", deny_unknown_fields)]
115pub struct NodeImage {
116 pub url: String,
118 pub width: u32,
120 pub height: u32,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub alt: Option<String>,
125}
126impl NodeImage {
127 pub fn validate(&self) -> Result<(), MindMapError> {
129 if self.url.trim().is_empty() {
130 return Err(MindMapError::InvalidOperation("Image URL is empty".into()));
131 }
132 if !(1..=4096).contains(&self.width) || !(1..=4096).contains(&self.height) {
133 return Err(MindMapError::InvalidOperation(
134 "Image size must be 1-4096 pixels".into(),
135 ));
136 }
137 Ok(())
138 }
139}
140
141#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase", deny_unknown_fields)]
144pub struct Node {
145 pub id: String,
147 pub topic: String,
149 #[serde(default, skip_serializing_if = "Vec::is_empty")]
151 pub children: Vec<Node>,
152 #[serde(default = "expanded_default")]
154 pub expanded: bool,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub direction: Option<NodeDirection>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub style: Option<NodeStyle>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub branch_color: Option<String>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub note: Option<String>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub image: Option<NodeImage>,
170 #[serde(
172 default,
173 deserialize_with = "present_json",
174 skip_serializing_if = "Option::is_none"
175 )]
176 pub metadata: Option<serde_json::Value>,
177}
178impl Node {
179 pub fn new(id: impl Into<String>, topic: impl Into<String>) -> Self {
181 Self {
182 id: id.into(),
183 topic: topic.into(),
184 children: Vec::new(),
185 expanded: true,
186 direction: None,
187 style: None,
188 branch_color: None,
189 note: None,
190 image: None,
191 metadata: None,
192 }
193 }
194 pub fn with_children(mut self, children: Vec<Node>) -> Self {
196 self.children = children;
197 self
198 }
199 pub fn find(&self, id: &str) -> Option<&Node> {
201 let mut pending = vec![self];
202 while let Some(node) = pending.pop() {
203 if node.id == id {
204 return Some(node);
205 }
206 pending.extend(node.children.iter().rev());
207 }
208 None
209 }
210 pub fn validate(&self) -> Result<(), MindMapError> {
212 let mut pending = vec![(self, 0usize)];
213 let mut ids = HashSet::new();
214 while let Some((node, depth)) = pending.pop() {
215 if depth > 128 {
216 return Err(MindMapError::TreeTooDeep);
217 }
218 if node.id.trim().is_empty() {
219 return Err(MindMapError::EmptyNodeId);
220 }
221 if !ids.insert(node.id.as_str()) {
222 return Err(MindMapError::DuplicateNodeId(node.id.clone()));
223 }
224 if let Some(image) = &node.image {
225 image.validate()?;
226 }
227 pending.extend(node.children.iter().map(|child| (child, depth + 1)));
228 }
229 Ok(())
230 }
231 pub(crate) fn find_mut(&mut self, id: &str) -> Option<&mut Node> {
232 if self.id == id {
233 return Some(self);
234 }
235 self.children
236 .iter_mut()
237 .find_map(|child| child.find_mut(id))
238 }
239}
240
241#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
243#[serde(rename_all = "camelCase", deny_unknown_fields)]
244pub struct MindMapData {
245 pub node_data: Node,
247 #[serde(default)]
249 pub direction: LayoutDirection,
250 #[serde(default)]
252 pub compact: bool,
253 #[serde(
255 default,
256 deserialize_with = "present_json",
257 skip_serializing_if = "Option::is_none"
258 )]
259 pub meta: Option<serde_json::Value>,
260}
261impl MindMapData {
262 pub fn new(root: Node) -> Self {
264 Self {
265 node_data: root,
266 direction: LayoutDirection::Right,
267 compact: false,
268 meta: None,
269 }
270 }
271 pub fn validate(&self) -> Result<(), MindMapError> {
273 self.node_data.validate()
274 }
275
276 pub fn from_json(json: &str) -> Result<Self, MindMapError> {
278 let mut deserializer = serde_json::Deserializer::from_str(json);
279 let document: Self =
280 serde_path_to_error::deserialize(&mut deserializer).map_err(|error| {
281 MindMapError::InvalidData {
282 path: error.path().to_string(),
283 message: error.inner().to_string(),
284 }
285 })?;
286 deserializer
287 .end()
288 .map_err(|error| MindMapError::InvalidData {
289 path: String::new(),
290 message: error.to_string(),
291 })?;
292 document.validate()?;
293 Ok(document)
294 }
295 pub fn to_json(&self) -> Result<String, MindMapError> {
297 self.validate()?;
298 serde_json::to_string_pretty(self).map_err(|error| MindMapError::InvalidData {
299 path: String::new(),
300 message: error.to_string(),
301 })
302 }
303}
304
305#[derive(Clone, Debug, PartialEq, Eq)]
307#[non_exhaustive]
308pub enum MindMapError {
309 EmptyNodeId,
311 DuplicateNodeId(String),
313 TreeTooDeep,
315 InvalidData {
317 path: String,
319 message: String,
321 },
322 NodeNotFound(String),
324 RootOperation,
326 AncestorMove,
328 ReadOnly,
330 Rejected,
332 StaleOperation,
334 NoHistory,
336 InvalidOperation(String),
338 Measurement(String),
340 InvalidGeometry,
342}
343impl fmt::Display for MindMapError {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 match self {
346 Self::EmptyNodeId => f.write_str("Node IDs must not be empty"),
347 Self::DuplicateNodeId(id) => write!(f, "Duplicate node ID: {id}"),
348 Self::TreeTooDeep => f.write_str("At most 128 nested levels are supported"),
349 Self::InvalidData { path, message } => {
350 write!(f, "Invalid document at {path}: {message}")
351 }
352 Self::NodeNotFound(id) => write!(f, "Node not found: {id}"),
353 Self::RootOperation => f.write_str("The root cannot be removed or relocated"),
354 Self::AncestorMove => f.write_str("Cannot move a node into its own subtree"),
355 Self::ReadOnly => f.write_str("The document is read-only"),
356 Self::Rejected => f.write_str("The operation was rejected by the before hook"),
357 Self::StaleOperation => f.write_str("The document changed during the operation"),
358 Self::NoHistory => f.write_str("No history entry is available"),
359 Self::InvalidOperation(message) => write!(f, "Invalid operation: {message}"),
360 Self::Measurement(message) => write!(f, "Cannot measure the mind map: {message}"),
361 Self::InvalidGeometry => f.write_str("The renderer returned invalid geometry"),
362 }
363 }
364}
365impl Error for MindMapError {}