#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
pub mod drift;
pub mod effective_note;
pub mod error;
pub mod history;
pub mod lifecycle;
pub mod overrides;
pub mod registry;
pub mod write;
pub use error::VaultError;
pub use history::NoteHistoryEntry;
pub use lifecycle::HISTORY_DIR_PREFIX;
pub use overrides::NoteMetadataOverride;
pub use registry::Vault;
pub use write::WriteResult;
#[async_trait::async_trait]
pub trait Registry: Send + Sync {
async fn tenant_count(&self) -> Result<u32, gradatum_core::error::GradatumError>;
async fn locus_count(&self) -> Result<u32, gradatum_core::error::GradatumError>;
async fn ensure_tenant(
&self,
tenant_id: &str,
) -> Result<(), gradatum_core::error::GradatumError>;
async fn read_note_by_id(
&self,
note_id: &str,
) -> Result<gradatum_core::note::Note, gradatum_core::error::GradatumError>;
async fn history_versions(
&self,
note_id: &str,
) -> Result<Vec<i64>, gradatum_core::error::GradatumError>;
async fn history_get(
&self,
note_id: &str,
ts_ms: i64,
) -> Result<gradatum_core::note::Note, gradatum_core::error::GradatumError>;
async fn history_restore(
&self,
note_id: &str,
ts_ms: i64,
) -> Result<String, gradatum_core::error::GradatumError>;
async fn history_diff(
&self,
note_id: &str,
a: &str,
b: &str,
) -> Result<Vec<String>, gradatum_core::error::GradatumError>;
async fn update_note_status(
&self,
note_id: &str,
target: gradatum_core::status::NoteStatus,
reason: Option<String>,
) -> Result<(), gradatum_core::error::GradatumError>;
}
#[async_trait::async_trait]
impl Registry for Vault {
async fn tenant_count(&self) -> Result<u32, gradatum_core::error::GradatumError> {
self.index.vault_id_count().await
}
async fn locus_count(&self) -> Result<u32, gradatum_core::error::GradatumError> {
self.index.locus_count().await
}
async fn ensure_tenant(
&self,
tenant_id: &str,
) -> Result<(), gradatum_core::error::GradatumError> {
self.index.ensure_vault_id(tenant_id).await
}
async fn read_note_by_id(
&self,
note_id: &str,
) -> Result<gradatum_core::note::Note, gradatum_core::error::GradatumError> {
use gradatum_core::error::GradatumError;
use ulid::Ulid;
let ulid = Ulid::from_string(note_id).map_err(|e| {
GradatumError::Storage(format!("read_note_by_id : ULID invalide {note_id:?} : {e}"))
})?;
let id = gradatum_core::identity::NoteId(ulid);
self.read_note(id).await.map_err(|e| match e {
crate::error::VaultError::Core(inner) => inner,
crate::error::VaultError::Storage(msg) => GradatumError::Storage(msg),
crate::error::VaultError::Markdown(msg) => {
GradatumError::Markdown(format!("read_note_by_id : {msg}"))
}
crate::error::VaultError::Conflict(hash) => GradatumError::Storage(format!(
"read_note_by_id : conflit inattendu hash={:?}",
hash
)),
})
}
async fn history_versions(
&self,
note_id: &str,
) -> Result<Vec<i64>, gradatum_core::error::GradatumError> {
use gradatum_core::error::GradatumError;
let id = self.parse_note_id(note_id)?;
self.history_versions(id)
.await
.map_err(|e| GradatumError::Storage(format!("history_versions : {e}")))
}
async fn history_get(
&self,
note_id: &str,
ts_ms: i64,
) -> Result<gradatum_core::note::Note, gradatum_core::error::GradatumError> {
use gradatum_core::error::GradatumError;
let id = self.parse_note_id(note_id)?;
self.history_get(id, ts_ms)
.await
.map_err(|e| GradatumError::Storage(format!("history_get : {e}")))
}
async fn history_restore(
&self,
note_id: &str,
ts_ms: i64,
) -> Result<String, gradatum_core::error::GradatumError> {
use gradatum_core::error::GradatumError;
let id = self.parse_note_id(note_id)?;
let snapshot = self
.history_get(id, ts_ms)
.await
.map_err(|e| GradatumError::Storage(format!("history_restore get snapshot: {e}")))?;
let written = self
.write_note_with_id(snapshot.frontmatter, snapshot.body.markdown, id)
.await
.map_err(|e| GradatumError::Storage(format!("history_restore write: {e}")))?;
Ok(written.content_hash.hex())
}
async fn history_diff(
&self,
note_id: &str,
a: &str,
b: &str,
) -> Result<Vec<String>, gradatum_core::error::GradatumError> {
let id = self.parse_note_id(note_id)?;
let body_a = self.resolve_history_body(id, a).await?;
let body_b = self.resolve_history_body(id, b).await?;
let lines_a: Vec<&str> = body_a.lines().collect();
let lines_b: Vec<&str> = body_b.lines().collect();
let diff = diff_lines_brut(&lines_a, &lines_b);
Ok(diff)
}
async fn update_note_status(
&self,
note_id: &str,
target: gradatum_core::status::NoteStatus,
reason: Option<String>,
) -> Result<(), gradatum_core::error::GradatumError> {
use gradatum_core::error::GradatumError;
let id = self.parse_note_id(note_id)?;
self.update_status(id, target, reason)
.await
.map_err(|e| match e {
crate::error::VaultError::Core(inner) => inner,
crate::error::VaultError::Storage(msg) => GradatumError::Storage(msg),
crate::error::VaultError::Markdown(msg) => {
GradatumError::Markdown(format!("update_note_status : {msg}"))
}
crate::error::VaultError::Conflict(hash) => GradatumError::Storage(format!(
"update_note_status : conflit inattendu hash={:?}",
hash
)),
})
}
}
impl Vault {
fn parse_note_id(
&self,
note_id: &str,
) -> Result<gradatum_core::identity::NoteId, gradatum_core::error::GradatumError> {
use gradatum_core::error::GradatumError;
use ulid::Ulid;
let ulid = Ulid::from_string(note_id)
.map_err(|e| GradatumError::Storage(format!("ULID invalide {note_id:?} : {e}")))?;
Ok(gradatum_core::identity::NoteId(ulid))
}
async fn resolve_history_body(
&self,
id: gradatum_core::identity::NoteId,
version_selector: &str,
) -> Result<String, gradatum_core::error::GradatumError> {
use gradatum_core::error::GradatumError;
if version_selector == "current" {
let note = self.read_note(id).await.map_err(|e| {
GradatumError::Storage(format!("resolve_history_body current: {e}"))
})?;
Ok(note.body.markdown)
} else {
let ts_ms = version_selector.parse::<i64>().map_err(|_| {
GradatumError::Storage(format!(
"sélecteur de version invalide : attendu 'current' ou timestamp ms, reçu {:?}",
version_selector
))
})?;
let snapshot = self.history_get(id, ts_ms).await.map_err(|e| {
GradatumError::Storage(format!("resolve_history_body snapshot: {e}"))
})?;
Ok(snapshot.body.markdown)
}
}
}
fn diff_lines_brut(lines_a: &[&str], lines_b: &[&str]) -> Vec<String> {
let max_len = lines_a.len().max(lines_b.len());
let mut result = Vec::with_capacity(max_len * 2);
let mut i = 0;
let mut j = 0;
while i < lines_a.len() || j < lines_b.len() {
match (lines_a.get(i), lines_b.get(j)) {
(Some(la), Some(lb)) => {
if la == lb {
result.push(format!(" {}", la));
i += 1;
j += 1;
} else {
result.push(format!("-{}", la));
result.push(format!("+{}", lb));
i += 1;
j += 1;
}
}
(Some(la), None) => {
result.push(format!("-{}", la));
i += 1;
}
(None, Some(lb)) => {
result.push(format!("+{}", lb));
j += 1;
}
(None, None) => break,
}
}
result
}
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_is_set() {
assert!(!VERSION.is_empty());
}
}