Skip to main content

force_sync/
error.rs

1//! Error types for force-sync.
2
3/// Database and migration errors.
4#[derive(Debug, thiserror::Error)]
5pub enum ForceSyncError {
6    /// Postgres pool acquisition failure.
7    #[error("database pool error: {0}")]
8    Pool(#[from] deadpool_postgres::PoolError),
9
10    /// Postgres client or query failure.
11    #[error("database error: {0}")]
12    Database(#[from] tokio_postgres::Error),
13
14    /// A replay cursor is required to append journal entries.
15    #[error("sync journal entries require a source cursor")]
16    MissingSourceCursor,
17
18    /// The transaction callback failed and the rollback also failed.
19    #[error(
20        "transaction callback failed and rollback also failed: callback={callback}; rollback={rollback}"
21    )]
22    TransactionRollback {
23        /// Error returned by the transaction callback.
24        callback: Box<Self>,
25        /// Error returned while attempting to roll back the transaction.
26        rollback: tokio_postgres::Error,
27    },
28
29    /// A required sync key part was empty.
30    #[error("sync key {part} cannot be empty")]
31    EmptySyncKeyPart {
32        /// The offending sync key part.
33        part: &'static str,
34    },
35
36    /// A requested sync record was not found.
37    #[error("missing {entity}")]
38    NotFound {
39        /// The missing entity name.
40        entity: &'static str,
41    },
42
43    /// A required engine configuration field was not provided.
44    #[error("missing required configuration: {field}")]
45    MissingConfiguration {
46        /// The missing configuration field.
47        field: &'static str,
48    },
49
50    /// A lease duration could not be represented as a Postgres interval timestamp.
51    #[error("lease duration is out of range")]
52    InvalidLeaseDuration,
53
54    /// A JSON payload could not be decoded.
55    #[error("json error: {0}")]
56    Json(#[from] serde_json::Error),
57
58    /// A Salesforce Pub/Sub operation failed.
59    #[error("pubsub error: {0}")]
60    PubSub(Box<force_pubsub::PubSubError>),
61
62    /// An outbox row carried an unexpected operation value.
63    #[error("invalid outbox operation: {op}")]
64    InvalidOutboxOperation {
65        /// The invalid operation value.
66        op: String,
67    },
68
69    /// An outbox row carried an already-encoded or otherwise invalid cursor.
70    #[error("invalid outbox cursor: {cursor}")]
71    InvalidOutboxCursor {
72        /// The invalid cursor value.
73        cursor: String,
74    },
75
76    /// A stored database value could not be decoded into a domain type.
77    #[error("invalid stored value for {field}: {value}")]
78    InvalidStoredValue {
79        /// The field or column name.
80        field: &'static str,
81        /// The invalid stored value.
82        value: String,
83    },
84
85    /// Placeholder variant while the crate surface is being implemented.
86    #[error("not implemented")]
87    NotImplemented,
88}
89
90impl From<force_pubsub::PubSubError> for ForceSyncError {
91    fn from(error: force_pubsub::PubSubError) -> Self {
92        Self::PubSub(Box::new(error))
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_pubsub_error_conversion() {
102        let pubsub_err = force_pubsub::PubSubError::Config("test".to_string());
103        let sync_err: ForceSyncError = pubsub_err.into();
104        assert!(matches!(sync_err, ForceSyncError::PubSub(_)));
105    }
106
107    #[test]
108    fn test_pool_error_conversion() {
109        let pool_err = deadpool_postgres::PoolError::Closed;
110        let sync_err: ForceSyncError = pool_err.into();
111        assert!(matches!(sync_err, ForceSyncError::Pool(_)));
112    }
113
114    #[test]
115    fn test_missing_config_display() {
116        let err = ForceSyncError::MissingConfiguration { field: "tenant_id" };
117        assert_eq!(err.to_string(), "missing required configuration: tenant_id");
118    }
119
120    #[test]
121    fn test_missing_source_cursor_display() {
122        let err = ForceSyncError::MissingSourceCursor;
123        assert_eq!(
124            err.to_string(),
125            "sync journal entries require a source cursor"
126        );
127    }
128
129    #[test]
130    fn test_missing_not_found_display() {
131        let err = ForceSyncError::NotFound { entity: "Account" };
132        assert_eq!(err.to_string(), "missing Account");
133    }
134
135    #[test]
136    fn test_invalid_lease_duration_display() {
137        let err = ForceSyncError::InvalidLeaseDuration;
138        assert_eq!(err.to_string(), "lease duration is out of range");
139    }
140
141    #[test]
142    fn test_not_implemented_display() {
143        let err = ForceSyncError::NotImplemented;
144        assert_eq!(err.to_string(), "not implemented");
145    }
146}