Skip to content

Add event-modeling JSON to Akces aggregate code generator#260

Open
Copilot wants to merge 18 commits intomainfrom
copilot/create-code-generator-for-aggregate
Open

Add event-modeling JSON to Akces aggregate code generator#260
Copilot wants to merge 18 commits intomainfrom
copilot/create-code-generator-for-aggregate

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 26, 2026

  • Add external_events property to Slice in JSON schema (references Element)
  • Remove externalEventHandlers from AggregateConfig in JSON schema
  • Remove ExternalEventHandler definition from $defs in JSON schema
  • Update Slice.java model to add externalEvents field with @JsonProperty("external_events")
  • Update AkcesCodeGenerator.java to handle external events from slices (groupByAggregate, SliceData)
  • Update SchemaValidationTest.java - replace ExternalEventHandler tests with external_events tests
  • Update SCHEMA_DIFFERENCES.md documentation
  • All 36 tests pass (16 schema + 17 codegen + 3 compilation)

⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.

Copilot AI and others added 3 commits March 26, 2026 12:21
…enerator

Creates a new main/codegen module that:
- Parses event-modeling JSON schema (extended with Akces config)
- Generates Command records, Event records, State records, and Aggregate classes
- Includes crypto-trading Account and Wallet aggregate JSON definitions
- All 16 tests pass verifying structural equivalence with actual crypto-trading code

Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/b53bdf75-8ad8-4f53-8221-e4538be6810f
@jwijgerd jwijgerd marked this pull request as ready for review March 26, 2026 12:56
Copilot AI review requested due to automatic review settings March 26, 2026 12:56
@jwijgerd
Copy link
Copy Markdown
Contributor

@copilot Am I correct that you augmented the schema for this? if so please add the augmented schema to the code. Also create a test suite that validates that the json files adhere to the schema

- Add akces-event-model.schema.json: extends the event-modeling spec with
  Akces-specific properties (packageName, aggregateConfig with stateFields,
  piiData, indexing, GDPR key generation)
- Add SchemaValidationTest with 13 tests validating JSON files conform to
  the augmented schema, plus negative tests for invalid inputs
- Add everit-json-schema test dependency for JSON Schema validation

Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/62653830-241d-483b-b0f5-18bb05af8e7f
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 26, 2026

@copilot Am I correct that you augmented the schema for this? if so please add the augmented schema to the code. Also create a test suite that validates that the json files adhere to t...

Yes, the schema was augmented with Akces-specific extensions (packageName, aggregateConfig with stateFields, piiData, indexed, generateGDPRKeyOnCreate, etc.) on top of the original event-modeling spec.

In 640eac5:

  • Added akces-event-model.schema.json to main/codegen/src/main/resources/ — the augmented JSON schema extending the event-modeling spec with Akces properties
  • Added SchemaValidationTest with 13 tests — validates both crypto-trading JSON files conform to the schema, plus negative tests for missing required fields, invalid types, and additional properties

Copy link
Copy Markdown
Contributor

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

Adds a new main/codegen module that parses event-modeling JSON (with Akces-specific extensions) and generates Akces aggregate scaffolding, plus tests/resources that validate the generator against the crypto-trading example aggregates.

Changes:

  • Registers a new Maven module (main/codegen) and publishes it via the BOM.
  • Introduces codegen model types and AkcesCodeGenerator for parsing definitions and generating command/event/state/aggregate sources.
  • Adds crypto-trading JSON definitions and TestNG tests validating generated output structure and filesystem writing.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
