Skip to main content

fur_cli/schema/
core.rs

1use chrono::Utc;
2use serde_json::{json, Value};
3use sha2::{Digest, Sha256};
4use std::fs;
5use std::path::Path;
6use uuid::Uuid;
7
8pub const CURRENT_SCHEMA: &str = "0.3";
9
10/// Upgrade a message to the current schema if needed.
11/// Returns true if the message was modified.
12pub fn upgrade_message_schema(msg: &mut Value) -> bool {
13    let mut changed = false;
14
15    let schema = msg["schema_version"].as_str().unwrap_or("0.1");
16
17    if schema != CURRENT_SCHEMA {
18        msg["schema_version"] = json!(CURRENT_SCHEMA);
19        changed = true;
20    }
21
22    if let Some(md_path) = msg["markdown"].as_str() {
23        if msg.get("markdown_meta").is_none() {
24            let md = Path::new(md_path);
25
26            if md.exists() {
27                if let Ok(bytes) = fs::read(md) {
28                    let mut hasher = Sha256::new();
29                    hasher.update(&bytes);
30
31                    let hash = format!("{:x}", hasher.finalize());
32
33                    let size = bytes.len();
34
35                    let filename = md.file_name().unwrap().to_string_lossy().to_string();
36
37                    msg["markdown_meta"] = json!({
38                        "hash": hash,
39                        "size": size,
40                        "filename": filename
41                    });
42
43                    changed = true;
44                }
45            }
46        }
47    }
48
49    changed
50}
51
52/*
53=== FUR Schema Constructors ===
54Centralized builders for index, conversation, and message JSON.
55
56Schema evolution supported via schema_version field.
57*/
58
59pub const SCHEMA_VERSION: &str = "0.3";
60
61pub fn make_index_metadata() -> Value {
62    json!({
63        "threads": [],
64        "active_thread": null,
65        "current_message": null,
66        "created_at": Utc::now().to_rfc3339(),
67        "schema_version": SCHEMA_VERSION
68    })
69}
70
71pub fn make_conversation_metadata(title: &str, id: &str) -> Value {
72    json!({
73        "id": id,
74        "created_at": Utc::now().to_rfc3339(),
75        "messages": [],
76        "tags": [],
77        "title": title,
78        "schema_version": SCHEMA_VERSION
79    })
80}
81
82fn compute_file_hash(path: &Path) -> Option<String> {
83    if !path.exists() {
84        return None;
85    }
86
87    let bytes = fs::read(path).ok()?;
88
89    let mut hasher = Sha256::new();
90    hasher.update(&bytes);
91    let result = hasher.finalize();
92
93    Some(format!("{:x}", result))
94}
95
96fn compute_file_size(path: &Path) -> Option<u64> {
97    fs::metadata(path).map(|m| m.len()).ok()
98}
99
100pub fn make_message_metadata(
101    avatar: &str,
102    text: Option<String>,
103    markdown: Option<String>,
104    img: Option<String>,
105    parent: Option<String>,
106) -> Value {
107    let id = Uuid::new_v4().to_string();
108    let timestamp = Utc::now().to_rfc3339();
109
110    let markdown_meta = markdown.as_ref().and_then(|p| {
111        let path = Path::new(p);
112
113        let hash = compute_file_hash(path);
114        let size = compute_file_size(path);
115        let filename = path
116            .file_name()
117            .and_then(|f| f.to_str())
118            .map(|s| s.to_string());
119
120        Some(json!({
121            "hash": hash,
122            "size": size,
123            "filename": filename
124        }))
125    });
126
127    json!({
128        "id": id,
129        "avatar": avatar,
130        "timestamp": timestamp,
131        "text": text,
132        "markdown": markdown,
133        "markdown_meta": markdown_meta,
134        "attachment": img,
135        "parent": parent,
136        "children": [],
137        "branches": [],
138        "schema_version": SCHEMA_VERSION
139    })
140}