Skip to main content

nodex_core/model/
node.rs

1use chrono::NaiveDate;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6use super::kind::Kind;
7use super::status::Status;
8
9/// A document node in the graph.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Node {
12    // === Identity ===
13    pub id: String,
14    #[serde(
15        serialize_with = "serialize_path_forward",
16        deserialize_with = "deserialize_path"
17    )]
18    pub path: PathBuf,
19    pub title: String,
20    pub kind: Kind,
21
22    // === Lifecycle ===
23    pub status: Status,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub created: Option<NaiveDate>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub updated: Option<NaiveDate>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub reviewed: Option<NaiveDate>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub owner: Option<String>,
32
33    // === Relations (from frontmatter) ===
34    #[serde(default, skip_serializing_if = "Vec::is_empty")]
35    pub supersedes: Vec<String>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub superseded_by: Option<String>,
38    #[serde(default, skip_serializing_if = "Vec::is_empty")]
39    pub implements: Vec<String>,
40    #[serde(default, skip_serializing_if = "Vec::is_empty")]
41    pub related: Vec<String>,
42    #[serde(default, skip_serializing_if = "Vec::is_empty")]
43    pub tags: Vec<String>,
44
45    // === Flags ===
46    #[serde(default)]
47    pub orphan_ok: bool,
48
49    // === Extension point for project-specific frontmatter fields ===
50    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
51    pub attrs: BTreeMap<String, serde_json::Value>,
52}
53
54/// Serialize a path with forward slashes so JSON output is stable
55/// across Windows and Unix. Shared across modules that serialise
56/// `PathBuf` fields to JSON.
57pub fn serialize_path_forward<S: serde::Serializer>(path: &Path, s: S) -> Result<S::Ok, S::Error> {
58    s.serialize_str(&path.to_string_lossy().replace('\\', "/"))
59}
60
61/// Deserialize a path from a JSON string.
62pub fn deserialize_path<'de, D: serde::Deserializer<'de>>(d: D) -> Result<PathBuf, D::Error> {
63    let s = String::deserialize(d)?;
64    Ok(PathBuf::from(s))
65}