Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/echo-registry-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ keywords = ["echo", "wasm", "registry"]
categories = ["wasm", "development-tools"]

[dependencies]

[features]
default = ["std"]
std = []
2 changes: 2 additions & 0 deletions crates/echo-registry-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
69 changes: 58 additions & 11 deletions crates/echo-wesley-gen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ struct Args {
/// Optional output path (defaults to stdout)
#[arg(short, long)]
out: Option<std::path::PathBuf>,

/// 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<()> {
Expand All @@ -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)?;
Expand All @@ -48,12 +56,29 @@ fn main() -> Result<()> {
Ok(())
}

fn generate_rust(ir: &WesleyIR) -> Result<String> {
fn generate_rust(ir: &WesleyIR, args: &Args) -> Result<String> {
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");
Expand All @@ -68,35 +93,51 @@ fn generate_rust(ir: &WesleyIR) -> Result<String> {
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)]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Emit #[cbor] only when minicbor generation is enabled

The generator now always emits #[cbor(index_only)] for every enum, but --minicbor is optional and cbor is only meaningful when Encode/Decode derives are present. In the default path (no --minicbor), generated code containing any enum fails to compile with cannot find attribute 'cbor' in this scope, so standard generation is broken for common schemas.

Useful? React with 👍 / 👎.

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),*
}
Expand Down Expand Up @@ -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 }
Expand Down
70 changes: 70 additions & 0 deletions crates/echo-wesley-gen/tests/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f32>"));
}

#[test]
fn test_rejects_unknown_version() {
let ir = r#"{
Expand Down