2026-05-17 19:59:03 +02:00
|
|
|
use geth_types::{DocumentId, ResourceId, UnixMillis};
|
2026-05-15 15:08:20 +02:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct DocumentResource {
|
|
|
|
|
pub id: DocumentId,
|
|
|
|
|
pub resource: ResourceId,
|
|
|
|
|
pub name: String,
|
2026-05-16 22:15:18 +02:00
|
|
|
pub sync_status: String,
|
|
|
|
|
pub state_bytes: u64,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 19:59:03 +02:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct DocumentState {
|
|
|
|
|
pub document: DocumentResource,
|
|
|
|
|
pub state_json: String,
|
|
|
|
|
pub updated_at: UnixMillis,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 22:15:18 +02:00
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
|
pub enum DocumentError {
|
|
|
|
|
#[error("invalid document name: {0}")]
|
|
|
|
|
InvalidName(String),
|
2026-05-17 19:59:03 +02:00
|
|
|
#[error("invalid document JSON state: {0}")]
|
|
|
|
|
InvalidState(#[from] serde_json::Error),
|
2026-05-16 22:15:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(())
|
2026-05-15 15:08:20 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-17 19:59:03 +02:00
|
|
|
pub fn normalize_document_state(state_json: &str) -> Result<String, DocumentError> {
|
|
|
|
|
let value: serde_json::Value = serde_json::from_str(state_json)?;
|
|
|
|
|
Ok(serde_json::to_string(&value)?)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 15:08:20 +02:00
|
|
|
#[must_use]
|
|
|
|
|
pub fn automerge_roadmap() -> &'static str {
|
|
|
|
|
"future documents use Automerge sync over Iroh with resource-local authorization"
|
|
|
|
|
}
|
2026-05-16 22:15:18 +02:00
|
|
|
|
|
|
|
|
#[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());
|
|
|
|
|
}
|
2026-05-17 19:59:03 +02:00
|
|
|
|
|
|
|
|
#[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());
|
|
|
|
|
}
|
2026-05-16 22:15:18 +02:00
|
|
|
}
|