Repository: redbadger/crux (language: rust)

Issue:
Typegen fails when a Uuid is included
I was trying to pass a `Uuid` value in an `Event` variant, but this breaks typegen. It seems like the typegen tries to create an "empty" Uuid, which doesn't work.

I've created a minimal reproduction in the counter example app in [this commit](https://github.com/wasnotrice/crux/commit/765b90dbd44ab139d3cfb9b3919a2629c52df364) on [this branch](https://github.com/wasnotrice/crux/tree/shared_types-uuid). I don't think it matters where the Uuid appears in the types registered with typegen.

Any ideas about how to tweak the typegen to handle Uuids?


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index f87b8c47..0400ebc3 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -25,6 +25,10 @@ jobs:
         with:
           components: rustfmt
 
+      - uses: pnpm/action-setup@v2
+        with:
+          version: 7.32.2
+
       - name: Check formatting
         shell: bash
         run: cargo fmt --all --check
@@ -103,7 +107,7 @@ jobs:
 
       - uses: pnpm/action-setup@v2
         with:
-          version: 7.18.1
+          version: 7.32.2
 
       - name: Check formatting
         shell: bash
diff --git a/Cargo.lock b/Cargo.lock
index 61fd33d7..97ab28a8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -248,6 +248,7 @@ dependencies = [
  "async-channel",
  "bcs",
  "crossbeam-channel",
+ "crux_http",
  "crux_macros",
  "derive_more",
  "futures",
@@ -1286,6 +1287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2"
 dependencies = [
  "getrandom 0.2.9",
+ "serde",
  "wasm-bindgen",
 ]
 
diff --git a/crux_core/Cargo.toml b/crux_core/Cargo.toml
index b4075032..6679ec1b 100644
--- a/crux_core/Cargo.toml
+++ b/crux_core/Cargo.toml
@@ -24,16 +24,18 @@ serde_json = "1.0.96"
 serde-generate = { version = "0.25.1", optional = true }
 serde-reflection = { version = "0.3.6", optional = true }
 thiserror = "1.0.40"
-uuid = { version = "1.3.2", features = ["v4", "wasm-bindgen", "js"] }
+uuid = { version = "1.3.2", features = ["v4", "wasm-bindgen", "js", "serde"] }
 wasm-bindgen = "0.2.84"
 
 [dev-dependencies]
 assert_matches = "1.5"
+async-channel = "1.8"
 crux_macros = { version = "0.2", path = "../crux_macros" }
+crux_http = { version = "0.3", path = "../crux_http" }
 serde = { version = "1.0.160", features = ["derive"] }
 static_assertions = "1.1"
-async-channel = "1.8"
 rand = "0.8"
+uuid = { version = "1.3.1", features = ["v4", "serde"] }
 
 [features]
 typegen = ["dep:serde-generate", "dep:serde-reflection"]
diff --git a/crux_core/src/typegen.rs b/crux_core/src/typegen.rs
index d0a68773..90e312d7 100644
--- a/crux_core/src/typegen.rs
+++ b/crux_core/src/typegen.rs
@@ -8,7 +8,7 @@
 //!
 //! Ensure that you have the following line in the `Cargo.toml` of your `shared_types` library.
 //!
-//! ```rust
+//! ```rust,ignore
 //! [build-dependencies]
 //! crux_core = { version = "0.3", features = ["typegen"] }
 //! ```
@@ -17,10 +17,38 @@
 //! * Create a `build.rs` in your `shared_types` library, that looks something like this:
 //!
 //! ```rust
+//! # mod shared {
+//! #     use crux_http::Http;
+//! #     use crux_macros::Effect;
+//! #     use serde::{Deserialize, Serialize};
+//! #     #[derive(Default)]
+//! #     pub struct App;
+//! #     #[derive(Serialize, Deserialize)]
+//! #     pub enum Event {
+//! #         None,
+//! #         SendUuid(uuid::Uuid),
+//! #     }
+//! #     #[derive(Serialize, Deserialize)]
+//! #     pub struct ViewModel;
+//! #     impl crux_core::App for App {
+//! #         type Event = Event;
+//! #         type Model = ();
+//! #         type ViewModel = ViewModel;
+//! #         type Capabilities = Capabilities;
+//! #         fn update(&self, _event: Event, _model: &mut Self::Model, _caps: &Capabilities) {}
+//! #         fn view(&self, _model: &Self::Model) -> Self::ViewModel {
+//! #             todo!();
+//! #         }
+//! #     }
+//! #     #[derive(Effect)]
+//! #     pub struct Capabilities {
+//! #         pub http: Http<Event>,
+//! #     }
+//! # }
 //! use anyhow::Result;
-//! use crux_core::{typegen::TypeGen, Request};
-//! use crux_http::{HttpRequest, HttpResponse};
-//! use shared::{Effect, Event, ViewModel};
+//! use crux_core::{typegen::TypeGen, bridge::Request};
+//! use crux_http::protocol::{HttpRequest, HttpResponse};
+//! use shared::{EffectFfi, Event, ViewModel};
 //! use std::path::PathBuf;
 //!
 //! fn main() {
@@ -30,7 +58,7 @@
 //!
 //!     register_types(&mut gen).expect("type registration failed");
 //!
-//!     let output_root = PathBuf::from("./generated");
+//!     let output_root = std::env::temp_dir().join("crux_core_typegen_doctest");
 //!
 //!     gen.swift("shared_types", output_root.join("swift"))
 //!         .expect("swift type gen failed");
@@ -46,12 +74,14 @@
 //! }
 //!
 //! fn register_types(gen: &mut TypeGen) -> Result<()> {
-//!     gen.register_type::<Request<Effect>>()?;
+//!     gen.register_type::<Request<EffectFfi>>()?;
 //!
-//!     gen.register_type::<Effect>()?;
+//!     gen.register_type::<EffectFfi>()?;
 //!     gen.register_type::<HttpRequest>()?;
 //!
-//!     gen.register_type::<Event>()?;
+//!     let sample_events = vec![Event::SendUuid(uuid::Uuid::new_v4())];
+//!     gen.register_type_with_samples(sample_events)?;
+//!
 //!     gen.register_type::<HttpResponse>()?;
 //!
 //!     gen.register_type::<ViewModel>()?;
@@ -69,9 +99,12 @@ use std::{
     path::{Path, PathBuf},
 };
 
+// Expose from `serde_reflection` for `register_type_with_samples()`
+use serde_reflection::Samples;
+
 enum State {
-    Tracer(Tracer),
-    Registry(Registry),
+    Registering(Tracer, Samples),
+    Generating(Registry),
 }
 
 /// The `TypeGen` struct stores the registered types so that they can be generated for foreign languages
@@ -83,7 +116,7 @@ pub struct TypeGen {
 impl Default for TypeGen {
     fn default() -> Self {
         TypeGen {
-            state: State::Tracer(Tracer::new(TracerConfig::default())),
+            state: State::Registering(Tracer::new(TracerConfig::default()), Samples::new()),
         }
     }
 }
@@ -97,17 +130,33 @@ impl TypeGen {
     /// For each of the types that you want to share with the Shell, call this method:
     /// e.g.
     /// ```rust
-    /// gen.register_type::<Request<Effect>>()?;
-    /// gen.register_type::<Effect>()?;
-    /// gen.register_type::<Event>()?;
-    /// gen.register_type::<ViewModel>()?;
+    /// # use crux_core::typegen::TypeGen;
+    /// # use std::marker::PhantomData;
+    /// # use serde::{Serialize, Deserialize};
+    /// # use anyhow::Error;
+    /// # #[derive(Serialize, Deserialize)]
+    /// # struct Request<T> { phantom: PhantomData<T> }
+    /// # #[derive(Serialize, Deserialize)]
+    /// # struct Effect {}
+    /// # #[derive(Serialize, Deserialize)]
+    /// # struct Event {}
+    /// # #[derive(Serialize, Deserialize)]
+    /// # struct ViewModel {}
+    /// # fn register() -> Result<(), Error> {
+    /// # let mut gen = TypeGen::new();
+    ///   gen.register_type::<Request<Effect>>()?;
+    ///   gen.register_type::<Effect>()?;
+    ///   gen.register_type::<Event>()?;
+    ///   gen.register_type::<ViewModel>()?;
+    /// # Ok(())
+    /// # }
     /// ```
     pub fn register_type<'de, T>(&mut self) -> Result<()>
     where
         T: serde::Deserialize<'de>,
     {
         match &mut self.state {
-            State::Tracer(tracer) => match tracer.trace_simple_type::<T>() {
+            State::Registering(tracer, _) => match tracer.trace_simple_type::<T>() {
                 Ok(_) => Ok(()),
                 Err(e) => bail!("type tracing failed: {}", e),
             },
@@ -115,9 +164,67 @@ impl TypeGen {
         }
     }
 
+    /// Usually, the simple `register_type()` method can generate the types you need.
+    /// Sometimes, though, you need to provide samples of your type. The `Uuid` type,
+    /// for example, requires a sample struct to help the typegen system understand
+    /// what it looks like. Use this method to provide samples when you register a
+    /// type.
+    ///
+    /// For each of the types that you want to share with the Shell, call this method,
+    /// providing samples of the type:
+    /// e.g.
+    /// ```rust
+    /// # use crux_core::typegen::TypeGen;
+    /// # use uuid::Uuid;
+    /// # use serde::{Serialize, Deserialize};
+    /// # use anyhow::Error;
+    /// # #[derive(Serialize, Deserialize)]
+    /// # struct MyUuid(Uuid);
+    /// # fn register() -> Result<(), Error> {
+    /// # let mut gen = TypeGen::new();
+    ///   let sample_data = vec![MyUuid(Uuid::new_v4())];
+    ///   gen.register_type_with_samples::<MyUuid>(sample_data)?;
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// Note: Because of the way that enums are handled by `serde_reflection`,
+    /// you may need to ensure that enums provided as samples have a first variant
+    /// that does not use custom deserialization.
+    pub fn register_type_with_samples<'de, T>(&'de mut self, sample_data: Vec<T>) -> Result<()>
+    where
+        T: serde::Deserialize<'de> + serde::Serialize,
+    {
+        match &mut self.state {
+            State::Registering(tracer, samples) => {
+                for sample in sample_data {
+                    match tracer.trace_value::<T>(samples, &sample) {
+                        Ok(_) => (),
+                        Err(e) => bail!("value tracing failed: {}", e),
+                    }
+                }
+
+                match tracer.trace_type::<T>(samples) {
+                    Ok(_) => Ok(()),
+                    Err(e) => bail!(
+                        r#"type tracing failed: {}
+                        If you are tracing an enum with a variant that uses custom deserialization,
+                        ensure that it is not the first variant."#,
+                        e
+                    ),
+                }
+            }
+            _ => bail!("code has been generated, too late to register types"),
+        }
+    }
+
     /// Generates types for Swift
     /// e.g.
     /// ```rust
+    /// # use crux_core::typegen::TypeGen;
+    /// # use std::env::temp_dir;
+    /// # let mut gen = TypeGen::new();
+    /// # let output_root = temp_dir().join("crux_core_typegen_doctest");
     /// gen.swift("shared_types", output_root.join("swift"))
     ///     .expect("swift type gen failed");
     /// ```
@@ -132,7 +239,7 @@ impl TypeGen {
 
         let generator = serde_generate::swift::CodeGenerator::new(&config);
         let registry = match &self.state {
-            State::Registry(registry) => registry,
+            State::Generating(registry) => registry,
             _ => panic!("registry creation failed"),
         };
 
@@ -160,6 +267,10 @@ impl TypeGen {
     /// Generates types for Java (for use with Kotlin)
     /// e.g.
     /// ```rust
+    /// # use crux_core::typegen::TypeGen;
+    /// # use std::env::temp_dir;
+    /// # let mut gen = TypeGen::new();
+    /// # let output_root = temp_dir().join("crux_core_typegen_doctest");
     /// gen.java(
     ///     "com.redbadger.crux_core.shared_types",
     ///     output_root.join("java"),
@@ -175,7 +286,7 @@ impl TypeGen {
             .with_encodings(vec![Encoding::Bcs]);
 
         let registry = match &self.state {
-            State::Registry(registry) => registry,
+            State::Generating(registry) => registry,
             _ => panic!("registry creation failed"),
         };
 
@@ -203,6 +314,10 @@ impl TypeGen {
     /// Generates types for TypeScript
     /// e.g.
     /// ```rust
+    /// # use crux_core::typegen::TypeGen;
+    /// # use std::env::temp_dir;
+    /// # let mut gen = TypeGen::new();
+    /// # let output_root = temp_dir().join("crux_core_typegen_doctest");
     /// gen.typescript("shared_types", output_root.join("typescript"))
     ///    .expect("typescript type gen failed");
     /// ```
@@ -225,7 +340,7 @@ impl TypeGen {
         copy(extensions_dir, path).expect("Could not copy TS runtime");
 
         let registry = match &self.state {
-            State::Registry(registry) => registry,
+            State::Generating(registry) => registry,
             _ => panic!("registry creation failed"),
         };
 
@@ -264,17 +379,17 @@ impl TypeGen {
     }
 
     fn ensure_registry(&mut self) -> Result<()> {
-        if let State::Tracer(_) = self.state {
+        if let State::Registering(_, _) = self.state {
             // replace the current state with a dummy tracer
             let old_state = mem::replace(
                 &mut self.state,
-                State::Tracer(Tracer::new(TracerConfig::default())),
+                State::Registering(Tracer::new(TracerConfig::default()), Samples::new()),
             );
 
             // convert tracer to registry
-            if let State::Tracer(tracer) = old_state {
+            if let State::Registering(tracer, _) = old_state {
                 // replace dummy with registry
-                self.state = State::Registry(tracer.registry().map_err(|e| anyhow!("{e}"))?);
+                self.state = State::Generating(tracer.registry().map_err(|e| anyhow!("{e}"))?);
             }
         }
         Ok(())
@@ -298,3 +413,37 @@ fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
 
     Ok(())
 }
+
+#[cfg(feature = "typegen")]
+#[cfg(test)]
+mod tests {
+    use crate::typegen::TypeGen;
+    use serde::{Deserialize, Serialize};
+    use uuid::Uuid;
+
+    #[derive(Serialize, Deserialize)]
+    struct MyUuid(Uuid);
+
+    #[test]
+    fn test_typegen_for_uuid_without_samples() {
+        let mut gen = TypeGen::new();
+        let result = gen.register_type::<MyUuid>();
+
+        assert!(
+            result.is_err(),
+            "typegen unexpectedly succeeded for Uuid, without samples"
+        )
+    }
+
+    #[test]
+    fn test_typegen_for_uuid_with_samples() {
+        let sample_data = vec![MyUuid(Uuid::new_v4())];
+        let mut gen = TypeGen::new();
+        let result = gen.register_type_with_samples(sample_data);
+        assert!(result.is_ok(), "typegen failed for Uuid, with samples");
+
+        let sample_data = vec!["a".to_string(), "b".to_string()];
+        let result = gen.register_type_with_samples(sample_data);
+        assert!(result.is_ok(), "typegen failed with second sample data set");
+    }
+}
diff --git a/examples/cat_facts/Cargo.lock b/examples/cat_facts/Cargo.lock
index e0a48ffb..d29afa34 100644
--- a/examples/cat_facts/Cargo.lock
+++ b/examples/cat_facts/Cargo.lock
@@ -2921,6 +2921,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2"
 dependencies = [
  "getrandom 0.2.9",
+ "serde",
  "wasm-bindgen",
 ]
 
diff --git a/examples/counter/Cargo.lock b/examples/counter/Cargo.lock
index 22921131..c042c6f9 100644
--- a/examples/counter/Cargo.lock
+++ b/examples/counter/Cargo.lock
@@ -4319,6 +4319,7 @@ dependencies = [
  "crux_platform",
  "crux_time",
  "shared",
+ "uuid 1.3.2",
 ]
 
 [[package]]
@@ -5538,6 +5539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2"
 dependencies = [
  "getrandom 0.2.9",
+ "serde",
  "wasm-bindgen",
 ]
 
diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml
index eabdad40..c6f6c75e 100644
--- a/examples/counter/Cargo.toml
+++ b/examples/counter/Cargo.toml
@@ -12,3 +12,4 @@ rust-version = "1.66"
 [workspace.dependencies]
 anyhow = "1.0.71"
 serde = "1.0.160"
+uuid = { version = "1.3.1", features = ["v4", "wasm-bindgen", "js", "serde"] }
diff --git a/examples/counter/shared/shared.xcodeproj/xcuserdata/stuartharris.xcuserdatad/xcschemes/xcschememanagement.plist b/examples/counter/shared/shared.xcodeproj/xcuserdata/stuartharris.xcuserdatad/xcschemes/xcschememanagement.plist
index 6d14cbc6..dc65927e 100644
--- a/examples/counter/shared/shared.xcodeproj/xcuserdata/stuartharris.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/examples/counter/shared/shared.xcodeproj/xcuserdata/stuartharris.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,12 +7,12 @@
 		<key>shared-cdylib.xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>0</integer>
+			<integer>3</integer>
 		</dict>
 		<key>shared-staticlib.xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>3</integer>
+			<integer>0</integer>
 		</dict>
 		<key>uniffi-bindgen-bin.xcscheme_^#shared#^_</key>
 		<dict>
diff --git a/examples/counter/shared/src/app.rs b/examples/counter/shared/src/app.rs
index 7a498fb3..32ed7d44 100644
--- a/examples/counter/shared/src/app.rs
+++ b/examples/counter/shared/src/app.rs
@@ -5,6 +5,7 @@ use crux_http::Http;
 use crux_macros::Effect;
 use serde::{Deserialize, Serialize};
 use url::Url;
+use uuid::Uuid;
 
 const API_URL: &str = "https://crux-counter.fly.dev";
 
@@ -27,6 +28,7 @@ pub enum Event {
     Increment,
     Decrement,
     StartWatch,
+    SendUuid(Uuid),
 
     // events local to the core
     #[serde(skip)]
@@ -101,6 +103,9 @@ impl crux_core::App for App {
                 model.confirmed = Some(true);
                 caps.render.render();
             }
+            Event::SendUuid(_uuid) => {
+                // No-op
+            }
         }
     }
 
diff --git a/examples/counter/shared_types/Cargo.toml b/examples/counter/shared_types/Cargo.toml
index b99a5dc7..670dc2ef 100644
--- a/examples/counter/shared_types/Cargo.toml
+++ b/examples/counter/shared_types/Cargo.toml
@@ -20,3 +20,4 @@ crux_kv = { version = "0.1", path = "../../../crux_kv" }
 crux_platform = { version = "0.1", path = "../../../crux_platform" }
 crux_time = { version = "0.1", path = "../../../crux_time" }
 shared = { path = "../shared" }
+uuid = { workspace = true }
diff --git a/examples/counter/shared_types/build.rs b/examples/counter/shared_types/build.rs
index 094767e7..87c155e2 100644
--- a/examples/counter/shared_types/build.rs
+++ b/examples/counter/shared_types/build.rs
@@ -3,6 +3,7 @@ use crux_core::{bridge::Request, typegen::TypeGen};
 use crux_http::protocol::{HttpRequest, HttpResponse};
 use shared::{sse::SseResponse, EffectFfi, Event, ViewModel};
 use std::path::PathBuf;
+use uuid::Uuid;
 
 fn main() {
     println!("cargo:rerun-if-changed=../shared");
@@ -29,7 +30,9 @@ fn register_types(gen: &mut TypeGen) -> Result<()> {
     gen.register_type::<EffectFfi>()?;
     gen.register_type::<HttpRequest>()?;
 
-    gen.register_type::<Event>()?;
+    let sample_events = vec![Event::SendUuid(Uuid::new_v4())];
+    gen.register_type_with_samples(sample_events)?;
+
     gen.register_type::<HttpResponse>()?;
     gen.register_type::<SseResponse>()?;
 
diff --git a/examples/hello_world/Cargo.lock b/examples/hello_world/Cargo.lock
index 0bd36250..dbe6c9f5 100644
--- a/examples/hello_world/Cargo.lock
+++ b/examples/hello_world/Cargo.lock
@@ -1109,6 +1109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2"
 dependencies = [
  "getrandom",
+ "serde",
  "wasm-bindgen",
 ]
 


Test patch:
diff --git a/crux_core/tests/typegen.rs b/crux_core/tests/typegen.rs
new file mode 100644
index 00000000..115a54ce
--- /dev/null
+++ b/crux_core/tests/typegen.rs
@@ -0,0 +1,66 @@
+mod shared {
+    use crux_http::Http;
+    use crux_macros::Effect;
+    use serde::{Deserialize, Serialize};
+
+    #[derive(Default)]
+    pub struct App;
+
+    #[derive(Serialize, Deserialize)]
+    pub enum Event {
+        None,
+        SendUuid(uuid::Uuid),
+    }
+    #[derive(Serialize, Deserialize)]
+    pub struct ViewModel;
+    impl crux_core::App for App {
+        type Event = Event;
+        type Model = ();
+        type ViewModel = ViewModel;
+        type Capabilities = Capabilities;
+        fn update(&self, _event: Event, _model: &mut Self::Model, _caps: &Capabilities) {}
+        fn view(&self, _model: &Self::Model) -> Self::ViewModel {
+            todo!();
+        }
+    }
+
+    #[derive(Effect)]
+    pub struct Capabilities {
+        pub http: Http<Event>,
+    }
+}
+mod test {
+    use super::shared::{EffectFfi, Event, ViewModel};
+    use crux_core::{bridge::Request, typegen::TypeGen};
+    use crux_http::protocol::{HttpRequest, HttpResponse};
+    use std::env::temp_dir;
+    use uuid::Uuid;
+
+    #[test]
+    fn generate_types() {
+        let mut gen = TypeGen::new();
+
+        gen.register_type::<Request<EffectFfi>>().unwrap();
+
+        gen.register_type::<EffectFfi>().unwrap();
+        gen.register_type::<HttpRequest>().unwrap();
+
+        let sample_events = vec![Event::SendUuid(Uuid::new_v4())];
+        gen.register_type_with_samples(sample_events).unwrap();
+
+        gen.register_type::<HttpResponse>().unwrap();
+
+        gen.register_type::<ViewModel>().unwrap();
+
+        let output_root = temp_dir().join("crux_core_typegen_test");
+
+        gen.swift("shared_types", output_root.join("swift"))
+            .expect("swift type gen failed");
+
+        gen.java("com.example.counter.shared_types", output_root.join("java"))
+            .expect("java type gen failed");
+
+        gen.typescript("shared_types", output_root.join("typescript"))
+            .expect("typescript type gen failed");
+    }
+}