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