Release v1.0.0 #2

Merged
Rene Nochebuena merged 5 commits from release/v1.0.0 into main 2025-04-13 09:55:45 -06:00
10 changed files with 684 additions and 2 deletions
Showing only changes of commit 4a8cbc0d29 - Show all commits

View 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 ./...

View 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 ./...

View 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
View 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__/

View File

@ -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! 🚀
[![Kingdom of Science Approved](https://img.shields.io/badge/Approved%20By-Kingdom%20of%20Science-blueviolet)]()
[![EVENT-DRIVEN](https://img.shields.io/badge/Patterns-SO_BADASS!-blueviolet)]()
[![10 BILLION](https://img.shields.io/badge/Throughput-10_Billion%25-blueviolet)]()
## 🚀 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
View 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
View 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
View 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
View 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
View 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