lunar-lib 0.11.0

Common utilities for lunar applications
Documentation
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use sled::IVec;

use crate::{
    database::{
        CompareAndSwapTransaction, Createable, CustomTransactionError, Database, Db, DbIdExt,
        Deleteable, Mergeable, TransactionError, deserialize_from_ivec,
    },
    id::Id,
};

#[derive(Clone)]
pub struct Entry<T: DatabaseEntry> {
    entry: T,
    id: Id<T>,
}

impl<T: DatabaseEntry> std::ops::Deref for Entry<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.entry
    }
}

impl<T: DatabaseEntry> std::ops::DerefMut for Entry<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.entry
    }
}

impl<T: DatabaseEntry> From<(T, Id<T>)> for Entry<T> {
    fn from(value: (T, Id<T>)) -> Self {
        Self::new(value.0, value.1)
    }
}

impl<T: DatabaseEntry> From<(Id<T>, T)> for Entry<T> {
    fn from(value: (Id<T>, T)) -> Self {
        Self::new(value.1, value.0)
    }
}

impl<T: DatabaseEntry> PartialEq for Entry<T> {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl<T: DatabaseEntry> Eq for Entry<T> {}

impl<T: DatabaseEntry> std::hash::Hash for Entry<T> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.id.hash(state);
    }
}

impl<T: DatabaseEntry> Entry<T> {
    pub fn new(item: T, id: Id<T>) -> Self {
        Self { entry: item, id }
    }

    pub fn id(&self) -> Id<T> {
        self.id
    }

    pub fn into_item(self) -> T {
        self.entry
    }

    pub(crate) fn from_sled_batch(
        value: sled::Result<(IVec, IVec)>,
    ) -> Result<Self, TransactionError> {
        let (k, v) = value.map_err(TransactionError::Sled)?;
        let id = Id::try_from(k).expect("All keys should be 32 bytes");
        let t: T = deserialize_from_ivec(v)?;
        Ok(t.to_entry(id))
    }
}

/// Transaction
impl<T: DatabaseEntry> Entry<T> {
    /// Upserts an entry to the database
    ///
    /// # Database Safety
    ///
    /// This function will upsert an item from the database plainly. It does not care to check if it points to other items that don't point back, as it does not know.
    /// Make sure any item you are upserting is fully linked
    ///
    /// For a safe version, see [`CompareAndSwapTransaction::tx_create()`] for types that implement [`Createable`]
    ///
    /// # Errors
    ///
    /// Errors if `sled` fails to open the entry's tree
    pub fn tx_upsert(
        mut self,
        cas_tx: &mut CompareAndSwapTransaction<T::DbInner>,
    ) -> Result<(), TransactionError> {
        T::pre_upsert(&mut self, cas_tx)?;

        let request = cas_tx.get_or_new_request();

        if let Some(get_mut) = request.get_mut(&self.id) {
            get_mut.new = Some(self.entry);
        } else {
            let old = request.tree().get(self.id)?;
            request.insert(self.id, old, Some(self.entry));
        }
        Ok(())
    }

    /// Inserts an entry from the database
    ///
    /// # Database Safety
    ///
    /// This function will insert an item from the database plainly. It does not care to check if it points to other items that don't point back, as it does not know.
    /// Make sure any item you are upserting is fully linked
    ///
    /// For a safe version, see [`CompareAndSwapTransaction::tx_create()`] for types that implement [`Createable`]
    ///
    /// # Errors
    ///
    /// - Errors with [`TransactionError::AlreadyInDatabase`] if the item already exists
    /// - Errors if [`CompareAndSwapTransaction::tx_upsert()`] errors
    pub fn tx_insert(
        self,
        cas_tx: &mut CompareAndSwapTransaction<T::DbInner>,
    ) -> Result<(), TransactionError> {
        if self.id.tx_check(cas_tx)? {
            return Err(TransactionError::AlreadyInDatabase);
        }
        self.tx_upsert(cas_tx)?;
        Ok(())
    }

    /// Merges from `self` into the existing database entry, upserting if there is none
    pub fn tx_patch(
        self,
        cas_tx: &mut CompareAndSwapTransaction<T::DbInner>,
    ) -> Result<(), TransactionError>
    where
        T: Mergeable + std::fmt::Debug,
    {
        self.id()
            .tx_fetch_and_update(
                |old, cas_tx| match old {
                    Some(mut old) => {
                        T::relink_references(&self, self.id(), cas_tx)?;
                        old.merge_data(self.entry);
                        Ok(Some(old))
                    }
                    None => Ok(Some(self.entry)),
                },
                cas_tx,
            )
            .map_err(TransactionError::from)
    }

    /// Merges from `from` into `self`
    ///
    /// This means the identifier of `self` will be preserved, while `from` is deleted
    pub fn tx_merge(
        self,
        from: Self,
        cas_tx: &mut CompareAndSwapTransaction<T::DbInner>,
    ) -> Result<Self, TransactionError>
    where
        T: Mergeable + Deleteable,
    {
        self.tx_merge_batch(std::iter::once(from), cas_tx)
    }

