From 3f71433b97b5a0602a01c6bbd54f8f05867089e5 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sat, 14 Feb 2026 06:57:44 -0800 Subject: [PATCH] feat: robust Wesley code generation bridge (PR 3) - Added --no-std flag to echo-wesley-gen for WASM guest compatibility. - Added --minicbor flag to echo-wesley-gen to emit bit-exact codecs. - Mapped GraphQL 'ID' to '[u8; 32]' in no_std mode for binary-first IDs. - Converted echo-registry-api to #![no_std]. - Added comprehensive integration tests for no_std/minicbor generation. --- crates/echo-registry-api/Cargo.toml | 4 ++ crates/echo-registry-api/src/lib.rs | 2 + crates/echo-wesley-gen/src/main.rs | 69 +++++++++++++++++---- crates/echo-wesley-gen/tests/generation.rs | 70 ++++++++++++++++++++++ 4 files changed, 134 insertions(+), 11 deletions(-) diff --git a/crates/echo-registry-api/Cargo.toml b/crates/echo-registry-api/Cargo.toml index 91885b1e..c350e7a5 100644 --- a/crates/echo-registry-api/Cargo.toml +++ b/crates/echo-registry-api/Cargo.toml @@ -13,3 +13,7 @@ keywords = ["echo", "wasm", "registry"] categories = ["wasm", "development-tools"] [dependencies] + +[features] +default = ["std"] +std = [] diff --git a/crates/echo-registry-api/src/lib.rs b/crates/echo-registry-api/src/lib.rs index c29b425e..2cc6af22 100644 --- a/crates/echo-registry-api/src/lib.rs +++ b/crates/echo-registry-api/src/lib.rs @@ -6,6 +6,8 @@ //! GraphQL/Wesley IR). Echo core and `warp-wasm` depend only on this crate and //! **must not** embed app-specific registries. +#![no_std] + /// Codec identifier used by the registry. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RegistryInfo { diff --git a/crates/echo-wesley-gen/src/main.rs b/crates/echo-wesley-gen/src/main.rs index 63480c7d..6a443023 100644 --- a/crates/echo-wesley-gen/src/main.rs +++ b/crates/echo-wesley-gen/src/main.rs @@ -27,6 +27,14 @@ struct Args { /// Optional output path (defaults to stdout) #[arg(short, long)] out: Option, + + /// Emit code compatible with no_std environments + #[arg(long, default_value_t = false)] + no_std: bool, + + /// Emit minicbor Encode/Decode implementations for all types + #[arg(long, default_value_t = false)] + minicbor: bool, } fn main() -> Result<()> { @@ -37,7 +45,7 @@ fn main() -> Result<()> { let ir: WesleyIR = serde_json::from_str(&buffer)?; validate_version(&ir)?; - let code = generate_rust(&ir)?; + let code = generate_rust(&ir, &args)?; if let Some(path) = args.out { std::fs::write(path, code)?; @@ -48,12 +56,29 @@ fn main() -> Result<()> { Ok(()) } -fn generate_rust(ir: &WesleyIR) -> Result { +fn generate_rust(ir: &WesleyIR, args: &Args) -> Result { let mut tokens = quote! { // Generated by echo-wesley-gen. Do not edit. - use serde::{Serialize, Deserialize}; }; + if args.no_std { + tokens.extend(quote! { + extern crate alloc; + use alloc::string::String; + use alloc::vec::Vec; + }); + } + + tokens.extend(quote! { + use serde::{Serialize, Deserialize}; + }); + + if args.minicbor { + tokens.extend(quote! { + use minicbor::{Encode, Decode}; + }); + } + // Metadata constants let schema_sha = ir.schema_sha256.as_deref().unwrap_or(""); let codec_id = ir.codec_id.as_deref().unwrap_or("cbor-canon-v1"); @@ -68,35 +93,51 @@ fn generate_rust(ir: &WesleyIR) -> Result { for type_def in &ir.types { let name = safe_ident(&type_def.name); + let mut derives = quote! { Debug, Clone, PartialEq, Serialize, Deserialize }; + if args.minicbor { + derives.extend(quote! { , Encode, Decode }); + } + match type_def.kind { TypeKind::Enum => { let variants = type_def.values.iter().map(|v| safe_ident(v)); tokens.extend(quote! { - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] + #[derive(#derives, Copy, Eq)] + #[cbor(index_only)] pub enum #name { #(#variants),* } }); } - TypeKind::Object => { - let fields = type_def.fields.iter().map(|f| { + TypeKind::Object | TypeKind::InputObject => { + let fields = type_def.fields.iter().enumerate().map(|(i, f)| { let field_name = safe_ident(&f.name); - let base_ty = map_type(&f.type_name); + let base_ty = map_type(&f.type_name, args); let list_ty: TokenStream = if f.list { quote! { Vec<#base_ty> } } else { quote! { #base_ty } }; - if f.required { + let field_tokens = if f.required { quote! { pub #field_name: #list_ty } } else { quote! { pub #field_name: Option<#list_ty> } + }; + + if args.minicbor { + let idx = i as u64; + quote! { + #[n(#idx)] + #field_tokens + } + } else { + field_tokens } }); tokens.extend(quote! { - #[derive(Debug, Clone, Serialize, Deserialize)] + #[derive(#derives)] pub struct #name { #(#fields),* } @@ -302,13 +343,19 @@ fn validate_version(ir: &WesleyIR) -> Result<()> { /// /// GraphQL `Float` intentionally maps to `f32` (not `f64`) so generated types /// integrate cleanly with Echo’s deterministic scalar foundation. -fn map_type(gql_type: &str) -> TokenStream { +fn map_type(gql_type: &str, args: &Args) -> TokenStream { match gql_type { "Boolean" => quote! { bool }, "String" => quote! { String }, "Int" => quote! { i32 }, "Float" => quote! { f32 }, - "ID" => quote! { String }, + "ID" => { + if args.no_std { + quote! { [u8; 32] } + } else { + quote! { String } + } + } other => { let ident = safe_ident(other); quote! { #ident } diff --git a/crates/echo-wesley-gen/tests/generation.rs b/crates/echo-wesley-gen/tests/generation.rs index 548b8425..5a55bcc6 100644 --- a/crates/echo-wesley-gen/tests/generation.rs +++ b/crates/echo-wesley-gen/tests/generation.rs @@ -114,6 +114,76 @@ fn test_ops_catalog_present() { ); } +#[test] +fn test_generate_no_std_minicbor() { + let ir = r#"{ + "ir_version": "echo-ir/v1", + "types": [ + { + "name": "Node", + "kind": "OBJECT", + "fields": [ + { "name": "id", "type": "ID", "required": true }, + { "name": "pos", "type": "Float", "required": true, "list": true } + ] + }, + { + "name": "Status", + "kind": "ENUM", + "values": ["ACTIVE", "INACTIVE"] + } + ], + "ops": [] + }"#; + + // Run with flags + let mut child = Command::new("cargo") + .args([ + "run", + "-p", + "echo-wesley-gen", + "--", + "--no-std", + "--minicbor", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn cargo run"); + + let mut stdin = child.stdin.take().expect("failed to get stdin"); + stdin + .write_all(ir.as_bytes()) + .expect("failed to write to stdin"); + drop(stdin); + + let output = child.wait_with_output().expect("failed to wait on child"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + output.status.success(), + "CLI failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + // Verify no_std artifacts + assert!(stdout.contains("extern crate alloc;")); + assert!(stdout.contains("use alloc::string::String;")); + assert!(stdout.contains("use alloc::vec::Vec;")); + + // Verify minicbor artifacts + assert!(stdout.contains("use minicbor::{Encode, Decode};")); + assert!(stdout + .contains("#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode)]")); + assert!(stdout.contains("#[cbor(index_only)]")); + assert!(stdout.contains("#[n(0u64)]")); + assert!(stdout.contains("#[n(1u64)]")); + + // Verify ID -> [u8; 32] mapping for no_std + assert!(stdout.contains("pub id: [u8; 32]")); + assert!(stdout.contains("pub pos: Vec")); +} + #[test] fn test_rejects_unknown_version() { let ir = r#"{