main/pom.xml Adds codegen to the main multi-module build.
bom/pom.xml Adds akces-codegen to dependency management for consumers.
main/codegen/pom.xml New module POM with Jackson 3 databind + TestNG; includes JSON test resources.
main/codegen/src/main/java/org/elasticsoftware/akces/codegen/AkcesCodeGenerator.java Core parser + generator for commands/events/state/aggregate scaffolding.
main/codegen/src/main/java/org/elasticsoftware/akces/codegen/GeneratedFile.java Simple value type representing generated sources.
main/codegen/src/main/java/org/elasticsoftware/akces/codegen/model/*.java JSON-mapped input model (event-modeling slices + Akces aggregate config).
main/codegen/src/test/java/org/elasticsoftware/akces/codegen/CryptoTradingCodeGenTest.java TestNG coverage validating generation for Account/Wallet and writing to disk.
main/codegen/src/test/resources/crypto-trading-account.json Example Account slice/config input for tests.
main/codegen/src/test/resources/crypto-trading-wallet.json Example Wallet slices/config input for tests.

String packageName,
Map<String, AggregateConfig> aggregateConfig,
List<Slice> slices
) {
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

EventModelDefinition doesn't defensively normalize aggregateConfig/slices when they are absent in JSON. AkcesCodeGenerator.groupByAggregate() iterates definition.slices() directly, so a missing/null slices field will cause an NPE. Consider adding a compact constructor that defaults aggregateConfig to Map.of() and slices to List.of() (and optionally validates packageName).

Suggested change
) {
) {
public EventModelDefinition {
if (aggregateConfig == null) {
aggregateConfig = Map.of();
}
if (slices == null) {
slices = List.of();
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +235
// Generate state record
if (config != null && !config.stateFields().isEmpty()) {
files.add(generateStateRecord(aggregatePackage, aggregateName, config));
}

// Generate aggregate class
files.add(generateAggregateClass(
aggregatePackage, aggregateName, config, slices,
allCommands, allSuccessEvents, allErrorEvents));

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

generateAggregate() only generates the state record when config != null && !config.stateFields().isEmpty(), but it always generates the aggregate class referencing <AggregateName>State. If an input JSON omits aggregateConfig (or just stateFields), the generated output won't compile because the state type is missing. Either always generate a minimal state record, or fail fast with a clear error when state config is absent for an aggregate.

Copilot uses AI. Check for mistakes.
Comment on lines +547 to +553
// Find events produced by this command (events in the same slice)
List<String> producesEvents = sliceData.successEvents().stream()
.map(e -> e.title() + "Event.class")
.toList();
List<String> errorEventNames = sliceData.errorEvents().stream()
.map(e -> e.title() + "Event.class")
.toList();
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

When generating @CommandHandler, the code currently assumes every command in a slice produces all success/error events from that slice (sliceData.successEvents() / sliceData.errorEvents()), and it ignores Element.dependencies. This will produce incorrect produces/errors lists as soon as a slice contains multiple commands or unrelated events. Consider using the dependencies to associate a command with its outbound events (or validate/enforce the invariant of a single command per slice and fail fast otherwise).

Suggested change
// Find events produced by this command (events in the same slice)
List<String> producesEvents = sliceData.successEvents().stream()
.map(e -> e.title() + "Event.class")
.toList();
List<String> errorEventNames = sliceData.errorEvents().stream()
.map(e -> e.title() + "Event.class")
.toList();
// Find events produced by this command.
// Prefer events whose dependencies reference this command; if none are found,
// fall back to all events in the slice to preserve existing behavior.
List<String> producesEvents = sliceData.successEvents().stream()
.filter(evt -> {
var deps = evt.dependencies();
var cmdId = cmd.id();
return deps != null && cmdId != null && deps.contains(cmdId);
})
.map(e -> e.title() + "Event.class")
.toList();
if (producesEvents.isEmpty()) {
producesEvents = sliceData.successEvents().stream()
.map(e -> e.title() + "Event.class")
.toList();
}
List<String> errorEventNames = sliceData.errorEvents().stream()
.filter(evt -> {
var deps = evt.dependencies();
var cmdId = cmd.id();
return deps != null && cmdId != null && deps.contains(cmdId);
})
.map(e -> e.title() + "Event.class")
.toList();
if (errorEventNames.isEmpty()) {
errorEventNames = sliceData.errorEvents().stream()
.map(e -> e.title() + "Event.class")
.toList();
}

Copilot uses AI. Check for mistakes.
Comment on lines +702 to +703
} else {
return "null /* " + sf.name() + " */";
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

generateStateConstructorArgsFromEvent() emits null /* field */ for any state field not present on the create event. This results in generated @EventSourcingHandler(create = true) code constructing a state object with null values even for fields annotated @NotNull, which is both misleading scaffolding and a likely runtime NPE. Prefer generating sensible defaults (e.g., empty collections) based on type/cardinality, or mark these arguments as TODOs without producing nulls for non-optional fields, or require the create event to supply all non-optional state fields.

