Compare commits

...

19 commits

Author SHA1 Message Date
0fe54ca21a Update dependencies 2025-02-20 22:30:18 +01:00
3be330db3f Bump version to 0.5.0 2024-09-04 00:34:57 +02:00
eb97fd22d9 Update dependencies 2024-09-04 00:34:57 +02:00
0c2e8e2ae5 Fix doc comment 2024-09-04 00:34:57 +02:00
a53254d2e7 Bump version to 0.4.0 2024-02-23 22:07:00 +01:00
b3959dc7c2 Update dependencies 2024-02-23 22:04:45 +01:00
6640f601f3 Bump version to 0.3.0 2023-12-27 00:36:53 +01:00
c6345b89ee Reformat changelog 2023-12-27 00:36:53 +01:00
c7012f476e Update dependencies 2023-12-26 16:40:31 +01:00
6fd284fed7 Bump version to 0.2.0 2023-05-14 15:53:25 +02:00
483c69efd9 Update dependencies 2023-05-14 15:52:40 +02:00
b4cf23b727 Update rusqlite 2023-04-01 07:43:00 +02:00
3fa39f5934 Add serde::from_row_via_name 2023-04-01 00:02:02 +02:00
b3ce269534 Add serde::from_row_via_index 2023-03-31 23:53:23 +02:00
5428719ad3 Add serde feature and dependency 2023-03-31 20:15:44 +02:00
351af3a983 Relax dependencies 2023-03-31 20:15:35 +02:00
5991ac6f2a Mark breaking changes in changelog 2023-03-31 16:08:55 +02:00
742bd158f1 Rename Action::Result to Action::Output 2023-03-31 16:05:48 +02:00
2bf19e0ea2 Overhaul error handling 2023-03-18 13:36:48 +01:00
6 changed files with 422 additions and 42 deletions

View file

@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
Procedure when bumping the version number: Procedure when bumping the version number:
1. Update dependencies in a separate commit 1. Update dependencies in a separate commit
2. Set version number in `Cargo.toml` 2. Set version number in `Cargo.toml`
3. Add new section in this changelog 3. Add new section in this changelog
@ -13,6 +14,45 @@ Procedure when bumping the version number:
## Unreleased ## Unreleased
### Changed
- **(breaking)** Bumped `rusqlite` dependency from `0.32` to `0.33`
## v0.5.0 - 2024-09-04
### Changed
- **(breaking)** Bumped `rusqlite` dependency from `0.31` to `0.32`
## v0.4.0 - 2024-02-23
### Changed
- **(breaking)** Bumped `rusqlite` dependency from `0.30` to `0.31`
## v0.3.0 - 2023-12-26
### Changed
- **(breaking)** Bumped `rusqlite` dependency from `0.29` to `0.30`
## v0.2.0 - 2023-05-14
### Added
- `serde` feature
- `serde::from_row_via_index`
- `serde::from_row_via_name`
### Changed
- **(breaking)**
Error handling of `Action`s is now more complex but more powerful. In
particular, `Action`s can now return almost arbitrary errors without nesting
`Result`s like before.
- **(breaking)** Renamed `Action::Result` to `Action::Output`
- **(breaking)** Bumped `rusqlite` dependency from `0.28` to `0.29`
## v0.1.0 - 2023-02-12 ## v0.1.0 - 2023-02-12
Initial release Initial release

View file

@ -1,11 +1,13 @@
[package] [package]
name = "vault" name = "vault"
version = "0.1.0" version = "0.5.0"
edition = "2021" edition = "2021"
[features] [features]
serde = ["dep:serde"]
tokio = ["dep:tokio"] tokio = ["dep:tokio"]
[dependencies] [dependencies]
rusqlite = "0.28.0" rusqlite = "0.33.0"
tokio = { version = "1.25.0", features = ["sync"], optional = true } serde = { version = "1.0.209", optional = true }
tokio = { version = "1.40.0", features = ["sync"], optional = true }

View file

