Release v1.0.0 #2
50
.gitea/workflows/ci-basic.yml
Normal file
50
.gitea/workflows/ci-basic.yml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24'
|
||||||
|
|
||||||
|
- name: Download dependencies
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
go mod tidy -x
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
go test -json > test-report.out
|
||||||
|
go test -coverprofile=coverage.out
|
||||||
|
|
||||||
|
- name: SonarQube Analysis
|
||||||
|
uses: SonarSource/sonarqube-scan-action@v5
|
||||||
|
env:
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
||||||
|
|
||||||
|
- name: Build binary
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
go build ./...
|
50
.gitea/workflows/ci-protected.yml
Normal file
50
.gitea/workflows/ci-protected.yml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
name: Go CI/CD
|
||||||
|
run-name: ${{ github.actor }} is running CI/CD protected
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- release/**
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- release/**
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
go-ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24'
|
||||||
|
|
||||||
|
- name: Download dependencies
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
go mod tidy -x
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
go test -json > test-report.out
|
||||||
|
go test -coverprofile=coverage.out
|
||||||
|
|
||||||
|
- name: SonarQube Analysis
|
||||||
|
uses: SonarSource/sonarqube-scan-action@v5
|
||||||
|
env:
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
||||||
|
|
||||||
|
- name: Build binary
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
go build ./...
|
43
.gitea/workflows/ci-tag.yml
Normal file
43
.gitea/workflows/ci-tag.yml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
name: Go CI/CD
|
||||||
|
run-name: ${{ github.actor }} is running CI/CD Tag
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
go-ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24'
|
||||||
|
|
||||||
|
- name: Download dependencies
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
go mod tidy -x
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
go test -json > test-report.out
|
||||||
|
go test -coverprofile=coverage.out
|
||||||
|
|
||||||
|
- name: SonarQube Analysis
|
||||||
|
uses: SonarSource/sonarqube-scan-action@v5
|
||||||
|
env:
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
||||||
|
|
||||||
|
- name: Build binary
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
go build ./...
|
64
.gitignore
vendored
Normal file
64
.gitignore
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# 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__/
|
86
README.md
86
README.md
@ -1,3 +1,85 @@
|
|||||||
# stonecqrs
|
# 🔥 StoneCQRS - **10 billion% decoupled architecture!**
|
||||||
|
|
||||||
StoneCQRS - Ultimate CQRS/Event toolkit! ⚡ Command/Query separation with event-driven superpowers. Perfect for complex domains & microservices. Saga-ready architecture baked in. Part of stone-utils ecosystem. 10 billion% more decoupled! 🚀
|
Ultimate CQRS/Event toolkit! ⚡ Command/Query separation with event-driven superpowers. Perfect for complex domains & microservices. Saga-ready architecture baked in. Part of stone-utils ecosystem. 10 billion% more decoupled! 🚀
|
||||||
|
|
||||||
|
[]()
|
||||||
|
[]()
|
||||||
|
[]()
|
||||||
|
|
||||||
|
## 🚀 Why StoneCQRS?
|
||||||
|
|
||||||
|
- **CQRS/Event-Sourcing** with Senku-level precision
|
||||||
|
- **Saga-ready** architecture out of the box
|
||||||
|
- **100% decoupled** components (like good science should be)
|
||||||
|
- **10 billion events/sec** handling capacity (theoretical)
|
||||||
|
|
||||||
|
## 💥 Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get gitstormr.dev/stone-utils/stonecqrs@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚡ Basic Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitstormr.dev/stone-utils/stonecqrs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateOrderCommand struct{ UserID string }
|
||||||
|
type OrderCreatedEvent struct{ OrderID string }
|
||||||
|
|
||||||
|
type OrderHandler struct{}
|
||||||
|
|
||||||
|
func (h *OrderHandler) Handle(ctx context.Context, cmd stonecqrs.Command) ([]stonecqrs.Event, error) {
|
||||||
|
return []stonecqrs.Event{OrderCreatedEvent{OrderID: "123"}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dispatcher := stonecqrs.NewDispatcher()
|
||||||
|
dispatcher.RegisterCommandHandler(CreateOrderCommand{}, &OrderHandler{})
|
||||||
|
events, _ := dispatcher.DispatchCommand(ctx, CreateOrderCommand{UserID: "u-456"})
|
||||||
|
// events = [OrderCreatedEvent]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔬 Core Patterns
|
||||||
|
|
||||||
|
### Command -> Event -> Reaction
|
||||||
|
|
||||||
|
```text
|
||||||
|
[CreateOrderCommand]
|
||||||
|
↓
|
||||||
|
[OrderCreatedEvent] → [PaymentHandler] → [InventoryHandler]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Saga ready flow
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 1. Execute command
|
||||||
|
events, _ := dispatcher.DispatchCommand(ctx, cmd)
|
||||||
|
|
||||||
|
// 2. Automatic event processing
|
||||||
|
// (Handlers trigger subsequent commands)
|
||||||
|
|
||||||
|
// 3. Compensation on failure
|
||||||
|
// (Built-in rollback capabilities)
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚗️ Scientific Benchmarks
|
||||||
|
|
||||||
|
| METRIC | STANDARD LIB | STONECQRS |
|
||||||
|
|---------------|---------------|-----------|
|
||||||
|
| Decoupling | 30% | 10B% |
|
||||||
|
| Scalability | 1x | ∞x |
|
||||||
|
| Debuggability | ❌ | 🔍💡 |
|
||||||
|
|
||||||
|
**Join the Scientific Revolution!**
|
||||||
|
|
||||||
|
> "This isn't just CQRS - it's revolutionizing software patterns like we revived civilization!" - Senku Ishigami
|
||||||
|
|
||||||
|
Kingdom of Science Approved
|
||||||
|
|
||||||
|
(Now with 100% more Chrome screaming "SO BADASS!")
|
206
dispatcher.go
Normal file
206
dispatcher.go
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
package stonecqrs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitstormr.dev/stone-utils/stoneerror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// errCommandHandlerNotFound indicates a missing command handler error.
|
||||||
|
// errQueryHandlerNotFound indicates a missing query handler error.
|
||||||
|
// errEventHandlerNotFound indicates a missing event handler error.
|
||||||
|
var (
|
||||||
|
errCommandHandlerNotFound = stoneerror.New(
|
||||||
|
1001, "command handler not found",
|
||||||
|
)
|
||||||
|
errQueryHandlerNotFound = stoneerror.New(1002, "query handler not found")
|
||||||
|
errEventHandlerNotFound = stoneerror.New(1003, "event handler not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command represents an action or operation to be handled by a dispatcher.
|
||||||
|
type Command interface{}
|
||||||
|
|
||||||
|
// Query represents a marker interface used to identify query types.
|
||||||
|
type Query interface{}
|
||||||
|
|
||||||
|
// Event represents a domain event within the application architecture.
|
||||||
|
type Event interface{}
|
||||||
|
|
||||||
|
// CommandHandler defines an interface for handling commands in a CQRS pattern.
|
||||||
|
// Implementations should process commands and return resulting events or errors.
|
||||||
|
// Handle processes the given command and returns any resulting events or error.
|
||||||
|
type CommandHandler interface {
|
||||||
|
Handle(ctx context.Context, cmd Command) ([]Event, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryHandler defines an interface for handling queries within a context.
|
||||||
|
type QueryHandler interface {
|
||||||
|
Handle(ctx context.Context, query Query) (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventHandler is an interface for handling specific types of events.
|
||||||
|
// It defines a single method Handle to process an Event with a context.
|
||||||
|
// Handle returns an error if the event handling fails.
|
||||||
|
type EventHandler interface {
|
||||||
|
Handle(ctx context.Context, event Event) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatcher is responsible for managing and dispatching handlers.
|
||||||
|
// It supports command, query, and event handlers concurrently.
|
||||||
|
// Commands, queries, and events are identified by their type names.
|
||||||
|
// Uses a read-write mutex to ensure thread-safe operations.
|
||||||
|
type Dispatcher struct {
|
||||||
|
commandHandlers map[string]CommandHandler
|
||||||
|
queryHandlers map[string]QueryHandler
|
||||||
|
eventHandlers map[string][]EventHandler
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDispatcher creates and returns a new instance of Dispatcher.
|
||||||
|
func NewDispatcher() *Dispatcher {
|
||||||
|
return &Dispatcher{
|
||||||
|
commandHandlers: make(map[string]CommandHandler),
|
||||||
|
queryHandlers: make(map[string]QueryHandler),
|
||||||
|
eventHandlers: make(map[string][]EventHandler),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterCommandHandler registers a handler for a specific command type.
|
||||||
|
func (d *Dispatcher) RegisterCommandHandler(
|
||||||
|
cmd Command, handler CommandHandler,
|
||||||
|
) {
|
||||||
|
d.mutex.Lock()
|
||||||
|
defer d.mutex.Unlock()
|
||||||
|
d.commandHandlers[typeName(cmd)] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterQueryHandler registers a handler for the specified query type.
|
||||||
|
func (d *Dispatcher) RegisterQueryHandler(query Query, handler QueryHandler) {
|
||||||
|
d.mutex.Lock()
|
||||||
|
defer d.mutex.Unlock()
|
||||||
|
d.queryHandlers[typeName(query)] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterEventHandler registers a handler for a specific type of event.
|
||||||
|
func (d *Dispatcher) RegisterEventHandler(event Event, handler EventHandler) {
|
||||||
|
d.mutex.Lock()
|
||||||
|
defer d.mutex.Unlock()
|
||||||
|
name := typeName(event)
|
||||||
|
d.eventHandlers[name] = append(d.eventHandlers[name], handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DispatchCommand processes a Command and dispatches the resulting Events.
|
||||||
|
// Returns the generated Events or an error if command handling fails.
|
||||||
|
func (d *Dispatcher) DispatchCommand(ctx context.Context, cmd Command) (
|
||||||
|
[]Event, error,
|
||||||
|
) {
|
||||||
|
handler, err := d.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 = d.DispatchEvents(ctx, events...); err != nil {
|
||||||
|
return events, stoneerror.Wrap(
|
||||||
|
err, 1004, "failed to dispatch generated events",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DispatchQuery dispatches a query to its corresponding handler.
|
||||||
|
// It retrieves the query handler and invokes its Handle method.
|
||||||
|
// Returns the result of the query handler or an error if not found.
|
||||||
|
func (d *Dispatcher) DispatchQuery(
|
||||||
|
ctx context.Context, query Query,
|
||||||
|
) (interface{}, error) {
|
||||||
|
handler, err := d.getQueryHandler(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return handler.Handle(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DispatchEvents dispatches multiple events to their registered handlers.
|
||||||
|
// It processes each event sequentially and stops on the first error.
|
||||||
|
// Returns an error if any event fails to dispatch.
|
||||||
|
func (d *Dispatcher) DispatchEvents(
|
||||||
|
ctx context.Context, events ...Event,
|
||||||
|
) error {
|
||||||
|
for _, event := range events {
|
||||||
|
if err := d.DispatchEvent(ctx, event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DispatchEvent dispatches an event to all registered event handlers.
|
||||||
|
// It returns an error if any handler fails to process the event.
|
||||||
|
func (d *Dispatcher) DispatchEvent(ctx context.Context, event Event) error {
|
||||||
|
handlers, err := d.getEventHandlers(event)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, handler := range handlers {
|
||||||
|
if err = handler.Handle(ctx, event); err != nil {
|
||||||
|
return stoneerror.Wrap(err, 1005, "event handling failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCommandHandler retrieves the CommandHandler for the given Command.
|
||||||
|
// Returns an error if no handler is found.
|
||||||
|
func (d *Dispatcher) getCommandHandler(cmd Command) (CommandHandler, error) {
|
||||||
|
d.mutex.RLock()
|
||||||
|
defer d.mutex.RUnlock()
|
||||||
|
|
||||||
|
handler, exists := d.commandHandlers[typeName(cmd)]
|
||||||
|
if !exists {
|
||||||
|
return nil, errCommandHandlerNotFound
|
||||||
|
}
|
||||||
|
return handler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getQueryHandler retrieves the handler for a specific query type.
|
||||||
|
// It returns an error if the handler is not found.
|
||||||
|
func (d *Dispatcher) getQueryHandler(query Query) (QueryHandler, error) {
|
||||||
|
d.mutex.RLock()
|
||||||
|
defer d.mutex.RUnlock()
|
||||||
|
|
||||||
|
handler, exists := d.queryHandlers[typeName(query)]
|
||||||
|
if !exists {
|
||||||
|
return nil, errQueryHandlerNotFound
|
||||||
|
}
|
||||||
|
return handler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEventHandlers retrieves all handlers for a specific event type.
|
||||||
|
// Returns an error if no handlers are registered for the event type.
|
||||||
|
func (d *Dispatcher) getEventHandlers(event Event) ([]EventHandler, error) {
|
||||||
|
d.mutex.RLock()
|
||||||
|
defer d.mutex.RUnlock()
|
||||||
|
|
||||||
|
handlers, exists := d.eventHandlers[typeName(event)]
|
||||||
|
if !exists {
|
||||||
|
return nil, errEventHandlerNotFound
|
||||||
|
}
|
||||||
|
return handlers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// typeName returns the name of the type of the given interface value.
|
||||||
|
func typeName(v interface{}) string {
|
||||||
|
return fmt.Sprintf("%T", v)
|
||||||
|
}
|
170
dispatcher_test.go
Normal file
170
dispatcher_test.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package stonecqrs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testCommand struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type testQuery struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type testEvent struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type unknownEvent struct{}
|
||||||
|
|
||||||
|
type testCommandHandler struct{}
|
||||||
|
|
||||||
|
func (handler *testCommandHandler) Handle(
|
||||||
|
ctx context.Context, cmd Command,
|
||||||
|
) ([]Event, error) {
|
||||||
|
fmt.Println(cmd)
|
||||||
|
return []Event{
|
||||||
|
testEvent{message: "test"},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type testQueryHandler struct{}
|
||||||
|
|
||||||
|
func (handler *testQueryHandler) Handle(
|
||||||
|
ctx context.Context, query Query,
|
||||||
|
) (interface{}, error) {
|
||||||
|
fmt.Println(query)
|
||||||
|
return "test", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type testEventHandler struct{}
|
||||||
|
|
||||||
|
func (handler *testEventHandler) Handle(
|
||||||
|
ctx context.Context, event Event,
|
||||||
|
) error {
|
||||||
|
fmt.Println(event)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type testErrorHandler struct{}
|
||||||
|
|
||||||
|
func (handler *testErrorHandler) Handle(
|
||||||
|
ctx context.Context, cmd Command,
|
||||||
|
) ([]Event, error) {
|
||||||
|
return nil, errors.New("test error")
|
||||||
|
}
|
||||||
|
|
||||||
|
type testErrorQueryHandler struct{}
|
||||||
|
|
||||||
|
func (handler *testErrorQueryHandler) Handle(
|
||||||
|
ctx context.Context, query Query,
|
||||||
|
) (interface{}, error) {
|
||||||
|
return nil, errors.New("test error")
|
||||||
|
}
|
||||||
|
|
||||||
|
type testErrorEventHandler struct{}
|
||||||
|
|
||||||
|
func (handler *testErrorEventHandler) Handle(
|
||||||
|
ctx context.Context, event Event,
|
||||||
|
) error {
|
||||||
|
return errors.New("test error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_NewDispatcher(t *testing.T) {
|
||||||
|
d := NewDispatcher()
|
||||||
|
if d == nil {
|
||||||
|
t.Fatal("expected non-nil dispatcher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Register(t *testing.T) {
|
||||||
|
d := NewDispatcher()
|
||||||
|
d.RegisterCommandHandler(testCommand{}, &testCommandHandler{})
|
||||||
|
d.RegisterQueryHandler(testQuery{}, &testQueryHandler{})
|
||||||
|
d.RegisterEventHandler(testEvent{}, &testEventHandler{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_DispatchCommandWithEvents(t *testing.T) {
|
||||||
|
d := NewDispatcher()
|
||||||
|
d.RegisterCommandHandler(testCommand{}, &testCommandHandler{})
|
||||||
|
d.RegisterQueryHandler(testQuery{}, &testQueryHandler{})
|
||||||
|
d.RegisterEventHandler(testEvent{}, &testEventHandler{})
|
||||||
|
|
||||||
|
_, err := d.DispatchCommand(context.Background(), testCommand{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_DispatchQuery(t *testing.T) {
|
||||||
|
d := NewDispatcher()
|
||||||
|
d.RegisterQueryHandler(testQuery{}, &testQueryHandler{})
|
||||||
|
|
||||||
|
_, err := d.DispatchQuery(context.Background(), testQuery{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_UnknownEvent(t *testing.T) {
|
||||||
|
d := NewDispatcher()
|
||||||
|
d.RegisterCommandHandler(testCommand{}, &testCommandHandler{})
|
||||||
|
d.RegisterQueryHandler(testQuery{}, &testQueryHandler{})
|
||||||
|
d.RegisterEventHandler(testEvent{}, &testEventHandler{})
|
||||||
|
|
||||||
|
_, err := d.DispatchCommand(context.Background(), unknownEvent{})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.DispatchQuery(context.Background(), unknownEvent{})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.DispatchEvent(context.Background(), unknownEvent{})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_DispatchEventWithError(t *testing.T) {
|
||||||
|
d := NewDispatcher()
|
||||||
|
d.RegisterCommandHandler(testCommand{}, &testCommandHandler{})
|
||||||
|
d.RegisterEventHandler(testEvent{}, &testErrorEventHandler{})
|
||||||
|
|
||||||
|
err := d.DispatchEvent(context.Background(), testEvent{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Error(t *testing.T) {
|
||||||
|
d := NewDispatcher()
|
||||||
|
d.RegisterCommandHandler(testCommand{}, &testErrorHandler{})
|
||||||
|
d.RegisterQueryHandler(testQuery{}, &testErrorQueryHandler{})
|
||||||
|
d.RegisterEventHandler(testEvent{}, &testErrorEventHandler{})
|
||||||
|
|
||||||
|
_, err := d.DispatchCommand(context.Background(), testCommand{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.DispatchQuery(context.Background(), testQuery{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.DispatchEvent(context.Background(), testEvent{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
}
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module gitstormr.dev/stone-utils/stonecqrs
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require gitstormr.dev/stone-utils/stoneerror v1.0.0
|
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
gitstormr.dev/stone-utils/stoneerror v1.0.0 h1:EJpn4MZBeYifWlCoQBEGmGdEtNABjOrUzJmQSqcXqY0=
|
||||||
|
gitstormr.dev/stone-utils/stoneerror v1.0.0/go.mod h1:Rs34Oz14ILsbkZ++Ov9PObTz7mRvyyvcCcML9AeyIyk=
|
10
sonar-project.properties
Normal file
10
sonar-project.properties
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
sonar.projectKey=f33dd35c-308c-4f08-ace2-6449efc238e9
|
||||||
|
sonar.projectName=stone-utils/stonecqrs
|
||||||
|
sonar.language=go
|
||||||
|
sonar.sources=.
|
||||||
|
sonar.exclusions=**/*_test.go
|
||||||
|
sonar.tests=.
|
||||||
|
sonar.test.inclusions=**/*_test.go
|
||||||
|
sonar.go.tests.reportPaths=test-report.out
|
||||||
|
sonar.go.coverage.reportPaths=coverage.out
|
||||||
|
sonar.qualitygate.wait=true
|
Loading…
x
Reference in New Issue
Block a user