use oxide_batch::{
ExplorerError, LifecycleError, OperatorError, OperatorRejection, RepositoryError,
RetentionError,
};
use crate::exit::ExitCategory;
use crate::output::Diagnostic;
pub fn repository(error: &RepositoryError) -> ExitCategory {
match error {
RepositoryError::JobInstanceNotFound { .. }
| RepositoryError::JobExecutionNotFound { .. }
| RepositoryError::StepExecutionNotFound { .. }
| RepositoryError::RestartStateNotFound { .. } => ExitCategory::TargetNotFound,
RepositoryError::CommitOutcomeUnknown => ExitCategory::OutcomeUnknown,
RepositoryError::ConcurrentModification
| RepositoryError::RetentionPlanStale
| RepositoryError::Lifecycle(LifecycleError::StaleVersion { .. }) => {
ExitCategory::OptimisticConflict
}
RepositoryError::SchemaUninitialized
| RepositoryError::MigrationRequired { .. }
| RepositoryError::NewerSchema { .. }
| RepositoryError::Unavailable => ExitCategory::RepositoryUnavailable,
RepositoryError::Identifier(_) => ExitCategory::Internal,
RepositoryError::Domain(_) | RepositoryError::IdentifierOutOfRange { .. } => {
ExitCategory::Usage
}
_ => ExitCategory::GuardRejected,
}
}
pub fn repository_diagnostic(error: &RepositoryError) -> Diagnostic {
let (code, detail) = match error {
RepositoryError::SchemaUninitialized => (
"SCHEMA_UNINITIALIZED",
"the metadata schema is not initialized",
),
RepositoryError::MigrationRequired { .. } => (
"MIGRATION_REQUIRED",
"the metadata schema requires migration before use",
),
RepositoryError::NewerSchema { .. } => (
"SCHEMA_NEWER",
"the metadata schema is newer than this build supports",
),
RepositoryError::Unavailable => ("REPOSITORY_UNAVAILABLE", "the repository is unavailable"),
RepositoryError::CommitOutcomeUnknown => (
"OUTCOME_UNKNOWN",
"the durable outcome is undetermined; replay the same operation identifier",
),
RepositoryError::UnsupportedCapability { .. } => (
"UNSUPPORTED_CAPABILITY",
"the adapter does not provide a capability this command requires",
),
RepositoryError::ConcurrentModification => (
"CONCURRENT_MODIFICATION",
"another committed unit of work invalidated the observed snapshot",
),
_ => ("REPOSITORY_REJECTED", "the repository rejected the action"),
};
Diagnostic::new(code, detail)
}
#[allow(clippy::match_same_arms)]
pub fn explorer(error: &ExplorerError) -> ExitCategory {
match error {
ExplorerError::PageSizeOutOfRange { .. } => ExitCategory::ConfigurationInvalid,
ExplorerError::AgeBoundTooSmall { .. } => ExitCategory::Usage,
ExplorerError::Cursor(_)
| ExplorerError::ResponseTooLarge { .. }
| ExplorerError::UnsupportedCapability => ExitCategory::GuardRejected,
ExplorerError::Timeout => ExitCategory::DeadlineExceeded,
ExplorerError::Repository(error) => repository(error),
_ => ExitCategory::GuardRejected,
}
}
pub fn explorer_diagnostic(error: &ExplorerError) -> Diagnostic {
match error {
ExplorerError::PageSizeOutOfRange { .. } => Diagnostic::new(
"PAGE_SIZE_OUT_OF_RANGE",
"the page size is outside its bound",
),
ExplorerError::AgeBoundTooSmall { minimum } => Diagnostic::new(
"AGE_BOUND_TOO_SMALL",
format!(
"the age bound must be at least {} seconds",
minimum.as_secs()
),
),
ExplorerError::Cursor(_) => Diagnostic::new(
"CURSOR_REJECTED",
"the continuation token does not belong to this query",
),
ExplorerError::ResponseTooLarge { limit } => Diagnostic::new(
"RESPONSE_TOO_LARGE",
format!("one row alone exceeds the {limit} byte response bound"),
),
ExplorerError::UnsupportedCapability => Diagnostic::new(
"UNSUPPORTED_CAPABILITY",
"the adapter does not support bounded keyset pagination",
),
ExplorerError::Timeout => Diagnostic::new(
"STATEMENT_TIMEOUT",
"the bounded query exceeded its statement timeout",
),
ExplorerError::Repository(error) => repository_diagnostic(error),
_ => Diagnostic::new("QUERY_REJECTED", "the bounded query was rejected"),
}
}
#[allow(clippy::match_same_arms)]
pub fn operator(error: &OperatorError) -> ExitCategory {
match error {
OperatorError::OperationIdConflict { .. } => ExitCategory::GuardRejected,
OperatorError::OperationOutcomeUnknown => ExitCategory::OutcomeUnknown,
OperatorError::InvalidRecoveryRequest(_) => ExitCategory::Usage,
OperatorError::Repository(error) => repository(error),
_ => ExitCategory::GuardRejected,
}
}
pub fn operator_diagnostic(error: &OperatorError) -> Diagnostic {
match error {
OperatorError::OperationIdConflict { .. } => Diagnostic::new(
"OPERATION_ID_CONFLICT",
"the operation identifier was already recorded for a different request",
),
OperatorError::OperationOutcomeUnknown => Diagnostic::new(
"OUTCOME_UNKNOWN",
"the durable outcome is undetermined; replay the same operation identifier",
),
OperatorError::InvalidRecoveryRequest(_) => Diagnostic::new(
"INVALID_RECOVERY_REQUEST",
"the recovery arguments do not form a valid audited request",
),
OperatorError::Repository(error) => repository_diagnostic(error),
_ => Diagnostic::new("OPERATOR_REJECTED", "the operator action was rejected"),
}
}
pub fn rejection(value: OperatorRejection) -> ExitCategory {
match value {
OperatorRejection::OptimisticConflict { .. } => ExitCategory::OptimisticConflict,
OperatorRejection::ExecutionNotFound | OperatorRejection::InstanceNotFound => {
ExitCategory::TargetNotFound
}
_ => ExitCategory::GuardRejected,
}
}
pub fn retention(error: &RetentionError) -> ExitCategory {
match error {
RetentionError::BatchBoundOutOfRange { .. }
| RetentionError::AgeBoundTooSmall { .. }
| RetentionError::NonTerminalStatus { .. }
| RetentionError::EmptyStatusSet => ExitCategory::Usage,
RetentionError::RetentionPlanStale => ExitCategory::OptimisticConflict,
RetentionError::InstanceHeld { .. } | RetentionError::OperationIdConflict { .. } => {
ExitCategory::GuardRejected
}
RetentionError::OperationOutcomeUnknown => ExitCategory::OutcomeUnknown,
RetentionError::Repository(error) => repository(error),
_ => ExitCategory::GuardRejected,
}
}
pub fn retention_diagnostic(error: &RetentionError) -> Diagnostic {
match error {
RetentionError::BatchBoundOutOfRange { .. } => Diagnostic::new(
"BATCH_BOUND_OUT_OF_RANGE",
"the purge batch bound is outside its accepted range",
),
RetentionError::AgeBoundTooSmall { minimum } => Diagnostic::new(
"AGE_BOUND_TOO_SMALL",
format!(
"the purge age bound must be at least {} seconds",
minimum.as_secs()
),
),
RetentionError::NonTerminalStatus { .. } => Diagnostic::new(
"NON_TERMINAL_STATUS",
"a purge may target only finished statuses",
),
RetentionError::EmptyStatusSet => Diagnostic::new(
"EMPTY_STATUS_SET",
"a purge must target at least one status",
),
RetentionError::RetentionPlanStale => Diagnostic::new(
"PLAN_STALE",
"a candidate changed after the plan was produced; nothing was deleted",
),
RetentionError::InstanceHeld { .. } => Diagnostic::new(
"INSTANCE_HELD",
"the instance is held and can be neither planned nor purged",
),
RetentionError::OperationIdConflict { .. } => Diagnostic::new(
"OPERATION_ID_CONFLICT",
"the operation identifier was already recorded for a different request",
),
RetentionError::OperationOutcomeUnknown => Diagnostic::new(
"OUTCOME_UNKNOWN",
"the durable outcome is undetermined; replay the same operation identifier",
),
RetentionError::Repository(error) => repository_diagnostic(error),
_ => Diagnostic::new("RETENTION_REJECTED", "the retention action was rejected"),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic)]
use oxide_batch::{ExecutionVersion, ExplorerError, OperatorRejection, RepositoryError};
use super::{explorer, rejection, repository, repository_diagnostic};
use crate::exit::ExitCategory;
#[test]
fn an_unknown_commit_reports_the_unknown_category() {
assert_eq!(
repository(&RepositoryError::CommitOutcomeUnknown),
ExitCategory::OutcomeUnknown
);
}
#[test]
fn an_unavailable_repository_reports_its_category() {
assert_eq!(
repository(&RepositoryError::Unavailable),
ExitCategory::RepositoryUnavailable
);
}
#[test]
fn a_stale_version_rejection_reports_a_conflict() {
assert_eq!(
rejection(OperatorRejection::OptimisticConflict {
current: ExecutionVersion::new(4)
}),
ExitCategory::OptimisticConflict
);
}
#[test]
fn a_missing_execution_reports_target_not_found() {
assert_eq!(
rejection(OperatorRejection::ExecutionNotFound),
ExitCategory::TargetNotFound
);
}
#[test]
fn a_statement_timeout_reports_the_deadline_category() {
assert_eq!(
explorer(&ExplorerError::Timeout),
ExitCategory::DeadlineExceeded
);
}
#[test]
fn diagnostics_carry_no_driver_text() {
let diagnostic = repository_diagnostic(&RepositoryError::Unavailable);
assert_eq!(diagnostic.code, "REPOSITORY_UNAVAILABLE");
assert!(!diagnostic.detail.is_empty());
}
}