mod vector_clock; use std::fmt::Display; use uuid::Uuid; use crate::vector_clock::VectorClock; use thiserror::Error; #[derive(Error, Debug)] pub enum Error { #[error("object with ID {0} not found")] NotFound(DerivedId), } #[derive(Default)] pub struct Doc { ops: Vec, } impl Doc { pub fn append_op(&mut self, actor_id: &Uuid, payload: OpPayload) { // Increment the last clock for the provided actor let clock = self .ops .last() .map(|Op { clock, .. }| clock.inc(actor_id)) // For an empty document, initialize a new clock .unwrap_or_else(|| VectorClock::default().inc(actor_id)); self.ops.push(Op { id: Uuid::now_v7(), clock, payload, }); } pub fn realize(&self) -> Result { let mut realized = RealizedDoc::default(); for op in &self.ops { op.apply(&mut realized)?; } Ok(realized) } } #[derive(Debug)] pub struct Op { id: Uuid, payload: OpPayload, clock: VectorClock, } impl Op { fn apply(&self, realized: &mut RealizedDoc) -> Result<(), Error> { match &self.payload { OpPayload::CreateGrid { rows, base_cells_per_row, } => { let rows = (0..*rows) .map(|row_idx| { let cells = (0..*base_cells_per_row) .map(|cell_idx| Cell { id: self.id.derive_id("cell", cell_idx), }) .collect(); Row { id: self.id.derive_id("row", row_idx), cells, } }) .collect(); realized.grids.push(Grid { id: self.id.derive_id("grid", 0), rows, }); } OpPayload::ChangeSubdivisions { grid_id, row_id, start_cell_id, end_cell_id, subdivisions, } => { let grid = realized .grids .iter_mut() .find(|g| g.id == *grid_id) .ok_or(Error::NotFound(grid_id.clone()))?; let row = grid .rows .iter_mut() .find(|r| r.id == *row_id) .ok_or(Error::NotFound(row_id.clone()))?; let start_cell_idx = row .cells .iter() .position(|c| c.id == *start_cell_id) .ok_or(Error::NotFound(start_cell_id.clone()))?; let end_cell_idx = row .cells .iter() .position(|c| c.id == *end_cell_id) .ok_or(Error::NotFound(end_cell_id.clone()))?; let (i, j) = if start_cell_idx <= end_cell_idx { (start_cell_idx, end_cell_idx) } else { (end_cell_idx, start_cell_idx) }; row.cells.splice( i..(j + 1), (0..*subdivisions).map(|subdivision_idx| Cell { id: self.id.derive_id("cell", subdivision_idx), }), ); } } Ok(()) } } #[derive(Debug)] pub enum OpPayload { CreateGrid { rows: usize, base_cells_per_row: usize, }, ChangeSubdivisions { grid_id: DerivedId, row_id: DerivedId, start_cell_id: DerivedId, end_cell_id: DerivedId, subdivisions: usize, }, } #[derive(Default, Debug)] pub struct RealizedDoc { grids: Vec, } #[derive(Debug)] pub struct Grid { id: DerivedId, rows: Vec, } #[derive(Debug)] pub struct Row { id: DerivedId, cells: Vec, } #[derive(Debug)] pub struct Cell { id: DerivedId, } #[derive(PartialEq, Eq, Debug, Clone)] pub struct DerivedId { // TODO These IDs can be interned on the Doc id: Uuid, tag: &'static str, index: usize, } impl Display for DerivedId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}:{}:{}", self.id, self.tag, self.index) } } trait DerivableId { fn derive_id(&self, tag: &'static str, index: usize) -> DerivedId; } impl DerivableId for Uuid { fn derive_id(&self, tag: &'static str, index: usize) -> DerivedId { DerivedId { id: self.clone(), tag, index, } } } #[cfg(test)] mod tests { use super::*; #[test] fn realize_doc() { let actor_id = Uuid::now_v7(); let mut doc = Doc::default(); doc.append_op( &actor_id, OpPayload::CreateGrid { rows: 4, base_cells_per_row: 16, }, ); let realized = doc.realize().unwrap(); assert!(realized.grids.len() == 1); let grid = realized.grids.first().unwrap(); assert!(grid.rows.len() == 4); let row = grid.rows.first().unwrap(); assert!(row.cells.len() == 16); doc.append_op( &actor_id, OpPayload::ChangeSubdivisions { grid_id: grid.id.clone(), row_id: row.id.clone(), start_cell_id: row.cells[0].id.clone(), end_cell_id: row.cells[3].id.clone(), subdivisions: 3, }, ); let realized2 = doc.realize().unwrap(); assert_eq!(realized2.grids[0].rows[0].cells.len(), 15); assert_eq!(realized2.grids[0].rows[1].cells.len(), 16); } }