Skip to main content

open_gpui_canvas/persistence/
codec.rs

1use super::{CanvasCheckpoint, CanvasLogEntry};
2use crate::{DocumentError, validate_canvas_document_format_version};
3use std::{error::Error, fmt};
4
5pub const CANVAS_PERSISTENCE_CODEC_VERSION: u32 = 1;
6
7fn default_persistence_codec_version() -> u32 {
8    CANVAS_PERSISTENCE_CODEC_VERSION
9}
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum CanvasPersistenceRecordKind {
14    Checkpoint,
15    LogEntry,
16}
17
18impl CanvasPersistenceRecordKind {
19    pub fn as_str(self) -> &'static str {
20        match self {
21            Self::Checkpoint => "checkpoint",
22            Self::LogEntry => "log_entry",
23        }
24    }
25}
26
27impl fmt::Display for CanvasPersistenceRecordKind {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        f.write_str(self.as_str())
30    }
31}
32
33#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
34#[serde(tag = "record_kind", content = "payload", rename_all = "snake_case")]
35pub enum CanvasPersistenceRecord {
36    Checkpoint(CanvasCheckpoint),
37    LogEntry(CanvasLogEntry),
38}
39
40impl CanvasPersistenceRecord {
41    pub fn kind(&self) -> CanvasPersistenceRecordKind {
42        match self {
43            Self::Checkpoint(_) => CanvasPersistenceRecordKind::Checkpoint,
44            Self::LogEntry(_) => CanvasPersistenceRecordKind::LogEntry,
45        }
46    }
47
48    pub fn sequence(&self) -> u64 {
49        match self {
50            Self::Checkpoint(checkpoint) => checkpoint.sequence,
51            Self::LogEntry(entry) => entry.sequence(),
52        }
53    }
54
55    pub fn document_format_version(&self) -> u32 {
56        match self {
57            Self::Checkpoint(checkpoint) => checkpoint.snapshot.format_version,
58            Self::LogEntry(_) => crate::CANVAS_DOCUMENT_FORMAT_VERSION,
59        }
60    }
61}
62
63impl From<CanvasCheckpoint> for CanvasPersistenceRecord {
64    fn from(value: CanvasCheckpoint) -> Self {
65        Self::Checkpoint(value)
66    }
67}
68
69impl From<CanvasLogEntry> for CanvasPersistenceRecord {
70    fn from(value: CanvasLogEntry) -> Self {
71        Self::LogEntry(value)
72    }
73}
74
75#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
76pub struct CanvasPersistenceEnvelope {
77    #[serde(default = "default_persistence_codec_version")]
78    pub codec_version: u32,
79    pub document_format_version: u32,
80    pub record: CanvasPersistenceRecord,
81}
82
83impl CanvasPersistenceEnvelope {
84    pub fn new(record: impl Into<CanvasPersistenceRecord>) -> Self {
85        let record = record.into();
86        Self {
87            codec_version: CANVAS_PERSISTENCE_CODEC_VERSION,
88            document_format_version: record.document_format_version(),
89            record,
90        }
91    }
92
93    pub fn kind(&self) -> CanvasPersistenceRecordKind {
94        self.record.kind()
95    }
96
97    pub fn sequence(&self) -> u64 {
98        self.record.sequence()
99    }
100}
101
102#[derive(Debug, Eq, PartialEq)]
103pub enum CanvasPersistenceCodecError {
104    UnsupportedCodecVersion {
105        expected: u32,
106        found: u32,
107    },
108    UnsupportedDocumentFormatVersion {
109        expected: u32,
110        found: u32,
111    },
112    RecordFormatVersionMismatch {
113        envelope: u32,
114        payload: u32,
115    },
116    UnexpectedRecordKind {
117        expected: CanvasPersistenceRecordKind,
118        found: CanvasPersistenceRecordKind,
119    },
120    Json(String),
121}
122
123impl CanvasPersistenceCodecError {
124    fn json(error: serde_json::Error) -> Self {
125        Self::Json(error.to_string())
126    }
127}
128
129impl fmt::Display for CanvasPersistenceCodecError {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match self {
132            Self::UnsupportedCodecVersion { expected, found } => write!(
133                f,
134                "unsupported canvas persistence codec version `{found}`, expected `{expected}`"
135            ),
136            Self::UnsupportedDocumentFormatVersion { expected, found } => write!(
137                f,
138                "unsupported canvas persistence document format version `{found}`, expected `{expected}`"
139            ),
140            Self::RecordFormatVersionMismatch { envelope, payload } => write!(
141                f,
142                "canvas persistence envelope document format version `{envelope}` does not match payload version `{payload}`"
143            ),
144            Self::UnexpectedRecordKind { expected, found } => write!(
145                f,
146                "canvas persistence record kind `{found}` cannot be decoded as `{expected}`"
147            ),
148            Self::Json(error) => write!(f, "canvas persistence JSON codec error: {error}"),
149        }
150    }
151}
152
153impl Error for CanvasPersistenceCodecError {}
154
155pub trait CanvasPersistenceCodec {
156    type Error: fmt::Debug + fmt::Display;
157
158    fn encode_checkpoint(&self, checkpoint: &CanvasCheckpoint) -> Result<Vec<u8>, Self::Error>;
159
160    fn decode_checkpoint(&self, bytes: &[u8]) -> Result<CanvasCheckpoint, Self::Error>;
161
162    fn encode_log_entry(&self, entry: &CanvasLogEntry) -> Result<Vec<u8>, Self::Error>;
163
164    fn decode_log_entry(&self, bytes: &[u8]) -> Result<CanvasLogEntry, Self::Error>;
165}
166
167#[derive(Clone, Copy, Debug, Default)]
168pub struct CanvasJsonPersistenceCodec;
169
170impl CanvasJsonPersistenceCodec {
171    pub fn encode_record(
172        &self,
173        record: impl Into<CanvasPersistenceRecord>,
174    ) -> Result<Vec<u8>, CanvasPersistenceCodecError> {
175        serde_json::to_vec(&CanvasPersistenceEnvelope::new(record))
176            .map_err(CanvasPersistenceCodecError::json)
177    }
178
179    pub fn decode_record(
180        &self,
181        bytes: &[u8],
182    ) -> Result<CanvasPersistenceRecord, CanvasPersistenceCodecError> {
183        Ok(self.decode_envelope(bytes)?.record)
184    }
185
186    pub fn decode_envelope(
187        &self,
188        bytes: &[u8],
189    ) -> Result<CanvasPersistenceEnvelope, CanvasPersistenceCodecError> {
190        let envelope: CanvasPersistenceEnvelope =
191            serde_json::from_slice(bytes).map_err(CanvasPersistenceCodecError::json)?;
192        validate_persistence_envelope(&envelope)?;
193        Ok(envelope)
194    }
195}
196
197impl CanvasPersistenceCodec for CanvasJsonPersistenceCodec {
198    type Error = CanvasPersistenceCodecError;
199
200    fn encode_checkpoint(&self, checkpoint: &CanvasCheckpoint) -> Result<Vec<u8>, Self::Error> {
201        self.encode_record(checkpoint.clone())
202    }
203
204    fn decode_checkpoint(&self, bytes: &[u8]) -> Result<CanvasCheckpoint, Self::Error> {
205        let envelope = self.decode_envelope(bytes)?;
206        let found = envelope.kind();
207        let CanvasPersistenceRecord::Checkpoint(checkpoint) = envelope.record else {
208            return Err(CanvasPersistenceCodecError::UnexpectedRecordKind {
209                expected: CanvasPersistenceRecordKind::Checkpoint,
210                found,
211            });
212        };
213        if checkpoint.snapshot.format_version != envelope.document_format_version {
214            return Err(CanvasPersistenceCodecError::RecordFormatVersionMismatch {
215                envelope: envelope.document_format_version,
216                payload: checkpoint.snapshot.format_version,
217            });
218        }
219        Ok(checkpoint)
220    }
221
222    fn encode_log_entry(&self, entry: &CanvasLogEntry) -> Result<Vec<u8>, Self::Error> {
223        self.encode_record(entry.clone())
224    }
225
226    fn decode_log_entry(&self, bytes: &[u8]) -> Result<CanvasLogEntry, Self::Error> {
227        let envelope = self.decode_envelope(bytes)?;
228        let found = envelope.kind();
229        let CanvasPersistenceRecord::LogEntry(entry) = envelope.record else {
230            return Err(CanvasPersistenceCodecError::UnexpectedRecordKind {
231                expected: CanvasPersistenceRecordKind::LogEntry,
232                found,
233            });
234        };
235        Ok(entry)
236    }
237}
238
239fn validate_persistence_envelope(
240    envelope: &CanvasPersistenceEnvelope,
241) -> Result<(), CanvasPersistenceCodecError> {
242    if envelope.codec_version != CANVAS_PERSISTENCE_CODEC_VERSION {
243        return Err(CanvasPersistenceCodecError::UnsupportedCodecVersion {
244            expected: CANVAS_PERSISTENCE_CODEC_VERSION,
245            found: envelope.codec_version,
246        });
247    }
248
249    if let Err(DocumentError::UnsupportedFormatVersion { expected, found }) =
250        validate_canvas_document_format_version(envelope.document_format_version)
251    {
252        return Err(
253            CanvasPersistenceCodecError::UnsupportedDocumentFormatVersion { expected, found },
254        );
255    }
256
257    let payload_format_version = envelope.record.document_format_version();
258    if payload_format_version != envelope.document_format_version {
259        return Err(CanvasPersistenceCodecError::RecordFormatVersionMismatch {
260            envelope: envelope.document_format_version,
261            payload: payload_format_version,
262        });
263    }
264
265    Ok(())
266}