Search code examples
goevent-sourcing

How to generate Golang code from bytecode of some kind close to pseudocode programming languages in the domain of Event-Sourcing


I have many services, and each service has an entity that has the same interface:

type Entity interface{
  ApplyEvent(evt Event)
}

type Event interface{
  ID() ID
  Version() int
  Body() any
}

Each service owns only its own entities, however, because of this, I cannot access another entity unless I duplicate the code or import entities from the repository for another service.

Moreover, the ApplyEvent method simply changes the fields of the entity. For example

type Person struct{
  Name string
  Age int
}


func (p *Person) ApplyEvent(e Event){
  evt:= e.Body().(NameChangedEvent)
  p.Name = evt.NewName
}

In other words, the logic is always very simple and does not require concurrency execution.

I had the idea to try to study the use of some statically typed object-oriented languages adapted to this domain, or, if there are none, to twist the existing ones. Something like Solidity for smart contracts.

 pragma ...;


// Creating a aggregate
aggregate Person {

    name string;
    age int;
    
    function applyNameChanged(evt NameChangedEvent) public {
        name = evt.name
    };
}

// creating a event
event NameChangedEvent{
  name string;
}

This code is then compiled and Golang code is generated from its byte code.

Then tell me in which direction it is worth moving in this approach. Are there any ready-made solutions for this?


