turso-mappers 0.1.0

Row mappers for turso
Documentation
#![allow(clippy::uninlined_format_args)]

pub use turso_mappers_derive::TryFromRow;

#[doc = include_str!("../README.md")]
#[cfg(doctest)]
pub struct ReadmeDocTests;

#[derive(Debug)]
pub enum TursoMapperError {
    ColumnNotFound(String),
    InvalidType(String),
    NullValue(String),
    ConversionError(String),
    IoError(std::io::Error),
    TursoError(turso::Error),
}

impl From<turso::Error> for TursoMapperError {
    fn from(err: turso::Error) -> Self {
        TursoMapperError::TursoError(err)
    }
}

impl From<std::io::Error> for TursoMapperError {
    fn from(err: std::io::Error) -> Self {
        TursoMapperError::IoError(err)
    }
}

impl std::fmt::Display for TursoMapperError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TursoMapperError::ColumnNotFound(msg) => write!(f, "Column not found: {}", msg),
            TursoMapperError::InvalidType(msg) => write!(f, "Invalid type: {}", msg),
            TursoMapperError::NullValue(msg) => write!(f, "Null value: {}", msg),
            TursoMapperError::ConversionError(msg) => write!(f, "Conversion error: {}", msg),
            TursoMapperError::IoError(err) => write!(f, "IO error: {}", err),
            TursoMapperError::TursoError(err) => write!(f, "Turso error: {}", err),
        }
    }
}

impl std::error::Error for TursoMapperError {}

pub type TursoMapperResult<T> = Result<T, TursoMapperError>;

pub trait MapRows {
    fn map_rows<F, T>(self, f: F) -> impl Future<Output = TursoMapperResult<Vec<T>>>
    where
        F: Fn(turso::Row) -> TursoMapperResult<T>,
        T: Send;
}

impl MapRows for turso::Rows {
    async fn map_rows<F, T>(mut self, f: F) -> TursoMapperResult<Vec<T>>
    where
        F: Fn(turso::Row) -> TursoMapperResult<T>,
        T: Send,
    {
        let mut rows = vec![];

        while let Some(row) = self.next().await? {
            let t: T = f(row)?;
            rows.push(t);
        }

        Ok(rows)
    }
}

pub trait TryFromRow {
    fn try_from_row(row: turso::Row) -> TursoMapperResult<Self>
    where
        Self: Sized;
}

#[cfg(test)]
mod tests {
    use super::{TryFromRow, TursoMapperResult};
    use crate::{MapRows, TursoMapperError};
    use turso::{Builder, Row};
    use turso_core::types::Text;

    struct CustomerWithManualTryFromRow {
        id: i64,
        name: String,
    }

    impl TryFromRow for CustomerWithManualTryFromRow {
        fn try_from_row(row: Row) -> TursoMapperResult<Self> {
            Ok(CustomerWithManualTryFromRow {
                id: *row
                    .get_value(0)?
                    .as_integer()
                    .ok_or_else(|| TursoMapperError::ConversionError("id is not an integer".to_string()))?,
                name: row
                    .get_value(1)?
                    .as_text()
                    .ok_or_else(|| TursoMapperError::ConversionError("name is not a string".to_string()))?
                    .clone(),
            })
        }
    }

    #[derive(TryFromRow)]
    struct Customer {
        id: i64,
        name: String,
    }

