diff --git a/Cargo.lock b/Cargo.lock index 260cb6f9..d8737057 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,6 +291,12 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + [[package]] name = "bitflags" version = "1.3.2" @@ -2005,6 +2011,39 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "logos" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7251356ef8cb7aec833ddf598c6cb24d17b689d20b993f9d11a3d764e34e6458" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f80069600c0d66734f5ff52cc42f2dabd6b29d205f333d61fd7832e9e9963f" +dependencies = [ + "beef", + "fnv", + "lazy_static", + "proc-macro2", + "quote", + "regex-syntax", + "syn", +] + +[[package]] +name = "logos-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24fb722b06a9dc12adb0963ed585f19fc61dc5413e6a9be9422ef92c091e731d" +dependencies = [ + "logos-codegen", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -4231,6 +4270,18 @@ dependencies = [ "utf8-tokio", ] +[[package]] +name = "wasm-wave" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd23c879ec35d708f4eff3456f33c415d113363a2e38420098bf42976bacb31" +dependencies = [ + "indexmap", + "logos", + "thiserror 2.0.17", + "wit-parser 0.243.0", +] + [[package]] name = "wasmparser" version = "0.220.1" @@ -5384,6 +5435,24 @@ dependencies = [ "wasmparser 0.240.0", ] +[[package]] +name = "wit-parser" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df983a8608e513d8997f435bb74207bf0933d0e49ca97aa9d8a6157164b9b7fc" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.243.0", +] + [[package]] name = "writeable" version = "0.6.2" @@ -5613,6 +5682,23 @@ dependencies = [ "wrpc-transport-nats", ] +[[package]] +name = "wrpc-wave" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "futures", + "tokio", + "tokio-util", + "tracing", + "wasm-tokio", + "wasm-wave", + "wit-bindgen-wrpc", + "wrpc-pack", + "wrpc-transport", +] + [[package]] name = "wtransport" version = "0.6.1" diff --git a/crates/wave/Cargo.toml b/crates/wave/Cargo.toml new file mode 100644 index 00000000..1487b486 --- /dev/null +++ b/crates/wave/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "wrpc-wave" +version = "0.1.0" +description = "wRPC runtime-agnostic encoding and decoding support for wasm-wave traits" + +authors.workspace = true +categories.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true + +[features] +default = ["sync", "pack"] +sync = ["dep:futures"] +pack = ["dep:wrpc-pack"] + +[dependencies] +anyhow = { workspace = true, features = ["std"] } +bytes = { workspace = true } +futures = { workspace = true, features = ["executor"], optional = true } +tokio = { workspace = true, features = ["io-util"] } +tokio-util = { workspace = true, features = ["codec"] } +tracing = { workspace = true, features = ["attributes"] } +wasm-tokio = { workspace = true } +wasm-wave = "0.243" +wrpc-pack = { path = "../pack", optional = true } +wrpc-transport = { workspace = true } + +[dev-dependencies] +futures = { workspace = true, features = ["executor"] } +tokio = { workspace = true, features = ["net"] } +wit-bindgen-wrpc = { workspace = true } diff --git a/crates/wave/src/core/decode.rs b/crates/wave/src/core/decode.rs new file mode 100644 index 00000000..7fbb95e2 --- /dev/null +++ b/crates/wave/src/core/decode.rs @@ -0,0 +1,599 @@ +//! Async value decoder for wasm-wave types +//! +//! This module provides `read_value`, which mirrors `wrpc_runtime_wasmtime::codec::read_value` +//! but works with `wasm_wave::value::Value` instead of `wasmtime::component::Val`. +//! +//! ## Design +//! +//! This decoder uses async reads that block until complete data is available +//! +//! ## Usage +//! +//! ```no_run +//! use wrpc_wave::read_value; +//! use wasm_wave::value::Type; +//! use tokio::io::AsyncRead; +//! +//! # async fn example(reader: &mut R) -> std::io::Result<()> { +//! let mut pinned = std::pin::pin!(reader); +//! let value = read_value(&mut pinned, &wasm_wave::value::Type::U32).await?; +//! # Ok(()) +//! # } +//! ``` + +use std::borrow::Cow; +use std::pin::Pin; + +use tokio::io::{AsyncRead, AsyncReadExt as _}; +use wasm_tokio::{ + cm::AsyncReadValue as _, AsyncReadCore as _, AsyncReadLeb128 as _, AsyncReadUtf8 as _, +}; +use wasm_wave::value::{Type, Value}; +use wasm_wave::wasm::{WasmType, WasmTypeKind, WasmValue as _}; + +/// Read a wasm-wave `Value` from an async reader +/// +/// This mirrors `wrpc_runtime_wasmtime::codec::read_value` +/// but without the `Index` trait bound and `path` parameter, +/// since wasm-wave doesn't support resources, async or streams +/// +/// # Errors +/// +/// - `UnexpectedEof` - Not enough data in stream +/// - `InvalidData` - Malformed encoding (invalid UTF-8, discriminant, etc.) +/// - Other `io::Error` from underlying reader +#[allow(clippy::too_many_lines)] +pub async fn read_value(r: &mut Pin<&mut R>, ty: &Type) -> std::io::Result +where + R: AsyncRead + Unpin, +{ + match ty.kind() { + WasmTypeKind::Bool => { + let v = r.read_bool().await?; + Ok(Value::make_bool(v)) + } + WasmTypeKind::S8 => { + let v = r.read_i8().await?; + Ok(Value::make_s8(v)) + } + WasmTypeKind::U8 => { + let v = r.read_u8().await?; + Ok(Value::make_u8(v)) + } + WasmTypeKind::S16 => { + let v = r.read_i16_leb128().await?; + Ok(Value::make_s16(v)) + } + WasmTypeKind::U16 => { + let v = r.read_u16_leb128().await?; + Ok(Value::make_u16(v)) + } + WasmTypeKind::S32 => { + let v = r.read_i32_leb128().await?; + Ok(Value::make_s32(v)) + } + WasmTypeKind::U32 => { + let v = r.read_u32_leb128().await?; + Ok(Value::make_u32(v)) + } + WasmTypeKind::S64 => { + let v = r.read_i64_leb128().await?; + Ok(Value::make_s64(v)) + } + WasmTypeKind::U64 => { + let v = r.read_u64_leb128().await?; + Ok(Value::make_u64(v)) + } + WasmTypeKind::F32 => { + let v = r.read_f32_le().await?; + Ok(Value::make_f32(v)) + } + WasmTypeKind::F64 => { + let v = r.read_f64_le().await?; + Ok(Value::make_f64(v)) + } + WasmTypeKind::Char => { + let v = r.read_char_utf8().await?; + Ok(Value::make_char(v)) + } + WasmTypeKind::String => { + let mut s = String::default(); + r.read_core_name(&mut s).await?; + Ok(Value::make_string(Cow::Owned(s))) + } + WasmTypeKind::List => { + let n = r.read_u32_leb128().await?; + let n = n.try_into().unwrap_or(usize::MAX); + let element_type = ty + .list_element_type() + .ok_or_else(|| io_error("list type missing element type"))?; + + let mut elements = Vec::with_capacity(n); + for _ in 0..n { + let element = Box::pin(read_value(r, &element_type)).await?; + elements.push(element); + } + + Value::make_list(ty, elements).map_err(io_error) + } + WasmTypeKind::Record => { + let field_types: Vec<_> = ty.record_fields().collect(); + let mut field_names = Vec::with_capacity(field_types.len()); + let mut field_values = Vec::with_capacity(field_types.len()); + + for (name, field_type) in field_types { + let value = Box::pin(read_value(r, &field_type)).await?; + field_names.push(name.to_string()); + field_values.push(value); + } + + // Create the fields iterator from owned strings + let fields = field_names + .iter() + .map(|s| s.as_str()) + .zip(field_values.into_iter()); + + Value::make_record(ty, fields).map_err(io_error) + } + WasmTypeKind::Tuple => { + let element_types: Vec<_> = ty.tuple_element_types().collect(); + let mut elements = Vec::with_capacity(element_types.len()); + + for element_type in element_types { + let element = Box::pin(read_value(r, &element_type)).await?; + elements.push(element); + } + + Value::make_tuple(ty, elements).map_err(io_error) + } + WasmTypeKind::Variant => { + let discriminant = r.read_u32_leb128().await?; + let cases: Vec<_> = ty.variant_cases().collect(); + + let discriminant_idx: usize = discriminant + .try_into() + .map_err(|_| io_error("variant discriminant too large"))?; + + let (case_name, payload_type) = cases.get(discriminant_idx).ok_or_else(|| { + io_error(format!("unknown variant discriminant: {}", discriminant)) + })?; + + let payload = if let Some(payload_type) = payload_type { + let value = Box::pin(read_value(r, payload_type)).await?; + Some(value) + } else { + None + }; + + Value::make_variant(ty, case_name, payload).map_err(io_error) + } + WasmTypeKind::Enum => { + let discriminant = r.read_u32_leb128().await?; + let names: Vec<_> = ty.enum_cases().collect(); + + let discriminant_idx: usize = discriminant + .try_into() + .map_err(|_| io_error("enum discriminant too large"))?; + + let name = names + .get(discriminant_idx) + .ok_or_else(|| io_error(format!("unknown enum discriminant: {}", discriminant)))?; + + Value::make_enum(ty, name).map_err(io_error) + } + WasmTypeKind::Option => { + let ok = r.read_u8().await?; + + if ok != 0 { + let inner_type = ty + .option_some_type() + .ok_or_else(|| io_error("option type missing some type"))?; + let value = Box::pin(read_value(r, &inner_type)).await?; + Value::make_option(ty, Some(value)).map_err(io_error) + } else { + Value::make_option(ty, None).map_err(io_error) + } + } + WasmTypeKind::Result => { + let ok = r.read_u8().await?; + let (ok_type, err_type) = ty + .result_types() + .ok_or_else(|| io_error("result type missing ok/err types"))?; + + if ok == 0 { + // Ok variant + if let Some(ok_ty) = ok_type { + let value = Box::pin(read_value(r, &ok_ty)).await?; + Value::make_result(ty, Ok(Some(value))) + } else { + Value::make_result(ty, Ok(None)) + } + } else if ok == 1 { + // Err variant + if let Some(err_ty) = err_type { + let value = Box::pin(read_value(r, &err_ty)).await?; + Value::make_result(ty, Err(Some(value))) + } else { + Value::make_result(ty, Err(None)) + } + } else { + return Err(io_error(format!("invalid result discriminant: {}", ok))); + } + .map_err(io_error) + } + WasmTypeKind::Flags => { + let names: Vec<_> = ty.flags_names().collect(); + let byte_count = names.len().div_ceil(8); + + let mut buf = vec![0u8; byte_count]; + r.read_exact(&mut buf).await?; + + let mut flag_names = Vec::new(); + for (i, name) in names.iter().enumerate() { + if buf[i / 8] & (1 << (i % 8)) != 0 { + flag_names.push(name.as_ref()); + } + } + + Value::make_flags(ty, flag_names).map_err(io_error) + } + WasmTypeKind::Unsupported => Err(io_error("unsupported value type")), + _ => Err(io_error(format!("unsupported value type: {:?}", ty.kind()))), + } +} + +// Helper to convert any error to io::Error with InvalidData kind +fn io_error(err: impl std::fmt::Display) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::InvalidData, err.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use wasm_wave::value::Type; + + // Test helper - uses futures::executor (no tokio runtime needed in tests) + fn decode_sync(ty: &Type, data: &[u8]) -> std::io::Result { + let mut cursor = std::io::Cursor::new(data); + futures::executor::block_on(async { + let mut pinned = std::pin::pin!(&mut cursor); + read_value(&mut pinned, ty).await + }) + } + + // Bool types + #[test] + fn test_decode_bool_true() -> anyhow::Result<()> { + let value = decode_sync(&Type::BOOL, &[1u8])?; + assert_eq!(value.unwrap_bool(), true); + Ok(()) + } + + #[test] + fn test_decode_bool_false() -> anyhow::Result<()> { + let value = decode_sync(&Type::BOOL, &[0u8])?; + assert_eq!(value.unwrap_bool(), false); + Ok(()) + } + + // Integer types + #[test] + fn test_decode_s8_positive() -> anyhow::Result<()> { + let value = decode_sync(&Type::S8, &[42u8])?; + assert_eq!(value.unwrap_s8(), 42); + Ok(()) + } + + #[test] + fn test_decode_s8_negative() -> anyhow::Result<()> { + let value = decode_sync(&Type::S8, &[(-42i8) as u8])?; + assert_eq!(value.unwrap_s8(), -42); + Ok(()) + } + + #[test] + fn test_decode_u8() -> anyhow::Result<()> { + let value = decode_sync(&Type::U8, &[255u8])?; + assert_eq!(value.unwrap_u8(), 255); + Ok(()) + } + + #[test] + fn test_decode_s16() -> anyhow::Result<()> { + let value = decode_sync(&Type::S16, &[0xE8, 0x07])?; + assert_eq!(value.unwrap_s16(), 1000); + Ok(()) + } + + #[test] + fn test_decode_u16() -> anyhow::Result<()> { + let value = decode_sync(&Type::U16, &[0xE8, 0x07])?; + assert_eq!(value.unwrap_u16(), 1000); + Ok(()) + } + + #[test] + fn test_decode_s32() -> anyhow::Result<()> { + let value = decode_sync(&Type::S32, &[0xA0, 0x8D, 0x06])?; + assert_eq!(value.unwrap_s32(), 100000); + Ok(()) + } + + #[test] + fn test_decode_u32() -> anyhow::Result<()> { + let value = decode_sync(&Type::U32, &[42u8])?; + assert_eq!(value.unwrap_u32(), 42); + Ok(()) + } + + #[test] + fn test_decode_s64() -> anyhow::Result<()> { + let value = decode_sync(&Type::S64, &[0x7F])?; + assert_eq!(value.unwrap_s64(), -1); + Ok(()) + } + + #[test] + fn test_decode_u64() -> anyhow::Result<()> { + let value = decode_sync(&Type::U64, &[0xB9, 0x60])?; + assert_eq!(value.unwrap_u64(), 12345); + Ok(()) + } + + // Float types + #[test] + fn test_decode_f32() -> anyhow::Result<()> { + let value = decode_sync(&Type::F32, &3.14f32.to_le_bytes())?; + assert!((value.unwrap_f32() - 3.14).abs() < 0.01); + Ok(()) + } + + #[test] + fn test_decode_f64() -> anyhow::Result<()> { + let value = decode_sync(&Type::F64, &3.14159265359f64.to_le_bytes())?; + assert!((value.unwrap_f64() - 3.14159265359).abs() < 0.00000001); + Ok(()) + } + + // Char types + #[test] + fn test_decode_char() -> anyhow::Result<()> { + let value = decode_sync(&Type::CHAR, &[0x41])?; + assert_eq!(value.unwrap_char(), 'A'); + Ok(()) + } + + #[test] + fn test_decode_char_unicode() -> anyhow::Result<()> { + let value = decode_sync(&Type::CHAR, &[0xF0, 0x9F, 0x98, 0x80])?; + assert_eq!(value.unwrap_char(), '😀'); + Ok(()) + } + + // String types + #[test] + fn test_decode_string() -> anyhow::Result<()> { + let value = decode_sync(&Type::STRING, &[5, b'h', b'e', b'l', b'l', b'o'])?; + assert_eq!(value.unwrap_string().as_ref(), "hello"); + Ok(()) + } + + // List types + #[test] + fn test_decode_list_empty() -> anyhow::Result<()> { + let list_type = Type::list(Type::U32); + let value = decode_sync(&list_type, &[0u8])?; + let list: Vec<_> = value.unwrap_list().collect(); + assert_eq!(list.len(), 0); + Ok(()) + } + + #[test] + fn test_decode_list_u32() -> anyhow::Result<()> { + let list_type = Type::list(Type::U32); + let value = decode_sync(&list_type, &[3, 1, 2, 3])?; + let list: Vec<_> = value.unwrap_list().collect(); + assert_eq!(list.len(), 3); + assert_eq!(list[0].unwrap_u32(), 1); + assert_eq!(list[1].unwrap_u32(), 2); + assert_eq!(list[2].unwrap_u32(), 3); + Ok(()) + } + + // Record types + #[test] + fn test_decode_record() -> anyhow::Result<()> { + use anyhow::Context as _; + let record_type = Type::record([("x", Type::U32), ("y", Type::U32)]) + .context("failed to create record type")?; + let value = decode_sync(&record_type, &[10, 20])?; + let fields: Vec<_> = value.unwrap_record().collect(); + assert_eq!(fields.len(), 2); + assert_eq!(fields[0].0.as_ref(), "x"); + assert_eq!(fields[0].1.unwrap_u32(), 10); + assert_eq!(fields[1].0.as_ref(), "y"); + assert_eq!(fields[1].1.unwrap_u32(), 20); + Ok(()) + } + + // Tuple types + #[test] + fn test_decode_tuple() -> anyhow::Result<()> { + use anyhow::Context as _; + let tuple_type = + Type::tuple([Type::U32, Type::BOOL]).context("failed to create tuple type")?; + let value = decode_sync(&tuple_type, &[42, 1])?; + let elements: Vec<_> = value.unwrap_tuple().collect(); + assert_eq!(elements.len(), 2); + assert_eq!(elements[0].unwrap_u32(), 42); + assert_eq!(elements[1].unwrap_bool(), true); + Ok(()) + } + + // Variant types + #[test] + fn test_decode_variant_no_payload() -> anyhow::Result<()> { + use anyhow::Context as _; + let variant_type = Type::variant([("none", None), ("some", Some(Type::U32))]) + .context("failed to create variant type")?; + let value = decode_sync(&variant_type, &[0])?; + let (case, payload) = value.unwrap_variant(); + assert_eq!(case.as_ref(), "none"); + assert!(payload.is_none()); + Ok(()) + } + + #[test] + fn test_decode_variant_with_payload() -> anyhow::Result<()> { + use anyhow::Context as _; + let variant_type = Type::variant([("none", None), ("some", Some(Type::U32))]) + .context("failed to create variant type")?; + let value = decode_sync(&variant_type, &[1, 42])?; + let (case, payload) = value.unwrap_variant(); + assert_eq!(case.as_ref(), "some"); + assert_eq!(payload.unwrap().unwrap_u32(), 42); + Ok(()) + } + + // Enum types + #[test] + fn test_decode_enum() -> anyhow::Result<()> { + use anyhow::Context as _; + let enum_type = + Type::enum_ty(["red", "green", "blue"]).context("failed to create enum type")?; + let value = decode_sync(&enum_type, &[1])?; + assert_eq!(value.unwrap_enum().as_ref(), "green"); + Ok(()) + } + + // Option types + #[test] + fn test_decode_option_none() -> anyhow::Result<()> { + let option_type = Type::option(Type::U32); + let value = decode_sync(&option_type, &[0u8])?; + assert!(value.unwrap_option().is_none()); + Ok(()) + } + + #[test] + fn test_decode_option_some() -> anyhow::Result<()> { + let option_type = Type::option(Type::U32); + let value = decode_sync(&option_type, &[1, 42])?; + assert_eq!(value.unwrap_option().unwrap().unwrap_u32(), 42); + Ok(()) + } + + // Result types + #[test] + fn test_decode_result_ok_with_value() -> anyhow::Result<()> { + let result_type = Type::result(Some(Type::U32), Some(Type::STRING)); + let value = decode_sync(&result_type, &[0, 42])?; + let result = value.unwrap_result(); + assert!(result.is_ok()); + assert_eq!(result.unwrap().unwrap().unwrap_u32(), 42); + Ok(()) + } + + #[test] + fn test_decode_result_ok_no_value() -> anyhow::Result<()> { + let result_type = Type::result(None, Some(Type::STRING)); + let value = decode_sync(&result_type, &[0])?; + let result = value.unwrap_result(); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); + Ok(()) + } + + #[test] + fn test_decode_result_err_with_value() -> anyhow::Result<()> { + let result_type = Type::result(Some(Type::U32), Some(Type::STRING)); + let value = decode_sync(&result_type, &[1, 4, b'f', b'a', b'i', b'l'])?; + let result = value.unwrap_result(); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().unwrap().unwrap_string().as_ref(), + "fail" + ); + Ok(()) + } + + #[test] + fn test_decode_result_err_no_value() -> anyhow::Result<()> { + let result_type = Type::result(Some(Type::U32), None); + let value = decode_sync(&result_type, &[1])?; + let result = value.unwrap_result(); + assert!(result.is_err()); + assert!(result.unwrap_err().is_none()); + Ok(()) + } + + // Flags types + #[test] + fn test_decode_flags_none_set() -> anyhow::Result<()> { + use anyhow::Context as _; + use std::collections::HashSet; + let flags_type = Type::flags(["read", "write", "execute", "append"]) + .context("failed to create flags type")?; + let value = decode_sync(&flags_type, &[0x00])?; + let flags: HashSet<_> = value.unwrap_flags().map(|s| s.into_owned()).collect(); + assert_eq!(flags.len(), 0); + Ok(()) + } + + #[test] + fn test_decode_flags_single_byte() -> anyhow::Result<()> { + use anyhow::Context as _; + use std::collections::HashSet; + let flags_type = Type::flags(["read", "write", "execute", "append"]) + .context("failed to create flags type")?; + let value = decode_sync(&flags_type, &[0x05])?; + let flags: HashSet<_> = value.unwrap_flags().map(|s| s.into_owned()).collect(); + assert_eq!(flags.len(), 2); + assert!(flags.contains("read")); + assert!(flags.contains("execute")); + Ok(()) + } + + #[test] + fn test_decode_flags_two_bytes() -> anyhow::Result<()> { + use anyhow::Context as _; + use std::collections::HashSet; + let flags_type = Type::flags([ + "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", + ]) + .context("failed to create flags type")?; + let value = decode_sync(&flags_type, &[0x01, 0x02])?; + let flags: HashSet<_> = value.unwrap_flags().map(|s| s.into_owned()).collect(); + assert_eq!(flags.len(), 2); + assert!(flags.contains("f0")); + assert!(flags.contains("f9")); + Ok(()) + } + + #[test] + fn test_decode_flags_three_bytes() -> anyhow::Result<()> { + use anyhow::Context as _; + use std::collections::HashSet; + let flags_type = Type::flags([ + "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "f13", + "f14", "f15", "f16", "f17", "f18", "f19", + ]) + .context("failed to create flags type")?; + let value = decode_sync(&flags_type, &[0x01, 0x01, 0x01])?; + let flags: HashSet<_> = value.unwrap_flags().map(|s| s.into_owned()).collect(); + assert_eq!(flags.len(), 3); + assert!(flags.contains("f0")); + assert!(flags.contains("f8")); + assert!(flags.contains("f16")); + Ok(()) + } + + // Error cases + #[test] + fn test_incomplete_data_eof() { + let result = decode_sync(&Type::U32, &[]); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof); + } +} diff --git a/crates/wave/src/core/encode.rs b/crates/wave/src/core/encode.rs new file mode 100644 index 00000000..7fbfe6e2 --- /dev/null +++ b/crates/wave/src/core/encode.rs @@ -0,0 +1,973 @@ +//! Value encoder for wasm-wave traits that works similarly to `ValEncoder` + +use core::iter::zip; +use core::ops::{BitOrAssign, Shl}; + +use std::collections::HashSet; + +use anyhow::{bail, Context as _}; +use bytes::{BufMut as _, BytesMut}; +use tokio_util::codec::Encoder; +use tracing::instrument; +use wasm_tokio::{CoreNameEncoder, Leb128Encoder, Utf8Codec}; +use wasm_wave::wasm::{WasmType, WasmTypeKind, WasmValue}; + +/// Encoder for wasm-wave values with type information stored in the encoder. +/// This mirrors the `ValEncoder` from `wrpc-runtime-wasmtime`. +/// +/// ## Usage +/// +/// ```no_run +/// use wrpc_wave::WaveEncoder; +/// use wasm_wave::value::{Value, Type}; +/// use wasm_wave::wasm::WasmValue; +/// use bytes::BytesMut; +/// use wit_bindgen_wrpc::tokio_util::codec::Encoder; +/// +/// let value = Value::make_u32(42); +/// let ty = Type::U32; +/// let mut encoder = WaveEncoder::new(&ty); +/// let mut buf = BytesMut::new(); +/// encoder.encode(&value, &mut buf).unwrap(); +/// ``` +/// +/// Note: +/// this crate is runtime-agnostic, which means it has no access to a "store" +/// This means it cannot handle resources or any async encoding +#[derive(Debug)] +pub struct WaveEncoder<'a, T: WasmType> { + /// The type information for encoding + pub ty: &'a T, +} + +impl<'a, T: WasmType> WaveEncoder<'a, T> { + /// Creates a new `WaveEncoder` with the given type information. + #[must_use] + pub fn new(ty: &'a T) -> Self { + Self { ty } + } + + /// Creates a new encoder with a different type, reusing the current encoder's context. + /// This is useful for encoding nested values with different types. + #[must_use] + pub fn with_type<'b>(&'b mut self, ty: &'b T) -> WaveEncoder<'b, T> { + WaveEncoder { ty } + } +} +fn find_enum_discriminant<'a, T>( + iter: impl IntoIterator, + names: impl IntoIterator, + discriminant: &str, +) -> anyhow::Result { + zip(iter, names) + .find_map(|(i, name)| (name == discriminant).then_some(i)) + .context("unknown enum discriminant") +} + +fn find_variant_discriminant<'a, T>( + iter: impl IntoIterator, + cases: impl IntoIterator)>, + discriminant: &str, +) -> anyhow::Result<(T, Option<&'a str>)> { + zip(iter, cases) + .find_map(|(i, (name, ty))| (name == discriminant).then_some((i, ty))) + .context("unknown variant discriminant") +} + +#[inline] +fn flag_bits<'a, T: BitOrAssign + Shl + From>( + names: impl IntoIterator, + flags: impl IntoIterator, +) -> T { + let mut v = T::from(0); + let flags: HashSet<&str> = flags.into_iter().collect(); + for (i, name) in zip(0u8.., names) { + if flags.contains(name) { + v |= T::from(1) << i; + } + } + v +} + +// Generic implementation for any type implementing WasmValue and WasmType +impl<'a, V, T> Encoder<&V> for WaveEncoder<'a, T> +where + V: WasmValue, + T: WasmType, +{ + type Error = anyhow::Error; + + #[allow(clippy::too_many_lines)] + #[instrument(level = "trace", skip(self, val))] + fn encode(&mut self, val: &V, dst: &mut BytesMut) -> Result<(), Self::Error> { + match val.kind() { + WasmTypeKind::Bool => { + dst.reserve(1); + dst.put_u8(val.unwrap_bool().into()); + Ok(()) + } + WasmTypeKind::S8 => { + dst.reserve(1); + dst.put_i8(val.unwrap_s8()); + Ok(()) + } + WasmTypeKind::U8 => { + dst.reserve(1); + dst.put_u8(val.unwrap_u8()); + Ok(()) + } + WasmTypeKind::S16 => Leb128Encoder + .encode(val.unwrap_s16(), dst) + .context("failed to encode s16"), + WasmTypeKind::U16 => Leb128Encoder + .encode(val.unwrap_u16(), dst) + .context("failed to encode u16"), + WasmTypeKind::S32 => Leb128Encoder + .encode(val.unwrap_s32(), dst) + .context("failed to encode s32"), + WasmTypeKind::U32 => Leb128Encoder + .encode(val.unwrap_u32(), dst) + .context("failed to encode u32"), + WasmTypeKind::S64 => Leb128Encoder + .encode(val.unwrap_s64(), dst) + .context("failed to encode s64"), + WasmTypeKind::U64 => Leb128Encoder + .encode(val.unwrap_u64(), dst) + .context("failed to encode u64"), + WasmTypeKind::F32 => { + dst.reserve(4); + dst.put_f32_le(val.unwrap_f32()); + Ok(()) + } + WasmTypeKind::F64 => { + dst.reserve(8); + dst.put_f64_le(val.unwrap_f64()); + Ok(()) + } + WasmTypeKind::Char => Utf8Codec + .encode(val.unwrap_char(), dst) + .context("failed to encode char"), + WasmTypeKind::String => CoreNameEncoder + .encode(val.unwrap_string().as_ref(), dst) + .context("failed to encode string"), + WasmTypeKind::List => { + let elements: Vec<_> = val.unwrap_list().map(|v| v.into_owned()).collect(); + let n = u32::try_from(elements.len()).context("list length does not fit in u32")?; + dst.reserve(5 + elements.len()); + Leb128Encoder + .encode(n, dst) + .context("failed to encode list length")?; + let element_type = self + .ty + .list_element_type() + .context("list type should have element type")?; + for element in elements { + let mut enc = self.with_type(&element_type); + enc.encode(&element, dst) + .context("failed to encode list element")?; + } + Ok(()) + } + WasmTypeKind::Record => { + let fields: Vec<_> = val + .unwrap_record() + .map(|(name, v)| (name.into_owned(), v.into_owned())) + .collect(); + let field_types: Vec<_> = self.ty.record_fields().map(|(_, ty)| ty).collect(); + dst.reserve(fields.len()); + for ((_name, field_value), field_type) in fields.iter().zip(field_types.iter()) { + let mut enc = self.with_type(field_type); + enc.encode(field_value, dst) + .context("failed to encode record field")?; + } + Ok(()) + } + WasmTypeKind::Tuple => { + let elements: Vec<_> = val.unwrap_tuple().map(|v| v.into_owned()).collect(); + let element_types: Vec<_> = self.ty.tuple_element_types().collect(); + dst.reserve(elements.len()); + for (element, element_type) in elements.iter().zip(element_types.iter()) { + let mut enc = self.with_type(element_type); + enc.encode(element, dst) + .context("failed to encode tuple element")?; + } + Ok(()) + } + WasmTypeKind::Variant => { + let (case_name, payload) = val.unwrap_variant(); + let case_name = case_name.into_owned(); + + // Get the type to find the discriminant index + let cases: Vec<_> = self + .ty + .variant_cases() + .map(|(name, payload_ty)| (name.into_owned(), payload_ty)) + .collect(); + + let (discriminant_idx, _case_ty) = find_variant_discriminant( + 0u32.., + cases + .iter() + .map(|(name, _ty)| (name.as_str(), None::<&str>)), + case_name.as_str(), + )?; + + match cases.len() { + ..=0x0000_00ff => { + dst.reserve(2 + usize::from(payload.is_some())); + Leb128Encoder.encode(discriminant_idx as u8, dst)?; + } + 0x0000_0100..=0x0000_ffff => { + dst.reserve(3 + usize::from(payload.is_some())); + Leb128Encoder.encode(discriminant_idx as u16, dst)?; + } + 0x0001_0000..=0x00ff_ffff => { + dst.reserve(4 + usize::from(payload.is_some())); + Leb128Encoder.encode(discriminant_idx, dst)?; + } + 0x0100_0000..=0xffff_ffff => { + dst.reserve(5 + usize::from(payload.is_some())); + Leb128Encoder.encode(discriminant_idx, dst)?; + } + _ => bail!("case count does not fit in u32"), + } + + if let Some(payload_val) = payload { + // Find the payload type for this variant case + let payload_type = self + .ty + .variant_cases() + .find_map(|(name, payload_ty)| (name == case_name).then_some(payload_ty)) + .flatten() + .context("variant case should have payload type")?; + let mut enc = self.with_type(&payload_type); + enc.encode(&*payload_val, dst) + .context("failed to encode variant payload")?; + } + Ok(()) + } + WasmTypeKind::Enum => { + let case_name = val.unwrap_enum().into_owned(); + + // Get the type to find the discriminant index + let names: Vec<_> = self.ty.enum_cases().map(|s| s.into_owned()).collect(); + + let discriminant_idx = find_enum_discriminant( + 0u32.., + names.iter().map(|s| s.as_str()), + case_name.as_str(), + )?; + + match names.len() { + ..=0x0000_00ff => { + dst.reserve(2); + Leb128Encoder.encode(discriminant_idx as u8, dst)?; + } + 0x0000_0100..=0x0000_ffff => { + dst.reserve(3); + Leb128Encoder.encode(discriminant_idx as u16, dst)?; + } + 0x0001_0000..=0x00ff_ffff => { + dst.reserve(4); + Leb128Encoder.encode(discriminant_idx, dst)?; + } + 0x0100_0000..=0xffff_ffff => { + dst.reserve(5); + Leb128Encoder.encode(discriminant_idx, dst)?; + } + _ => bail!("name count does not fit in u32"), + } + Ok(()) + } + WasmTypeKind::Option => match val.unwrap_option() { + None => { + dst.reserve(1); + dst.put_u8(0); + Ok(()) + } + Some(inner) => { + dst.reserve(2); + dst.put_u8(1); + let inner_type = self + .ty + .option_some_type() + .context("option type should have some type")?; + let mut enc = self.with_type(&inner_type); + enc.encode(&*inner, dst) + .context("failed to encode `option::some` value")?; + Ok(()) + } + }, + WasmTypeKind::Result => { + let (ok_type, err_type) = self + .ty + .result_types() + .context("result type should have ok and err types")?; + match val.unwrap_result() { + Ok(ok_val) => { + match ok_val { + Some(val) => { + dst.reserve(2); + dst.put_u8(0); + if let Some(ok_ty) = ok_type { + let mut enc = self.with_type(&ok_ty); + enc.encode(&*val, dst) + .context("failed to encode `result::ok` value")?; + } + } + None => { + dst.reserve(1); + dst.put_u8(0); + } + } + Ok(()) + } + Err(err_val) => { + match err_val { + Some(val) => { + dst.reserve(2); + dst.put_u8(1); + if let Some(err_ty) = err_type { + let mut enc = self.with_type(&err_ty); + enc.encode(&*val, dst) + .context("failed to encode `result::err` value")?; + } + } + None => { + dst.reserve(1); + dst.put_u8(1); + } + } + Ok(()) + } + } + } + WasmTypeKind::Flags => { + let flag_names: Vec<_> = val.unwrap_flags().map(|s| s.into_owned()).collect(); + + // Get the type to know all possible flag names for bit encoding + let all_names: Vec<_> = self.ty.flags_names().map(|s| s.into_owned()).collect(); + + let flags_set: HashSet<&str> = flag_names.iter().map(|s| s.as_str()).collect(); + let vs = flag_names.iter().map(String::as_str); + + match all_names.len() { + ..=8 => { + dst.reserve(1); + dst.put_u8(flag_bits(all_names.iter().map(|s| s.as_str()), vs)); + } + 9..=16 => { + dst.reserve(2); + dst.put_u16_le(flag_bits(all_names.iter().map(|s| s.as_str()), vs)); + } + 17..=24 => { + dst.reserve(3); + dst.put_slice( + &u32::to_le_bytes(flag_bits(all_names.iter().map(|s| s.as_str()), vs)) + [..3], + ); + } + 25..=32 => { + dst.reserve(4); + dst.put_u32_le(flag_bits(all_names.iter().map(|s| s.as_str()), vs)); + } + 33..=40 => { + dst.reserve(5); + dst.put_slice( + &u64::to_le_bytes(flag_bits(all_names.iter().map(|s| s.as_str()), vs)) + [..5], + ); + } + 41..=48 => { + dst.reserve(6); + dst.put_slice( + &u64::to_le_bytes(flag_bits(all_names.iter().map(|s| s.as_str()), vs)) + [..6], + ); + } + 49..=56 => { + dst.reserve(7); + dst.put_slice( + &u64::to_le_bytes(flag_bits(all_names.iter().map(|s| s.as_str()), vs)) + [..7], + ); + } + 57..=64 => { + dst.reserve(8); + dst.put_u64_le(flag_bits(all_names.iter().map(|s| s.as_str()), vs)); + } + 65..=72 => { + dst.reserve(9); + dst.put_slice( + &u128::to_le_bytes(flag_bits(all_names.iter().map(|s| s.as_str()), vs)) + [..9], + ); + } + 73..=80 => { + dst.reserve(10); + dst.put_slice( + &u128::to_le_bytes(flag_bits(all_names.iter().map(|s| s.as_str()), vs)) + [..10], + ); + } + 81..=88 => { + dst.reserve(11); + dst.put_slice( + &u128::to_le_bytes(flag_bits(all_names.iter().map(|s| s.as_str()), vs)) + [..11], + ); + } + 89..=96 => { + dst.reserve(12); + dst.put_slice( + &u128::to_le_bytes(flag_bits(all_names.iter().map(|s| s.as_str()), vs)) + [..12], + ); + } + 97..=104 => { + dst.reserve(13); + dst.put_slice( + &u128::to_le_bytes(flag_bits(all_names.iter().map(|s| s.as_str()), vs)) + [..13], + ); + } + 105..=112 => { + dst.reserve(14); + dst.put_slice( + &u128::to_le_bytes(flag_bits(all_names.iter().map(|s| s.as_str()), vs)) + [..14], + ); + } + 113..=120 => { + dst.reserve(15); + dst.put_slice( + &u128::to_le_bytes(flag_bits(all_names.iter().map(|s| s.as_str()), vs)) + [..15], + ); + } + 121..=128 => { + dst.reserve(16); + dst.put_u128_le(flag_bits(all_names.iter().map(|s| s.as_str()), vs)); + } + bits @ 129.. => { + let mut cap = bits / 8; + if bits % 8 != 0 { + cap = cap.saturating_add(1); + } + let mut buf = vec![0; cap]; + for (i, name) in all_names.iter().enumerate() { + if flags_set.contains(name.as_str()) { + buf[i / 8] |= 1 << (i % 8); + } + } + dst.extend_from_slice(&buf); + } + } + Ok(()) + } + WasmTypeKind::Unsupported => { + bail!("unsupported value type") + } + _ => bail!("unsupported value type: {:?}", val.kind()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bytes::BytesMut; + use wasm_wave::value::{Type, Value}; + + #[test] + fn test_encode_bool_true() -> anyhow::Result<()> { + let value = Value::make_bool(true); + let ty = Type::BOOL; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + assert_eq!(buf.as_ref(), &[1u8]); + Ok(()) + } + + #[test] + fn test_encode_bool_false() -> anyhow::Result<()> { + let value = Value::make_bool(false); + let ty = Type::BOOL; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + assert_eq!(buf.as_ref(), &[0u8]); + Ok(()) + } + + // ====== Integer Types Tests ====== + + #[test] + fn test_encode_s8_positive() -> anyhow::Result<()> { + let value = Value::make_s8(42); + let ty = Type::S8; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + assert_eq!(buf.as_ref(), &[42u8]); + Ok(()) + } + + #[test] + fn test_encode_s8_negative() -> anyhow::Result<()> { + let value = Value::make_s8(-42); + let ty = Type::S8; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + assert_eq!(buf.as_ref(), &[(-42i8) as u8]); + Ok(()) + } + + #[test] + fn test_encode_u8() -> anyhow::Result<()> { + let value = Value::make_u8(255); + let ty = Type::U8; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + assert_eq!(buf.as_ref(), &[255u8]); + Ok(()) + } + + #[test] + fn test_encode_s16() -> anyhow::Result<()> { + let value = Value::make_s16(1000); + let ty = Type::S16; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + // 1000 in LEB128: 0xE8 0x07 + assert_eq!(buf.as_ref(), &[0xE8, 0x07]); + Ok(()) + } + + #[test] + fn test_encode_u16() -> anyhow::Result<()> { + let value = Value::make_u16(1000); + let ty = Type::U16; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + // 1000 in LEB128: 0xE8 0x07 + assert_eq!(buf.as_ref(), &[0xE8, 0x07]); + Ok(()) + } + + #[test] + fn test_encode_s32() -> anyhow::Result<()> { + let value = Value::make_s32(100000); + let ty = Type::S32; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + // 100000 in LEB128: 0xA0 0x8D 0x06 + assert_eq!(buf.as_ref(), &[0xA0, 0x8D, 0x06]); + Ok(()) + } + + #[test] + fn test_encode_u32() -> anyhow::Result<()> { + let value = Value::make_u32(42); + let ty = Type::U32; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + // 42 in LEB128 is just [42] + assert_eq!(buf.as_ref(), &[42u8]); + Ok(()) + } + + #[test] + fn test_encode_s64() -> anyhow::Result<()> { + let value = Value::make_s64(-1); + let ty = Type::S64; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + // -1 in LEB128: 0x7F + assert_eq!(buf.as_ref(), &[0x7F]); + Ok(()) + } + + #[test] + fn test_encode_u64() -> anyhow::Result<()> { + let value = Value::make_u64(12345); + let ty = Type::U64; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + // 12345 in LEB128: 0xB9 0x60 + assert_eq!(buf.as_ref(), &[0xB9, 0x60]); + Ok(()) + } + + // ====== Float Types Tests ====== + + #[test] + fn test_encode_f32() -> anyhow::Result<()> { + let value = Value::make_f32(3.14); + let ty = Type::F32; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + assert_eq!(buf.as_ref(), &3.14f32.to_le_bytes()); + Ok(()) + } + + #[test] + fn test_encode_f64() -> anyhow::Result<()> { + let value = Value::make_f64(3.14159265359); + let ty = Type::F64; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + assert_eq!(buf.as_ref(), &3.14159265359f64.to_le_bytes()); + Ok(()) + } + + // ====== Char Tests ====== + + #[test] + fn test_encode_char() -> anyhow::Result<()> { + let value = Value::make_char('A'); + let ty = Type::CHAR; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + // UTF-8 encoding of 'A' (0x41) + assert_eq!(buf.as_ref(), &[0x41]); + Ok(()) + } + + #[test] + fn test_encode_char_unicode() -> anyhow::Result<()> { + let value = Value::make_char('😀'); + let ty = Type::CHAR; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + // UTF-8 encoding of '😀' (U+1F600): 0xF0 0x9F 0x98 0x80 + assert_eq!(buf.as_ref(), &[0xF0, 0x9F, 0x98, 0x80]); + Ok(()) + } + + #[test] + fn test_encode_string() -> anyhow::Result<()> { + use std::borrow::Cow; + let value = Value::make_string(Cow::Borrowed("hello")); + let ty = Type::STRING; + let mut encoder = WaveEncoder::new(&ty); + let mut buf = BytesMut::new(); + encoder.encode(&value, &mut buf)?; + + // String encoding: length (5 in LEB128) + "hello" + assert_eq!(buf.as_ref(), &[5, b'h', b'e', b'l', b'l', b'o']); + Ok(()) + } + + // ====== List Tests ====== + + #[test] + fn test_encode_list_empty() -> anyhow::Result<()> { + let element_type = Type::U32; + let list_type = Type::list(element_type.clone()); + let values: Vec = vec![]; + let list_value = Value::make_list(&list_type, values)?; + + let mut encoder = WaveEncoder::new(&list_type); + let mut buf = BytesMut::new(); + encoder.encode(&list_value, &mut buf)?; + + // Empty list: length 0 + assert_eq!(buf.as_ref(), &[0]); + Ok(()) + } + + #[test] + fn test_encode_list() -> anyhow::Result<()> { + let element_type = Type::U32; + let list_type = Type::list(element_type.clone()); + let values = vec![Value::make_u32(1), Value::make_u32(2), Value::make_u32(3)]; + let list_value = Value::make_list(&list_type, values)?; + + let mut encoder = WaveEncoder::new(&list_type); + let mut buf = BytesMut::new(); + encoder.encode(&list_value, &mut buf)?; + + // List encoding: length (3) + elements (1, 2, 3 in LEB128) + assert_eq!(buf.as_ref(), &[3, 1, 2, 3]); + Ok(()) + } + + // ====== Record Tests ====== + + #[test] + fn test_encode_record() -> anyhow::Result<()> { + let record_type = Type::record([("x", Type::U32), ("y", Type::U32)]) + .context("failed to create record type")?; + let record_value = Value::make_record( + &record_type, + [("x", Value::make_u32(10)), ("y", Value::make_u32(20))], + )?; + + let mut encoder = WaveEncoder::new(&record_type); + let mut buf = BytesMut::new(); + encoder.encode(&record_value, &mut buf)?; + + // Record with x=10, y=20 (LEB128) + assert_eq!(buf.as_ref(), &[10, 20]); + Ok(()) + } + + // ====== Tuple Tests ====== + + #[test] + fn test_encode_tuple() -> anyhow::Result<()> { + let tuple_type = + Type::tuple([Type::U32, Type::BOOL]).context("failed to create tuple type")?; + let tuple_value = + Value::make_tuple(&tuple_type, [Value::make_u32(42), Value::make_bool(true)])?; + + let mut encoder = WaveEncoder::new(&tuple_type); + let mut buf = BytesMut::new(); + encoder.encode(&tuple_value, &mut buf)?; + + // Tuple with (42, true) + assert_eq!(buf.as_ref(), &[42, 1]); + Ok(()) + } + + // ====== Variant Tests ====== + + #[test] + fn test_encode_variant_no_payload() -> anyhow::Result<()> { + let variant_type = Type::variant([("none", None), ("some", Some(Type::U32))]) + .context("failed to create variant type")?; + let variant_value = Value::make_variant(&variant_type, "none", None)?; + + let mut encoder = WaveEncoder::new(&variant_type); + let mut buf = BytesMut::new(); + encoder.encode(&variant_value, &mut buf)?; + + // Variant "none" (discriminant 0, no payload) + assert_eq!(buf.as_ref(), &[0]); + Ok(()) + } + + #[test] + fn test_encode_variant_with_payload() -> anyhow::Result<()> { + let variant_type = Type::variant([("none", None), ("some", Some(Type::U32))]) + .context("failed to create variant type")?; + let variant_value = Value::make_variant(&variant_type, "some", Some(Value::make_u32(42)))?; + + let mut encoder = WaveEncoder::new(&variant_type); + let mut buf = BytesMut::new(); + encoder.encode(&variant_value, &mut buf)?; + + // Variant "some" with payload 42 (discriminant 1, payload 42) + assert_eq!(buf.as_ref(), &[1, 42]); + Ok(()) + } + + // ====== Enum Tests ====== + + #[test] + fn test_encode_enum() -> anyhow::Result<()> { + let enum_type = Type::enum_ty(["red", "blue"]).expect("enum type should be valid"); + let enum_value = Value::make_enum(&enum_type, "red")?; + + let mut encoder = WaveEncoder::new(&enum_type); + let mut buf = BytesMut::new(); + encoder.encode(&enum_value, &mut buf)?; + + // First enum case should encode as discriminant 0 + assert_eq!(buf.as_ref(), &[0]); + Ok(()) + } + + // ====== Option Tests ====== + + #[test] + fn test_encode_option_none() -> anyhow::Result<()> { + let inner_type = Type::U32; + let option_type = Type::option(inner_type); + let option_value = Value::make_option(&option_type, None)?; + + let mut encoder = WaveEncoder::new(&option_type); + let mut buf = BytesMut::new(); + encoder.encode(&option_value, &mut buf)?; + + // None is encoded as 0 + assert_eq!(buf.as_ref(), &[0]); + Ok(()) + } + + #[test] + fn test_encode_option_some() -> anyhow::Result<()> { + let inner_type = Type::U32; + let option_type = Type::option(inner_type.clone()); + let inner_value = Value::make_u32(42); + let option_value = Value::make_option(&option_type, Some(inner_value))?; + + let mut encoder = WaveEncoder::new(&option_type); + let mut buf = BytesMut::new(); + encoder.encode(&option_value, &mut buf)?; + + // Some(42) is encoded as 1 (discriminant) + 42 (value) + assert_eq!(buf.as_ref(), &[1, 42]); + Ok(()) + } + + // ====== Result Tests ====== + + #[test] + fn test_encode_result_ok_with_value() -> anyhow::Result<()> { + let result_type = Type::result(Some(Type::U32), Some(Type::STRING)); + let result_value = Value::make_result(&result_type, Ok(Some(Value::make_u32(42))))?; + + let mut encoder = WaveEncoder::new(&result_type); + let mut buf = BytesMut::new(); + encoder.encode(&result_value, &mut buf)?; + + // Ok(42): discriminant 0, value 42 + assert_eq!(buf.as_ref(), &[0, 42]); + Ok(()) + } + + #[test] + fn test_encode_result_ok_no_value() -> anyhow::Result<()> { + let result_type = Type::result(None, Some(Type::STRING)); + let result_value = Value::make_result(&result_type, Ok(None))?; + + let mut encoder = WaveEncoder::new(&result_type); + let mut buf = BytesMut::new(); + encoder.encode(&result_value, &mut buf)?; + + // Ok(none): discriminant 0 + assert_eq!(buf.as_ref(), &[0]); + Ok(()) + } + + #[test] + fn test_encode_result_err_with_value() -> anyhow::Result<()> { + use std::borrow::Cow; + let result_type = Type::result(Some(Type::U32), Some(Type::STRING)); + let result_value = Value::make_result( + &result_type, + Err(Some(Value::make_string(Cow::Borrowed("fail")))), + )?; + + let mut encoder = WaveEncoder::new(&result_type); + let mut buf = BytesMut::new(); + encoder.encode(&result_value, &mut buf)?; + + // Err("fail"): discriminant 1, length 4, "fail" + assert_eq!(buf.as_ref(), &[1, 4, b'f', b'a', b'i', b'l']); + Ok(()) + } + + #[test] + fn test_encode_result_err_no_value() -> anyhow::Result<()> { + let result_type = Type::result(Some(Type::U32), None); + let result_value = Value::make_result(&result_type, Err(None))?; + + let mut encoder = WaveEncoder::new(&result_type); + let mut buf = BytesMut::new(); + encoder.encode(&result_value, &mut buf)?; + + // Err(none): discriminant 1 + assert_eq!(buf.as_ref(), &[1]); + Ok(()) + } + + // ====== Flags Tests ====== + + #[test] + fn test_encode_flags_empty() -> anyhow::Result<()> { + let flags_type = + Type::flags(["read", "write", "execute"]).expect("flags type should be valid"); + let flags_value = Value::make_flags(&flags_type, [])?; + + let mut encoder = WaveEncoder::new(&flags_type); + let mut buf = BytesMut::new(); + encoder.encode(&flags_value, &mut buf)?; + + // No flags set: 0b00000000 = 0 + assert_eq!(buf.as_ref(), &[0]); + Ok(()) + } + + #[test] + fn test_encode_flags_single_byte() -> anyhow::Result<()> { + let flags_type = + Type::flags(["read", "write", "execute"]).expect("flags type should be valid"); + let flags_value = Value::make_flags(&flags_type, ["read", "write"])?; + + let mut encoder = WaveEncoder::new(&flags_type); + let mut buf = BytesMut::new(); + encoder.encode(&flags_value, &mut buf)?; + + // read=bit0, write=bit1 -> 0b00000011 = 3 + assert_eq!(buf.as_ref(), &[0b00000011]); + Ok(()) + } + + #[test] + fn test_encode_flags_two_bytes() -> anyhow::Result<()> { + let flags_type = Type::flags([ + "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", + ]) + .context("failed to create flags type")?; + let flags_value = Value::make_flags(&flags_type, ["f0", "f9"])?; + + let mut encoder = WaveEncoder::new(&flags_type); + let mut buf = BytesMut::new(); + encoder.encode(&flags_value, &mut buf)?; + + // f0 and f9 set: bit 0 in byte 0, bit 1 in byte 1 + // byte 0: 0b00000001 = 0x01 + // byte 1: 0b00000010 = 0x02 + assert_eq!(buf.as_ref(), &[0x01, 0x02]); + Ok(()) + } + + #[test] + fn test_encode_flags_three_bytes() -> anyhow::Result<()> { + let flags_type = Type::flags([ + "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "f13", + "f14", "f15", "f16", "f17", "f18", "f19", + ]) + .context("failed to create flags type")?; + let flags_value = Value::make_flags(&flags_type, ["f0", "f8", "f16"])?; + + let mut encoder = WaveEncoder::new(&flags_type); + let mut buf = BytesMut::new(); + encoder.encode(&flags_value, &mut buf)?; + + // f0, f8, f16 set: bit 0 in byte 0, bit 0 in byte 1, bit 0 in byte 2 + assert_eq!(buf.as_ref(), &[0x01, 0x01, 0x01]); + Ok(()) + } +} diff --git a/crates/wave/src/core/mod.rs b/crates/wave/src/core/mod.rs new file mode 100644 index 00000000..1c80d24e --- /dev/null +++ b/crates/wave/src/core/mod.rs @@ -0,0 +1,2 @@ +pub mod decode; +pub mod encode; diff --git a/crates/wave/src/lib.rs b/crates/wave/src/lib.rs new file mode 100644 index 00000000..02459a1b --- /dev/null +++ b/crates/wave/src/lib.rs @@ -0,0 +1,107 @@ +//! wRPC encoding/decoding support for wasm-wave values +//! +//! This crate provides encoding and decoding for wasm-wave values with wRPC transport. +//! +//! ## Encoding +//! +//! ### 1. Encoder approach (tokio-util codec) +//! +//! This approach mirrors the `ValEncoder` from `wrpc-runtime-wasmtime`, +//! where you create an encoder with a type reference and then encode values directly: +//! +//! ```no_run +//! use wrpc_wave::WaveEncoder; +//! use wasm_wave::value::{Value, Type}; +//! use wasm_wave::wasm::WasmValue; +//! use bytes::BytesMut; +//! use wit_bindgen_wrpc::tokio_util::codec::Encoder; +//! +//! let value = Value::make_u32(42); +//! let ty = Type::U32; +//! let mut encoder = WaveEncoder::new(&ty); +//! let mut buf = BytesMut::new(); +//! encoder.encode(&value, &mut buf).unwrap(); +//! ``` +//! +//! ### 2. `wrpc-pack` compatibility layer +//! +//! This approach bundles wasm-wave values with their types and uses the `wrpc_pack::pack` function: +//! +//! ``` +//! # #[cfg(feature = "pack")] +//! # { +//! use wrpc_wave::WasmTypedValue; +//! use wasm_wave::value::{Value, Type}; +//! use wasm_wave::wasm::WasmValue; +//! use wrpc_pack::pack; +//! use bytes::BytesMut; +//! +//! let value = Value::make_u32(42); +//! let typed_value = WasmTypedValue(value, Type::U32); +//! let mut buf = BytesMut::new(); +//! pack(typed_value, &mut buf).unwrap(); +//! # } +//! ``` +//! +//! This approach requires the `pack` feature to be enabled and the `wrpc-pack` crate. +//! +//! ## Decoding +//! +//! ### 1. Async API (recommended) +//! +//! Use [`read_value`] for async decoding from any `AsyncRead` source: +//! +//! ```no_run +//! use wrpc_wave::read_value; +//! use wasm_wave::value::Type; +//! +//! # async fn example() -> std::io::Result<()> { +//! let mut stream = tokio::net::TcpStream::connect("127.0.0.1:8080").await?; +//! let mut pinned = std::pin::pin!(&mut stream); +//! let value = read_value(&mut pinned, &Type::U32).await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ### 2. Sync API +//! +//! Use [`read_value_sync`] for synchronous decoding from byte slices +//! (requires the `sync` feature): +//! +//! ``` +//! # #[cfg(feature = "sync")] +//! # { +//! use wrpc_wave::read_value_sync; +//! use wasm_wave::value::Type; +//! use wasm_wave::wasm::WasmValue; +//! +//! let data = vec![42]; // u8 value +//! let value = read_value_sync(&Type::U8, &data).unwrap(); +//! assert_eq!(value.unwrap_u8(), 42); +//! # } +//! ``` + +// Core encoding/decoding - no feature flags required +mod core; + +// Sync decoding - requires sync feature (uses futures::executor) +#[cfg(feature = "sync")] +mod sync; + +// wrpc-pack integration - requires pack feature +#[cfg(feature = "pack")] +mod pack; + +// Re-export async decoder +pub use core::decode::read_value; + +// Re-export encoder +pub use core::encode::WaveEncoder; + +// Re-export sync decoder (requires sync feature) +#[cfg(feature = "sync")] +pub use sync::decode::read_value_sync; + +// Re-export wrpc-pack integration (requires pack feature) +#[cfg(feature = "pack")] +pub use pack::{TypedWaveEncoder, WasmTypedValue}; diff --git a/crates/wave/src/pack.rs b/crates/wave/src/pack.rs new file mode 100644 index 00000000..17a2c2f6 --- /dev/null +++ b/crates/wave/src/pack.rs @@ -0,0 +1,614 @@ +//! Typed value wrapper for use with wrpc-pack +//! +//! This module provides `WasmTypedValue` which bundles a value and its type together, +//! allowing it to be used with the `wrpc_pack::pack` function. + +use bytes::BytesMut; +use tokio_util::codec::Encoder; +use wasm_wave::value::{Type, Value}; +use wasm_wave::wasm::{WasmType, WasmValue}; +use wrpc_pack::NoopStream; +use wrpc_transport::Deferred; + +use crate::core::encode::WaveEncoder; + +/// Encoder that implements the necessary traits for use with `pack`. +/// +/// Unlike `WaveEncoder` which stores a reference to the type, +/// this encoder is stateless and implements `Default`, which is required by the +/// `wrpc_transport::Encode` trait. +#[derive(Debug, Default)] +pub struct TypedWaveEncoder; + +impl Deferred for TypedWaveEncoder { + fn take_deferred(&mut self) -> Option> { + // NoopStream doesn't support async operations, so we never have deferred values + None + } +} + +// Generic implementation for any WasmValue + WasmType pair (owned) +impl Encoder> for TypedWaveEncoder +where + V: WasmValue, + T: WasmType, +{ + type Error = anyhow::Error; + + fn encode(&mut self, v: WasmTypedValue, dst: &mut BytesMut) -> Result<(), Self::Error> { + let mut encoder = WaveEncoder::new(&v.1); + encoder.encode(&v.0, dst) + } +} + +// Generic implementation for any WasmValue + WasmType pair (borrowed) +impl Encoder<&WasmTypedValue> for TypedWaveEncoder +where + V: WasmValue, + T: WasmType, +{ + type Error = anyhow::Error; + + fn encode(&mut self, v: &WasmTypedValue, dst: &mut BytesMut) -> Result<(), Self::Error> { + let mut encoder = WaveEncoder::new(&v.1); + encoder.encode(&v.0, dst) + } +} + +/// This wrapper contains both a value implementing [`WasmValue`] and its type implementing [`WasmType`]. +/// Both are required for proper encoding of complex types like variants, enums, and flags. +/// +/// The generic parameters allow this wrapper to work with any implementations of the traits, +/// such as `wasm_wave::value::Value` and `wasm_wave::value::Type`, or `wasmtime::component::Val`. +/// +/// # Examples +/// +/// Using with the default `Value` and `Type`: +/// ``` +/// use wrpc_wave::WasmTypedValue; +/// use wasm_wave::value::{Value, Type}; +/// use wasm_wave::wasm::WasmValue; +/// +/// let value = Value::make_u32(42); +/// let ty = Type::U32; +/// let typed_value = WasmTypedValue(value, ty); +/// ``` +/// +/// Due to default type parameters, this is equivalent to `WasmTypedValue`. +#[derive(Debug, Clone)] +pub struct WasmTypedValue(pub V, pub T); + +impl From<(V, T)> for WasmTypedValue { + fn from((value, ty): (V, T)) -> Self { + WasmTypedValue(value, ty) + } +} + +impl From> for (V, T) { + fn from(wrpc_value: WasmTypedValue) -> Self { + (wrpc_value.0, wrpc_value.1) + } +} + +// Generic implementations for wrpc_transport::Encode +impl wrpc_transport::Encode for WasmTypedValue +where + V: WasmValue, + T: WasmType, +{ + type Encoder = TypedWaveEncoder; +} + +impl wrpc_transport::Encode for &WasmTypedValue +where + V: WasmValue, + T: WasmType, +{ + type Encoder = TypedWaveEncoder; +} + +#[cfg(test)] +mod tests { + use super::*; + use bytes::BytesMut; + use wasm_wave::value::Value; + use wrpc_pack::pack; + + #[test] + fn test_pack_bool() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(true, &mut params_bytes_regular)?; + + let value = Value::make_bool(true); + let ty = Type::BOOL; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for bool" + ); + Ok(()) + } + + #[test] + fn test_pack_s8() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(42i8, &mut params_bytes_regular)?; + + let value = Value::make_s8(42); + let ty = Type::S8; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for s8" + ); + Ok(()) + } + + #[test] + fn test_pack_u8() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(42u8, &mut params_bytes_regular)?; + + let value = Value::make_u8(42); + let ty = Type::U8; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for u8" + ); + Ok(()) + } + + #[test] + fn test_pack_s16() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(1234i16, &mut params_bytes_regular)?; + + let value = Value::make_s16(1234); + let ty = Type::S16; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for s16" + ); + Ok(()) + } + + #[test] + fn test_pack_u16() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(1234u16, &mut params_bytes_regular)?; + + let value = Value::make_u16(1234); + let ty = Type::U16; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for u16" + ); + Ok(()) + } + + #[test] + fn test_pack_s32() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(123456i32, &mut params_bytes_regular)?; + + let value = Value::make_s32(123456); + let ty = Type::S32; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for s32" + ); + Ok(()) + } + + #[test] + fn test_pack_u32() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(123456u32, &mut params_bytes_regular)?; + + let value = Value::make_u32(123456); + let ty = Type::U32; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for u32" + ); + Ok(()) + } + + #[test] + fn test_pack_s64() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(1234567890i64, &mut params_bytes_regular)?; + + let value = Value::make_s64(1234567890); + let ty = Type::S64; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for s64" + ); + Ok(()) + } + + #[test] + fn test_pack_u64() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(1234567890u64, &mut params_bytes_regular)?; + + let value = Value::make_u64(1234567890); + let ty = Type::U64; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for u64" + ); + Ok(()) + } + + #[test] + fn test_pack_f32() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(3.14f32, &mut params_bytes_regular)?; + + let value = Value::make_f32(3.14); + let ty = Type::F32; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for f32" + ); + Ok(()) + } + + #[test] + fn test_pack_f64() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(3.14159f64, &mut params_bytes_regular)?; + + let value = Value::make_f64(3.14159); + let ty = Type::F64; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for f64" + ); + Ok(()) + } + + #[test] + fn test_pack_char() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack('A', &mut params_bytes_regular)?; + + let value = Value::make_char('A'); + let ty = Type::CHAR; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for char" + ); + Ok(()) + } + + #[test] + fn test_pack_string() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack("hello", &mut params_bytes_regular)?; + + use std::borrow::Cow; + let value = Value::make_string(Cow::Borrowed("hello")); + let ty = Type::STRING; + let wrpc_value = WasmTypedValue(value, ty); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for string" + ); + Ok(()) + } + + #[test] + fn test_pack_list() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(vec![1u32, 2u32, 3u32], &mut params_bytes_regular)?; + + let element_type = Type::U32; + let list_type = Type::list(element_type.clone()); + let values = vec![Value::make_u32(1), Value::make_u32(2), Value::make_u32(3)]; + let list_value = Value::make_list(&list_type, values)?; + let wrpc_value = WasmTypedValue(list_value, list_type); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for list" + ); + Ok(()) + } + + #[test] + fn test_pack_record() -> anyhow::Result<()> { + // Records are encoded as tuples (just values, no field names) + // So we'll test with a record that has two u32 fields + // This should encode the same as a tuple (u32, u32) + let mut params_bytes_regular = BytesMut::new(); + pack((42u32, 100u32), &mut params_bytes_regular)?; + + let record_type = Type::record([("a", Type::U32), ("b", Type::U32)]) + .expect("record type should be valid"); + let record_value = Value::make_record( + &record_type, + [("a", Value::make_u32(42)), ("b", Value::make_u32(100))], + )?; + let wrpc_value = WasmTypedValue(record_value, record_type); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for record" + ); + Ok(()) + } + + #[test] + fn test_pack_tuple() -> anyhow::Result<()> { + // Test 1: Pack (5,) using regular pack + let mut params_bytes_regular = BytesMut::new(); + pack((5u32,), &mut params_bytes_regular)?; + + // Test 2: Pack a wasm-wave Value representing (5,) as a tuple + let value_5 = Value::make_u32(5); + let tuple_type = Type::tuple([Type::U32]).expect("tuple type should be valid"); + let tuple_value = Value::make_tuple(&tuple_type, [value_5])?; + let wrpc_tuple_value = WasmTypedValue(tuple_value, tuple_type); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_tuple_value, &mut params_bytes_wave)?; + + // Test 3: Verify both produce the same bytes + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes" + ); + + Ok(()) + } + + #[test] + fn test_pack_variant() -> anyhow::Result<()> { + // Test variant with payload + // Variant encoding: discriminant index + optional payload + // We'll create a variant with two cases: "none" (no payload) and "some" (with u32 payload) + // Test the "some" case + let variant_type = Type::variant([("none", None), ("some", Some(Type::U32))]) + .expect("variant type should be valid"); + + // For comparison, we'll use Option::Some(42) which encodes similarly + let mut params_bytes_regular = BytesMut::new(); + pack(Some(42u32), &mut params_bytes_regular)?; + + let variant_value = Value::make_variant(&variant_type, "some", Some(Value::make_u32(42)))?; + let wrpc_value = WasmTypedValue(variant_value, variant_type); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + // Note: Variant encoding is similar to Option but uses discriminant index + // The bytes should match for a simple case like this + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for variant" + ); + Ok(()) + } + + #[test] + fn test_pack_enum() -> anyhow::Result<()> { + // Enums encode as just the discriminant index + // We'll create a simple enum with two cases: "red" and "blue" + // Test encoding "red" (discriminant 0) + let enum_type = Type::enum_ty(["red", "blue"]).expect("enum type should be valid"); + + // For comparison, we can't directly compare with a Rust enum easily + // So we'll just test that it encodes without error and produces some bytes + let enum_value = Value::make_enum(&enum_type, "red")?; + let wrpc_value = WasmTypedValue(enum_value, enum_type); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + // Enum should encode as a single byte (discriminant 0 for first case in small enum) + assert!( + !params_bytes_wave.is_empty(), + "Enum should produce non-empty bytes" + ); + // For a 2-case enum, it should use u8 encoding (1 byte for discriminant) + assert_eq!( + params_bytes_wave.len(), + 1, + "Small enum should encode as 1 byte" + ); + assert_eq!( + params_bytes_wave[0], 0, + "First enum case should encode as discriminant 0" + ); + Ok(()) + } + + #[test] + fn test_pack_option_none() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(Option::::None, &mut params_bytes_regular)?; + + let inner_type = Type::U32; + let option_type = Type::option(inner_type); + let option_value = Value::make_option(&option_type, None)?; + let wrpc_value = WasmTypedValue(option_value, option_type); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for option::none" + ); + Ok(()) + } + + #[test] + fn test_pack_option_some() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(Some(42u32), &mut params_bytes_regular)?; + + let inner_type = Type::U32; + let option_type = Type::option(inner_type.clone()); + let inner_value = Value::make_u32(42); + let option_value = Value::make_option(&option_type, Some(inner_value))?; + let wrpc_value = WasmTypedValue(option_value, option_type); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for option::some" + ); + Ok(()) + } + + #[test] + fn test_pack_result_ok() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack(Result::::Ok(42), &mut params_bytes_regular)?; + + let ok_type = Some(Type::U32); + let err_type = Some(Type::STRING); + let result_type = Type::result(ok_type.clone(), err_type); + let ok_value = Value::make_u32(42); + let result_value = Value::make_result(&result_type, Ok(Some(ok_value)))?; + let wrpc_value = WasmTypedValue(result_value, result_type); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for result::ok" + ); + Ok(()) + } + + #[test] + fn test_pack_result_err() -> anyhow::Result<()> { + let mut params_bytes_regular = BytesMut::new(); + pack( + Result::::Err("error".to_string()), + &mut params_bytes_regular, + )?; + + let ok_type = Some(Type::U32); + let err_type = Some(Type::STRING); + let result_type = Type::result(ok_type, err_type.clone()); + use std::borrow::Cow; + let err_value = Value::make_string(Cow::Borrowed("error")); + let result_value = Value::make_result(&result_type, Err(Some(err_value)))?; + let wrpc_value = WasmTypedValue(result_value, result_type); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + assert_eq!( + params_bytes_regular.as_ref(), + params_bytes_wave.as_ref(), + "Regular pack and wasm-wave pack should produce identical bytes for result::err" + ); + Ok(()) + } + + #[test] + fn test_pack_flags() -> anyhow::Result<()> { + // Flags encode as bit flags + // We'll create a flags type with 3 flags: "read", "write", "execute" + // Test encoding with "read" and "write" set + let flags_type = + Type::flags(["read", "write", "execute"]).expect("flags type should be valid"); + + // For comparison, we can't directly compare with a Rust type easily + // So we'll test that it encodes correctly + let flags_value = Value::make_flags(&flags_type, ["read", "write"])?; + let wrpc_value = WasmTypedValue(flags_value, flags_type); + let mut params_bytes_wave = BytesMut::new(); + pack(wrpc_value, &mut params_bytes_wave)?; + + // For 3 flags, it should use u8 encoding (1 byte) + assert_eq!( + params_bytes_wave.len(), + 1, + "Small flags should encode as 1 byte" + ); + // read=bit0, write=bit1, execute=bit2 + // read + write = 0b00000011 = 3 + assert_eq!( + params_bytes_wave[0], 0b00000011, + "Flags with read and write should encode as 0b00000011" + ); + Ok(()) + } +} diff --git a/crates/wave/src/sync/decode.rs b/crates/wave/src/sync/decode.rs new file mode 100644 index 00000000..04cc97b8 --- /dev/null +++ b/crates/wave/src/sync/decode.rs @@ -0,0 +1,110 @@ +//! Synchronous wrapper for value decoder using futures::executor +//! +//! This module provides a synchronous wrapper around the async +//! `read_value` function. It uses `futures::executor::block_on` which works +//! without a tokio runtime, +//! +//! # Design +//! +//! - Uses `std::io::Cursor` to wrap byte slices +//! - Blocks on async `read_value` using `futures::executor::block_on` +//! - Errors if there's unconsumed data (stricter than `wrpc-pack::unpack`) +//! +//! Note: `unpack` is not supported, as +//! 1. `wasm-wave` doesn't't expose (`pub(super)`) its internal type system, +//! so it can't be used a generic passed to `unpack` +//! 2. `unpack` only accepts one argument (`buf`) +//! but, because of (1), we need to be able to pass a `Type` to decode + +use std::io::Cursor; + +use anyhow::{bail, Context as _}; +use futures::executor::block_on; +use wasm_wave::value::{Type, Value}; + +use crate::core::decode::read_value; + +/// Synchronously decode a wasm-wave value from bytes. +/// +/// This is a synchronous wrapper around the async [`read_value`] function, +/// using `futures::executor::block_on` +/// +/// # Errors +/// +/// Returns an error if: +/// - The data is incomplete for the given type +/// - The data is malformed +/// - There are unconsumed bytes after decoding (stricter than `wrpc-pack::unpack`) +/// +/// # Examples +/// +/// ``` +/// use wasm_wave::value::Type; +/// use wasm_wave::wasm::WasmValue; +/// use wrpc_wave::read_value_sync; +/// +/// let data = vec![42]; // u8 value +/// let value = read_value_sync(&Type::U8, &data).unwrap(); +/// assert_eq!(value.unwrap_u8(), 42); +/// ``` +pub fn read_value_sync(ty: &Type, data: &[u8]) -> anyhow::Result { + let mut cursor = Cursor::new(data); + + let value = block_on(async { + let mut pinned = std::pin::pin!(&mut cursor); + read_value(&mut pinned, ty).await + }) + .context("failed to decode value")?; + + // Safety: Error on unconsumed data + let consumed = cursor.position() as usize; + if consumed < data.len() { + bail!( + "unconsumed data: {} of {} bytes remain", + data.len() - consumed, + data.len() + ); + } + + Ok(value) +} + +#[cfg(test)] +mod tests { + use super::*; + use wasm_wave::wasm::WasmValue as _; + + // Note: These tests focus on the sync wrapper's specific behavior (blocking, + // unconsumed data detection). The underlying async decoder is thoroughly tested + // in core/decode.rs - we don't duplicate all those test cases here. + + #[test] + fn test_sync_wrapper_works() { + // Verify the sync wrapper successfully decodes a simple value + let data = vec![42]; + let value = read_value_sync(&Type::U8, &data).unwrap(); + assert_eq!(value.unwrap_u8(), 42); + } + + #[test] + fn test_unconsumed_data_error() { + // The sync wrapper should error if there's unconsumed data after decoding + // (This is stricter than wrpc-pack::unpack which ignores leftover bytes) + let data = vec![42, 99, 100]; // u8 only needs 1 byte + let result = read_value_sync(&Type::U8, &data); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("unconsumed data")); + assert!(err_msg.contains("2 of 3 bytes remain")); + } + + #[test] + fn test_incomplete_data_error() { + // The sync wrapper should propagate errors from the underlying async decoder + let mut data = vec![3]; // length = 3 in leb128 + data.extend_from_slice(&[10, 20]); // only 2 elements instead of 3 + let ty = Type::list(Type::U8); + let result = read_value_sync(&ty, &data); + assert!(result.is_err(), "Should fail when data is incomplete"); + } +} diff --git a/crates/wave/src/sync/mod.rs b/crates/wave/src/sync/mod.rs new file mode 100644 index 00000000..5ac80bbf --- /dev/null +++ b/crates/wave/src/sync/mod.rs @@ -0,0 +1 @@ +pub mod decode;