use crate::domain::{Node, Properties, PropertyValue, Timestamp};
use chrono::Utc;
use serde::{Deserialize, Serialize};
pub type NoteId = LuhmannId;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct LuhmannId {
pub parts: Vec<LuhmannPart>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum LuhmannPart {
Number(u32),
Letter(char),
}
impl LuhmannId {
pub fn parse(s: &str) -> Option<Self> {
let mut parts = Vec::new();
let mut chars = s.chars().peekable();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
let mut num_str = String::new();
while let Some(&ch) = chars.peek() {
if ch.is_ascii_digit() {
num_str.push(ch);
chars.next();
} else {
break;
}
}
if let Ok(n) = num_str.parse::<u32>() {
parts.push(LuhmannPart::Number(n));
}
} else if c.is_ascii_alphabetic() {
parts.push(LuhmannPart::Letter(c.to_ascii_lowercase()));
chars.next();
} else {
chars.next(); }
}
if parts.is_empty() {
None
} else {
Some(Self { parts })
}
}
pub fn parent(&self) -> Option<Self> {
if self.parts.len() <= 1 {
None
} else {
Some(Self {
parts: self.parts[..self.parts.len() - 1].to_vec(),
})
}
}
pub fn next_sibling(&self) -> Option<Self> {
if let Some(last) = self.parts.last() {
let mut new_parts = self.parts.clone();
match last {
LuhmannPart::Number(n) => {
new_parts.pop();
new_parts.push(LuhmannPart::Number(n + 1));
}
LuhmannPart::Letter(c) => {
if let Some(next_char) = (*c as u8 + 1).try_into().ok() {
if next_char <= 'z' {
new_parts.pop();
new_parts.push(LuhmannPart::Letter(next_char));
} else {
return None; }
}
}
}
Some(Self { parts: new_parts })
} else {
None
}
}
pub fn first_child(&self) -> Self {
let mut new_parts = self.parts.clone();
match self.parts.last() {
Some(LuhmannPart::Number(_)) => {
new_parts.push(LuhmannPart::Letter('a'));
}
Some(LuhmannPart::Letter(_)) | None => {
new_parts.push(LuhmannPart::Number(1));
}
}
Self { parts: new_parts }
}
pub fn level(&self) -> usize {
self.parts.len()
}
pub fn is_descendant_of(&self, other: &Self) -> bool {
if other.parts.len() >= self.parts.len() {
return false;
}
self.parts[..other.parts.len()] == other.parts[..]
}
}
impl std::fmt::Display for LuhmannId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for part in &self.parts {
match part {
LuhmannPart::Number(n) => write!(f, "{}", n)?,
LuhmannPart::Letter(c) => write!(f, "{}", c)?,
}
}
Ok(())
}
}
impl std::str::FromStr for LuhmannId {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s).ok_or_else(|| format!("Invalid Luhmann ID: {}", s))
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum LinkType {
References,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Note {
pub id: NoteId, pub title: String,
pub content: String,
pub tags: Vec<String>,
pub agent_id: Option<String>, pub created_at: Timestamp,
pub updated_at: Timestamp,
}
impl Note {
pub fn new(id: LuhmannId, title: impl Into<String>, content: impl Into<String>) -> Self {
let now = Utc::now();
Self {
id,
title: title.into(),
content: content.into(),
tags: Vec::new(),
agent_id: None,
created_at: now,
updated_at: now,
}
}
pub fn to_node(&self) -> Node {
let mut props = Properties::new();
props.insert(
"title".to_string(),
PropertyValue::String(self.title.clone()),
);
props.insert(
"content".to_string(),
PropertyValue::String(self.content.clone()),
);
props.insert(
"luhmann_id".to_string(),
PropertyValue::String(self.id.to_string()),
);
props.insert(
"tags".to_string(),
PropertyValue::List(
self.tags
.iter()
.map(|t| PropertyValue::String(t.clone()))
.collect(),
),
);
if let Some(ref agent_id) = self.agent_id {
props.insert(
"agent_id".to_string(),
PropertyValue::String(agent_id.clone()),
);
}
let node_id = crate::domain::string_to_node_id(&self.id.to_string());
let mut node = Node::new("note", props);
node.id = node_id;
node.created_at = self.created_at;
node.updated_at = self.updated_at;
node
}
pub fn from_node(node: &Node) -> Option<Self> {
if node.node_type != "note" {
return None;
}
let title = node.get_property("title")?.as_str()?.to_string();
let content = node.get_property("content")?.as_str()?.to_string();
let luhmann_id = node
.get_property("luhmann_id")
.and_then(|v| v.as_str())
.and_then(|s| LuhmannId::parse(s))?;
let tags = node
.get_property("tags")
.and_then(|v| match v {
PropertyValue::List(list) => Some(
list.iter()
.filter_map(|item| item.as_str().map(String::from))
.collect(),
),
_ => None,
})
.unwrap_or_default();
let agent_id = node
.get_property("agent_id")
.and_then(|v| v.as_str())
.map(String::from);
Some(Self {
id: luhmann_id,
title,
content,
tags,
agent_id,
created_at: node.created_at,
updated_at: node.updated_at,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NoteLink {
pub from_note_id: NoteId,
pub to_note_id: NoteId,
pub link_type: LinkType,
pub context: Option<String>,
}
impl NoteLink {
pub fn new(
from_note_id: NoteId,
to_note_id: NoteId,
link_type: LinkType,
context: Option<String>,
) -> Self {
Self {
from_note_id,
to_note_id,
link_type,
context,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoteCounter {
pub next_main_id: u32,
pub created_at: Timestamp,
}
impl NoteCounter {
pub fn new() -> Self {
Self {
next_main_id: 1,
created_at: Utc::now(),
}
}
pub fn to_node(&self) -> Node {
let mut props = Properties::new();
props.insert(
"next_main_id".to_string(),
PropertyValue::Integer(self.next_main_id as i64),
);
let mut node = Node::new("note_counter", props);
node.id = crate::domain::string_to_node_id("__kb_counter__");
node.created_at = self.created_at;
node
}
pub fn from_node(node: &Node) -> Option<Self> {
if node.node_type != "note_counter" {
return None;
}
let next_main_id = node
.get_property("next_main_id")
.and_then(|v| match v {
PropertyValue::Integer(n) => Some(*n as u32),
_ => Some(1),
})
.unwrap_or(1);
Some(Self {
next_main_id,
created_at: node.created_at,
})
}
pub fn next_main_topic_id(&mut self) -> LuhmannId {
let id = LuhmannId {
parts: vec![LuhmannPart::Number(self.next_main_id)],
};
self.next_main_id += 1;
id
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_luhmann_id_parsing() {
let id = LuhmannId::parse("1a2b").unwrap();
assert_eq!(id.parts.len(), 4);
assert!(matches!(id.parts[0], LuhmannPart::Number(1)));
assert!(matches!(id.parts[1], LuhmannPart::Letter('a')));
assert!(matches!(id.parts[2], LuhmannPart::Number(2)));
assert!(matches!(id.parts[3], LuhmannPart::Letter('b')));
}
#[test]
fn test_luhmann_id_display() {
let id = LuhmannId::parse("1a2").unwrap();
assert_eq!(id.to_string(), "1a2");
}
#[test]
fn test_luhmann_parent() {
let id = LuhmannId::parse("1a2").unwrap();
let parent = id.parent().unwrap();
assert_eq!(parent.to_string(), "1a");
}
#[test]
fn test_luhmann_next_sibling() {
let id = LuhmannId::parse("1a").unwrap();
let next = id.next_sibling().unwrap();
assert_eq!(next.to_string(), "1b");
let id2 = LuhmannId::parse("1").unwrap();
let next2 = id2.next_sibling().unwrap();
assert_eq!(next2.to_string(), "2");
}
#[test]
fn test_luhmann_first_child() {
let id = LuhmannId::parse("1").unwrap();
let child = id.first_child();
assert_eq!(child.to_string(), "1a");
let id2 = LuhmannId::parse("1a").unwrap();
let child2 = id2.first_child();
assert_eq!(child2.to_string(), "1a1");
}
}