#![forbid(unsafe_code)]
use ropey::Rope;
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum SpecError {
#[error("crdt engine not implemented at M0a: {kind:?} (lands at M2 behind the convergence gate)")]
Unimplemented { kind: CrdtKind },
#[error("operation '{op}' has no wire form on the M0a single-writer SoloDoc")]
NoWireForm { op: &'static str },
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CrdtKind {
Solo,
Loro,
YCrdt,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CrdtUpdate(pub Vec<u8>);
pub trait CrdtDoc: Send + Sync {
fn materialize(&self) -> Rope;
fn kind(&self) -> CrdtKind;
fn apply_update(&mut self, _update: &CrdtUpdate) -> Result<(), SpecError> {
Err(SpecError::NoWireForm { op: "apply_update" })
}
}
pub struct SoloDoc {
text: Rope,
}
impl SoloDoc {
#[must_use]
pub fn from_str(body: &str) -> Self {
Self {
text: Rope::from_str(body),
}
}
}
impl CrdtDoc for SoloDoc {
fn materialize(&self) -> Rope {
self.text.clone()
}
fn kind(&self) -> CrdtKind {
CrdtKind::Solo
}
}
pub fn open_doc(kind: CrdtKind, body: &str) -> Result<Box<dyn CrdtDoc>, SpecError> {
match kind {
CrdtKind::Solo => Ok(Box::new(SoloDoc::from_str(body))),
CrdtKind::Loro | CrdtKind::YCrdt => Err(SpecError::Unimplemented { kind }),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn solo_materializes_its_body() {
let doc = open_doc(CrdtKind::Solo, "# hello\nworld").expect("solo opens");
assert_eq!(doc.materialize().to_string(), "# hello\nworld");
assert_eq!(doc.kind(), CrdtKind::Solo);
}
#[test]
fn loro_is_typed_unimplemented_not_silent_ok() {
assert!(matches!(
open_doc(CrdtKind::Loro, "x"),
Err(SpecError::Unimplemented { kind: CrdtKind::Loro })
));
}
#[test]
fn ycrdt_is_typed_unimplemented() {
assert!(matches!(
open_doc(CrdtKind::YCrdt, "x"),
Err(SpecError::Unimplemented { kind: CrdtKind::YCrdt })
));
}
#[test]
fn solo_has_no_wire_form() {
let mut doc = SoloDoc::from_str("x");
assert!(matches!(
doc.apply_update(&CrdtUpdate(vec![])),
Err(SpecError::NoWireForm { op: "apply_update" })
));
}
proptest::proptest! {
#[test]
fn solo_materialize_round_trips_arbitrary_body(body in ".*") {
let doc = open_doc(CrdtKind::Solo, &body).expect("solo opens");
proptest::prop_assert_eq!(doc.materialize().to_string(), body);
}
}
}