    #[tokio::test]
    async fn can_get_structs_using_map() -> TursoMapperResult<()> {
        let db = Builder::new_local(":memory:").build().await?;
        let conn = db.connect()?;

        conn.execute("CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL);", ()).await?;
        conn.execute("INSERT INTO customer (name) VALUES ('Charlie');", ()).await?;
        conn.execute("INSERT INTO customer (name) VALUES ('Sarah');", ()).await?;

        let rows = conn.query("SELECT * FROM customer;", ()).await?;

        let customers = rows
            .map_rows(|row| {
                Ok(CustomerWithManualTryFromRow {
                    id: *row
                        .get_value(0)?
                        .as_integer()
                        .ok_or_else(|| TursoMapperError::ConversionError("id is not an integer".to_string()))?,
                    name: row
                        .get_value(1)?
                        .as_text()
                        .ok_or_else(|| TursoMapperError::ConversionError("name is not a string".to_string()))?
                        .clone(),
                })
            })
            .await?;

        assert_eq!(customers.len(), 2);
        assert_eq!(customers[0].id, 1);
        assert_eq!(customers[1].id, 2);
        assert_eq!(customers[0].name, "Charlie");
        assert_eq!(customers[1].name, "Sarah");

        Ok(())
    }

    #[tokio::test]
    async fn manual_try_from_row_impl_works() -> TursoMapperResult<()> {
        let row: Row = Row::from_iter([turso_core::Value::Integer(1), turso_core::Value::Text(Text::new("Charlie"))].iter());

        let customer = CustomerWithManualTryFromRow::try_from_row(row)?;

        assert_eq!(customer.id, 1);
        assert_eq!(customer.name, "Charlie");

        Ok(())
    }

    #[tokio::test]
    async fn derive_macro_try_from_row_impl_works() -> TursoMapperResult<()> {
        let row: Row = Row::from_iter([turso_core::Value::Integer(1), turso_core::Value::Text(Text::new("Charlie"))].iter());

        let customer = Customer::try_from_row(row)?;

        assert_eq!(customer.id, 1);
        assert_eq!(customer.name, "Charlie");

        Ok(())
    }

    #[tokio::test]
    async fn end_to_end_test_with_manual_mapping() -> TursoMapperResult<()> {
        // Note that this is not testing anything in this crate, it's just here as a functional baseline and to compare to
        // end_to_end_test_with_map_rows_and_try_from_row

        let db = Builder::new_local(":memory:").build().await?;
        let conn = db.connect()?;

        conn.execute("CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL);", ()).await?;
        conn.execute("INSERT INTO customer (name) VALUES ('Charlie');", ()).await?;
        conn.execute("INSERT INTO customer (name) VALUES ('Sarah');", ()).await?;

        let mut rows = conn.query("SELECT id, name FROM customer;", ()).await?;

        let mut customers = vec![];

        while let Some(row) = rows.next().await? {
            customers.push(Customer {
                id: *row
                    .get_value(0)?
                    .as_integer()
                    .ok_or_else(|| TursoMapperError::ConversionError("id is not an integer".to_string()))?,
                name: row
                    .get_value(1)?
                    .as_text()
                    .ok_or_else(|| TursoMapperError::ConversionError("name is not a string".to_string()))?
                    .clone(),
            });
        }

        assert_eq!(customers.len(), 2);
        assert_eq!(customers[0].id, 1);
        assert_eq!(customers[1].id, 2);
        assert_eq!(customers[0].name, "Charlie");
        assert_eq!(customers[1].name, "Sarah");

        Ok(())
    }

    #[tokio::test]
    async fn end_to_end_test_with_map_rows_and_try_from_row() -> TursoMapperResult<()> {
        let db = Builder::new_local(":memory:").build().await?;
        let conn = db.connect()?;

        conn.execute("CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL);", ()).await?;
        conn.execute("INSERT INTO customer (name) VALUES ('Charlie');", ()).await?;
        conn.execute("INSERT INTO customer (name) VALUES ('Sarah');", ()).await?;

        let customers = conn
            .query("SELECT id, name FROM customer;", ()).await?
            .map_rows(Customer::try_from_row).await?;

        assert_eq!(customers.len(), 2);
        assert_eq!(customers[0].id, 1);
        assert_eq!(customers[1].id, 2);
        assert_eq!(customers[0].name, "Charlie");
        assert_eq!(customers[1].name, "Sarah");

        Ok(())
    }
}