catcolab_document_types/
lib.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use serde_wasm_bindgen::{Serializer, from_value};
4use wasm_bindgen::prelude::*;
5
6mod v0;
7pub mod v1;
8pub mod v2;
9
10#[cfg(feature = "backend")]
11pub mod automerge_json;
12
13#[cfg(feature = "backend")]
14pub mod automerge_util;
15
16#[cfg(test)]
17mod test_utils;
18
19#[cfg(all(test, feature = "backend"))]
20pub(crate) mod common_test;
21
22pub mod current {
23    // this should always track the latest version, and is the only version
24    // that is exported from document-types
25    pub use crate::v2::*;
26}
27
28/// Generate type defs for dependencies supporting `serde` but not `tsify`.
29///
30/// To define `Value`, we could borrow the definition of `JsonValue` from `ts-rs`:
31/// <https://github.com/Aleph-Alpha/ts-rs/blob/main/ts-rs/tests/integration/serde_json.rs>.
32/// However, this causes mysterious TS errors, so we use `unknown` instead.
33///
34/// TODO: Do not use `NonEmpty` in wasm-bound types to avoid need for alias.
35#[wasm_bindgen(typescript_custom_section)]
36const TS_APPEND_CONTENT: &'static str = r#"
37type NonEmpty<T> = Array<T>;
38export type Uuid = string;
39type Ustr = string;
40type Value = unknown;
41"#;
42
43pub static CURRENT_VERSION: &str = "2";
44
45#[wasm_bindgen(js_name = "currentVersion")]
46pub fn current_version() -> String {
47    CURRENT_VERSION.to_string()
48}
49
50#[derive(Serialize, Debug)]
51pub enum VersionedDocument {
52    V0(v0::Document),
53    V1(v1::Document),
54    V2(v2::Document),
55}
56
57impl<'de> Deserialize<'de> for VersionedDocument {
58    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59    where
60        D: serde::Deserializer<'de>,
61    {
62        let value = Value::deserialize(deserializer)?;
63
64        let version = value.get("version").and_then(Value::as_str).unwrap_or("0");
65
66        match version {
67            "0" => {
68                let doc: v0::Document =
69                    serde_json::from_value(value).map_err(serde::de::Error::custom)?;
70                Ok(VersionedDocument::V0(doc))
71            }
72            "1" => {
73                let doc: v1::Document =
74                    serde_json::from_value(value).map_err(serde::de::Error::custom)?;
75                Ok(VersionedDocument::V1(doc))
76            }
77            "2" => {
78                let doc: v2::Document =
79                    serde_json::from_value(value).map_err(serde::de::Error::custom)?;
80                Ok(VersionedDocument::V2(doc))
81            }
82            other => Err(serde::de::Error::custom(format!("unsupported version {other}"))),
83        }
84    }
85}
86
87impl VersionedDocument {
88    pub fn to_current(self) -> current::Document {
89        match self {
90            VersionedDocument::V0(v0) => {
91                // Recursive call to VersionedDocument::to_current
92                VersionedDocument::V1(v1::Document::migrate_from_v0(v0)).to_current()
93            }
94
95            VersionedDocument::V1(v1) => {
96                // Recursive call to VersionedDocument::to_current
97                VersionedDocument::V2(v2::Document::migrate_from_v1(v1)).to_current()
98            }
99
100            VersionedDocument::V2(old2) => old2,
101        }
102    }
103}
104
105#[wasm_bindgen(js_name = "migrateDocument")]
106pub fn migrate_document(input: JsValue) -> Result<JsValue, JsValue> {
107    let doc: VersionedDocument =
108        from_value(input).map_err(|e| JsValue::from_str(&format!("deserialize error: {e}")))?;
109
110    let current_doc = doc.to_current();
111
112    // By default some types will serialize to more complicated JS type (like HashMap -> Map) instead of
113    // a "plain" JSON type. JS !== JSON
114    let serializer = Serializer::json_compatible();
115
116    let output = current_doc
117        .serialize(&serializer)
118        .map_err(|e| JsValue::from_str(&format!("serialize error: {e}")))?;
119
120    Ok(output)
121}
122
123#[cfg(test)]
124mod migration_tests {
125    use super::VersionedDocument;
126    use crate::test_utils::test_example_documents;
127
128    #[test]
129    fn test_v0_examples_migrate_to_current() {
130        test_example_documents::<VersionedDocument, _>("examples/v0", |doc, _| {
131            // ensure it migrates without panic
132            let _ = doc.to_current();
133        });
134    }
135}