    /// Merges all items from the `batch` into the `self` entry
    ///
    /// This means the identifier of `self` will be preserved, while all items from `batch` are deleted
    pub fn tx_merge_batch(
        self,
        batch: impl IntoIterator<Item = Self>,
        cas_tx: &mut CompareAndSwapTransaction<T::DbInner>,
    ) -> Result<Self, TransactionError>
    where
        T: Mergeable + Deleteable,
    {
        let merged = batch.into_iter().try_fold(
            self,
            |mut current, from| -> Result<Entry<T>, TransactionError> {
                T::relink_references(&from, current.id, cas_tx)?;
                if from.id != current.id {
                    from.id.tx_delete(cas_tx)?;
                }
                current.merge_data(from.entry);
                Ok(current)
            },
        )?;
        merged.clone().tx_upsert(cas_tx)?;
        Ok(merged)
    }

    pub fn tx_create(
        args: T::CreateArgs,
        cas_tx: &mut CompareAndSwapTransaction<T::DbInner>,
    ) -> Result<Self, CustomTransactionError<T::Err>>
    where
        T: Createable,
    {
        let entry = T::create(args, cas_tx)?;
        entry
            .clone()
            .tx_upsert(cas_tx)
            .map_err(CustomTransactionError::from)?;
        Ok(entry)
    }
}

/// Database wrappers
impl<T: DatabaseEntry> Entry<T> {
    /// Wrapper around [`Entry::tx_upsert`]
    pub fn db_upsert(self, db: &Db<T::DbInner>) -> Result<(), TransactionError> {
        db.transaction(false, None, |cas_tx| {
            self.clone()
                .tx_upsert(cas_tx)
                .map_err(CustomTransactionError::from)
        })
        .map_err(TransactionError::from)
    }

    /// Wrapper around [`Entry::tx_insert`]
    pub fn db_insert(self, db: &Db<T::DbInner>) -> Result<(), TransactionError> {
        db.transaction(false, None, |cas_tx| {
            self.clone()
                .tx_insert(cas_tx)
                .map_err(CustomTransactionError::from)
        })
        .map_err(TransactionError::from)
    }

    /// Wrapper around `[Entry::tx_patch]`
    pub fn db_patch(self, db: &Db<T::DbInner>) -> Result<(), TransactionError>
    where
        T: Mergeable + std::fmt::Debug,
    {
        db.transaction(false, None, |cas_tx| {
            self.clone()
                .tx_patch(cas_tx)
                .map_err(CustomTransactionError::from)
        })
        .map_err(TransactionError::from)
    }

    /// Wrapper around [`Entry::tx_merge`]
    pub fn db_merge(self, from: Self, db: &Db<T::DbInner>) -> Result<Self, TransactionError>
    where
        T: Mergeable + Deleteable,
    {
        db.transaction(false, None, |cas_tx| {
            self.clone()
                .tx_merge(from.clone(), cas_tx)
                .map_err(CustomTransactionError::from)
        })
        .map_err(TransactionError::from)
    }

    /// Wrapper around [`Entry::tx_merge_batch`]
    pub fn db_merge_batch(
        self,
        batch: impl IntoIterator<Item = Self>,
        db: &Db<T::DbInner>,
    ) -> Result<Self, TransactionError>
    where
        T: Mergeable + Deleteable,
    {
        let batch: Vec<Self> = batch.into_iter().collect();
        db.transaction(false, None, |cas_tx| {
            self.clone()
                .tx_merge_batch(batch.clone(), cas_tx)
                .map_err(CustomTransactionError::from)
        })
        .map_err(TransactionError::from)
    }

    /// Wrapper around [`Entry::tx_create`]
    pub fn db_create(
        args: T::CreateArgs,
        db: &Db<T::DbInner>,
    ) -> Result<Self, CustomTransactionError<T::Err>>
    where
        T: Createable,
    {
        db.transaction(false, None, |cas_tx| Self::tx_create(args.clone(), cas_tx))
    }

    /// Gets all entries of [`Self`] in from the input `db`
    ///
    /// # Errors
    ///
    /// Errors if [`sled`] fails to retrieve any entry
    pub fn db_get_list(
        cursor: Option<Id<T>>,
        limit: usize,
        db: &Db<T::DbInner>,
    ) -> Result<Vec<Self>, TransactionError> {
        use std::ops::Bound;

        let tree = db.entry_tree::<T>();

        let start: Bound<Id<T>> = match cursor {
            Some(key) => Bound::Excluded(key),
            None => Bound::Unbounded,
        };

        tree.range((start, Bound::Unbounded))
            .take(limit)
            .map(Entry::from_sled_batch)
            .collect()
    }
}

/// Defines an item that can be stored in the database
///
/// # Notes
///
/// For structs that should not be able to be written to the database manually, you can define [`read_only()`] function to return [`true`]
/// For structs where you want to do special logic to them before they are upserted, you can define [`pre_upsert()`]
pub trait DatabaseEntry:
    Serialize + DeserializeOwned + for<'de> Deserialize<'de> + Clone + 'static
{
    type DbInner: Database;

    /// The current version number of the struct, used for updating
    const VERSION_NUMBER: u32;

    /// A unique name for the tree of this entry
    const TREE_NAME: &str;

    /// This function is called before a value is upserted, allowing you to make last moment changes before its upserted into the database, like field sorting
    #[allow(unused_variables)]
    fn pre_upsert(
        entry: &mut Entry<Self>,
        cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
    ) -> Result<(), TransactionError> {
        Ok(())
    }

    #[must_use]
    fn to_entry(self, id: Id<Self>) -> Entry<Self> {
        Entry::new(self, id)
    }
}