Skip to content

Commit 53289cb

Browse files
lutterclaude
andcommitted
gnd: Add codegen verification tests against graph-cli fixtures
Add integration tests that verify gnd codegen produces compatible output to graph-cli codegen using golden test fixtures from the graph-cli repo. Changes: - Add walkdir and similar dev-dependencies for test infrastructure - Create codegen_verification.rs with tests for 8 fixtures - Fix ABI preprocessing to use correct default param names - Fix typescript class method ordering to match graph-cli output Known differences that are normalized in tests: - Int8 import (gnd always includes it) - Trailing commas (gnd uses them) - 2D array accessors (gnd correctly uses toStringMatrix) Skipped fixtures with documented reasons: - derived-from-with-interface: derived field loaders not implemented - invalid-graphql-schema: gnd generates schema.ts for invalid schemas - no-network-names: template ABI types not in subdirectories Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c60b8e3 commit 53289cb

File tree

5 files changed

+444
-8
lines changed

5 files changed

+444
-8
lines changed

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gnd/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,5 @@ pgtemp = { git = "https://github.com/graphprotocol/pgtemp", branch = "initdb-arg
6666

6767
[dev-dependencies]
6868
tempfile = "3"
69+
walkdir = "2"
70+
similar = "2"

gnd/src/codegen/typescript.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -303,10 +303,11 @@ impl Display for ClassMember {
303303
}
304304

305305
/// Code that can be part of a class body (method, static method, or member).
306-
#[allow(dead_code)]
306+
#[derive(Debug, Clone)]
307307
pub enum ClassCode {
308308
Method(Method),
309309
StaticMethod(StaticMethod),
310+
#[allow(dead_code)]
310311
Member(ClassMember),
311312
}
312313

@@ -327,6 +328,9 @@ pub struct Class {
327328
pub extends: Option<String>,
328329
pub export: bool,
329330
pub members: Vec<ClassMember>,
331+
/// Methods in insertion order (regular and static)
332+
code: Vec<ClassCode>,
333+
// Keep separate vectors for backward compatibility with existing code
330334
pub methods: Vec<Method>,
331335
pub static_methods: Vec<StaticMethod>,
332336
}
@@ -338,6 +342,7 @@ impl Class {
338342
extends: None,
339343
export: false,
340344
members: Vec::new(),
345+
code: Vec::new(),
341346
methods: Vec::new(),
342347
static_methods: Vec::new(),
343348
}
@@ -358,10 +363,12 @@ impl Class {
358363
}
359364

360365
pub fn add_method(&mut self, method: Method) {
366+
self.code.push(ClassCode::Method(method.clone()));
361367
self.methods.push(method);
362368
}
363369

364370
pub fn add_static_method(&mut self, method: StaticMethod) {
371+
self.code.push(ClassCode::StaticMethod(method.clone()));
365372
self.static_methods.push(method);
366373
}
367374
}
@@ -382,12 +389,9 @@ impl Display for Class {
382389
writeln!(f, "{}", member)?;
383390
}
384391

385-
// Write methods (regular and static)
386-
for method in &self.static_methods {
387-
write!(f, "{}", method)?;
388-
}
389-
for method in &self.methods {
390-
write!(f, "{}", method)?;
392+
// Write methods in insertion order (maintains graph-cli output order)
393+
for code in &self.code {
394+
write!(f, "{}", code)?;
391395
}
392396

393397
writeln!(f, "}}")

gnd/src/commands/codegen.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,14 +228,90 @@ fn generate_schema_types(schema_path: &Path, output_dir: &Path) -> Result<()> {
228228
Ok(())
229229
}
230230

231+
/// Preprocess ABI JSON to add default names for unnamed parameters.
232+
/// The ethabi crate requires all parameters to have names, but Solidity ABIs
233+
/// can have unnamed parameters. This function adds `param0`, `param1`, etc.
234+
fn preprocess_abi_json(abi_str: &str) -> Result<String> {
235+
let mut abi: serde_json::Value =
236+
serde_json::from_str(abi_str).context("Failed to parse ABI JSON")?;
237+
238+
if let Some(items) = abi.as_array_mut() {
239+
for item in items {
240+
if let Some(obj) = item.as_object_mut() {
241+
let is_event = obj
242+
.get("type")
243+
.and_then(|t| t.as_str())
244+
.map(|t| t == "event")
245+
.unwrap_or(false);
246+
247+
// Add anonymous: false for events if missing
248+
if is_event && !obj.contains_key("anonymous") {
249+
obj.insert("anonymous".to_string(), serde_json::Value::Bool(false));
250+
}
251+
252+
// Process inputs for events and functions
253+
if let Some(inputs) = obj.get_mut("inputs") {
254+
add_default_names_to_params(inputs, is_event);
255+
}
256+
// Process outputs for functions
257+
if let Some(outputs) = obj.get_mut("outputs") {
258+
add_default_names_to_params(outputs, false);
259+
}
260+
}
261+
}
262+
}
263+
264+
serde_json::to_string(&abi).context("Failed to serialize processed ABI")
265+
}
266+
267+
/// Add required fields to ABI parameters.
268+
/// - For event inputs: adds "name" (using `param{index}` if missing) and "indexed": false
269+
/// - For function inputs/outputs: adds empty "name" if missing (lets ABI codegen use correct prefix)
270+
/// - For tuple components: adds empty "name" if missing
271+
fn add_default_names_to_params(params: &mut serde_json::Value, is_event_input: bool) {
272+
if let Some(params_arr) = params.as_array_mut() {
273+
for (index, param) in params_arr.iter_mut().enumerate() {
274+
if let Some(obj) = param.as_object_mut() {
275+
// Check if name is missing (not present at all)
276+
let name_missing = !obj.contains_key("name");
277+
278+
if name_missing {
279+
// For events, use param{index} to match graph-cli behavior
280+
// For functions, use empty string so ABI codegen applies correct prefix
281+
let default_name = if is_event_input {
282+
format!("param{}", index)
283+
} else {
284+
String::new()
285+
};
286+
obj.insert("name".to_string(), serde_json::Value::String(default_name));
287+
}
288+
289+
// Add indexed: false for event inputs if missing
290+
if is_event_input && !obj.contains_key("indexed") {
291+
obj.insert("indexed".to_string(), serde_json::Value::Bool(false));
292+
}
293+
294+
// Recursively process tuple components (always use empty names)
295+
if let Some(components) = obj.get_mut("components") {
296+
add_default_names_to_params(components, false);
297+
}
298+
}
299+
}
300+
}
301+
}
302+
231303
/// Generate types from an ABI file.
232304
fn generate_abi_types(name: &str, abi_path: &Path, output_dir: &Path) -> Result<()> {
233305
step(Step::Load, &format!("Load ABI from {}", abi_path.display()));
234306

235307
let abi_str = fs::read_to_string(abi_path)
236308
.with_context(|| format!("Failed to read ABI file: {:?}", abi_path))?;
237309

238-
let contract: Contract = serde_json::from_str(&abi_str)
310+
// Preprocess ABI to add default names for unnamed parameters
311+
let processed_abi = preprocess_abi_json(&abi_str)
312+
.with_context(|| format!("Failed to preprocess ABI: {:?}", abi_path))?;
313+
314+
let contract: Contract = serde_json::from_str(&processed_abi)
239315
.with_context(|| format!("Failed to parse ABI JSON: {:?}", abi_path))?;
240316

241317
step(Step::Generate, &format!("Generate types for ABI {}", name));

0 commit comments

Comments
 (0)