Skip to content

Add event node support#87

Open
the-orex wants to merge 5 commits intocodama-idl:mainfrom
the-orex:handle-events
Open

Add event node support#87
the-orex wants to merge 5 commits intocodama-idl:mainfrom
the-orex:handle-events

Conversation

@the-orex
Copy link
Copy Markdown

@the-orex the-orex commented Apr 6, 2026

Following up on jup-ag/codama@6487af5

This PR adds support for event nodes

Hopefully that helps 🙏

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends Codama’s node model and default processing pipeline to support event nodes, including schema/JSON updates and visitor + macro support to populate ProgramNode.events.

Changes:

  • Add EventNode to codama-nodes, wire it into Node/ProgramNode, and update serialization expectations.
  • Introduce SetEventsVisitor and run it in the default plugin so events get collected into program/root nodes and merged across modules.
  • Add CodamaEvent / CodamaEvents derive entrypoints and add/extend tests across crates to cover event extraction, directives, skipping, and merging.

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated no comments.

Show a summary per file
File Description
codama/tests/system/mod.rs Updates system test IDL JSON to include an events array.
codama/tests/membership/mod.rs Updates membership test IDL JSON to include an events array.
codama-plugin-core/src/default_plugin.rs Runs SetEventsVisitor as part of the default program item pipeline.
codama-nodes/src/root_node.rs Updates root-node JSON expectations to include program events.
codama-nodes/src/program_node.rs Adds events: Vec<EventNode> and an add_event helper; updates tests/JSON.
codama-nodes/src/node.rs Adds Node::Event(EventNode) and wires it into HasKind.
codama-nodes/src/lib.rs Registers and re-exports the new event_node module.
codama-nodes/src/event_node.rs Introduces the EventNode definition with JSON roundtrip tests.
codama-macros/src/lib.rs Adds proc-macro derives CodamaEvent and CodamaEvents.
codama-macros/tests/codama_events_derive/_pass.rs Adds a passing trybuild case for CodamaEvents.
codama-macros/tests/codama_event_derive/_pass.rs Adds a passing trybuild case for CodamaEvent.
codama-macros/tests/codama_event_derive/multiple_types.fail.rs Adds a failing trybuild case for conflicting #[codama(type=...)] on events.
codama-macros/tests/codama_event_derive/multiple_types.fail.stderr Adds expected compiler output for the failing event derive case.
codama-korok-visitors/src/set_events_visitor.rs Implements event extraction into EventNode / ProgramNode.events (incl. directives + discriminators).
codama-korok-visitors/src/lib.rs Exposes SetEventsVisitor.
codama-korok-visitors/src/combine_modules_visitor.rs Merges/dedupes events when combining modules and scraps.
codama-korok-visitors/tests/lib.rs Registers the new set_events_visitor test module.
codama-korok-visitors/tests/set_events_visitor/* Adds coverage for CodamaEvent(s), program directive behavior, and skip semantics.
codama-korok-visitors/tests/combine_modules_visitor/* Extends combine-modules tests to include event merging/deduplication.
codama-attributes/src/codama_directives/program_directive.rs Allows #[codama(program(...))] to wrap Node::Event into a ProgramNode.
codama-attributes/src/attributes.rs Treats CodamaEvent / CodamaEvents as “program items” for attribute filtering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codama-idl codama-idl deleted a comment from trevor-cortex Apr 9, 2026
@lorisleiva
Copy link
Copy Markdown
Member

@trevor-cortex

Copy link
Copy Markdown

@trevor-cortex trevor-cortex left a comment

Choose a reason for hiding this comment

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

Summary

Adds first-class EventNode support across the stack: a new node type in codama-nodes, a SetEventsVisitor to collect events from #[derive(CodamaEvent)] structs and #[derive(CodamaEvents)] enums, wiring into ProgramNode.events + CombineModulesVisitor merging/dedup, the program(...) directive wrapping events into programs, and test coverage that mirrors accounts/instructions/errors. The implementation closely follows the SetAccountsVisitor pattern, which is the right base to port from.

Overall this looks solid and the test coverage is thorough. A few things worth looking at before merging:

Key things to watch out for

  1. EventNode.data field type — typed as plain TypeNode, but every other struct-shaped top-level node (e.g. AccountNode.data) is typed as NestedTypeNode<StructTypeNode>. The visitor already operates on NestedTypeNode<StructTypeNode> internally (see parse_struct / parse_enum_variant / map_nested_type_node) and then widens to TypeNode only when calling EventNode::new. This inconsistency is worth resolving — see inline comment.
  2. Tuple enum variants are rejected for CodamaEventsSetInstructionsVisitor handles tuple variants, but SetEventsVisitor's parse_enum_variant only matches Struct and Empty. Intentional? If so, it's worth a doc comment; if not, it's inconsistent with instructions.
  3. Merge-order change in merge_program_nodeserrors and pdas were swapped. Cosmetic (each vector is merged independently), but worth confirming the ordering matches the struct field order on purpose.

Notes for subsequent reviewers

  • SetEventsVisitor is essentially SetAccountsVisitor minus the PDA/seed handling. Good portability, but the EventNode.data type divergence is a real deviation from that pattern.
  • _pass.rs in codama_event_derive accepts both a struct and an (empty) enum, and the from_enum visitor test comment ("No visitor error because there is already a compilation error.") is copy-pasted from the accounts tests — there is no actual compilation error, deriving CodamaEvent on an enum is a silent no-op. This is pre-existing convention across CodamaAccount/CodamaEvent so I'm not flagging it, but worth being aware of.
  • The skip_preserves_sibling_discriminator_counting test is good — confirms that #[codama(skip)] still advances the discriminator counter, matching SetInstructionsVisitor/SetErrorsVisitor semantics.
  • No tests for merging two enums annotated with CodamaEvents and #[codama(program(...))] pointing at the same external program — the program_directive.rs tests only cover a single enum/struct each. Not strictly needed since this goes through the shared merge_program_nodes code path, but worth verifying manually once.


// Children.
pub data: TypeNode,
#[serde(default, skip_serializing_if = "crate::is_default")]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consider typing data as NestedTypeNode<StructTypeNode> instead of TypeNode.

AccountNode.data is declared as NestedTypeNode<StructTypeNode>, which guarantees at the type level that the event's data is always a struct (optionally wrapped in FixedSize/SizePrefix/HiddenPrefix/etc). The visitor already operates on that type:

  • parse_struct returns CodamaResult<(CamelCaseString, NestedTypeNode<StructTypeNode>)>
  • parse_enum_variant returns the same
  • map_nested_type_node(...) is used to insert the enum discriminator into the innermost struct

...and only widens to TypeNode at the very last moment when calling EventNode::new(name, data) (because U: Into<TypeNode>). Downstream consumers of EventNode.data now have to re-validate that the data is struct-shaped, whereas AccountNode consumers get it for free.

Suggested change:

#[node]
pub struct EventNode {
    pub name: CamelCaseString,
    #[serde(default, skip_serializing_if = "crate::is_default")]
    pub docs: Docs,

    // Children.
    pub data: NestedTypeNode<StructTypeNode>,
    #[serde(default, skip_serializing_if = "crate::is_default")]
    pub discriminators: Vec<DiscriminatorNode>,
}

impl EventNode {
    pub fn new<T, U>(name: T, data: U) -> Self
    where
        T: Into<CamelCaseString>,
        U: Into<NestedTypeNode<StructTypeNode>>,
    { ... }
}

This would make the visitor's EventNode::new(name, data) calls work unchanged (since NestedTypeNode<StructTypeNode>: Into<NestedTypeNode<StructTypeNode>> and StructTypeNode: Into<NestedTypeNode<StructTypeNode>> both hold), and bring EventNode in line with the rest of the node model. If there's an intentional reason to allow non-struct event payloads here (e.g. to mirror a specific TS shape), it'd be worth calling out in a doc comment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@trevor-cortex No, this is based on a new node definition that has already happened on the codama-idl/codama repository (the JS counterpart). The event must technically accept any TypeNode and therefore could even accept tuple structs. Can you adjust your suggestions accordingly?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ah, got it — if the canonical JS EventNode takes any TypeNode (including tuple structs), then EventNode.data: TypeNode is correct and the node shouldn't change. In that case the issue flips: it's the visitor that's overly restrictive. Right now parse_struct / parse_enum_variant both return NestedTypeNode<StructTypeNode> and hard-error on anything else, so tuple structs and non-struct payloads that the node type is supposed to allow can never actually reach it.

A few adjustments to bring the visitor in line with the relaxed node:

  1. parse_struct should return (CamelCaseString, TypeNode) and accept any TypeNode from the DefinedTypeNode, not just NestedTypeNode<StructTypeNode>. The error branch ("its type is not a NestedTypeNode<StructTypeNode>") then goes away entirely — any defined type is valid.

  2. parse_enum_variant needs a Tuple arm. Currently it only matches EnumVariantTypeNode::Struct and EnumVariantTypeNode::Empty; EnumVariantTypeNode::Tuple falls through to the error. For consistency with the node type allowing tuple payloads, it should return the tuple's TupleTypeNode (wrapped in whatever NestedTypeNode layers are present) as a plain TypeNode.

  3. Discriminator injection needs rethinking for the enum path. The current code does:

    let data = data.map_nested_type_node(|node| {
        let mut fields = node.fields;
        fields.insert(0, discriminator);
        StructTypeNode { fields }
    });

    This only works when data is NestedTypeNode<StructTypeNode>. Once tuple/empty variants are allowed, there's a design question: where does the discriminator field go when the payload is a tuple or a non-struct type? Options:

    • (a) Wrap non-struct payloads in a synthetic struct with { discriminator, data: <payload> } — clean but changes the shape.
    • (b) Only inject the discriminator field when the payload is struct-shaped, and for tuple/empty variants rely purely on the FieldDiscriminatorNode in discriminators without a corresponding struct field (which would then need a different discriminator node kind, since FieldDiscriminatorNode points at a field that doesn't exist).
    • (c) Mirror exactly whatever the JS codama repo does for CodamaEvents on tuple variants — if there's a reference implementation there, that's the safest bet.

    Do you know which direction the JS side went? That'd settle it. If the JS SetEventsVisitor equivalent just doesn't support tuple variants inside CodamaEvents enums (and only the standalone struct path accepts arbitrary TypeNode), then the minimum change is just (1) — relaxing parse_struct — and leaving parse_enum_variant struct/empty-only with a doc comment explaining why.

  4. Error message in parse_struct should be rewritten to match whatever constraint actually remains (likely "could not resolve a DefinedTypeNode for this struct" rather than a struct-shape requirement).

Happy to narrow this down once you confirm which of (a)/(b)/(c) matches the JS behavior — my guess is it's closest to just supporting the struct path for now and the tuple-variant case is a future extension, in which case this reduces to a small relaxation of parse_struct + a doc comment on parse_enum_variant.

pub struct StructTest;

#[derive(CodamaEvent)]
pub enum EnumTest {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Worth double-checking: this pass test asserts that #[derive(CodamaEvent)] on an enum EnumTest {} compiles, but in set_events_visitor/from_codama_event.rs::from_enum, the test expects korok.node to be None after running the visitor (with the comment "No visitor error because there is already a compilation error"). Those two statements contradict each other — the derive does compile, and the visitor silently no-ops.

This same pattern exists for CodamaAccount/set_accounts_visitor (the comment was clearly copied from there), so it's not a regression introduced by this PR. But if the intent is actually "deriving CodamaEvent on an enum should be an error", the macro needs a validation step; if the intent is "permissive, the visitor decides", the misleading comment should be removed. No action strictly required for this PR — flagging for awareness.

Copy link
Copy Markdown
Member

@lorisleiva lorisleiva left a comment

Choose a reason for hiding this comment

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

Looks good to me! @stegaBOB are you happy with it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants