Phase 3 of the rebrand cascade per locked decision D4. MagnotiaError -> Error in crates/core/src/error.rs (the crate name already qualifies it). 92 usages across 14 .rs files renamed via word-boundary sed. One collision required disambiguation: lumotia_storage already had its own local Error type (introduced by the slop-pass Area A residuals work). crates/storage/src/error.rs aliases the imported core error as CoreError on import; the From<Error> for CoreError boundary impl and the CoreError::Storage construction site use the alias. cargo build --workspace passes. cargo test --workspace: 330 pass, 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
195 lines
6.0 KiB
Rust
195 lines
6.0 KiB
Rust
//! Storage-local typed error.
|
|
//!
|
|
//! Backend code that wants to branch on storage failure modes pattern-matches
|
|
//! on [`Error`] directly. The crate boundary into [`lumotia_core::error::Error`]
|
|
//! flattens this into a [`Error::Storage`] variant carrying a serde-friendly
|
|
//! `kind`, an `operation` label, and the Display output as `detail` — so the
|
|
//! frontend (which still receives stringified errors from Tauri commands today)
|
|
//! keeps working, and Area E can later expose the typed `kind` to the FE without
|
|
//! touching call sites.
|
|
//!
|
|
//! The `sqlx::Error` source is deliberately not serialized. Sources stay sources;
|
|
//! only the boundary shape is wire-friendly.
|
|
|
|
use std::borrow::Cow;
|
|
use std::path::PathBuf;
|
|
|
|
use lumotia_core::error::{Error as CoreError, StorageKind};
|
|
|
|
/// Kinds of database-open operation that can fail before the pool is ready.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum OpenOp {
|
|
Connect,
|
|
ReadOnlyConnect,
|
|
ForeignKeysPragma,
|
|
}
|
|
|
|
impl OpenOp {
|
|
fn label(self) -> &'static str {
|
|
match self {
|
|
OpenOp::Connect => "connect",
|
|
OpenOp::ReadOnlyConnect => "read_only_connect",
|
|
OpenOp::ForeignKeysPragma => "foreign_keys_pragma",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for OpenOp {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(self.label())
|
|
}
|
|
}
|
|
|
|
/// Per-step phase inside the migration framework.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum MigrationStep {
|
|
SchemaVersionTableCreate,
|
|
SchemaVersionQuery,
|
|
TxBegin,
|
|
Apply,
|
|
RecordVersion,
|
|
Commit,
|
|
}
|
|
|
|
impl MigrationStep {
|
|
fn label(self) -> &'static str {
|
|
match self {
|
|
MigrationStep::SchemaVersionTableCreate => "schema_version_table_create",
|
|
MigrationStep::SchemaVersionQuery => "schema_version_query",
|
|
MigrationStep::TxBegin => "tx_begin",
|
|
MigrationStep::Apply => "apply",
|
|
MigrationStep::RecordVersion => "record_version",
|
|
MigrationStep::Commit => "commit",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for MigrationStep {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(self.label())
|
|
}
|
|
}
|
|
|
|
/// Entities the storage layer can fail to find or validate.
|
|
///
|
|
/// Seeded with only the three real entities (Transcript, Task, Profile) — add
|
|
/// more here only when a typed `NotFound` / `InvalidReference` case actually
|
|
/// requires them.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum Entity {
|
|
Transcript,
|
|
Task,
|
|
Profile,
|
|
ImplementationRule,
|
|
Feedback,
|
|
}
|
|
|
|
impl Entity {
|
|
fn label(self) -> &'static str {
|
|
match self {
|
|
Entity::Transcript => "transcript",
|
|
Entity::Task => "task",
|
|
Entity::Profile => "profile",
|
|
Entity::ImplementationRule => "implementation_rule",
|
|
Entity::Feedback => "feedback",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for Entity {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(self.label())
|
|
}
|
|
}
|
|
|
|
/// Storage-crate-local typed error.
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum Error {
|
|
#[error("database open failed during {operation}: {source}")]
|
|
DatabaseOpen {
|
|
operation: OpenOp,
|
|
#[source]
|
|
source: sqlx::Error,
|
|
},
|
|
|
|
#[error("migration step {step} (version {version:?}) failed: {source}")]
|
|
Migration {
|
|
version: Option<i64>,
|
|
step: MigrationStep,
|
|
#[source]
|
|
source: sqlx::Error,
|
|
},
|
|
|
|
#[error("query failed during {operation}: {source}")]
|
|
Query {
|
|
operation: Cow<'static, str>,
|
|
#[source]
|
|
source: sqlx::Error,
|
|
},
|
|
|
|
#[error("{entity} not found: '{key}'")]
|
|
NotFound { entity: Entity, key: String },
|
|
|
|
#[error("invalid {entity} reference: {reason}")]
|
|
InvalidReference {
|
|
entity: Entity,
|
|
reason: Cow<'static, str>,
|
|
},
|
|
|
|
#[error("filesystem operation failed at '{}': {source}", path.display())]
|
|
Filesystem {
|
|
path: PathBuf,
|
|
#[source]
|
|
source: std::io::Error,
|
|
},
|
|
}
|
|
|
|
impl Error {
|
|
/// Discriminator for the boundary conversion into [`Error`].
|
|
pub fn kind(&self) -> StorageKind {
|
|
match self {
|
|
Error::DatabaseOpen { .. } => StorageKind::DatabaseOpen,
|
|
Error::Migration { .. } => StorageKind::Migration,
|
|
Error::Query { .. } => StorageKind::Query,
|
|
Error::NotFound { .. } => StorageKind::NotFound,
|
|
Error::InvalidReference { .. } => StorageKind::InvalidReference,
|
|
Error::Filesystem { .. } => StorageKind::Filesystem,
|
|
}
|
|
}
|
|
|
|
/// Operation label that flows into the [`Error::Storage`] boundary
|
|
/// shape. For per-query failures this is the caller-provided label;
|
|
/// for the other variants it's the variant's intrinsic label.
|
|
pub fn operation_label(&self) -> Cow<'static, str> {
|
|
match self {
|
|
Error::DatabaseOpen { operation, .. } => Cow::Borrowed(operation.label()),
|
|
Error::Migration { step, .. } => Cow::Borrowed(step.label()),
|
|
Error::Query { operation, .. } => operation.clone(),
|
|
Error::NotFound { entity, .. } => Cow::Owned(format!("not_found:{}", entity.label())),
|
|
Error::InvalidReference { entity, .. } => {
|
|
Cow::Owned(format!("invalid_reference:{}", entity.label()))
|
|
}
|
|
Error::Filesystem { .. } => Cow::Borrowed("filesystem"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Boundary conversion — flattens the typed error into the wire-friendly
|
|
/// [`CoreError::Storage`] variant. Lives in the storage crate (not in core)
|
|
/// to avoid a `core -> storage` dependency cycle.
|
|
impl From<Error> for CoreError {
|
|
fn from(error: Error) -> Self {
|
|
let kind = error.kind();
|
|
let operation = error.operation_label().into_owned();
|
|
let detail = error.to_string();
|
|
CoreError::Storage {
|
|
kind,
|
|
operation,
|
|
detail,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `Result` alias for storage-crate functions.
|
|
pub type Result<T> = std::result::Result<T, Error>;
|