tlpt 0.7.0

Telepathy: syncable, append-only logs and sets
Documentation
use futures::StreamExt;
use litl::{impl_debug_as_litl, impl_serde_enum_of_distinct_tagged_values};
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;

use crate::{
    blob::{BlobDiff, BlobError, BlobID, BlobState, BlobStateInfo},
    log::{LogDiff, LogError, LogID, LogState, LogStateInfo, LogWriteAccess},
    set::{SetDiff, SetError, SetID, SetState, SetStateInfo, SetWriteAccess},
    telepathic::{ApplyDiffErrorFor, ApplyDiffSuccess, Telepathic, TelepathicDiff},
};

#[derive(Debug)]
pub enum ObjectState {
    Log(Box<LogState>),
    Set(Box<SetState>),
    Blob(Box<BlobState>),
}

#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub enum ObjectID {
    Log(LogID),
    Set(SetID),
    Blob(BlobID),
}

impl_serde_enum_of_distinct_tagged_values!(ObjectID,
    "log" => (Log, LogID),
    "set" => (Set, SetID),
    "blob" => (Blob, BlobID)
);

impl ObjectID {
    #[track_caller]
    pub fn expect_log(&self) -> LogID {
        match self {
            ObjectID::Log(id) => *id,
            _ => panic!("Expected LogID, got {:?}", self),
        }
    }

    #[track_caller]
    pub fn expect_set(&self) -> SetID {
        match self {
            ObjectID::Set(id) => *id,
            _ => panic!("Expected SetID, got {:?}", self),
        }
    }

    #[track_caller]
    pub fn expect_blob(&self) -> BlobID {
        match self {
            ObjectID::Blob(id) => *id,
            _ => panic!("Expected BlobID, got {:?}", self),
        }
    }

    pub fn test_random() -> Self {
        ObjectID::Blob(BlobID::test_random())
    }
}

impl_debug_as_litl!(ObjectID);

pub enum WriteAccess {
    Log(LogWriteAccess),
    Set(SetWriteAccess),
}

impl_serde_enum_of_distinct_tagged_values!(WriteAccess,
    "log" => (Log, LogWriteAccess),
    "set" => (Set, SetWriteAccess)
);
impl_debug_as_litl!(WriteAccess);

#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
// #[serde(untagged)]
pub enum StateInfo {
    Log(LogStateInfo),
    Set(SetStateInfo),
    Blob(BlobStateInfo),
}

#[derive(Clone, Serialize, Deserialize)]
// #[serde(untagged)]
pub enum Diff {
    Log(LogDiff),
    Set(SetDiff),
    Blob(BlobDiff),
}

impl Diff {
    #[track_caller]
    pub fn expect_log(&self) -> &LogDiff {
        match self {
            Diff::Log(diff) => diff,
            _ => panic!("Expected LogDiff, got {:?}", self),
        }
    }

    #[track_caller]
    pub fn expect_set(&self) -> &SetDiff {
        match self {
            Diff::Set(diff) => diff,
            _ => panic!("Expected SetDiff, got {:?}", self),
        }
    }

    #[track_caller]
    pub fn expect_blob(&self) -> &BlobDiff {
        match self {
            Diff::Blob(diff) => diff,
            _ => panic!("Expected BlobDiff, got {:?}", self),
        }
    }
}

impl_debug_as_litl!(Diff);

#[derive(Debug, Error)]
pub enum ObjectError {
    #[error(transparent)]
    Log(LogError),
    #[error(transparent)]
    Set(SetError),
    #[error(transparent)]
    Blob(BlobError),
}

impl Telepathic for ObjectState {
    type ID = ObjectID;
    type WriteAccess = WriteAccess;
    type StateInfo = StateInfo;
    type Diff = Diff;
    type Error = ObjectError;

    fn id(&self) -> Self::ID {
        match self {
            ObjectState::Log(log) => ObjectID::Log(log.id()),
            ObjectState::Set(set) => ObjectID::Set(set.id()),
            ObjectState::Blob(blob) => ObjectID::Blob(blob.id()),
        }
    }

