Skip to content

Event Model DSL: Event ADT auto-generation via Template Haskell #577

@NickSeagull

Description

@NickSeagull

Parent Issue

Part of #573 — Event Model DSL

Summary

Implement Template Haskell that, given a list of individual event types per entity, auto-generates:

  • The sum type wrapper (Event ADT)
  • EventOf type family instance
  • EventVariantOf instances per variant
  • JSON instances (FromJSON/ToJSON)
  • The Event typeclass instance

Motivation

With events-as-types (each event is its own type localized to its slice), the Event ADT becomes pure boilerplate. Auto-generating it from the event model eliminates manual wiring and ensures the ADT stays in sync with the declared events.

Without this, developers must manually maintain a growing sum type and keep it in sync with every new event — a tedious and error-prone process that defeats the purpose of a clean event model DSL.

Scope

  • TH function (e.g., deriveEntityEvents ''Proposal [''PdfUploaded, ''EvaluationTriggered])
  • Generates data ProposalEvent = ProposalEventPdfUploaded PdfUploaded | ...
  • Generates type instance EventOf Proposal = ProposalEvent
  • Generates EventVariantOf instances per variant
  • Generates Event instance (routing getEventEntityIdImpl through each variant)
  • Generates FromJSON/ToJSON instances for the ADT

Key Code Example

-- Developer writes individual event types:
data PdfUploaded = PdfUploaded { entityId :: Uuid, pdfRef :: FileRef }
  deriving (Generic)
data EvaluationTriggered = EvaluationTriggered { entityId :: Uuid }
  deriving (Generic)

-- TH generates everything:
deriveEntityEvents ''Proposal [''PdfUploaded, ''EvaluationTriggered]

The single deriveEntityEvents call replaces what would otherwise be ~30–50 lines of repetitive boilerplate per entity.

Dependencies

Affected Files

  • New TH module in core/service/ (e.g., core/service/Service/EventModel/TH.hs)
  • Entity definition files that call deriveEntityEvents

Implementation Hints

  • Use Language.Haskell.TH to splice declarations at compile time
  • reify each event type name to extract field info for routing getEventEntityIdImpl
  • Generate constructor names by prefixing the entity name (e.g., ProposalEvent + PdfUploadedProposalEventPdfUploaded)
  • Use deriveJSON from aeson-th or generate FromJSON/ToJSON manually via TH for the sum type
  • The EventVariantOf instance per variant just needs to map the variant constructor back to the original type

Acceptance Criteria

  • deriveEntityEvents TH function is implemented and exported
  • Generated ADT compiles and type-checks with GHC
  • EventOf type family instance is correctly generated
  • EventVariantOf instances are generated for each variant
  • Event typeclass instance correctly routes getEventEntityIdImpl
  • FromJSON/ToJSON instances work for round-trip serialization
  • Unit tests cover the generated code behaviour
  • hlint passes on the new module

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions