Docs: Comprehensive inline rustdoc and architectural summary PDF

This commit is contained in:
anthonyrawlins
2026-03-03 18:05:53 +11:00
parent cc03616918
commit 0f28e4b669
2932 changed files with 14552 additions and 74 deletions

View File

@@ -1,4 +1,4 @@
// chrs-mail library implementation
//! chrs-mail library implementation
use std::path::Path;
use chrono::{DateTime, Utc};
@@ -9,42 +9,84 @@ use thiserror::Error;
use uuid::Uuid;
/// Represents a mail message stored in the mailbox.
///
/// # Definition
/// `Message` is a data structure that models a single mail exchange between two peers.
/// It contains a unique identifier, sender and recipient identifiers, a topic string, a JSON payload,
/// and timestamps for when the message was sent and optionally when it was read.
///
/// # Implementation Details
/// - `id` is a **Uuid** generated by the caller to guarantee global uniqueness.
/// - `payload` uses `serde_json::Value` so arbitrary JSON can be attached to the message.
/// - `sent_at` and `read_at` are stored as `chrono::DateTime<Utc>` to provide timezoneagnostic timestamps.
///
/// # Rationale
/// This struct provides a lightweight, serialisable representation of a message that can be persisted
/// in the SQLitebacked mailbox (see `Mailbox`). Keeping the payload as JSON allows different subsystems
/// of the CHORUS platform to embed domainspecific data without requiring a rigid schema.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Message {
/// Globally unique identifier for the message.
pub id: Uuid,
/// Identifier of the sending peer.
pub from_peer: String,
/// Identifier of the receiving peer.
pub to_peer: String,
/// Topic or channel of the message; used for routing/filters.
pub topic: String,
/// Arbitrary JSON payload containing the message body.
pub payload: JsonValue,
/// Timestamp (UTC) when the message was sent.
pub sent_at: DateTime<Utc>,
/// Optional timestamp (UTC) when the recipient read the message.
pub read_at: Option<DateTime<Utc>>,
}
/// Errors that can occur while using the Mailbox.
/// Errors that can occur while using the `Mailbox`.
///
/// Each variant wraps an underlying error type from a dependency, allowing callers to
/// react appropriately (e.g., retry on SQLite errors, surface serialization problems, etc.).
#[derive(Debug, Error)]
pub enum MailError {
/// Propagates any `rusqlite::Error` encountered while interacting with the SQLite DB.
#[error("SQLite error: {0}")]
Sqlite(#[from] rusqlite::Error),
/// Propagates JSON (de)serialization errors from `serde_json`.
#[error("JSON serialization error: {0}")]
Json(#[from] serde_json::Error),
/// Propagates UUID parsing errors.
#[error("UUID parsing error: {0}")]
Uuid(#[from] uuid::Error),
/// Propagates chrono parsing errors, primarily when deserialising timestamps from string.
#[error("Chrono parsing error: {0}")]
ChronoParse(#[from] chrono::ParseError),
}
/// Wrapper around a SQLite connection providing mail-box functionalities.
/// Wrapper around a SQLite connection providing mailboxstyle functionalities.
///
/// The `Mailbox` abstracts a SQLite database that stores `Message` records. It offers a minimal
/// API for opening/creating the DB, sending messages, receiving pending messages for a peer, and
/// marking messages as read.
///
/// # Architectural Rationale
/// Using SQLite (via `rusqlite`) provides a zeroconfiguration, filebased persistence layer that is
/// portable across the various environments where CHORUS components may run. The wrapper isolates the
/// rest of the codebase from raw SQL handling, ensuring a single place for schema evolution and error
/// mapping.
pub struct Mailbox {
conn: Connection,
}
impl Mailbox {
/// Open (or create) a mailbox database at `path`.
///
/// The function creates the SQLite file if it does not exist, enables WAL mode for better
/// concurrency, and ensures the `messages` table is present.
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, MailError> {
let conn = Connection::open(path)?;
// Enable WAL mode.
// Enable WAL mode for improved concurrency and durability.
conn.pragma_update(None, "journal_mode", &"WAL")?;
// Create table.
// Create the `messages` table if it does not already exist.
conn.execute(
"CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
@@ -61,6 +103,9 @@ impl Mailbox {
}
/// Store a new message in the mailbox.
///
/// The `payload` field is serialised to a JSON string before insertion. The `read_at` column is
/// initialised to `NULL` because the message has not yet been consumed.
pub fn send(&self, msg: &Message) -> Result<(), MailError> {
let payload_str = serde_json::to_string(&msg.payload)?;
self.conn.execute(
@@ -79,6 +124,9 @@ impl Mailbox {
}
/// Retrieve all unread messages addressed to `peer_id`.
///
/// The query filters on `to_peer` and `read_at IS NULL`. Returned rows are transformed back into
/// `Message` structs, parsing the UUID, JSON payload, and RFC3339 timestamps.
pub fn receive_pending(&self, peer_id: &str) -> Result<Vec<Message>, MailError> {
let mut stmt = self.conn.prepare(
"SELECT id, from_peer, to_peer, topic, payload, sent_at, read_at
@@ -97,16 +145,13 @@ impl Mailbox {
// Parse Uuid
let id = Uuid::parse_str(&id_str)
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(e)))?;
// Parse JSON
// Parse JSON payload
let payload: JsonValue = serde_json::from_str(&payload_str)
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Text, Box::new(e)))?;
// Parse Timestamps
// Parse timestamps
let sent_at = DateTime::parse_from_rfc3339(&sent_at_str)
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Text, Box::new(e)))?
.with_timezone(&Utc);
let read_at = match read_at_opt {
Some(s) => Some(
DateTime::parse_from_rfc3339(&s)
@@ -135,6 +180,8 @@ impl Mailbox {
}
/// Mark a message as read by setting its `read_at` timestamp.
///
/// The current UTC time is stored in the `read_at` column for the row with the matching `id`.
pub fn mark_read(&self, msg_id: Uuid) -> Result<(), MailError> {
let now = Utc::now().to_rfc3339();
self.conn.execute(
@@ -177,11 +224,11 @@ mod tests {
let pending = mailbox.receive_pending("bob")?;
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].id, msg.id);
mailbox.mark_read(msg.id)?;
let pending2 = mailbox.receive_pending("bob")?;
assert!(pending2.is_empty());
fs::remove_file(db_path).unwrap();
Ok(())
}