Suggested change
} else {
return "null /* " + sf.name() + " */";
} else if (sf.optional()) {
// Optional state field: using null is acceptable scaffolding.
return "null /* " + sf.name() + " (optional) */";
} else {
// Non-optional state field is not present on create event:
// emit a TODO placeholder that fails at compile time so the
// developer must map this field explicitly instead of
// silently constructing a @NotNull field with null.
String placeholder =
"MISSING_REQUIRED_STATE_FIELD_" + sf.name().toUpperCase(Locale.ROOT);
return "/* TODO: initialize non-optional field '" + sf.name()
+ "' from event */ " + placeholder;

Copilot uses AI. Check for mistakes.
@jwijgerd
Copy link
Copy Markdown
Contributor

@copilot can you create another test that ouputs the files and then compiles them? to verify that the generated code actually compiles

- Add CompilationTest that generates Java source files from event-modeling
  JSON, writes them to disk, and compiles them using javax.tools.JavaCompiler
  to verify the generated code actually compiles
- Add akces-api test dependency to provide Akces interfaces and annotations
  on the test compilation classpath
- Fix generateCommandHandler to map event constructor args from the event's
  fields (not the command's fields), using typed default values for event
  fields not present in the command

Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/53fc3b58-a104-49f1-8c00-051a5c41fb69
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 26, 2026

@copilot can you create another test that ouputs the files and then compiles them? to verify that the generated code actually compiles

Added in 4532a42. CompilationTest generates files to a temp directory and compiles them using javax.tools.JavaCompiler with the Akces API on the classpath. Three tests: Account aggregate, Wallet aggregate, and both together to verify no naming collisions. Also fixed the event constructor arg generation — it now maps from the event's fields (not the command's) and uses typed defaults for fields not present in the command.

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
@jwijgerd
Copy link
Copy Markdown
Contributor

@copilot can you also create the java files in the target/generated-test-sources directory (which is managed by maven). Don't clean the files (this will be done by maven clean)

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 26, 2026

@copilot can you also create the java files in the target/generated-test-sources directory (which is managed by maven). Don't clean the files (this will be done by maven clean)

Done in 262e897. CompilationTest now writes generated Java files to target/generated-test-sources/akces-codegen instead of a temp directory, and no longer deletes them — mvn clean handles cleanup.

@jwijgerd
Copy link
Copy Markdown
Contributor

@copilot compile into the test-classes directory in the target folder

@jwijgerd
Copy link
Copy Markdown
Contributor

@copilot the code must be written in the root of the source folder (and not in the akces-codegen subfolder. Also the folder names must follow the package naming. i.e. com.example.aggregates = com/example/aggregates

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 26, 2026

@copilot the code must be written in the root of the source folder (and not in the akces-codegen subfolder. Also the folder names must follow the package naming. i.e. com.example.aggregates = `com/e...

Fixed in b3cfe20. Generated sources are now written directly to target/generated-test-sources/ (no akces-codegen subfolder). The directory structure follows the package naming: target/generated-test-sources/com/example/aggregates/account/commands/CreateAccountCommand.java.

@jwijgerd
Copy link
Copy Markdown
Contributor

@copilot you're missing the piiData annotation on the Command and Event definitions.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 26, 2026

@copilot you're missing the piiData annotation on the Command and Event definitions.

Fixed in 75a7756. The Field model now includes a piiData property, and the code generator emits @PIIData annotations on command and event record fields when piiData: true is set in the JSON definition. Updated the JSON schema, crypto-trading-account.json test resource, and tests to verify PII annotations on all three record types (command, event, state).

@jwijgerd
Copy link
Copy Markdown
Contributor

@copilot create a markdown document describing the differences between our json schema and the original schema of martin dilger found here: https://github.com/dilgerma/event-modeling-spec/blob/main/eventmodeling.schema.json

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 30, 2026

@copilot create a markdown document describing the differences between our json schema and the original schema of martin dilger found here: https://github.com/dilgerma/event-modeling-s...

Added in c040b01. main/codegen/SCHEMA_DIFFERENCES.md documents all differences: 2 new root-level properties (packageName, aggregateConfig), 2 new definitions (AggregateConfig, StateField), 1 modified definition (Field — added piiData), and lists the 9 definitions that remain identical to the original.

jwijgerd and others added 2 commits March 30, 2026 15:08
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Map<String, Element> allErrorEvents) {

String stateClassName = aggregateName + "State";
String aggregateLower = aggregatePackage.substring(aggregatePackage.lastIndexOf('.') + 1);
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.

3 participants