    fn try_apply_diff(
        &mut self,
        diff: Self::Diff,
    ) -> Result<
        ApplyDiffSuccess<Self::StateInfo, Self::ID, Self::Diff>,
        ApplyDiffErrorFor<Self::Error>,
    > {
        match (self, diff) {
            (ObjectState::Log(log), Diff::Log(delta)) => log
                .try_apply_diff(delta)
                .map(
                    |ApplyDiffSuccess {
                         new_state_info: new_state,
                         effective_diff,
                     }| ApplyDiffSuccess {
                        new_state_info: StateInfo::Log(new_state),
                        effective_diff: Diff::Log(effective_diff),
                    },
                )
                .map_err(|e| match e {
                    ApplyDiffErrorFor::InvalidKnownStateAssumption => {
                        ApplyDiffErrorFor::InvalidKnownStateAssumption
                    }
                    ApplyDiffErrorFor::Other(e) => ApplyDiffErrorFor::Other(ObjectError::Log(e)),
                }),
            (ObjectState::Set(set), Diff::Set(delta)) => set
                .try_apply_diff(delta)
                .map(
                    |ApplyDiffSuccess {
                         new_state_info: new_state,
                         effective_diff,
                     }| ApplyDiffSuccess {
                        new_state_info: StateInfo::Set(new_state),
                        effective_diff: Diff::Set(effective_diff),
                    },
                )
                .map_err(|e| match e {
                    ApplyDiffErrorFor::InvalidKnownStateAssumption => {
                        ApplyDiffErrorFor::InvalidKnownStateAssumption
                    }
                    ApplyDiffErrorFor::Other(e) => ApplyDiffErrorFor::Other(ObjectError::Set(e)),
                }),
            (ObjectState::Blob(blob), Diff::Blob(delta)) => blob
                .try_apply_diff(delta)
                .map(
                    |ApplyDiffSuccess {
                         new_state_info: new_state,
                         effective_diff,
                     }| ApplyDiffSuccess {
                        new_state_info: StateInfo::Blob(new_state),
                        effective_diff: Diff::Blob(effective_diff),
                    },
                )
                .map_err(|e| match e {
                    ApplyDiffErrorFor::InvalidKnownStateAssumption => {
                        ApplyDiffErrorFor::InvalidKnownStateAssumption
                    }
                    ApplyDiffErrorFor::Other(e) => ApplyDiffErrorFor::Other(ObjectError::Blob(e)),
                }),
            (s, diff) => unreachable!("Invalid object state/diff combination {s:?} {diff:?}"),
        }
    }

    fn state_info(&self) -> Option<Self::StateInfo> {
        match self {
            ObjectState::Log(log) => log.state_info().map(StateInfo::Log),
            ObjectState::Set(set) => set.state_info().map(StateInfo::Set),
            ObjectState::Blob(blob) => blob.state_info().map(StateInfo::Blob),
        }
    }

    fn diff_since(&self, state_info: Option<&Self::StateInfo>) -> Option<Self::Diff> {
        match (self, state_info) {
            (ObjectState::Log(log), Some(StateInfo::Log(state_info))) => {
                log.diff_since(Some(state_info)).map(Diff::Log)
            }
            (ObjectState::Log(log), None) => log.diff_since(None).map(Diff::Log),
            (ObjectState::Set(set), Some(StateInfo::Set(state_info))) => {
                set.diff_since(Some(state_info)).map(Diff::Set)
            }
            (ObjectState::Set(set), None) => set.diff_since(None).map(Diff::Set),
            (ObjectState::Blob(blob), Some(StateInfo::Blob(state_info))) => {
                blob.diff_since(Some(state_info)).map(Diff::Blob)
            }
            (ObjectState::Blob(blob), None) => blob.diff_since(None).map(Diff::Blob),
            _ => {
                unreachable!("Invalid object state/state_info combination {self:?} {state_info:?}")
            }
        }
    }

    fn load(
        id: ObjectID,
        storage: Box<dyn crate::StorageBackend>,
    ) -> std::pin::Pin<Box<dyn futures::Stream<Item = Self::Diff>>> {
        match id {
            ObjectID::Log(id) => LogState::load(id, storage).map(Diff::Log).boxed_local(),
            ObjectID::Set(id) => SetState::load(id, storage).map(Diff::Set).boxed_local(),
            ObjectID::Blob(id) => BlobState::load(id, storage).map(Diff::Blob).boxed_local(),
        }
    }

    fn store(
        effective_diff: Diff,
        storage: Box<dyn crate::StorageBackend>,
    ) -> std::pin::Pin<Box<dyn futures::Future<Output = ()>>> {
        match effective_diff {
            Diff::Log(diff) => LogState::store(diff, storage),
            Diff::Set(diff) => SetState::store(diff, storage),
            Diff::Blob(diff) => BlobState::store(diff, storage),
        }
    }
}

impl TelepathicDiff for Diff {
    type ID = ObjectID;

    fn id(&self) -> Self::ID {
        match self {
            Diff::Log(log) => ObjectID::Log(log.id),
            Diff::Set(set) => ObjectID::Set(set.id),
            Diff::Blob(blob) => ObjectID::Blob(blob.id),
        }
    }
}

#[cfg(test)]
mod test {
    use crate::{log::LogState, set::SetState, BlobState, ObjectID};

    #[test]
    fn object_id_serialization_roundtrips() {
        let (log, _) = LogState::new::<()>(None);
        let id = ObjectID::Log(log.id);
        let serialized = litl::to_vec(&id).unwrap();
        let deserialized: ObjectID = litl::from_slice(&serialized).unwrap();
        println!("{}", String::from_utf8(serialized).unwrap());
        assert_eq!(id, deserialized);

        let (set, _) = SetState::new::<()>(None);
        let id = ObjectID::Set(set.id);
        let serialized = litl::to_vec(&id).unwrap();
        let deserialized: ObjectID = litl::from_slice(&serialized).unwrap();
        println!("{}", String::from_utf8(serialized).unwrap());
        assert_eq!(id, deserialized);

        let blob = BlobState::new::<()>(());
        let id = ObjectID::Blob(blob.id);
        let serialized = litl::to_vec(&id).unwrap();
        let deserialized: ObjectID = litl::from_slice(&serialized).unwrap();
        println!("{}", String::from_utf8(serialized).unwrap());
        assert_eq!(id, deserialized);
    }
}