use geth_types::{DocumentId, ResourceId, UnixMillis}; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct DocumentResource { pub id: DocumentId, pub resource: ResourceId, pub name: String, pub sync_status: String, pub state_bytes: u64, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct DocumentState { pub document: DocumentResource, pub state_json: String, pub updated_at: UnixMillis, } #[derive(Debug, thiserror::Error)] pub enum DocumentError { #[error("invalid document name: {0}")] InvalidName(String), #[error("invalid document JSON state: {0}")] InvalidState(#[from] serde_json::Error), } pub fn validate_document_name(name: &str) -> Result<(), DocumentError> { if name.is_empty() || !name .bytes() .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')) { return Err(DocumentError::InvalidName(name.to_owned())); } Ok(()) } pub fn normalize_document_state(state_json: &str) -> Result { let value: serde_json::Value = serde_json::from_str(state_json)?; Ok(serde_json::to_string(&value)?) } #[must_use] pub fn automerge_roadmap() -> &'static str { "future documents use Automerge sync over Iroh with resource-local authorization" } #[cfg(test)] mod tests { use super::*; #[test] fn document_name_validation_rejects_paths_and_empty_names() { assert!(validate_document_name("notes").is_ok()); assert!(validate_document_name("notes.v1").is_ok()); assert!(validate_document_name("").is_err()); assert!(validate_document_name("../notes").is_err()); assert!(validate_document_name("notes/main").is_err()); assert!(validate_document_name("notes main").is_err()); } #[test] fn document_state_is_validated_and_normalized_json() { assert_eq!( normalize_document_state(r#"{ "title": "notes", "done": false }"#).expect("normalize"), r#"{"done":false,"title":"notes"}"# ); assert!(normalize_document_state("{").is_err()); } }