@ -9,12 +9,17 @@
// Clippy lints // Clippy lints
#![warn(clippy::use_self)] #![warn(clippy::use_self)]
#[cfg(feature = "serde")]
pub mod serde;
pub mod simple; pub mod simple;
#[cfg(feature = "tokio")] #[cfg(feature = "tokio")]
pub mod tokio; pub mod tokio;
use rusqlite::{Connection, Transaction}; use rusqlite::{Connection, Transaction};
#[cfg(feature = "serde")]
pub use self::serde::*;
/// An action that can be performed on a [`Connection`]. /// An action that can be performed on a [`Connection`].
/// ///
/// Both commands and queries are considered actions. Commands usually have a /// Both commands and queries are considered actions. Commands usually have a
@ -23,8 +28,9 @@ use rusqlite::{Connection, Transaction};
/// Actions are usually passed to a vault which will then execute them and /// Actions are usually passed to a vault which will then execute them and
/// return the result. The way in which this occurs depends on the vault. /// return the result. The way in which this occurs depends on the vault.
pub trait Action { pub trait Action {
type Result; type Output;
fn run(self, conn: &mut Connection) -> rusqlite::Result<Self::Result>; type Error;
fn run(self, conn: &mut Connection) -> Result<Self::Output, Self::Error>;
} }
/// A single database migration. /// A single database migration.

325
src/serde.rs Normal file
View file

