Commit e9992ad2 authored by Robert Newton's avatar Robert Newton
Browse files

Update readme and fix bin path. Remove repo method wrappers

parent 10b2c3db
# Domain Events
[![GoDoc](https://godoc.org/gitlab.com/rnewton/domain-events?status.svg)](https://godoc.org/gitlab.com/rnewton/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.
......@@ -12,45 +14,18 @@ This specific package provides the interfaces for interacting with these events
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
}
## Hooks
func (a *SomethingService) UpdateSomething(all, sorts, of, params string) error {
existing := a.SomethingRepository.SomethingOfIdentity(all)
existing.Sorts = sorts
existing.Of = of
In order to have your entity trigger a domain event automatically, you need to implement the appropriate interface as below:
return events.Update(a.EventPublisher, existing, a.SomethingRepository.SaveSomething)
}
```
- `BeforeCreate() *events.Event`
- `AfterCreate() *events.Event`
- `BeforeUpdate() *events.Event`
- `AfterUpdate() *events.Event`
- `BeforeDelete() *events.Event`
- `AfterDelete() *events.Event`
Domain:
It'll look something like:
```go
package domain
......@@ -82,7 +57,32 @@ func (s *Something) AfterUpdate() *events.Event {
}
```
Entry point (background job):
Then generate the repository using the `evt` tool.
```
evt -e domain/model/something/something.go -r infrastructure/persistence/db/something_repository_gen.go
```
It'll create a `SomethingRepository` for you that automatically hooks up the change hooks. In your service locator, instantiate the repository you generated:
```go
func (c *Container) SomethingRepository() *db.SomethingRepository {
if c.somethingRepo == nil {
c.somethingRepo := &db.SomethingRepository{
DB: c.Database(),
Pub: c.DomainEventsPublisher(),
}
}
return c.somethingRepo
}
```
And when you call `SomethingRepository.Save(*Something)`, it'll automatically call `*Something.AfterUpdate` for you. It does this _without_ reflection because you've already generated the code. This means it's very quick!
The generated file is overwritten every time you run `evt` so it shouldn't be modified. If you need your repository to have custom behavior, create another file that implements the new methods you need. The generated repository is exported and can be extended with new `func (r *SomethingRepository) YourMethod` definitions.
Listen for the events elsewhere. Say a background job cli:
```go
package main
......@@ -107,28 +107,3 @@ func main() {
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`
......@@ -35,7 +35,7 @@ import (
// WARNING: This is an autogenerated file. DO NOT MODIFY. If you need to add custom functionality
// to this repository, create a new file and add your functions using the same receiver
// func (r *{{ .AggregateEntity }}Repository) YourCustomFunction() { ... }
// func (r *{{ .AggregateEntity }}Repository) YourCustomMethod() { ... }
// {{ .AggregateEntity }}Repository handles database persistence for {{ .AggregateEntity }} entities
type {{ .AggregateEntity }}Repository struct {
......
package main
import "gitlab.com/rnewton/domain-events/tools/cmd"
import "gitlab.com/rnewton/domain-events/evt/cmd"
func main() {
cmd.Execute()
......
......@@ -29,87 +29,3 @@ type BeforeDeleteHook interface {
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.")
}
}
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