From 68060fea13cdc26568b1a51e1bf4326885c23d63 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 13 Dec 2018 14:40:59 +0100 Subject: [PATCH] feat(postgres): Introduce chained error variants Introduces error variants for external crate errors and internal errors. Additional context can be provided at sites where errors occur using a simple `.context()` call. --- finito-postgres/src/error.rs | 62 +++++++++++++++++++++++++++++++++++- finito-postgres/src/lib.rs | 62 +++++++++++++++++------------------- 2 files changed, 91 insertions(+), 33 deletions(-) diff --git a/finito-postgres/src/error.rs b/finito-postgres/src/error.rs index 0fb30a99d..aacc219f0 100644 --- a/finito-postgres/src/error.rs +++ b/finito-postgres/src/error.rs @@ -2,8 +2,68 @@ //! occur while dealing with persisted state machines. use std::result; +use std::fmt::Display; +use uuid::Uuid; + +// errors to chain: +use serde_json::Error as JsonError; +use postgres::Error as PgError; pub type Result = result::Result; #[derive(Debug)] -pub enum Error { SomeError } +pub struct Error { + pub kind: ErrorKind, + pub context: Option, +} + +#[derive(Debug)] +pub enum ErrorKind { + /// Errors occuring during JSON serialization of FSM types. + Serialization(String), + + /// Errors occuring during communication with the database. + Database(String), + + /// State machine could not be found. + FSMNotFound(Uuid), + + /// Action could not be found. + ActionNotFound(Uuid), +} + +impl > From for Error { + fn from(err: E) -> Error { + Error { + kind: err.into(), + context: None, + } + } +} + +impl From for ErrorKind { + fn from(err: JsonError) -> ErrorKind { + ErrorKind::Serialization(err.to_string()) + } +} + +impl From for ErrorKind { + fn from(err: PgError) -> ErrorKind { + ErrorKind::Database(err.to_string()) + } +} + +/// Helper trait that makes it possible to supply contextual +/// information with an error. +pub trait ResultExt { + fn context(self, ctx: C) -> Result; +} + +impl > ResultExt for result::Result { + fn context(self, ctx: C) -> Result { + self.map_err(|err| Error { + context: Some(format!("{}", ctx)), + .. err.into() + }) + } +} diff --git a/finito-postgres/src/lib.rs b/finito-postgres/src/lib.rs index 46309a04c..844e8f79f 100644 --- a/finito-postgres/src/lib.rs +++ b/finito-postgres/src/lib.rs @@ -17,8 +17,9 @@ extern crate uuid; #[cfg(test)] extern crate finito_door; mod error; -pub use error::{Result, Error}; +pub use error::{Result, Error, ErrorKind}; +use error::ResultExt; use chrono::prelude::{DateTime, Utc}; use finito::{FSM, FSMBackend}; use postgres::{Connection, GenericConnection}; @@ -118,9 +119,9 @@ impl FSMBackend for FinitoPostgres { let id = Uuid::new_v4(); let fsm = S::FSM_NAME.to_string(); - let state = serde_json::to_value(initial).expect("TODO"); + let state = serde_json::to_value(initial).context("failed to serialise FSM")?; - self.conn.execute(query, &[&id, &fsm, &state]).expect("TODO"); + self.conn.execute(query, &[&id, &fsm, &state]).context("failed to insert FSM")?; return Ok(id); @@ -146,7 +147,7 @@ impl FSMBackend for FinitoPostgres { S::State: From<&'a State>, S::Event: Serialize + DeserializeOwned, S::Action: Serialize + DeserializeOwned { - let tx = self.conn.transaction().expect("TODO"); + let tx = self.conn.transaction().context("could not begin transaction")?; let state = get_machine_internal(&tx, key, true)?; // Advancing the FSM consumes the event, so it is persisted first: @@ -163,8 +164,8 @@ impl FSMBackend for FinitoPostgres { } // And finally the state is updated: - update_state(&tx, key, &new_state).expect("TODO"); - tx.commit().expect("TODO"); + update_state(&tx, key, &new_state)?; + tx.commit().context("could not commit transaction")?; self.run_actions::(key, action_ids); @@ -211,9 +212,9 @@ pub fn insert_machine(conn: &C, initial: S) -> Result where let id = Uuid::new_v4(); let fsm = S::FSM_NAME.to_string(); - let state = serde_json::to_value(initial).expect("TODO"); + let state = serde_json::to_value(initial).context("failed to serialize FSM")?; - conn.execute(query, &[&id, &fsm, &state]).expect("TODO"); + conn.execute(query, &[&id, &fsm, &state])?; return Ok(id); } @@ -233,9 +234,10 @@ where let id = Uuid::new_v4(); let fsm = S::FSM_NAME.to_string(); - let event_value = serde_json::to_value(event).expect("TODO"); + let event_value = serde_json::to_value(event) + .context("failed to serialize event")?; - conn.execute(query, &[&id, &fsm, &fsm_id, &event_value]).expect("TODO"); + conn.execute(query, &[&id, &fsm, &fsm_id, &event_value])?; return Ok(id) } @@ -254,12 +256,13 @@ fn insert_action(conn: &C, let id = Uuid::new_v4(); let fsm = S::FSM_NAME.to_string(); - let action_value = serde_json::to_value(action).expect("TODO"); + let action_value = serde_json::to_value(action) + .context("failed to serialize action")?; conn.execute( query, &[&id, &fsm, &fsm_id, &event_id, &action_value, &ActionStatus::Pending] - ).expect("TODO"); + )?; return Ok(id) } @@ -274,13 +277,11 @@ fn update_state(conn: &C, UPDATE machines SET state = $1 WHERE id = $2 "#; - let state_value = serde_json::to_value(state).expect("TODO"); - let res_count = conn.execute(query, &[&state_value, &fsm_id]) - .expect("TODO"); + let state_value = serde_json::to_value(state).context("failed to serialize FSM")?; + let res_count = conn.execute(query, &[&state_value, &fsm_id])?; if res_count != 1 { - // TODO: not found error! - unimplemented!() + Err(ErrorKind::FSMNotFound(fsm_id).into()) } else { Ok(()) } @@ -307,13 +308,12 @@ fn get_machine_internal(conn: &C, SELECT state FROM machines WHERE id = $1 "#); - let rows = conn.query(&query, &[&id]).expect("TODO"); + let rows = conn.query(&query, &[&id]).context("failed to retrieve FSM")?; if let Some(row) = rows.into_iter().next() { - Ok(serde_json::from_value(row.get(0)).expect("TODO")) + Ok(serde_json::from_value(row.get(0)).context("failed to deserialize FSM")?) } else { - // TODO: return appropriate not found error - Err(Error::SomeError) + Err(ErrorKind::FSMNotFound(id).into()) } } @@ -328,14 +328,14 @@ fn get_action(conn: &C, id: Uuid) -> Result<(ActionStatus, S::Action)> whe WHERE id = $1 AND fsm = $2 "#); - let rows = conn.query(&query, &[&id, &S::FSM_NAME]).expect("TODO"); + let rows = conn.query(&query, &[&id, &S::FSM_NAME])?; if let Some(row) = rows.into_iter().next() { - let action = serde_json::from_value(row.get(1)).expect("TODO"); + let action = serde_json::from_value(row.get(1)) + .context("failed to deserialize FSM action")?; Ok((row.get(0), action)) } else { - // TODO: return appropriate not found error - Err(Error::SomeError) + Err(ErrorKind::ActionNotFound(id).into()) } } @@ -352,15 +352,13 @@ fn update_action_status(conn: &C, WHERE id = $3 AND fsm = $4 "#; - let result = conn.execute(&query, &[&status, &error, &id, &S::FSM_NAME]) - .expect("TODO"); + let result = conn.execute(&query, &[&status, &error, &id, &S::FSM_NAME])?; if result != 1 { - // TODO: Fail in the most gruesome way! - unimplemented!() + Err(ErrorKind::ActionNotFound(id).into()) + } else { + Ok(()) } - - Ok(()) } /// Execute a single action in case it is pending or retryable. Holds @@ -408,6 +406,6 @@ fn run_action(tx: Transaction, id: Uuid, state: &S::State, _fsm: PhantomData< }, }; - tx.commit().expect("TODO"); + tx.commit().context("failed to commit transaction")?; Ok(result) }