sqlsync-reducer 0.3.2

SQLSync is a collaborative offline-first wrapper around SQLite. A SQLSync Reducer is responsible for executing mutations on the database both locally and on the server.
Documentation
use std::{
    collections::BTreeMap,
    convert::From,
    error::Error,
    fmt::{self, Display, Formatter},
    panic,
    str::FromStr,
};

use log::Level;
use serde::{Deserialize, Serialize};
use thiserror::Error;

pub type RequestId = u32;

pub type Requests = Option<BTreeMap<RequestId, Request>>;
pub type Responses = Option<BTreeMap<RequestId, u32>>;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum SqliteValue {
    Null,
    Integer(i64),
    Real(f64),
    Text(String),
    Blob(Vec<u8>),
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Row(Vec<SqliteValue>);

impl From<Vec<SqliteValue>> for Row {
    fn from(v: Vec<SqliteValue>) -> Self {
        Self(v)
    }
}

impl FromIterator<SqliteValue> for Row {
    fn from_iter<T: IntoIterator<Item = SqliteValue>>(iter: T) -> Self {
        Self(iter.into_iter().collect())
    }
}

#[derive(Serialize, Deserialize, Debug)]
pub enum Request {
    Query {
        sql: String,
        params: Vec<SqliteValue>,
    },
    Exec {
        sql: String,
        params: Vec<SqliteValue>,
    },
}

#[derive(Serialize, Deserialize, Debug)]
pub struct QueryResponse {
    pub columns: Vec<String>,
    pub rows: Vec<Row>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct ExecResponse {
    pub changes: usize,
}

#[derive(Serialize, Deserialize, Debug, Error)]
pub enum ErrorResponse {
    #[error("SQLite Error({code}): {message}")]
    SqliteError { code: i32, message: String },
    #[error("Unknown: {0}")]
    Unknown(String),
}

#[derive(Serialize, Deserialize)]
pub struct LogRecord {
    level: String,
    message: String,
    file: Option<String>,
    line: Option<u32>,
}

impl From<&panic::PanicInfo<'_>> for LogRecord {
    fn from(info: &panic::PanicInfo) -> Self {
        let loc = info.location();
        LogRecord {
            level: log::Level::Error.to_string(),
            message: info.to_string(),
            file: loc.map(|l| l.file().to_string()),
            line: loc.map(|l| l.line()),
        }
    }
}

impl From<&log::Record<'_>> for LogRecord {
    fn from(record: &log::Record) -> Self {
        Self {
            level: record.level().to_string(),
            message: record.args().to_string(),
            file: record.file().map(|s| s.to_string()),
            line: record.line(),
        }
    }
}

impl LogRecord {
    pub fn log(&self) {
        log::logger().log(
            &log::Record::builder()
                .level(Level::from_str(&self.level).unwrap_or(Level::Error))
                .file(self.file.as_deref())
                .line(self.line)
                .module_path(Some("wasm guest"))
                .args(format_args!("{}", self.message))
                .build(),
        );
    }
}

#[derive(Serialize, Deserialize, Debug)]
pub enum ReducerError {
    ConversionError {
        value: SqliteValue,
        target_type: String,
    },

    Unknown(String),
}

impl Display for ReducerError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "ReducerError: {:?}", self)
    }
}

impl<E: Error> From<E> for ReducerError {
    fn from(e: E) -> Self {
        Self::Unknown(e.to_string())
    }
}

// conversion utilities for Row and SqliteValue

impl Row {
    pub fn get<'a, T>(&'a self, idx: usize) -> Result<T, ReducerError>
    where
        T: TryFrom<&'a SqliteValue, Error = ReducerError>,
    {
        T::try_from(self.get_value(idx))
    }

    pub fn maybe_get<'a, T>(&'a self, idx: usize) -> Result<Option<T>, ReducerError>
    where
        T: TryFrom<&'a SqliteValue, Error = ReducerError>,
    {
        match self.get_value(idx) {
            SqliteValue::Null => Ok(None),
            v => Ok(Some(T::try_from(v)?)),
        }
    }

    pub fn get_value(&self, idx: usize) -> &SqliteValue {
        self.0.get(idx).expect("row index out of bounds")
    }
}

macro_rules! impl_types_for_sqlvalue {
    ($e:path, $($t:ty),*) => {
        $(
            impl From<$t> for SqliteValue {
                fn from(t: $t) -> Self {
                    $e(t.into())
                }
            }

            impl TryFrom<&SqliteValue> for $t {
                type Error = ReducerError;

                fn try_from(value: &SqliteValue) -> Result<Self, Self::Error> {
                    match value {
                        $e(i) => Ok(*i as $t),
                        v => Err(ReducerError::ConversionError {
                            value: v.clone(),
                            target_type: stringify!($t).to_owned(),
                        }),
                    }
                }
            }
        )*
    };
}

impl_types_for_sqlvalue!(SqliteValue::Integer, i8, i16, i32, i64);
impl_types_for_sqlvalue!(SqliteValue::Real, f32, f64);

impl<T> From<Option<T>> for SqliteValue
where
    SqliteValue: From<T>,
{
    fn from(o: Option<T>) -> Self {
        match o {
            Some(t) => Self::from(t),
            None => Self::Null,
        }
    }
}

impl From<Vec<u8>> for SqliteValue {
    fn from(b: Vec<u8>) -> Self {
        Self::Blob(b)
    }
}

impl TryFrom<&SqliteValue> for Vec<u8> {
    type Error = ReducerError;

    fn try_from(value: &SqliteValue) -> Result<Self, Self::Error> {
        match value {
            SqliteValue::Blob(b) => Ok(b.clone()),
            v => Err(ReducerError::ConversionError {
                value: v.clone(),
                target_type: "Vec<u8>".to_owned(),
            }),
        }
    }
}

impl From<&str> for SqliteValue {
    fn from(s: &str) -> Self {
        Self::Text(s.to_string())
    }
}

impl<'a> TryFrom<&'a SqliteValue> for &'a str {
    type Error = ReducerError;

    fn try_from(value: &SqliteValue) -> Result<&str, Self::Error> {
        match value {
            SqliteValue::Text(s) => Ok(s.as_str()),
            v => Err(ReducerError::ConversionError {
                value: v.clone(),
                target_type: "&str".to_owned(),
            }),
        }
    }
}

impl From<String> for SqliteValue {
    fn from(s: String) -> Self {
        Self::Text(s)
    }
}

impl TryFrom<&SqliteValue> for String {
    type Error = ReducerError;

    fn try_from(value: &SqliteValue) -> Result<Self, Self::Error> {
        match value {
            SqliteValue::Text(s) => Ok(s.clone()),
            v => Err(ReducerError::ConversionError {
                value: v.clone(),
                target_type: "String".to_owned(),
            }),
        }
    }
}

impl From<bool> for SqliteValue {
    fn from(b: bool) -> Self {
        Self::Integer(b as i64)
    }
}

impl TryFrom<&SqliteValue> for bool {
    type Error = ReducerError;

    fn try_from(value: &SqliteValue) -> Result<Self, Self::Error> {
        match value {
            SqliteValue::Integer(i) => Ok(*i != 0),
            v => Err(ReducerError::ConversionError {
                value: v.clone(),
                target_type: "bool".to_owned(),
            }),
        }
    }
}