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 + PdfUploaded → ProposalEventPdfUploaded)
- 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
Parent Issue
Part of #573 — Event Model DSL
Summary
Implement Template Haskell that, given a list of individual event types per entity, auto-generates:
EventOftype family instanceEventVariantOfinstances per variantFromJSON/ToJSON)Eventtypeclass instanceMotivation
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
deriveEntityEvents ''Proposal [''PdfUploaded, ''EvaluationTriggered])data ProposalEvent = ProposalEventPdfUploaded PdfUploaded | ...type instance EventOf Proposal = ProposalEventEventVariantOfinstances per variantEventinstance (routinggetEventEntityIdImplthrough each variant)FromJSON/ToJSONinstances for the ADTKey Code Example
The single
deriveEntityEventscall replaces what would otherwise be ~30–50 lines of repetitive boilerplate per entity.Dependencies
EventVariantOftypeclass subtask (part of Event Model DSL: textual representation for Event Models #573)Affected Files
core/service/(e.g.,core/service/Service/EventModel/TH.hs)deriveEntityEventsImplementation Hints
Language.Haskell.THto splice declarations at compile timereifyeach event type name to extract field info for routinggetEventEntityIdImplProposalEvent+PdfUploaded→ProposalEventPdfUploaded)deriveJSONfromaeson-thor generateFromJSON/ToJSONmanually via TH for the sum typeEventVariantOfinstance per variant just needs to map the variant constructor back to the original typeAcceptance Criteria
deriveEntityEventsTH function is implemented and exportedEventOftype family instance is correctly generatedEventVariantOfinstances are generated for each variantEventtypeclass instance correctly routesgetEventEntityIdImplFromJSON/ToJSONinstances work for round-trip serializationhlintpasses on the new module