Commit 1b04d782 authored by Robert Newton's avatar Robert Newton
Browse files

Initial commit

parents
coverage.out
# Domain Events
This is a package with several implementations for handling Domain events. In [Domain Driven Design](http://www.martinfowler.com/eaaDev/DomainEvent.html), a domain event is a way of indicating that some event has happened that has relevance to the domain.
This specific package provides the interfaces for interacting with these events as a part of a larger application.
## Installation
`go get gitlab.com/rnewton/domain-events`
## Usage
This library is designed to take advantage of a Four-layered Architecture. The specific publishers/subscribers in-use are a infrastructure concern. These are injected into the application layer as dependencies and the domain layer handles the hooks for when events are triggered.
Infrastructure:
```go
func (c *Container) SomethingService() *application.SomethingService {
if c.somethingService == nil {
c.somethingService := &application.SomethingService{
SomethingRepository: c.SomethingRepository(),
EventPublisher: c.DomainEventsPublisher(),
}
}
return c.somethingService
}
```
Application Service:
```go
package application
import (
"gitlab.com/rnewton/domain-events"
)
type SomethingService struct {
SomethingRepository domain.SomethingRepository
EventPublisher events.Publisher
}
func (a *SomethingService) UpdateSomething(all, sorts, of, params string) error {
existing := a.SomethingRepository.SomethingOfIdentity(all)
existing.Sorts = sorts
existing.Of = of
return events.Update(a.EventPublisher, existing, a.SomethingRepository.SaveSomething)
}
```
Domain:
```go
package domain
import (
"time"
"gitlab.com/rnewton/domain-events"
)
type Something struct {
All string
Sorts string
Of string
Params string
}
func (s *Something) AfterUpdate() *events.Event {
return &events.Event{
UUID: "Something of significance",
CreatedAt: time.Now(),
Name: "SomethingUpdated",
Data: map[string]string{
"anything": "that we want to capture",
"can": "be reflected in this because",
"type": "is interface{}"
},
}
}
```
Entry point (background job):
```go
package main
import (
"fmt"
"infrastructure"
"gitlab.com/rnewton/domain-events"
)
func handleSomethingUpdated(event *events.Event) error {
fmt.Println("It's all good!")
return nil
}
func main() {
...
listener := container.DomainEventsListener()
listener.RegisterHandler("SomethingUpdated", handleSomethingUpdated)
listener.Listen()
}
```
In the example, the `SomethingUpdated` is triggered from the `Something` entity function, `AfterUpdate()`. The `SomethingRepository` function, `SaveSomething()` is _wrapped_ by `events.Update()` so that the `BeforeUpdate` and `AfterUpdate` hooks will trigger on either side of the repository function. In a background job, we can instantiate a listener to pick up the triggered events and handle them.
## Hooks
In order to have your entity trigger a domain event automatically, you need to implement the appropriate interface as below:
- `BeforeCreate() error`
- `AfterCreate() error`
- `BeforeUpdate() error`
- `AfterUpdate() error`
- `BeforeDelete() error`
- `AfterDelete() error`
And then in your application service use:
- `events.Create(events.Publisher, entity interface{}, events.RepositoryCreateHandler)`
- `events.Update(events.Publisher, entity interface{}, events.RepositoryUpdateHandler)`
- `events.Delete(events.Publisher, entity interface{}, events.RepositoryDeleteHandler)`
to wrap the repository persistence handlers:
- `func Create(entity domain.YourEntityType) error`
- `func Update(entity domain.YourEntityType) error`
- `func Delete(entity domain.YourEntityType) error`
// Package events provides interfaces for publishing and handling domain events in Go.
// In Domain Driven Design, a domain event is a way of indicating that some event has
// happened that has relevance to the domain.
package events
import "time"
// Event is a generic wrapper for all domain events
type Event struct {
UUID string `json:"uuid"`
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"`
Data interface{} `json:"data"`
}
package events
// BeforeCreateHook is used for entities to trigger events before being created
type BeforeCreateHook interface {
BeforeCreate() *Event
}
// AfterCreateHook is used for entities to trigger events after being created
type AfterCreateHook interface {
AfterCreate() *Event
}
// BeforeUpdateHook is used for entities to trigger events before being updated
type BeforeUpdateHook interface {
BeforeUpdate() *Event
}
// AfterUpdateHook is used for entities to trigger events after being updated
type AfterUpdateHook interface {
AfterUpdate() *Event
}
// BeforeDeleteHook is used for entities to trigger events before being deleted
type BeforeDeleteHook interface {
BeforeDelete() *Event
}
// AfterDeleteHook is used for entities to trigger events after being deleted
type AfterDeleteHook interface {
AfterDelete() *Event
}
// RepositoryCreateHandler is the function signature that a repository must use for handling create persistence
type RepositoryCreateHandler func(interface{}) error
// Create wraps the repository create handler such that events are published before and/or after the operation occurs
func Create(pub Publisher, entity interface{}, createHandler RepositoryCreateHandler) error {
if entity, ok := entity.(BeforeCreateHook); ok {
if event := entity.BeforeCreate(); event != nil {
if err := pub.Publish(event); err != nil {
return err
}
}
}
if err := createHandler(entity); err != nil {
return err
}
if entity, ok := entity.(AfterCreateHook); ok {
if event := entity.AfterCreate(); event != nil {
if err := pub.Publish(event); err != nil {
return err
}
}
}
return nil
}
// RepositoryUpdateHandler is the function signature that a repository must use for handling update persistence
type RepositoryUpdateHandler func(interface{}) error
// Update wraps the repository update handler such that events are published before and/or after the operation occurs
func Update(pub Publisher, entity interface{}, updateHandler RepositoryUpdateHandler) error {
if entity, ok := entity.(BeforeUpdateHook); ok {
if event := entity.BeforeUpdate(); event != nil {
if err := pub.Publish(event); err != nil {
return err
}
}
}
if err := updateHandler(entity); err != nil {
return err
}
if entity, ok := entity.(AfterUpdateHook); ok {
if event := entity.AfterUpdate(); event != nil {
if err := pub.Publish(event); err != nil {
return err
}
}
}
return nil
}
// RepositoryDeleteHandler is the function signature that a repository must use for handling delete persistence
type RepositoryDeleteHandler func(interface{}) error
// Delete wraps the repository delete handler such that events are published before and/or after the operation occurs
func Delete(pub Publisher, entity interface{}, deleteHandler RepositoryDeleteHandler) error {
if entity, ok := entity.(BeforeDeleteHook); ok {
if event := entity.BeforeDelete(); event != nil {
if err := pub.Publish(event); err != nil {
return err
}
}
}
if err := deleteHandler(entity); err != nil {
return err
}
if entity, ok := entity.(AfterDeleteHook); ok {
if event := entity.AfterDelete(); event != nil {
if err := pub.Publish(event); err != nil {
return err
}
}
}
return nil
}
package events
import (
"errors"
"strings"
"testing"
)
type testPublisher struct {
BeforeFail bool
AfterFail bool
Events map[string]*Event
}
func (p *testPublisher) Publish(event *Event) error {
p.Events[event.Name] = event
if p.BeforeFail && strings.Contains(event.Name, "Before") {
return errors.New("I was told to fail before")
}
if p.AfterFail && strings.Contains(event.Name, "After") {
return errors.New("I was told to fail after")
}
return nil
}
type testEntity struct{}
func (e *testEntity) BeforeCreate() *Event {
return &Event{Name: "BeforeCreateTestEvent"}
}
func (e *testEntity) AfterCreate() *Event {
return &Event{Name: "AfterCreateTestEvent"}
}
func (e *testEntity) BeforeUpdate() *Event {
return &Event{Name: "BeforeUpdateTestEvent"}
}
func (e *testEntity) AfterUpdate() *Event {
return &Event{Name: "AfterUpdateTestEvent"}
}
func (e *testEntity) BeforeDelete() *Event {
return &Event{Name: "BeforeDeleteTestEvent"}
}
func (e *testEntity) AfterDelete() *Event {
return &Event{Name: "AfterDeleteTestEvent"}
}
func testHandler(entity interface{}) error {
return nil
}
func failHandler(entity interface{}) error {
return errors.New("I was told to fail")
}
func TestBeforeCreateHookSucceed(t *testing.T) {
pub := &testPublisher{Events: map[string]*Event{}}
entity := &testEntity{}
err := Create(pub, entity, testHandler)
if err != nil {
t.Fatal("BeforeCreate should succeed. Got error.")
}
if _, ok := pub.Events["BeforeCreateTestEvent"]; !ok {
t.Fatal("Publisher should have received BeforeCreateTestEvent event.")
}
}
func TestAfterCreateHookSucceed(t *testing.T) {
pub := &testPublisher{Events: map[string]*Event{}}
entity := &testEntity{}
err := Create(pub, entity, testHandler)
if err != nil {
t.Fatal("AfterCreate should succeed. Got error.")
}
if _, ok := pub.Events["AfterCreateTestEvent"]; !ok {
t.Fatal("Publisher should have received AfterCreateTestEvent event.")
}
}
func TestBeforeCreateHookFail(t *testing.T) {
pub := &testPublisher{BeforeFail: true, Events: map[string]*Event{}}
entity := &testEntity{}
err := Create(pub, entity, testHandler)
if err == nil {
t.Fatal("BeforeCreate should fail. No error received.")
}
if _, ok := pub.Events["BeforeCreateTestEvent"]; !ok {
t.Fatal("Publisher should have received BeforeCreateTestEvent event.")
}
}
func TestAfterCreateHookFail(t *testing.T) {
pub := &testPublisher{AfterFail: true, Events: map[string]*Event{}}
entity := &testEntity{}
err := Create(pub, entity, testHandler)
if err == nil {
t.Fatal("AfterCreate should fail. No error received.")
}
if _, ok := pub.Events["AfterCreateTestEvent"]; !ok {
t.Fatal("Publisher should have received AfterCreateTestEvent event.")
}
}
func TestAfterCreateHandlerFail(t *testing.T) {
pub := &testPublisher{Events: map[string]*Event{}}
entity := &testEntity{}
err := Create(pub, entity, failHandler)
if err == nil {
t.Fatal("AfterCreate repo handler should fail. No error received.")
}
if _, ok := pub.Events["BeforeCreateTestEvent"]; !ok {
t.Fatal("Publisher should still have received BeforeCreateTestEvent event.")
}
if _, ok := pub.Events["AfterCreateTestEvent"]; ok {
t.Fatal("Publisher shouldn't have received AfterCreateTestEvent event.")
}
}
func TestBeforeUpdateHookSucceed(t *testing.T) {
pub := &testPublisher{Events: map[string]*Event{}}
entity := &testEntity{}
err := Update(pub, entity, testHandler)
if err != nil {
t.Fatal("BeforeUpdate should succeed. Got error.")
}
if _, ok := pub.Events["BeforeUpdateTestEvent"]; !ok {
t.Fatal("Publisher should have received BeforeUpdateTestEvent event.")
}
}
func TestAfterUpdateHookSucceed(t *testing.T) {
pub := &testPublisher{Events: map[string]*Event{}}
entity := &testEntity{}
err := Update(pub, entity, testHandler)
if err != nil {
t.Fatal("AfterUpdate should succeed. Got error.")
}
if _, ok := pub.Events["AfterUpdateTestEvent"]; !ok {
t.Fatal("Publisher should have received AfterUpdateTestEvent event.")
}
}
func TestBeforeUpdateHookFail(t *testing.T) {
pub := &testPublisher{BeforeFail: true, Events: map[string]*Event{}}
entity := &testEntity{}
err := Update(pub, entity, testHandler)
if err == nil {
t.Fatal("BeforeUpdate should fail. No error received.")
}
if _, ok := pub.Events["BeforeUpdateTestEvent"]; !ok {
t.Fatal("Publisher should have received BeforeUpdateTestEvent event.")
}
}
func TestAfterUpdateHookFail(t *testing.T) {
pub := &testPublisher{AfterFail: true, Events: map[string]*Event{}}
entity := &testEntity{}
err := Update(pub, entity, testHandler)
if err == nil {
t.Fatal("AfterUpdate should fail. No error received.")
}
if _, ok := pub.Events["AfterUpdateTestEvent"]; !ok {
t.Fatal("Publisher should have received AfterUpdateTestEvent event.")
}
}
func TestAfterUpdateHandlerFail(t *testing.T) {
pub := &testPublisher{Events: map[string]*Event{}}
entity := &testEntity{}
err := Update(pub, entity, failHandler)
if err == nil {
t.Fatal("AfterUpdate repo handler should fail. No error received.")
}
if _, ok := pub.Events["BeforeUpdateTestEvent"]; !ok {
t.Fatal("Publisher should still have received BeforeUpdateTestEvent event.")
}
if _, ok := pub.Events["AfterUpdateTestEvent"]; ok {
t.Fatal("Publisher shouldn't have received AfterUpdateTestEvent event.")
}
}
func TestBeforeDeleteHookSucceed(t *testing.T) {
pub := &testPublisher{Events: map[string]*Event{}}
entity := &testEntity{}
err := Delete(pub, entity, testHandler)
if err != nil {
t.Fatal("BeforeDelete should succeed. Got error.")
}
if _, ok := pub.Events["BeforeDeleteTestEvent"]; !ok {
t.Fatal("Publisher should have received BeforeDeleteTestEvent event.")
}
}
func TestAfterDeleteHookSucceed(t *testing.T) {
pub := &testPublisher{Events: map[string]*Event{}}
entity := &testEntity{}
err := Delete(pub, entity, testHandler)
if err != nil {
t.Fatal("AfterDelete should succeed. Got error.")
}
if _, ok := pub.Events["AfterDeleteTestEvent"]; !ok {
t.Fatal("Publisher should have received AfterDeleteTestEvent event.")
}
}
func TestBeforeDeleteHookFail(t *testing.T) {
pub := &testPublisher{BeforeFail: true, Events: map[string]*Event{}}
entity := &testEntity{}
err := Delete(pub, entity, testHandler)
if err == nil {
t.Fatal("BeforeDelete should fail. No error received.")
}
if _, ok := pub.Events["BeforeDeleteTestEvent"]; !ok {
t.Fatal("Publisher should have received BeforeDeleteTestEvent event.")
}
}
func TestAfterDeleteHookFail(t *testing.T) {
pub := &testPublisher{AfterFail: true, Events: map[string]*Event{}}
entity := &testEntity{}
err := Delete(pub, entity, testHandler)
if err == nil {
t.Fatal("AfterDelete should fail. No error received.")
}
if _, ok := pub.Events["AfterDeleteTestEvent"]; !ok {
t.Fatal("Publisher should have received AfterDeleteTestEvent event.")
}
}
func TestAfterDeleteHandlerFail(t *testing.T) {
pub := &testPublisher{Events: map[string]*Event{}}
entity := &testEntity{}
err := Delete(pub, entity, failHandler)
if err == nil {
t.Fatal("AfterDelete repo handler should fail. No error received.")
}
if _, ok := pub.Events["BeforeDeleteTestEvent"]; !ok {
t.Fatal("Publisher should still have received BeforeDeleteTestEvent event.")
}
if _, ok := pub.Events["AfterDeleteTestEvent"]; ok {
t.Fatal("Publisher shouldn't have received AfterDeleteTestEvent event.")
}
}
package events
// EventHandler defines the function signature required for handling events
type EventHandler func(*Event) error
// Listener defines the interface required for listening for new events and handling them
type Listener interface {
// RegisterHandler registers an event handler for events of the given string name
RegisterHandler(string, EventHandler)
// Listen spawns a non-blocking goroutine to listen for new events
Listen()
// Halt stops the listen goroutine
Halt()
}
package events
// Publisher describes the interface for pushing events
type Publisher interface {
// Publish pushes out the given event to the configured messaging interface
Publish(*Event) error
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment