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