Add CQRS library 'Singularity' with core handlers and tests
All checks were successful
Go CI/CD / go-ci (push) Successful in 36s
All checks were successful
Go CI/CD / go-ci (push) Successful in 36s
Introduces the 'Singularity' implementation, a CQRS Bus that supports commands, queries, and events, along with middleware extensibility. Includes comprehensive tests, modular files for commands, queries, and events, as well as CI/CD workflows.
This commit is contained in:
parent
0708376c0b
commit
bc3c5aa846
30
.gitea/workflows/ci-basic.yml
Normal file
30
.gitea/workflows/ci-basic.yml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: Go CI/CD
|
||||||
|
run-name: ${{ github.actor }} is running CI/CD basic
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- main
|
||||||
|
- release/**
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
branches-ignore:
|
||||||
|
- main
|
||||||
|
- release/**
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
go-ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GOPROXY: https://proxy.golang.org,direct
|
||||||
|
GOPRIVATE: gitstormr.dev
|
||||||
|
GONOSUMDB: gitstormr.dev
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: https://gitstormr.dev/actions/go-ci@v1.1.1
|
||||||
|
with:
|
||||||
|
workflow-type: 'basic'
|
||||||
|
go-version: '1.24'
|
||||||
|
build-type: 'library'
|
||||||
|
publish-docker: 'false'
|
25
.gitea/workflows/ci-protected-pr.yml
Normal file
25
.gitea/workflows/ci-protected-pr.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: Go CI/CD
|
||||||
|
run-name: ${{ github.actor }} is running CI/CD protected
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- release/**
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
go-ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GOPROXY: https://proxy.golang.org,direct
|
||||||
|
GOPRIVATE: gitstormr.dev
|
||||||
|
GONOSUMDB: gitstormr.dev
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: https://gitstormr.dev/actions/go-ci@v1.1.1
|
||||||
|
with:
|
||||||
|
workflow-type: 'basic'
|
||||||
|
go-version: '1.24'
|
||||||
|
build-type: 'library'
|
||||||
|
publish-docker: 'false'
|
25
.gitea/workflows/ci-protected-push.yml
Normal file
25
.gitea/workflows/ci-protected-push.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: Go CI/CD
|
||||||
|
run-name: ${{ github.actor }} is running CI/CD protected
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- release/**
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
go-ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GOPROXY: https://proxy.golang.org,direct
|
||||||
|
GOPRIVATE: gitstormr.dev
|
||||||
|
GONOSUMDB: gitstormr.dev
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: https://gitstormr.dev/actions/go-ci@v1.1.1
|
||||||
|
with:
|
||||||
|
workflow-type: 'protected'
|
||||||
|
go-version: '1.24'
|
||||||
|
build-type: 'library'
|
||||||
|
publish-docker: 'false'
|
23
.gitea/workflows/ci-tags.yml
Normal file
23
.gitea/workflows/ci-tags.yml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: Go CI/CD
|
||||||
|
run-name: ${{ github.actor }} is running CI/CD Tag
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
go-ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GOPROXY: https://proxy.golang.org,direct
|
||||||
|
GOPRIVATE: gitstormr.dev
|
||||||
|
GONOSUMDB: gitstormr.dev
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: https://gitstormr.dev/actions/go-ci@v1.1.1
|
||||||
|
with:
|
||||||
|
workflow-type: 'tag'
|
||||||
|
go-version: '1.24'
|
||||||
|
build-type: 'library'
|
||||||
|
publish-docker: 'false'
|
67
.gitignore
vendored
Normal file
67
.gitignore
vendored
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
bin
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the 'go tool cover' command
|
||||||
|
*.out
|
||||||
|
coverage.xml
|
||||||
|
test-report.xml
|
||||||
|
|
||||||
|
# Directory for Go modules
|
||||||
|
/vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# Editor configs
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
|
*.viminfo
|
||||||
|
*.un~
|
||||||
|
Session.vim
|
||||||
|
|
||||||
|
# JetBrains Rider specific
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/shelf/
|
||||||
|
|
||||||
|
# Sublime Text specific
|
||||||
|
*.sublime-workspace
|
||||||
|
*.sublime-project
|
||||||
|
|
||||||
|
# VSCode specific
|
||||||
|
.vscode/
|
||||||
|
.vscode/settings.json
|
||||||
|
.vscode/tasks.json
|
||||||
|
.vscode/launch.json
|
||||||
|
|
||||||
|
# Emacs specific
|
||||||
|
*~
|
||||||
|
\#*\#
|
||||||
|
.#*
|
||||||
|
|
||||||
|
# MacOS specific
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Node modules (in case of tools/scripts)
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Python virtual environments (for dev tools/scripts)
|
||||||
|
venv/
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
# Config file
|
||||||
|
config.json
|
49
README.md
49
README.md
@ -1,3 +1,48 @@
|
|||||||
# gravity-wells
|
# Gravity Wells
|
||||||
|
|
||||||
Lightweight CQRS and Event Bus forged with the gravity of domain-driven design. Commands bend reality. Queries catch light. Events echo through time.
|
_Forge gravity into your code. Shape time, distort space, and carve legacy into the architecture of tomorrow._
|
||||||
|
|
||||||
|
**Gravity Wells** is a lightweight CQRS and Event-Driven library, built to resonate with the spirit of domain-driven architectures.
|
||||||
|
At its core lies the **Singularity** — a gravitational nexus where commands, queries, and events intertwine, bending the causal fabric of your systems.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Philosophy
|
||||||
|
|
||||||
|
- **Commands** bend domain realities.
|
||||||
|
- **Queries** capture the light escaping from the core.
|
||||||
|
- **Events** ripple as gravitational waves across time.
|
||||||
|
|
||||||
|
Gravity Wells does not impose structures — it unlocks potential.
|
||||||
|
It does not dictate architectures — it forges paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Core Principles
|
||||||
|
|
||||||
|
- **Hexagonal Architecture (Ports & Adapters) friendly**
|
||||||
|
- **Built for CQRS, Event Sourcing, and Saga orchestration**
|
||||||
|
- **Extensible through Command, Query, and Event Middlewares**
|
||||||
|
- **Zero external dependencies: pure minimalism**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🪐 Infinite Expansion
|
||||||
|
|
||||||
|
Gravity Wells is only the first fragment of a greater constellation:
|
||||||
|
|
||||||
|
- `gravitywells/sagas`: Saga orchestrators, weaving constellations of process.
|
||||||
|
- `gravitywells/observatory`: Observability to record the curvatures of events.
|
||||||
|
- `gravitywells/eventstorm`: A DSL for mapping event storms across untamed domains.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> _Commands are gravity wells.
|
||||||
|
> Queries are photons escaping.
|
||||||
|
> Events are echoes traveling eternally across the fabric of systems._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📜 License
|
||||||
|
|
||||||
|
**MIT** — because knowledge, like gravity, belongs to everyone.
|
17
cqrs/bus.go
Normal file
17
cqrs/bus.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package cqrs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bus interface {
|
||||||
|
RegisterCommandHandler(cmd Command, handler CommandHandler)
|
||||||
|
RegisterQueryHandler(query Query, handler QueryHandler)
|
||||||
|
RegisterEventHandler(event Event, handler EventHandler)
|
||||||
|
UseCommandMiddleware(middleware CommandMiddleware)
|
||||||
|
UseQueryMiddleware(middleware QueryMiddleware)
|
||||||
|
UseEventMiddleware(middleware EventMiddleware)
|
||||||
|
ExecuteCommand(ctx context.Context, cmd Command) ([]Event, error)
|
||||||
|
ExecuteQuery(ctx context.Context, query Query) (interface{}, error)
|
||||||
|
EmitEvent(ctx context.Context, event Event) error
|
||||||
|
}
|
21
cqrs/command.go
Normal file
21
cqrs/command.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package cqrs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Command interface{}
|
||||||
|
|
||||||
|
type CommandMiddleware func(CommandHandler) CommandHandler
|
||||||
|
|
||||||
|
type CommandHandlerFunc func(ctx context.Context, cmd Command) ([]Event, error)
|
||||||
|
|
||||||
|
func (f CommandHandlerFunc) Handle(ctx context.Context, cmd Command) (
|
||||||
|
[]Event, error,
|
||||||
|
) {
|
||||||
|
return f(ctx, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandHandler interface {
|
||||||
|
Handle(ctx context.Context, cmd Command) ([]Event, error)
|
||||||
|
}
|
19
cqrs/event.go
Normal file
19
cqrs/event.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package cqrs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event interface{}
|
||||||
|
|
||||||
|
type EventMiddleware func(EventHandler) EventHandler
|
||||||
|
|
||||||
|
type EventHandlerFunc func(ctx context.Context, event Event) error
|
||||||
|
|
||||||
|
func (f EventHandlerFunc) Handle(ctx context.Context, event Event) error {
|
||||||
|
return f(ctx, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventHandler interface {
|
||||||
|
Handle(ctx context.Context, event Event) error
|
||||||
|
}
|
21
cqrs/query.go
Normal file
21
cqrs/query.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package cqrs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Query interface{}
|
||||||
|
|
||||||
|
type QueryHandler interface {
|
||||||
|
Handle(ctx context.Context, query Query) (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryHandlerFunc func(ctx context.Context, query Query) (Query, error)
|
||||||
|
|
||||||
|
func (f QueryHandlerFunc) Handle(ctx context.Context, query Query) (
|
||||||
|
interface{}, error,
|
||||||
|
) {
|
||||||
|
return f(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryMiddleware func(QueryHandler) QueryHandler
|
193
cqrs/singularity.go
Normal file
193
cqrs/singularity.go
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
package cqrs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type singularity struct {
|
||||||
|
commandHandlers map[string]CommandHandler
|
||||||
|
queryHandlers map[string]QueryHandler
|
||||||
|
eventHandlers map[string][]EventHandler
|
||||||
|
commandMiddlewares []CommandMiddleware
|
||||||
|
queryMiddlewares []QueryMiddleware
|
||||||
|
eventMiddlewares []EventMiddleware
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSingularity() Bus {
|
||||||
|
return &singularity{
|
||||||
|
commandHandlers: make(map[string]CommandHandler),
|
||||||
|
queryHandlers: make(map[string]QueryHandler),
|
||||||
|
eventHandlers: make(map[string][]EventHandler),
|
||||||
|
commandMiddlewares: make([]CommandMiddleware, 0),
|
||||||
|
queryMiddlewares: make([]QueryMiddleware, 0),
|
||||||
|
eventMiddlewares: make([]EventMiddleware, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) RegisterCommandHandler(
|
||||||
|
cmd Command, handler CommandHandler,
|
||||||
|
) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
handler = applyCommandMiddleware(handler, s.commandMiddlewares)
|
||||||
|
|
||||||
|
s.commandHandlers[typeName(cmd)] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) RegisterQueryHandler(query Query, handler QueryHandler) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
handler = applyQueryMiddleware(handler, s.queryMiddlewares)
|
||||||
|
|
||||||
|
s.queryHandlers[typeName(query)] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) RegisterEventHandler(event Event, handler EventHandler) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
handler = applyEventMiddleware(handler, s.eventMiddlewares)
|
||||||
|
|
||||||
|
name := typeName(event)
|
||||||
|
s.eventHandlers[name] = append(s.eventHandlers[name], handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) UseCommandMiddleware(middleware CommandMiddleware) {
|
||||||
|
s.commandMiddlewares = append(s.commandMiddlewares, middleware)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) UseQueryMiddleware(middleware QueryMiddleware) {
|
||||||
|
s.queryMiddlewares = append(s.queryMiddlewares, middleware)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) UseEventMiddleware(middleware EventMiddleware) {
|
||||||
|
s.eventMiddlewares = append(s.eventMiddlewares, middleware)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) ExecuteCommand(ctx context.Context, cmd Command) (
|
||||||
|
[]Event, error,
|
||||||
|
) {
|
||||||
|
handler, err := s.getCommandHandler(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
events, err := handler.Handle(ctx, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(events) > 0 {
|
||||||
|
if err = s.emitEvents(ctx, events...); err != nil {
|
||||||
|
return events, fmt.Errorf(
|
||||||
|
"command succeeded, but dispatching resulting events failed: %v",
|
||||||
|
typeName(cmd),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) ExecuteQuery(
|
||||||
|
ctx context.Context, query Query,
|
||||||
|
) (interface{}, error) {
|
||||||
|
handler, err := s.getQueryHandler(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return handler.Handle(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) EmitEvent(ctx context.Context, event Event) error {
|
||||||
|
handlers := s.getEventHandlers(event)
|
||||||
|
if len(handlers) == 0 {
|
||||||
|
log.Printf("[RuneBringer] No handler listening for event: %T", event)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, handler := range handlers {
|
||||||
|
if err := handler.Handle(ctx, event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) emitEvents(ctx context.Context, events ...Event) error {
|
||||||
|
for _, event := range events {
|
||||||
|
if err := s.EmitEvent(ctx, event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) getCommandHandler(cmd Command) (CommandHandler, error) {
|
||||||
|
s.mutex.RLock()
|
||||||
|
defer s.mutex.RUnlock()
|
||||||
|
handler, exists := s.commandHandlers[typeName(cmd)]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"no command handler registered for command type '%v'",
|
||||||
|
typeName(cmd),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return handler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) getQueryHandler(query Query) (QueryHandler, error) {
|
||||||
|
s.mutex.RLock()
|
||||||
|
defer s.mutex.RUnlock()
|
||||||
|
handler, exists := s.queryHandlers[typeName(query)]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"no query handler registered for query type '%v'", typeName(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return handler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singularity) getEventHandlers(event Event) []EventHandler {
|
||||||
|
s.mutex.RLock()
|
||||||
|
defer s.mutex.RUnlock()
|
||||||
|
return s.eventHandlers[typeName(event)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeName(v interface{}) string {
|
||||||
|
t := reflect.TypeOf(v)
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
return t.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyCommandMiddleware(
|
||||||
|
handler CommandHandler, middlewares []CommandMiddleware,
|
||||||
|
) CommandHandler {
|
||||||
|
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||||
|
handler = middlewares[i](handler)
|
||||||
|
}
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyQueryMiddleware(
|
||||||
|
handler QueryHandler, middlewares []QueryMiddleware,
|
||||||
|
) QueryHandler {
|
||||||
|
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||||
|
handler = middlewares[i](handler)
|
||||||
|
}
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyEventMiddleware(
|
||||||
|
handler EventHandler, middlewares []EventMiddleware,
|
||||||
|
) EventHandler {
|
||||||
|
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||||
|
handler = middlewares[i](handler)
|
||||||
|
}
|
||||||
|
return handler
|
||||||
|
}
|
273
cqrs/singularity_test.go
Normal file
273
cqrs/singularity_test.go
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
package cqrs_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitstormr.dev/code-raider/gravity-wells/cqrs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testEvent struct {
|
||||||
|
Echo string
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCommand struct {
|
||||||
|
Echo string
|
||||||
|
}
|
||||||
|
|
||||||
|
type testQuery struct {
|
||||||
|
Echo string
|
||||||
|
}
|
||||||
|
|
||||||
|
type successCommandHandler struct {
|
||||||
|
SendEvent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *successCommandHandler) Handle(
|
||||||
|
ctx context.Context, cmd cqrs.Command,
|
||||||
|
) ([]cqrs.Event, error) {
|
||||||
|
command, _ := cmd.(*testCommand)
|
||||||
|
|
||||||
|
log.Printf("Processing command: %s\n", command.Echo)
|
||||||
|
|
||||||
|
if s.SendEvent {
|
||||||
|
return []cqrs.Event{testEvent{Echo: command.Echo}}, nil
|
||||||
|
} else {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type successEventHandler struct{}
|
||||||
|
|
||||||
|
func (e *successEventHandler) Handle(
|
||||||
|
ctx context.Context, evt cqrs.Event,
|
||||||
|
) error {
|
||||||
|
event, _ := evt.(testEvent)
|
||||||
|
log.Printf("Processing event: %s\n", event.Echo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type failureCommandHandler struct{}
|
||||||
|
|
||||||
|
func (e *failureCommandHandler) Handle(
|
||||||
|
ctx context.Context, cmd cqrs.Command,
|
||||||
|
) ([]cqrs.Event, error) {
|
||||||
|
return nil, errors.New("failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
type successQueryHandler struct{}
|
||||||
|
|
||||||
|
func (e *successQueryHandler) Handle(
|
||||||
|
ctx context.Context, query cqrs.Query,
|
||||||
|
) (interface{}, error) {
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type failureQueryHandler struct{}
|
||||||
|
|
||||||
|
func (e *failureQueryHandler) Handle(
|
||||||
|
ctx context.Context, query cqrs.Query,
|
||||||
|
) (interface{}, error) {
|
||||||
|
return nil, errors.New("failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
type failureEventHandler struct{}
|
||||||
|
|
||||||
|
func (e *failureEventHandler) Handle(
|
||||||
|
ctx context.Context, evt cqrs.Event,
|
||||||
|
) error {
|
||||||
|
return errors.New("failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func logTestCommandMiddleware(next cqrs.CommandHandler) cqrs.CommandHandler {
|
||||||
|
return cqrs.CommandHandlerFunc(
|
||||||
|
func(ctx context.Context, cmd cqrs.Command) ([]cqrs.Event, error) {
|
||||||
|
log.Printf("Received command: %s\n", cmd)
|
||||||
|
return next.Handle(ctx, cmd)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logTestQueryMiddleware(next cqrs.QueryHandler) cqrs.QueryHandler {
|
||||||
|
return cqrs.QueryHandlerFunc(
|
||||||
|
func(ctx context.Context, query cqrs.Query) (cqrs.Query, error) {
|
||||||
|
log.Printf("Received query: %s\n", query)
|
||||||
|
return next.Handle(ctx, query)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logTestEventMiddleware(next cqrs.EventHandler) cqrs.EventHandler {
|
||||||
|
return cqrs.EventHandlerFunc(
|
||||||
|
func(ctx context.Context, evt cqrs.Event) error {
|
||||||
|
log.Printf("Received event: %s\n", evt)
|
||||||
|
return next.Handle(ctx, evt)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRuneBringer(t *testing.T) {
|
||||||
|
bus := cqrs.NewSingularity()
|
||||||
|
|
||||||
|
if bus == nil {
|
||||||
|
t.Error("RuneBringer should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
busType := reflect.TypeOf(bus)
|
||||||
|
|
||||||
|
if busType.Kind() == reflect.Ptr {
|
||||||
|
busType = busType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if busType.Name() != "singularity" {
|
||||||
|
t.Errorf(
|
||||||
|
"RuneBringer should be of type RuneBringer, got %s", busType.Name(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneBringer_CommandHandle(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
SendEvent bool
|
||||||
|
}{
|
||||||
|
{true},
|
||||||
|
{false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
bus := cqrs.NewSingularity()
|
||||||
|
bus.UseCommandMiddleware(logTestCommandMiddleware)
|
||||||
|
bus.UseEventMiddleware(logTestEventMiddleware)
|
||||||
|
bus.RegisterCommandHandler(
|
||||||
|
&testCommand{}, &successCommandHandler{SendEvent: tt.SendEvent},
|
||||||
|
)
|
||||||
|
bus.RegisterEventHandler(&testEvent{}, &successEventHandler{})
|
||||||
|
events, err := bus.ExecuteCommand(
|
||||||
|
context.Background(), &testCommand{Echo: "Hello"},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Error should be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.SendEvent {
|
||||||
|
if len(events) == 0 {
|
||||||
|
t.Errorf("Expected 1 event, got %d", len(events))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(events) != 0 {
|
||||||
|
t.Errorf("Expected 0 events, got %d", len(events))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneBringer_CommandHandleFailure(t *testing.T) {
|
||||||
|
bus := cqrs.NewSingularity()
|
||||||
|
bus.UseCommandMiddleware(logTestCommandMiddleware)
|
||||||
|
bus.RegisterCommandHandler(
|
||||||
|
&testCommand{}, &failureCommandHandler{},
|
||||||
|
)
|
||||||
|
_, err := bus.ExecuteCommand(
|
||||||
|
context.Background(), &testCommand{Echo: "Hello"},
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Error should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneBringer_CommandMissingHandler(t *testing.T) {
|
||||||
|
bus := cqrs.NewSingularity()
|
||||||
|
bus.UseCommandMiddleware(logTestCommandMiddleware)
|
||||||
|
_, err := bus.ExecuteCommand(
|
||||||
|
context.Background(), &testCommand{Echo: "Hello"},
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Error should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneBringer_CommandHandleEventFailure(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
SendEvent bool
|
||||||
|
EventHandler cqrs.EventHandler
|
||||||
|
}{
|
||||||
|
{true, &failureEventHandler{}},
|
||||||
|
{true, nil},
|
||||||
|
{false, nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
bus := cqrs.NewSingularity()
|
||||||
|
bus.UseCommandMiddleware(logTestCommandMiddleware)
|
||||||
|
bus.RegisterCommandHandler(
|
||||||
|
&testCommand{}, &successCommandHandler{SendEvent: tt.SendEvent},
|
||||||
|
)
|
||||||
|
|
||||||
|
if tt.EventHandler != nil {
|
||||||
|
bus.RegisterEventHandler(&testEvent{}, tt.EventHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := bus.ExecuteCommand(
|
||||||
|
context.Background(), &testCommand{Echo: "Hello"},
|
||||||
|
)
|
||||||
|
bus.RegisterEventHandler(&testEvent{}, tt.EventHandler)
|
||||||
|
if tt.SendEvent && tt.EventHandler != nil {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Error should not be nil")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Error should be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneBringer_QueryHandle(t *testing.T) {
|
||||||
|
bus := cqrs.NewSingularity()
|
||||||
|
bus.UseQueryMiddleware(logTestQueryMiddleware)
|
||||||
|
bus.RegisterQueryHandler(
|
||||||
|
&testQuery{}, &successQueryHandler{},
|
||||||
|
)
|
||||||
|
|
||||||
|
result, err := bus.ExecuteQuery(
|
||||||
|
context.Background(), &testQuery{Echo: "Hello"},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Error should be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
t.Error("Result should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneBringer_QueryHandleFailure(t *testing.T) {
|
||||||
|
bus := cqrs.NewSingularity()
|
||||||
|
bus.UseQueryMiddleware(logTestQueryMiddleware)
|
||||||
|
bus.RegisterQueryHandler(
|
||||||
|
&testQuery{}, &failureQueryHandler{},
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := bus.ExecuteQuery(
|
||||||
|
context.Background(), &testQuery{Echo: "Hello"},
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Error should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneBringer_QueryMissingHandler(t *testing.T) {
|
||||||
|
bus := cqrs.NewSingularity()
|
||||||
|
bus.UseQueryMiddleware(logTestQueryMiddleware)
|
||||||
|
|
||||||
|
_, err := bus.ExecuteQuery(
|
||||||
|
context.Background(), &testQuery{Echo: "Hello"},
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Error should not be nil")
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user