Solution

  • You can generate Go code for Event Sourcing using a simple DSL or code templates. One easy way is to create a config file (JSON, YAML) that defines your entities and events, then use Go's text/template package to generate the ApplyEvent methods automatically. You can run this with go generate during the build process. If you want more control, try to create a custom parser using tools like ANTLR or Pigeon, this will reduce code duplication and will keep services clean and maintainable.

    To generate folders like following:

    1. ./generated/person.go containing the Person aggregate with its event handlers
    2. ./generated/namechangedevent_event.go for the NameChangedEvent
    3. ./generated/agechangedevent_event.go for the AgeChangedEvent

    we will define DSL:

    package parser
    
    // Grammar for Event Sourcing DSL
    
    {
        type Aggregate struct {
            Name string
            Fields []Field
            EventHandlers []EventHandler
        }
    
        type Field struct {
            Name string
            Type string
        }
    
        type Event struct {
            Name string
            Fields []Field
        }
    
        type EventHandler struct {
            EventName string
            Assignments []Assignment
        }
    
        type Assignment struct {
            FieldName string
            EventField string
        }
    }
    
    Program <- ws aggs:AggregateDefinition* events:EventDefinition* EOF {
        return map[string]interface{}{
            "aggregates": aggs,
            "events": events,
        }, nil
    }
    
    AggregateDefinition <- "aggregate" ws name:Identifier ws '{' ws
        fields:FieldDefinition* 
        handlers:EventHandlerDefinition*
        ws '}' ws {
        
        agg := &Aggregate{Name: name.(string), Fields: []Field{}, EventHandlers: []EventHandler{}}
        
        if fields != nil {
            for _, f := range fields.([]interface{}) {
                agg.Fields = append(agg.Fields, f.(Field))
            }
        }
        
        if handlers != nil {
            for _, h := range handlers.([]interface{}) {
                agg.EventHandlers = append(agg.EventHandlers, h.(EventHandler))
            }
        }
        
        return agg, nil
    }
    
    EventDefinition <- "event" ws name:Identifier ws '{' ws
        fields:FieldDefinition*
        ws '}' ws {
        
        evt := &Event{Name: name.(string), Fields: []Field{}}
        
        if fields != nil {
            for _, f := range fields.([]interface{}) {
                evt.Fields = append(evt.Fields, f.(Field))
            }
        }
        
        return evt, nil
    }
    
    FieldDefinition <- name:Identifier ws type:Identifier ws ';' ws {
        return Field{Name: name.(string), Type: type.(string)}, nil
    }
    
    EventHandlerDefinition <- "apply" ws name:Identifier ws '{' ws
        assignments:AssignmentStatement*
        ws '}' ws {
        
        handler := EventHandler{EventName: name.(string), Assignments: []Assignment{}}
        
        if assignments != nil {
            for _, a := range assignments.([]interface{}) {
                handler.Assignments = append(handler.Assignments, a.(Assignment))
            }
        }
        
        return handler, nil
    }
    
    AssignmentStatement <- field:Identifier ws '=' ws eventField:QualifiedIdentifier ws ';' ws {
        return Assignment{FieldName: field.(string), EventField: eventField.(string)}, nil
    }
    
    QualifiedIdentifier <- parts:('evt.' Identifier) {
        return parts.([]interface{})[1].(string), nil
    }
    
    Identifier <- [a-zA-Z][a-zA-Z0-9]* {
        return string(c.text), nil
    }
    
    ws <- [ \t\n\r]* {
        return nil, nil
    }
    
    EOF <- !. {
        return nil, nil
    }
    

    Code generator for the parsed AST

    package generator
    
    import (
        "bytes"
        "fmt"
        "strings"
        "text/template"
    )
    
    // Templates for code generation
    const aggregateTemplate = `
    // Code generated - DO NOT EDIT.
    
    package {{.Package}}
    
    // {{.Name}} is an aggregate root
    type {{.Name}} struct {
        {{range .Fields}}{{.Name}} {{.Type}}
        {{end}}
    }
    
    {{range .EventHandlers}}
    // Apply{{.EventName}} handles the {{.EventName}}
    func (a *{{$.Name}}) Apply{{.EventName}}(evt {{.EventName}}) {
        {{range .Assignments}}a.{{.FieldName}} = evt.{{.EventField}}
        {{end}}
    }
    {{end}}
    
    // ApplyEvent implements the Entity interface
    func (a *{{.Name}}) ApplyEvent(e Event) {
        switch evt := e.Body().(type) {
        {{range .EventHandlers}}case {{.EventName}}:
            a.Apply{{.EventName}}(evt)
        {{end}}
        }
    }
    `
    
    const eventTemplate = `
    // Code generated - DO NOT EDIT.
    
    package {{.Package}}
    
    // {{.Name}} represents an event
    type {{.Name}} struct {
        {{range .Fields}}{{.Name}} {{.Type}}
        {{end}}
        id ID
        version int
    }
    
    // ID returns the event's ID
    func (e {{.Name}}) ID() ID {
        return e.id
    }
    
    // Version returns the event's version
    func (e {{.Name}}) Version() int {
        return e.version
    }
    
    // Body implements the Event interface
    func (e {{.Name}}) Body() any {
        return e
    }
    `
    
    // GenerateCode produces Go code from the parsed DSL
    func GenerateCode(packageName string, parsedResult map[string]interface{}) (map[string]string, error) {
        result := make(map[string]string)
        
        // Generate aggregate code
        for _, agg := range parsedResult["aggregates"].([]interface{}) {
            aggregate := agg.(*Aggregate)
            
            data := struct {
                Package string
                *Aggregate
            }{
                Package: packageName,
                Aggregate: aggregate,
            }
            
            buf := new(bytes.Buffer)
            tmpl := template.Must(template.New("aggregate").Parse(aggregateTemplate))
            if err := tmpl.Execute(buf, data); err != nil {
                return nil, err
            }
            
            result[fmt.Sprintf("%s.go", strings.ToLower(aggregate.Name))] = buf.String()
        }
        
        // Generate event code
        for _, evt := range parsedResult["events"].([]interface{}) {
            event := evt.(*Event)
            
            data := struct {
                Package string
                *Event
            }{
                Package: packageName,
                Event: event,
            }
            
            buf := new(bytes.Buffer)
            tmpl := template.Must(template.New("event").Parse(eventTemplate))
            if err := tmpl.Execute(buf, data); err != nil {
                return nil, err
            }
            
            result[fmt.Sprintf("%s_event.go", strings.ToLower(event.Name))] = buf.String()
        }
        
        return result, nil
    }
    

    Abstract main.go or main functional code where it used:

    // -----
        if len(os.Args) < 3 {
            fmt.Println("Usage: esgen <input.es> <output-dir> [package-name]")
            os.Exit(1)
        }
        
        inputFile := os.Args[1]
        outputDir := os.Args[2]
        packageName := "generated"
        
        if len(os.Args) > 3 {
            packageName = os.Args[3]
        }
        
        // reading the input file
        content, err := ioutil.ReadFile(inputFile)
        if err != nil {
            log.Fatalf("Failed to read input file: %v", err)
        }
        
        // parsing the DSL
        parsed, err := parser.Parse("", content)
        if err != nil {
            log.Fatalf("Failed to parse input: %v", err)
        }
        
        // code generation
        files, err := generator.GenerateCode(packageName, parsed.(map[string]interface{}))
        if err != nil {
            log.Fatalf("Failed to generate code: %v", err)
        }
        
        // output directory creation if doesn't exist
        if err := os.MkdirAll(outputDir, 0755); err != nil {
            log.Fatalf("Failed to create output directory: %v", err)
        }
        
        // writing generated files
        for filename, content := range files {
            outputPath := filepath.Join(outputDir, filename)
            if err := ioutil.WriteFile(outputPath, []byte(content), 0644); err != nil {
                log.Fatalf("Failed to write output file %s: %v", outputPath, err)
            }
            fmt.Printf("Generated: %s\n", outputPath)
        }
    // ------
    

    To use DSL create person.es file:

    aggregate Person {
        name string;
        age int;
        
        apply NameChangedEvent {
            name = evt.newName;
        }
        
        apply AgeChangedEvent {
            age = evt.newAge;
        }
    }
    
    event NameChangedEvent {
        newName string;
    }
    
    event AgeChangedEvent {
        newAge int;
    }
    

    Code execution:

    go run main.go person.es ./generated person