@ -0,0 +1,325 @@
use std::{error, fmt, str::Utf8Error};
use rusqlite::{
types::{FromSqlError, ValueRef},
Row,
};
use serde::{
de::{
self, value::BorrowedStrDeserializer, DeserializeSeed, Deserializer, MapAccess, SeqAccess,
Visitor,
},
forward_to_deserialize_any, Deserialize,
};
#[derive(Debug)]
enum Error {
ExpectedTupleLikeBaseType,
ExpectedStructLikeBaseType,
Utf8(Utf8Error),
Rusqlite(rusqlite::Error),
Custom(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ExpectedTupleLikeBaseType => write!(f, "expected tuple-like base type"),
Self::ExpectedStructLikeBaseType => write!(f, "expected struct-like base type"),
Self::Utf8(err) => err.fmt(f),
Self::Rusqlite(err) => err.fmt(f),
Self::Custom(msg) => msg.fmt(f),
}
}
}
impl error::Error for Error {}
impl de::Error for Error {
fn custom<T: fmt::Display>(msg: T) -> Self {
Self::Custom(msg.to_string())
}
}
impl From<Utf8Error> for Error {
fn from(value: Utf8Error) -> Self {
Self::Utf8(value)
}
}
impl From<rusqlite::Error> for Error {
fn from(value: rusqlite::Error) -> Self {
Self::Rusqlite(value)
}
}
struct ValueRefDeserializer<'de> {
value: ValueRef<'de>,
}
impl<'de> Deserializer<'de> for ValueRefDeserializer<'de> {
type Error = Error;
forward_to_deserialize_any! {
i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes byte_buf
unit unit_struct seq tuple tuple_struct map struct identifier
ignored_any
}
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
match self.value {
ValueRef::Null => visitor.visit_unit(),
ValueRef::Integer(v) => visitor.visit_i64(v),
ValueRef::Real(v) => visitor.visit_f64(v),
ValueRef::Text(v) => visitor.visit_borrowed_str(std::str::from_utf8(v)?),
ValueRef::Blob(v) => visitor.visit_borrowed_bytes(v),
}
}
fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
match self.value {
ValueRef::Integer(0) => visitor.visit_bool(false),
ValueRef::Integer(_) => visitor.visit_bool(true),
_ => self.deserialize_any(visitor),
}
}
fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
match self.value {
ValueRef::Null => visitor.visit_none(),
_ => visitor.visit_some(self),
}
}
fn deserialize_newtype_struct<V: Visitor<'de>>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, Self::Error> {
visitor.visit_newtype_struct(self)
}
fn deserialize_enum<V: Visitor<'de>>(
self,
name: &'static str,
variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> {
match self.value {
ValueRef::Text(v) => {
let v = BorrowedStrDeserializer::new(std::str::from_utf8(v)?);
v.deserialize_enum(name, variants, visitor)
}
_ => self.deserialize_any(visitor),
}
}
}
struct IndexedRowDeserializer<'de, 'stmt> {
row: &'de Row<'stmt>,
}
impl<'de> Deserializer<'de> for IndexedRowDeserializer<'de, '_> {
type Error = Error;
forward_to_deserialize_any! {
bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes
byte_buf option unit unit_struct map enum identifier ignored_any
}
fn deserialize_any<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value, Self::Error> {
Err(Error::ExpectedTupleLikeBaseType)
}
fn deserialize_newtype_struct<V: Visitor<'de>>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, Self::Error> {
visitor.visit_newtype_struct(self)
}
fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
visitor.visit_seq(IndexedRowSeq::new(self.row))
}
fn deserialize_tuple<V: Visitor<'de>>(
self,
_len: usize,
visitor: V,
) -> Result<V::Value, Self::Error> {
self.deserialize_seq(visitor)
}
fn deserialize_tuple_struct<V: Visitor<'de>>(
self,
_name: &'static str,
_len: usize,
visitor: V,
) -> Result<V::Value, Self::Error> {
self.deserialize_seq(visitor)
}
fn deserialize_struct<V: Visitor<'de>>(
self,
_name: &'static str,
fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> {
visitor.visit_map(IndexedRowMap::new(self.row, fields))
}
}
struct IndexedRowSeq<'de, 'stmt> {
row: &'de Row<'stmt>,
next_index: usize,
}
impl<'de, 'stmt> IndexedRowSeq<'de, 'stmt> {
fn new(row: &'de Row<'stmt>) -> Self {
Self { row, next_index: 0 }
}
}
impl<'de> SeqAccess<'de> for IndexedRowSeq<'de, '_> {
type Error = Error;
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
where
T: DeserializeSeed<'de>,
{
match self.row.get_ref(self.next_index) {
Ok(value) => {
self.next_index += 1;
seed.deserialize(ValueRefDeserializer { value }).map(Some)
}
Err(rusqlite::Error::InvalidColumnIndex(_)) => Ok(None),
Err(err) => Err(err)?,
}
}
}
struct IndexedRowMap<'de, 'stmt> {
row: &'de Row<'stmt>,
fields: &'static [&'static str],
next_index: usize,
}
impl<'de, 'stmt> IndexedRowMap<'de, 'stmt> {
fn new(row: &'de Row<'stmt>, fields: &'static [&'static str]) -> Self {
Self {
row,
fields,
next_index: 0,
}
}
}
impl<'de> MapAccess<'de> for IndexedRowMap<'de, '_> {
type Error = Error;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
where
K: DeserializeSeed<'de>,
{
if let Some(key) = self.fields.get(self.next_index) {
self.next_index += 1;
seed.deserialize(BorrowedStrDeserializer::new(key))
.map(Some)
} else {
Ok(None)
}
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
where
V: DeserializeSeed<'de>,
{
let value = self.row.get_ref(self.next_index - 1)?;
seed.deserialize(ValueRefDeserializer { value })
}
}
pub fn from_row_via_index<'de, T>(row: &'de Row<'_>) -> rusqlite::Result<T>
where
T: Deserialize<'de>,
{
T::deserialize(IndexedRowDeserializer { row })
.map_err(|err| FromSqlError::Other(Box::new(err)).into())
}
struct NamedRowDeserializer<'de, 'stmt> {
row: &'de Row<'stmt>,
}
impl<'de> Deserializer<'de> for NamedRowDeserializer<'de, '_> {
type Error = Error;
forward_to_deserialize_any! {
bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes
byte_buf option unit unit_struct newtype_struct seq tuple tuple_struct
map enum identifier ignored_any
}
fn deserialize_any<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value, Self::Error> {
Err(Error::ExpectedStructLikeBaseType)
}
fn deserialize_struct<V: Visitor<'de>>(
self,
_name: &'static str,
fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> {
visitor.visit_map(NamedRowMap::new(self.row, fields))
}
}
struct NamedRowMap<'de, 'stmt> {
row: &'de Row<'stmt>,
fields: &'static [&'static str],
next_index: usize,
}
impl<'de, 'stmt> NamedRowMap<'de, 'stmt> {
fn new(row: &'de Row<'stmt>, fields: &'static [&'static str]) -> Self {
Self {
row,
fields,
next_index: 0,
}
}
}
impl<'de> MapAccess<'de> for NamedRowMap<'de, '_> {
type Error = Error;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
where
K: DeserializeSeed<'de>,
{
if let Some(key) = self.fields.get(self.next_index) {
self.next_index += 1;
seed.deserialize(BorrowedStrDeserializer::new(key))
.map(Some)
} else {
Ok(None)
}
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
where
V: DeserializeSeed<'de>,
{
let value = self.row.get_ref(self.next_index - 1)?;
seed.deserialize(ValueRefDeserializer { value })
}
}
pub fn from_row_via_name<'de, T>(row: &'de Row<'_>) -> rusqlite::Result<T>
where
T: Deserialize<'de>,
{
T::deserialize(NamedRowDeserializer { row })
.map_err(|err| FromSqlError::Other(Box::new(err)).into())
}

View file

@ -34,13 +34,13 @@ impl SimpleVault {
/// ///
/// The `prepare` parameter allows access to the database after all /// The `prepare` parameter allows access to the database after all
/// migrations have occurred. This parameter could be replaced by executing /// migrations have occurred. This parameter could be replaced by executing
/// an [`Action`] performing the same operations.
/// ///
/// It is recommended to set a few pragmas before calling this function, for /// It is recommended to set a few pragmas before calling this function, for
/// example: /// example:
/// - `journal_mode` to `"wal"` /// - `journal_mode` to `"wal"`
/// - `foreign_keys` to `true` /// - `foreign_keys` to `true`
/// - `trusted_schema` to `false` /// - `trusted_schema` to `false`
/// an [`Action`] performing the same operations.
pub fn new_and_prepare( pub fn new_and_prepare(
mut conn: Connection, mut conn: Connection,
migrations: &[Migration], migrations: &[Migration],
@ -52,11 +52,7 @@ impl SimpleVault {
} }
/// Execute an [`Action`] and return the result. /// Execute an [`Action`] and return the result.
pub fn execute<A>(&mut self, action: A) -> rusqlite::Result<A::Result> pub fn execute<A: Action>(&mut self, action: A) -> Result<A::Output, A::Error> {
where
A: Action + Send + 'static,
A::Result: Send,
{
action.run(&mut self.0) action.run(&mut self.0)
} }
} }

View file

@ -1,6 +1,6 @@
//! A vault for use with [`tokio`]. //! A vault for use with [`tokio`].
use std::{any::Any, error, fmt, result, thread}; use std::{any::Any, error, fmt, thread};
use rusqlite::Connection; use rusqlite::Connection;
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
@ -12,16 +12,25 @@ use crate::{Action, Migration};
/// ///
/// This way, the trait that users of this crate interact with is kept simpler. /// This way, the trait that users of this crate interact with is kept simpler.
trait ActionWrapper { trait ActionWrapper {
fn run(self: Box<Self>, conn: &mut Connection) -> rusqlite::Result<Box<dyn Any + Send>>; fn run(
self: Box<Self>,
conn: &mut Connection,
) -> Result<Box<dyn Any + Send>, Box<dyn Any + Send>>;
} }
impl<T: Action> ActionWrapper for T impl<T: Action> ActionWrapper for T
where where
T::Result: Send + 'static, T::Output: Send + 'static,
T::Error: Send + 'static,
{ {
fn run(self: Box<Self>, conn: &mut Connection) -> rusqlite::Result<Box<dyn Any + Send>> { fn run(
let result = (*self).run(conn)?; self: Box<Self>,
Ok(Box::new(result)) conn: &mut Connection,
) -> Result<Box<dyn Any + Send>, Box<dyn Any + Send>> {
match (*self).run(conn) {
Ok(result) => Ok(Box::new(result)),
Err(err) => Err(Box::new(err)),
}
} }
} }
@ -29,46 +38,38 @@ where
enum Command { enum Command {
Action( Action(
Box<dyn ActionWrapper + Send>, Box<dyn ActionWrapper + Send>,
oneshot::Sender<rusqlite::Result<Box<dyn Any + Send>>>, oneshot::Sender<Result<Box<dyn Any + Send>, Box<dyn Any + Send>>>,
), ),
Stop(oneshot::Sender<()>), Stop(oneshot::Sender<()>),
} }
/// Error that can occur during execution of an [`Action`]. /// Error that can occur during execution of an [`Action`].
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error<E> {
/// The vault's thread has been stopped and its sqlite connection closed. /// The vault's thread has been stopped and its sqlite connection closed.
Stopped, Stopped,
/// A [`rusqlite::Error`] occurred while running the action. /// An error was returned by the [`Action`].
Rusqlite(rusqlite::Error), Action(E),
} }
impl fmt::Display for Error { impl<E: fmt::Display> fmt::Display for Error<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Stopped => "vault has been stopped".fmt(f), Self::Stopped => "vault has been stopped".fmt(f),
Self::Rusqlite(err) => err.fmt(f), Self::Action(err) => err.fmt(f),
} }
} }
} }
impl error::Error for Error { impl<E: error::Error> error::Error for Error<E> {
fn source(&self) -> Option<&(dyn error::Error + 'static)> { fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self { match self {
Self::Stopped => None, Self::Stopped => None,
Self::Rusqlite(err) => Some(err), Self::Action(err) => err.source(),
} }
} }
} }
impl From<rusqlite::Error> for Error {
fn from(value: rusqlite::Error) -> Self {
Self::Rusqlite(value)
}
}
pub type Result<R> = result::Result<R, Error>;
fn run(mut conn: Connection, mut rx: mpsc::UnboundedReceiver<Command>) { fn run(mut conn: Connection, mut rx: mpsc::UnboundedReceiver<Command>) {
while let Some(command) = rx.blocking_recv() { while let Some(command) = rx.blocking_recv() {
match command { match command {
@ -132,24 +133,34 @@ impl TokioVault {
} }
/// Execute an [`Action`] and return the result. /// Execute an [`Action`] and return the result.
pub async fn execute<A>(&self, action: A) -> Result<A::Result> pub async fn execute<A>(&self, action: A) -> Result<A::Output, Error<A::Error>>
where where
A: Action + Send + 'static, A: Action + Send + 'static,
A::Result: Send, A::Output: Send,
A::Error: Send,
{ {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.tx self.tx
.send(Command::Action(Box::new(action), tx)) .send(Command::Action(Box::new(action), tx))
.map_err(|_| Error::Stopped)?; .map_err(|_| Error::Stopped)?;
let result = rx.await.map_err(|_| Error::Stopped)??; let result = rx.await.map_err(|_| Error::Stopped)?;
// The ActionWrapper runs Action::run, which returns Action::Result. It // The ActionWrapper runs Action::run, which returns
// then wraps this into Any, which we're now trying to downcast again to // Result<Action::Result, Action::Error>. It then wraps the
// Action::Result. This should always work. // Action::Result and Action::Error into Any, which we're now trying to
let result = result.downcast().unwrap(); // downcast again to Action::Result and Action::Error. This should
// always work.
Ok(*result) match result {
Ok(result) => {
let result = *result.downcast::<A::Output>().unwrap();
Ok(result)
}
Err(err) => {
let err = *err.downcast::<A::Error>().unwrap();
Err(Error::Action(err))
}
}
} }
/// Stop the vault's thread and close its sqlite connection. /// Stop the vault's thread and close its sqlite connection.