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        "parents": [],
78        "children": [],
79        "title": title,
80        "schema_version": SCHEMA_VERSION
81    })
82}
83
84fn compute_file_hash(path: &Path) -> Option<String> {
85    if !path.exists() {
86        return None;
87    }
88
89    let bytes = fs::read(path).ok()?;
90
91    let mut hasher = Sha256::new();
92    hasher.update(&bytes);
93    let result = hasher.finalize();
94
95    Some(format!("{:x}", result))
96}
97
98fn compute_file_size(path: &Path) -> Option<u64> {
99    fs::metadata(path).map(|m| m.len()).ok()
100}
101
102pub fn make_message_metadata(
103    avatar: &str,
104    text: Option<String>,
105    markdown: Option<String>,
106    img: Option<String>,
107    parent: Option<String>,
108) -> Value {
109    let id = Uuid::new_v4().to_string();
110    let timestamp = Utc::now().to_rfc3339();
111
112    let markdown_meta = markdown.as_ref().and_then(|p| {
113        let path = Path::new(p);
114
115        let hash = compute_file_hash(path);
116        let size = compute_file_size(path);
117        let filename = path
118            .file_name()
119            .and_then(|f| f.to_str())
120            .map(|s| s.to_string());
121
122        Some(json!({
123            "hash": hash,
124            "size": size,
125            "filename": filename
126        }))
127    });
128
129    json!({
130        "id": id,
131        "avatar": avatar,
132        "timestamp": timestamp,
133        "text": text,
134        "markdown": markdown,
135        "markdown_meta": markdown_meta,
136        "attachment": img,
137        "parent": parent,
138        "children": [],
139        "branches": [],
140        "schema_version": SCHEMA_VERSION
141    })
142}