Skip to main content

fur_cli/schema/
core.rs

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