snapshot of https://github.com/ThreeDotsLabs/watermill.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
master / ci (push) Failing after 0s

This commit is contained in:
upstream-mirror
2026-09-15 21:27:46 +05:00
commit ce933d1277
538 changed files with 46504 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
package cqrs
import (
"context"
stdErrors "errors"
"github.com/ThreeDotsLabs/watermill"
"github.com/pkg/errors"
"github.com/ThreeDotsLabs/watermill/message"
)
type CommandBusConfig struct {
// GeneratePublishTopic is used to generate topic for publishing command.
GeneratePublishTopic CommandBusGeneratePublishTopicFn
// OnSend is called before publishing the command.
// The *message.Message can be modified.
//
// This option is not required.
OnSend CommandBusOnSendFn
// Marshaler is used to marshal and unmarshal commands.
// It is required.
Marshaler CommandEventMarshaler
// Logger instance used to log.
// If not provided, watermill.NopLogger is used.
Logger watermill.LoggerAdapter
}
func (c *CommandBusConfig) setDefaults() {
if c.Logger == nil {
c.Logger = watermill.NopLogger{}
}
}
func (c CommandBusConfig) Validate() error {
var err error
if c.Marshaler == nil {
err = stdErrors.Join(err, errors.New("missing Marshaler"))
}
if c.GeneratePublishTopic == nil {
err = stdErrors.Join(err, errors.New("missing GeneratePublishTopic"))
}
return err
}
type CommandBusGeneratePublishTopicFn func(CommandBusGeneratePublishTopicParams) (string, error)
type CommandBusGeneratePublishTopicParams struct {
CommandName string
Command any
}
type CommandBusOnSendFn func(params CommandBusOnSendParams) error
type CommandBusOnSendParams struct {
CommandName string
Command any
// Message is never nil and can be modified.
Message *message.Message
}
// CommandBus transports commands to command handlers.
type CommandBus struct {
publisher message.Publisher
config CommandBusConfig
}
// NewCommandBusWithConfig creates a new CommandBus.
func NewCommandBusWithConfig(publisher message.Publisher, config CommandBusConfig) (*CommandBus, error) {
if publisher == nil {
return nil, errors.New("missing publisher")
}
config.setDefaults()
if err := config.Validate(); err != nil {
return nil, errors.Wrap(err, "invalid config")
}
return &CommandBus{publisher, config}, nil
}
// NewCommandBus creates a new CommandBus.
// Deprecated: use NewCommandBusWithConfig instead.
func NewCommandBus(
publisher message.Publisher,
generateTopic func(commandName string) string,
marshaler CommandEventMarshaler,
) (*CommandBus, error) {
if publisher == nil {
return nil, errors.New("missing publisher")
}
if generateTopic == nil {
return nil, errors.New("missing generateTopic")
}
if marshaler == nil {
return nil, errors.New("missing marshaler")
}
return &CommandBus{publisher, CommandBusConfig{
GeneratePublishTopic: func(params CommandBusGeneratePublishTopicParams) (string, error) {
return generateTopic(params.CommandName), nil
},
Marshaler: marshaler,
}}, nil
}
// Send sends command to the command bus.
func (c CommandBus) Send(ctx context.Context, cmd any) error {
return c.SendWithModifiedMessage(ctx, cmd, nil)
}
func (c CommandBus) SendWithModifiedMessage(ctx context.Context, cmd any, modify func(*message.Message) error) error {
msg, topicName, err := c.newMessage(ctx, cmd)
if err != nil {
return err
}
if modify != nil {
if err := modify(msg); err != nil {
return errors.Wrap(err, "cannot modify message")
}
}
if err := c.publisher.Publish(topicName, msg); err != nil {
return err
}
return nil
}
func (c CommandBus) newMessage(ctx context.Context, command any) (*message.Message, string, error) {
msg, err := c.config.Marshaler.Marshal(command)
if err != nil {
return nil, "", err
}
commandName := c.config.Marshaler.Name(command)
topicName, err := c.config.GeneratePublishTopic(CommandBusGeneratePublishTopicParams{
CommandName: commandName,
Command: command,
})
if err != nil {
return nil, "", errors.Wrap(err, "cannot generate topic name")
}
msg.SetContext(ctx)
if c.config.OnSend != nil {
err := c.config.OnSend(CommandBusOnSendParams{
CommandName: commandName,
Command: command,
Message: msg,
})
if err != nil {
return nil, "", errors.Wrap(err, "cannot execute OnSend")
}
}
return msg, topicName, nil
}

View File

@@ -0,0 +1,224 @@
package cqrs_test
import (
"context"
"testing"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
)
func TestCommandBusConfig_Validate(t *testing.T) {
testCases := []struct {
Name string
ModifyValidConfig func(*cqrs.CommandBusConfig)
ExpectedErr error
}{
{
Name: "valid_config",
ModifyValidConfig: nil,
ExpectedErr: nil,
},
{
Name: "missing_Marshaler",
ModifyValidConfig: func(c *cqrs.CommandBusConfig) {
c.Marshaler = nil
},
ExpectedErr: errors.Errorf("missing Marshaler"),
},
{
Name: "missing_GeneratePublishTopic",
ModifyValidConfig: func(c *cqrs.CommandBusConfig) {
c.GeneratePublishTopic = nil
},
ExpectedErr: errors.Errorf("missing GeneratePublishTopic"),
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.Name, func(t *testing.T) {
validConfig := cqrs.CommandBusConfig{
GeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {
return "", nil
},
Marshaler: cqrs.JSONMarshaler{},
}
if tc.ModifyValidConfig != nil {
tc.ModifyValidConfig(&validConfig)
}
err := validConfig.Validate()
if tc.ExpectedErr == nil {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.ExpectedErr.Error())
}
})
}
}
func TestNewCommandBus(t *testing.T) {
pub := newPublisherStub()
config := cqrs.CommandBusConfig{
GeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {
return "", nil
},
Marshaler: cqrs.JSONMarshaler{},
}
require.NoError(t, config.Validate())
cb, err := cqrs.NewCommandBusWithConfig(pub, config)
assert.NotNil(t, cb)
assert.NoError(t, err)
config.GeneratePublishTopic = nil
require.Error(t, config.Validate())
cb, err = cqrs.NewCommandBusWithConfig(pub, config)
assert.Nil(t, cb)
assert.Error(t, err)
}
type contextKey string
func TestCommandBus_Send_ContextPropagation(t *testing.T) {
publisher := newPublisherStub()
commandBus, err := cqrs.NewCommandBusWithConfig(
publisher,
cqrs.CommandBusConfig{
GeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {
return "whatever", nil
},
Marshaler: cqrs.JSONMarshaler{},
},
)
require.NoError(t, err)
ctx := context.WithValue(context.Background(), contextKey("key"), "value")
err = commandBus.Send(ctx, "message")
require.NoError(t, err)
assert.Equal(t, ctx, publisher.messages["whatever"][0].Context())
}
func TestCommandBus_Send_topic_name(t *testing.T) {
cb, err := cqrs.NewCommandBusWithConfig(
assertPublishTopicPublisher{ExpectedTopic: "cqrs_test.TestCommand", T: t},
cqrs.CommandBusConfig{
GeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {
return params.CommandName, nil
},
Marshaler: cqrs.JSONMarshaler{},
},
)
require.NoError(t, err)
err = cb.Send(context.Background(), TestCommand{})
require.NoError(t, err)
}
func TestCommandBus_Send_OnSend(t *testing.T) {
publisher := newPublisherStub()
cb, err := cqrs.NewCommandBusWithConfig(
publisher,
cqrs.CommandBusConfig{
GeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {
return "whatever", nil
},
Marshaler: cqrs.JSONMarshaler{},
OnSend: func(params cqrs.CommandBusOnSendParams) error {
params.Message.Metadata.Set("key", "value")
return nil
},
},
)
require.NoError(t, err)
err = cb.Send(context.Background(), TestCommand{})
require.NoError(t, err)
assert.Equal(t, "value", publisher.messages["whatever"][0].Metadata.Get("key"))
}
func TestCommandBus_SendWithModifiedMessage(t *testing.T) {
publisher := newPublisherStub()
cb, err := cqrs.NewCommandBusWithConfig(
publisher,
cqrs.CommandBusConfig{
GeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {
return "whatever", nil
},
Marshaler: cqrs.JSONMarshaler{},
},
)
require.NoError(t, err)
err = cb.SendWithModifiedMessage(context.Background(), TestCommand{}, func(message *message.Message) error {
message.Metadata.Set("key", "value")
return nil
})
require.NoError(t, err)
assert.Equal(t, "value", publisher.messages["whatever"][0].Metadata.Get("key"))
}
func TestCommandBus_SendWithModifiedMessage_modify_error(t *testing.T) {
publisher := newPublisherStub()
cb, err := cqrs.NewCommandBusWithConfig(
publisher,
cqrs.CommandBusConfig{
GeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {
return "whatever", nil
},
Marshaler: cqrs.JSONMarshaler{},
},
)
require.NoError(t, err)
expectedErr := errors.New("some error")
err = cb.SendWithModifiedMessage(
context.Background(),
TestCommand{},
func(message *message.Message) error {
return expectedErr
},
)
assert.ErrorContains(t, err, expectedErr.Error())
}
func TestCommandBus_Send_OnSend_error(t *testing.T) {
publisher := newPublisherStub()
expectedErr := errors.New("some error")
cb, err := cqrs.NewCommandBusWithConfig(
publisher,
cqrs.CommandBusConfig{
GeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {
return "whatever", nil
},
Marshaler: cqrs.JSONMarshaler{},
OnSend: func(params cqrs.CommandBusOnSendParams) error {
return expectedErr
},
},
)
require.NoError(t, err)
err = cb.Send(context.Background(), TestCommand{})
require.EqualError(t, err, "cannot execute OnSend: some error")
}

View File

@@ -0,0 +1,59 @@
package cqrs
import (
"context"
)
// CommandHandler receives a command defined by NewCommand and handles it with the Handle method.
// If using DDD, CommandHandler may modify and persist the aggregate.
//
// In contrast to EventHandler, every Command must have only one CommandHandler.
//
// One instance of CommandHandler is used during handling messages.
// When multiple commands are delivered at the same time, Handle method can be executed multiple times at the same time.
// Because of that, Handle method needs to be thread safe!
type CommandHandler interface {
// HandlerName is the name used in message.Router while creating handler.
//
// It will be also passed to CommandsSubscriberConstructor.
// May be useful, for example, to create a consumer group per each handler.
//
// WARNING: If HandlerName was changed and is used for generating consumer groups,
// it may result with **reconsuming all messages**!
HandlerName() string
NewCommand() any
Handle(ctx context.Context, cmd any) error
}
type genericCommandHandler[Command any] struct {
handleFunc func(ctx context.Context, cmd *Command) error
handlerName string
}
// NewCommandHandler creates a new CommandHandler implementation based on provided function
// and command type inferred from function argument.
func NewCommandHandler[Command any](
handlerName string,
handleFunc func(ctx context.Context, cmd *Command) error,
) CommandHandler {
return &genericCommandHandler[Command]{
handleFunc: handleFunc,
handlerName: handlerName,
}
}
func (c genericCommandHandler[Command]) HandlerName() string {
return c.handlerName
}
func (c genericCommandHandler[Command]) NewCommand() any {
tVar := new(Command)
return tVar
}
func (c genericCommandHandler[Command]) Handle(ctx context.Context, cmd any) error {
command := cmd.(*Command)
return c.handleFunc(ctx, command)
}

View File

@@ -0,0 +1,32 @@
package cqrs_test
import (
"context"
"fmt"
"testing"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
"github.com/stretchr/testify/assert"
)
type SomeCommand struct {
Foo string
}
func TestNewCommandHandler(t *testing.T) {
cmdToSend := &SomeCommand{"bar"}
ch := cqrs.NewCommandHandler(
"some_handler",
func(ctx context.Context, cmd *SomeCommand) error {
assert.Equal(t, cmdToSend, cmd)
return fmt.Errorf("some error")
},
)
assert.Equal(t, "some_handler", ch.HandlerName())
assert.Equal(t, &SomeCommand{}, ch.NewCommand())
err := ch.Handle(context.Background(), cmdToSend)
assert.EqualError(t, err, "some error")
}

View File

@@ -0,0 +1,382 @@
package cqrs
import (
stdErrors "errors"
"fmt"
"github.com/pkg/errors"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/message"
)
type CommandProcessorConfig struct {
// GenerateSubscribeTopic is used to generate topic for subscribing command.
GenerateSubscribeTopic CommandProcessorGenerateSubscribeTopicFn
// SubscriberConstructor is used to create subscriber for CommandHandler.
SubscriberConstructor CommandProcessorSubscriberConstructorFn
// OnHandle is called before handling command.
// OnHandle works in a similar way to middlewares: you can inject additional logic before and after handling a command.
//
// Because of that, you need to explicitly call params.Handler.Handle() to handle the command.
// func(params CommandProcessorOnHandleParams) (err error) {
// // logic before handle
// // (...)
//
// err := params.Handler.Handle(params.Message.Context(), params.Command)
//
// // logic after handle
// // (...)
//
// return err
// }
//
// This option is not required.
OnHandle CommandProcessorOnHandleFn
// Marshaler is used to marshal and unmarshal commands.
// It is required.
Marshaler CommandEventMarshaler
// Logger instance used to log.
// If not provided, watermill.NopLogger is used.
Logger watermill.LoggerAdapter
// If true, CommandProcessor will ack messages even if CommandHandler returns an error.
// If RequestReplyBackend is not null and sending reply fails, the message will be nack-ed anyway.
//
// Warning: It's not recommended to use this option when you are using requestreply component
// (requestreply.NewCommandHandler or requestreply.NewCommandHandlerWithResult), as it may ack the
// command when sending reply failed.
//
// When you are using requestreply, you should use requestreply.PubSubBackendConfig.AckCommandErrors.
AckCommandHandlingErrors bool
// disableRouterAutoAddHandlers is used to keep backwards compatibility.
// it is set when CommandProcessor is created by NewCommandProcessor.
// Deprecated: please migrate to NewCommandProcessorWithConfig.
disableRouterAutoAddHandlers bool
}
func (c *CommandProcessorConfig) setDefaults() {
if c.Logger == nil {
c.Logger = watermill.NopLogger{}
}
}
func (c CommandProcessorConfig) Validate() error {
var err error
if c.Marshaler == nil {
err = stdErrors.Join(err, errors.New("missing Marshaler"))
}
if c.GenerateSubscribeTopic == nil {
err = stdErrors.Join(err, errors.New("missing GenerateSubscribeTopic"))
}
if c.SubscriberConstructor == nil {
err = stdErrors.Join(err, errors.New("missing SubscriberConstructor"))
}
return err
}
type CommandProcessorGenerateSubscribeTopicFn func(CommandProcessorGenerateSubscribeTopicParams) (string, error)
type CommandProcessorGenerateSubscribeTopicParams struct {
CommandName string
CommandHandler CommandHandler
}
// CommandProcessorSubscriberConstructorFn creates subscriber for CommandHandler.
// It allows you to create a separate customized Subscriber for every command handler.
type CommandProcessorSubscriberConstructorFn func(CommandProcessorSubscriberConstructorParams) (message.Subscriber, error)
type CommandProcessorSubscriberConstructorParams struct {
CommandName string
HandlerName string
Handler CommandHandler
}
type CommandProcessorOnHandleFn func(params CommandProcessorOnHandleParams) error
type CommandProcessorOnHandleParams struct {
Handler CommandHandler
CommandName string
Command any
// Message is never nil and can be modified.
Message *message.Message
}
// CommandProcessor determines which CommandHandler should handle the command received from the command bus.
type CommandProcessor struct {
router *message.Router
handlers []CommandHandler
config CommandProcessorConfig
}
func NewCommandProcessorWithConfig(router *message.Router, config CommandProcessorConfig) (*CommandProcessor, error) {
config.setDefaults()
if err := config.Validate(); err != nil {
return nil, err
}
if router == nil && !config.disableRouterAutoAddHandlers {
return nil, errors.New("missing router")
}
return &CommandProcessor{
router: router,
config: config,
}, nil
}
// NewCommandProcessor creates a new CommandProcessor.
// Deprecated. Use NewCommandProcessorWithConfig instead.
func NewCommandProcessor(
handlers []CommandHandler,
generateTopic func(commandName string) string,
subscriberConstructor CommandsSubscriberConstructor,
marshaler CommandEventMarshaler,
logger watermill.LoggerAdapter,
) (*CommandProcessor, error) {
if len(handlers) == 0 {
return nil, errors.New("missing handlers")
}
if generateTopic == nil {
return nil, errors.New("missing generateTopic")
}
if subscriberConstructor == nil {
return nil, errors.New("missing subscriberConstructor")
}
cp, err := NewCommandProcessorWithConfig(
nil,
CommandProcessorConfig{
GenerateSubscribeTopic: func(params CommandProcessorGenerateSubscribeTopicParams) (string, error) {
return generateTopic(params.CommandName), nil
},
SubscriberConstructor: func(params CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return subscriberConstructor(params.HandlerName)
},
Marshaler: marshaler,
Logger: logger,
disableRouterAutoAddHandlers: true,
},
)
if err != nil {
return nil, err
}
for _, handler := range handlers {
if err := cp.AddHandlers(handler); err != nil {
return nil, err
}
}
return cp, nil
}
// CommandsSubscriberConstructor creates subscriber for CommandHandler.
// It allows you to create a separate customized Subscriber for every command handler.
//
// Deprecated: please use CommandProcessorSubscriberConstructorFn instead.
type CommandsSubscriberConstructor func(handlerName string) (message.Subscriber, error)
// AddHandlers adds a new CommandHandler to the CommandProcessor and adds it to the router.
func (p *CommandProcessor) AddHandlers(handlers ...CommandHandler) error {
handledCommands := map[string]struct{}{}
for _, handler := range handlers {
commandName := p.config.Marshaler.Name(handler.NewCommand())
if _, ok := handledCommands[commandName]; ok {
return DuplicateCommandHandlerError{commandName}
}
handledCommands[commandName] = struct{}{}
}
if p.config.disableRouterAutoAddHandlers {
p.handlers = append(p.handlers, handlers...)
return nil
}
for _, handler := range handlers {
if _, err := p.addHandlerToRouter(p.router, handler); err != nil {
return err
}
p.handlers = append(p.handlers, handler)
}
return nil
}
// AddHandler adds a new CommandHandler to the CommandProcessor and adds it to the router.
func (p *CommandProcessor) AddHandler(handler CommandHandler) (*message.Handler, error) {
if p.config.disableRouterAutoAddHandlers {
p.handlers = append(p.handlers, handler)
return nil, nil
}
h, err := p.addHandlerToRouter(p.router, handler)
if err != nil {
return nil, err
}
p.handlers = append(p.handlers, handler)
return h, nil
}
// DuplicateCommandHandlerError occurs when a handler with the same name already exists.
type DuplicateCommandHandlerError struct {
CommandName string
}
func (d DuplicateCommandHandlerError) Error() string {
return fmt.Sprintf("command handler for command %s already exists", d.CommandName)
}
// AddHandlersToRouter adds the CommandProcessor's handlers to the given router.
// It should be called only once per CommandProcessor instance.
//
// It is required to call AddHandlersToRouter only if command processor is created with NewCommandProcessor (disableRouterAutoAddHandlers is set to true).
// Deprecated: please migrate to command processor created by NewCommandProcessorWithConfig.
func (p CommandProcessor) AddHandlersToRouter(r *message.Router) error {
if !p.config.disableRouterAutoAddHandlers {
return errors.New("AddHandlersToRouter should be called only when using deprecated NewCommandProcessor")
}
for i := range p.Handlers() {
handler := p.handlers[i]
if _, err := p.addHandlerToRouter(r, handler); err != nil {
return err
}
}
return nil
}
func (p CommandProcessor) addHandlerToRouter(r *message.Router, handler CommandHandler) (*message.Handler, error) {
handlerName := handler.HandlerName()
commandName := p.config.Marshaler.Name(handler.NewCommand())
topicName, err := p.config.GenerateSubscribeTopic(CommandProcessorGenerateSubscribeTopicParams{
CommandName: commandName,
CommandHandler: handler,
})
if err != nil {
return nil, errors.Wrapf(err, "cannot generate topic for command handler %s", handlerName)
}
logger := p.config.Logger.With(watermill.LogFields{
"command_handler_name": handlerName,
"topic": topicName,
})
handlerFunc, err := p.routerHandlerFunc(handler, logger)
if err != nil {
return nil, err
}
logger.Debug("Adding CQRS command handler to router", nil)
subscriber, err := p.config.SubscriberConstructor(CommandProcessorSubscriberConstructorParams{
CommandName: commandName,
HandlerName: handlerName,
Handler: handler,
})
if err != nil {
return nil, errors.Wrap(err, "cannot create subscriber for command processor")
}
return r.AddConsumerHandler(
handlerName,
topicName,
subscriber,
handlerFunc,
), nil
}
// Handlers returns the CommandProcessor's handlers.
func (p CommandProcessor) Handlers() []CommandHandler {
return p.handlers
}
func (p CommandProcessor) routerHandlerFunc(handler CommandHandler, logger watermill.LoggerAdapter) (message.NoPublishHandlerFunc, error) {
cmd := handler.NewCommand()
cmdName := p.config.Marshaler.Name(cmd)
if err := p.validateCommand(cmd); err != nil {
return nil, err
}
return func(msg *message.Message) error {
cmd := handler.NewCommand()
messageCmdName := p.config.Marshaler.NameFromMessage(msg)
if messageCmdName != cmdName {
logger.Trace("Received different command type than expected, ignoring", watermill.LogFields{
"message_uuid": msg.UUID,
"expected_command_type": cmdName,
"received_command_type": messageCmdName,
})
return nil
}
logger.Debug("Handling command", watermill.LogFields{
"message_uuid": msg.UUID,
"received_command_type": messageCmdName,
})
ctx := CtxWithOriginalMessage(msg.Context(), msg)
msg.SetContext(ctx)
if err := p.config.Marshaler.Unmarshal(msg, cmd); err != nil {
return err
}
handle := func(params CommandProcessorOnHandleParams) (err error) {
return params.Handler.Handle(ctx, params.Command)
}
if p.config.OnHandle != nil {
handle = p.config.OnHandle
}
err := handle(CommandProcessorOnHandleParams{
Handler: handler,
CommandName: messageCmdName,
Command: cmd,
Message: msg,
})
if p.config.AckCommandHandlingErrors && err != nil {
logger.Error("Error when handling command, acking (AckCommandHandlingErrors is enabled)", err, nil)
return nil
}
if err != nil {
logger.Debug("Error when handling command, nacking", watermill.LogFields{"err": err})
return err
}
return nil
}, nil
}
func (p CommandProcessor) validateCommand(cmd interface{}) error {
// CommandHandler's NewCommand must return a pointer, because it is used to unmarshal
if err := isPointer(cmd); err != nil {
return errors.Wrap(err, "command must be a non-nil pointer")
}
return nil
}

View File

@@ -0,0 +1,522 @@
package cqrs_test
import (
"context"
"sync/atomic"
"testing"
"time"
"github.com/ThreeDotsLabs/watermill"
"github.com/pkg/errors"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCommandProcessorConfig_Validate(t *testing.T) {
testCases := []struct {
Name string
ModifyValidConfig func(*cqrs.CommandProcessorConfig)
ExpectedErr error
}{
{
Name: "valid_config",
ModifyValidConfig: nil,
ExpectedErr: nil,
},
{
Name: "missing_Marshaler",
ModifyValidConfig: func(c *cqrs.CommandProcessorConfig) {
c.Marshaler = nil
},
ExpectedErr: errors.Errorf("missing Marshaler"),
},
{
Name: "missing_SubscriberConstructor",
ModifyValidConfig: func(c *cqrs.CommandProcessorConfig) {
c.SubscriberConstructor = nil
},
ExpectedErr: errors.Errorf("missing SubscriberConstructor"),
},
{
Name: "missing_GenerateHandlerSubscribeTopic",
ModifyValidConfig: func(c *cqrs.CommandProcessorConfig) {
c.GenerateSubscribeTopic = nil
},
ExpectedErr: errors.Errorf("missing GenerateSubscribeTopic"),
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.Name, func(t *testing.T) {
validConfig := cqrs.CommandProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: cqrs.JSONMarshaler{},
}
if tc.ModifyValidConfig != nil {
tc.ModifyValidConfig(&validConfig)
}
err := validConfig.Validate()
if tc.ExpectedErr == nil {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.ExpectedErr.Error())
}
})
}
}
func TestNewCommandProcessor(t *testing.T) {
config := cqrs.CommandProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: cqrs.JSONMarshaler{},
}
require.NoError(t, config.Validate())
router, err := message.NewRouter(message.RouterConfig{}, watermill.NewStdLogger(false, false))
require.NoError(t, err)
cp, err := cqrs.NewCommandProcessorWithConfig(router, config)
assert.NotNil(t, cp)
assert.NoError(t, err)
config.SubscriberConstructor = nil
require.Error(t, config.Validate())
cp, err = cqrs.NewCommandProcessorWithConfig(router, config)
assert.Nil(t, cp)
assert.Error(t, err)
}
type nonPointerCommandHandler struct {
}
func (nonPointerCommandHandler) HandlerName() string {
return "nonPointerCommandHandler"
}
func (nonPointerCommandHandler) NewCommand() interface{} {
return TestCommand{}
}
func (nonPointerCommandHandler) Handle(ctx context.Context, cmd interface{}) error {
panic("not implemented")
}
func TestCommandProcessor_non_pointer_command(t *testing.T) {
ts := NewTestServices()
handler := nonPointerCommandHandler{}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
router,
cqrs.CommandProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = commandProcessor.AddHandlers(handler)
assert.IsType(t, cqrs.NonPointerError{}, errors.Cause(err))
}
// TestCommandProcessor_multiple_same_command_handlers checks, that we don't register multiple handlers for the same command.
func TestCommandProcessor_multiple_same_command_handlers(t *testing.T) {
ts := NewTestServices()
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
router,
cqrs.CommandProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = commandProcessor.AddHandlers(
&CaptureCommandHandler{},
&CaptureCommandHandler{},
)
require.Error(t, err)
assert.EqualValues(t, cqrs.DuplicateCommandHandlerError{CommandName: "cqrs_test.TestCommand"}, err)
assert.Equal(t, "command handler for command cqrs_test.TestCommand already exists", err.Error())
}
type mockSubscriber struct {
MessagesToSend []*message.Message
WaitForAckBeforeSendingNext bool
out chan *message.Message
}
func (m *mockSubscriber) Subscribe(ctx context.Context, topic string) (<-chan *message.Message, error) {
m.out = make(chan *message.Message)
go func() {
for _, msg := range m.MessagesToSend {
m.out <- msg
if m.WaitForAckBeforeSendingNext {
<-msg.Acked()
}
}
}()
return m.out, nil
}
func (m mockSubscriber) Close() error {
close(m.out)
return nil
}
func TestCommandProcessor_AckCommandHandlingErrors_option_true(t *testing.T) {
logger := watermill.NewCaptureLogger()
marshaler := cqrs.JSONMarshaler{}
msgToSend, err := marshaler.Marshal(&TestCommand{ID: "1"})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msgToSend,
},
}
router, err := message.NewRouter(message.RouterConfig{}, logger)
require.NoError(t, err)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
router,
cqrs.CommandProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
return "commands", nil
},
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
Marshaler: marshaler,
Logger: logger,
AckCommandHandlingErrors: true,
},
)
require.NoError(t, err)
expectedErr := errors.New("test error")
err = commandProcessor.AddHandlers(cqrs.NewCommandHandler(
"handler", func(ctx context.Context, cmd *TestCommand) error {
return expectedErr
}),
)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msgToSend.Acked():
// ok
case <-msgToSend.Nacked():
// nack received
t.Fatal("nack received, message should be acked")
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for ack")
}
// it's pretty important to not ack message silently, so let's assert if it's logged properly
expectedLogMessage := watermill.CapturedMessage{
Level: watermill.ErrorLogLevel,
Fields: map[string]any{
"command_handler_name": "handler",
"topic": "commands",
},
Msg: "Error when handling command, acking (AckCommandHandlingErrors is enabled)",
Err: expectedErr,
}
assert.True(
t,
logger.Has(expectedLogMessage),
"expected log message not found, logs: %#v",
logger.Captured(),
)
}
func TestCommandProcessor_AckCommandHandlingErrors_option_false(t *testing.T) {
logger := watermill.NewCaptureLogger()
marshaler := cqrs.JSONMarshaler{}
msgToSend, err := marshaler.Marshal(&TestCommand{ID: "1"})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msgToSend,
},
}
router, err := message.NewRouter(message.RouterConfig{}, logger)
require.NoError(t, err)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
router,
cqrs.CommandProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
return "commands", nil
},
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
Marshaler: marshaler,
Logger: logger,
AckCommandHandlingErrors: false,
},
)
require.NoError(t, err)
expectedErr := errors.New("test error")
err = commandProcessor.AddHandlers(cqrs.NewCommandHandler(
"handler", func(ctx context.Context, cmd *TestCommand) error {
return expectedErr
}),
)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msgToSend.Acked():
// nack received
t.Fatal("ack received, message should be nacked")
case <-msgToSend.Nacked():
// ok
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for ack")
}
}
func TestNewCommandProcessor_OnHandle(t *testing.T) {
ts := NewTestServices()
msg1, err := ts.Marshaler.Marshal(&TestCommand{ID: "1"})
require.NoError(t, err)
msg2, err := ts.Marshaler.Marshal(&TestCommand{ID: "2"})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg1,
msg2,
},
}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
handlerCalled := 0
defer func() {
// for msg 1 we are not calling handler - but returning before
assert.Equal(t, 1, handlerCalled)
}()
handler := cqrs.NewCommandHandler("test", func(ctx context.Context, cmd *TestCommand) error {
handlerCalled++
return nil
})
onHandleCalled := int64(0)
config := cqrs.CommandProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
return "commands", nil
},
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
OnHandle: func(params cqrs.CommandProcessorOnHandleParams) error {
atomic.AddInt64(&onHandleCalled, 1)
assert.IsType(t, &TestCommand{}, params.Command)
assert.Equal(t, "cqrs_test.TestCommand", params.CommandName)
assert.Equal(t, handler, params.Handler)
if params.Command.(*TestCommand).ID == "1" {
assert.Equal(t, msg1, params.Message)
return errors.New("test error")
} else {
assert.Equal(t, msg2, params.Message)
}
return params.Handler.Handle(params.Message.Context(), params.Command)
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
}
cp, err := cqrs.NewCommandProcessorWithConfig(router, config)
require.NoError(t, err)
err = cp.AddHandlers(handler)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg1.Nacked():
// ok
case <-msg1.Acked():
// ack received
t.Fatal("ack received, message should be nacked")
}
select {
case <-msg2.Acked():
// ok
case <-msg2.Nacked():
// nack received
}
assert.EqualValues(t, 2, onHandleCalled)
}
func TestCommandProcessor_AddHandlersToRouter_without_disableRouterAutoAddHandlers(t *testing.T) {
ts := NewTestServices()
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
cp, err := cqrs.NewCommandProcessorWithConfig(
router,
cqrs.CommandProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
return "commands", nil
},
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return ts.CommandsPubSub, nil
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = cp.AddHandlersToRouter(router)
assert.ErrorContains(t, err, "AddHandlersToRouter should be called only when using deprecated NewCommandProcessor")
}
func TestCommandProcessor_original_msg_set_to_ctx(t *testing.T) {
logger := watermill.NewCaptureLogger()
marshaler := cqrs.JSONMarshaler{}
msgToSend, err := marshaler.Marshal(&TestCommand{ID: "1"})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msgToSend,
},
}
router, err := message.NewRouter(message.RouterConfig{}, logger)
require.NoError(t, err)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
router,
cqrs.CommandProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
return "commands", nil
},
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
Marshaler: marshaler,
Logger: logger,
AckCommandHandlingErrors: true,
},
)
require.NoError(t, err)
var msgFromCtx *message.Message
err = commandProcessor.AddHandlers(cqrs.NewCommandHandler(
"handler", func(ctx context.Context, cmd *TestCommand) error {
msgFromCtx = cqrs.OriginalMessageFromCtx(ctx)
return nil
}),
)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msgToSend.Acked():
// ok
case <-msgToSend.Nacked():
// nack received
t.Fatal("nack received, message should be acked")
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for ack")
}
require.NotNil(t, msgFromCtx)
assert.Equal(t, msgToSend, msgFromCtx)
}

199
components/cqrs/cqrs.go Normal file
View File

@@ -0,0 +1,199 @@
package cqrs
import (
stdErrors "errors"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/pkg/errors"
)
// Deprecated: use CommandProcessor and EventProcessor instead.
type FacadeConfig struct {
// GenerateCommandsTopic generates topic name based on the command name.
// Command name is generated by CommandEventMarshaler's Name method.
//
// It allows you to use topic per command or one topic for every command.
GenerateCommandsTopic func(commandName string) string
// CommandHandlers return command handlers which should be executed.
CommandHandlers func(commandBus *CommandBus, eventBus *EventBus) []CommandHandler
// CommandsPublisher is Publisher used to publish commands.
CommandsPublisher message.Publisher
// CommandsSubscriberConstructor is constructor for subscribers which will subscribe for messages.
// It will be called for every command handler.
// It allows you to create separated customized Subscriber for every command handler.
CommandsSubscriberConstructor CommandsSubscriberConstructor
// GenerateEventsTopic generates topic name based on the event name.
// Event name is generated by CommandEventMarshaler's Name method.
//
// It allows you to use topic per command or one topic for every command.
GenerateEventsTopic func(eventName string) string
// EventHandlers return event handlers which should be executed.
EventHandlers func(commandBus *CommandBus, eventBus *EventBus) []EventHandler
// EventsPublisher is Publisher used to publish commands.
EventsPublisher message.Publisher
// EventsSubscriberConstructor is constructor for subscribers which will subscribe for messages.
// It will be called for every event handler.
// It allows you to create separated customized Subscriber for every event handler.
EventsSubscriberConstructor EventsSubscriberConstructor
// Router is a Watermill router, which will be used to handle events and commands.
// Router handlers will be automatically generated by AddHandlersToRouter of Command and Event handlers.
Router *message.Router
CommandEventMarshaler CommandEventMarshaler
Logger watermill.LoggerAdapter
}
func (c FacadeConfig) Validate() error {
var err error
if c.CommandsEnabled() {
if c.GenerateCommandsTopic == nil {
err = stdErrors.Join(err, errors.New("GenerateCommandsTopic is nil"))
}
if c.CommandsSubscriberConstructor == nil {
err = stdErrors.Join(err, errors.New("CommandsSubscriberConstructor is nil"))
}
if c.CommandsPublisher == nil {
err = stdErrors.Join(err, errors.New("CommandsPublisher is nil"))
}
}
if c.EventsEnabled() {
if c.GenerateEventsTopic == nil {
err = stdErrors.Join(err, errors.New("GenerateEventsTopic is nil"))
}
if c.EventsSubscriberConstructor == nil {
err = stdErrors.Join(err, errors.New("EventsSubscriberConstructor is nil"))
}
if c.EventsPublisher == nil {
err = stdErrors.Join(err, errors.New("EventsPublisher is nil"))
}
}
if c.Router == nil {
err = stdErrors.Join(err, errors.New("Router is nil"))
}
if c.Logger == nil {
err = stdErrors.Join(err, errors.New("Logger is nil"))
}
if c.CommandEventMarshaler == nil {
err = stdErrors.Join(err, errors.New("CommandEventMarshaler is nil"))
}
return err
}
func (c FacadeConfig) EventsEnabled() bool {
return c.GenerateEventsTopic != nil || c.EventsPublisher != nil || c.EventsSubscriberConstructor != nil
}
func (c FacadeConfig) CommandsEnabled() bool {
return c.GenerateCommandsTopic != nil || c.CommandsPublisher != nil || c.CommandsSubscriberConstructor != nil
}
// Deprecated: use CommandHandler and EventHandler instead.
//
// Facade is a facade for creating the Command and Event buses and processors.
// It was created to avoid boilerplate, when using CQRS in the standard way.
// You can also create buses and processors manually, drawing inspiration from how it's done in NewFacade.
type Facade struct {
commandsTopic func(commandName string) string
commandBus *CommandBus
eventsTopic func(eventName string) string
eventBus *EventBus
commandEventMarshaler CommandEventMarshaler
}
func (f Facade) CommandBus() *CommandBus {
return f.commandBus
}
func (f Facade) EventBus() *EventBus {
return f.eventBus
}
func (f Facade) CommandEventMarshaler() CommandEventMarshaler {
return f.commandEventMarshaler
}
// Deprecated: use CommandHandler and EventHandler instead.
func NewFacade(config FacadeConfig) (*Facade, error) {
if err := config.Validate(); err != nil {
return nil, errors.Wrap(err, "invalid config")
}
c := &Facade{
commandsTopic: config.GenerateCommandsTopic,
eventsTopic: config.GenerateEventsTopic,
commandEventMarshaler: config.CommandEventMarshaler,
}
if config.CommandsEnabled() {
var err error
c.commandBus, err = NewCommandBus(
config.CommandsPublisher,
config.GenerateCommandsTopic,
config.CommandEventMarshaler,
)
if err != nil {
return nil, errors.Wrap(err, "cannot create command bus")
}
} else {
config.Logger.Info("Empty GenerateCommandsTopic, command bus will be not created", nil)
}
if config.EventsEnabled() {
var err error
c.eventBus, err = NewEventBus(config.EventsPublisher, config.GenerateEventsTopic, config.CommandEventMarshaler)
if err != nil {
return nil, errors.Wrap(err, "cannot create event bus")
}
} else {
config.Logger.Info("Empty GenerateEventsTopic, event bus will be not created", nil)
}
if config.CommandHandlers != nil {
commandProcessor, err := NewCommandProcessor(
config.CommandHandlers(c.commandBus, c.eventBus),
config.GenerateCommandsTopic,
config.CommandsSubscriberConstructor,
config.CommandEventMarshaler,
config.Logger,
)
if err != nil {
return nil, errors.Wrap(err, "cannot create command processor")
}
if err := commandProcessor.AddHandlersToRouter(config.Router); err != nil {
return nil, err
}
}
if config.EventHandlers != nil {
eventProcessor, err := NewEventProcessor(
config.EventHandlers(c.commandBus, c.eventBus),
config.GenerateEventsTopic,
config.EventsSubscriberConstructor,
config.CommandEventMarshaler,
config.Logger,
)
if err != nil {
return nil, errors.Wrap(err, "cannot create event processor")
}
if err := eventProcessor.AddHandlersToRouter(config.Router); err != nil {
return nil, err
}
}
return c, nil
}

View File

@@ -0,0 +1,490 @@
package cqrs_test
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/ThreeDotsLabs/watermill/pubsub/gochannel"
)
// TestCQRS is functional test of CQRS command handler and event handler.
func TestCQRS(t *testing.T) {
testCases := []struct {
Name string
CreateCqrs func(t *testing.T, cc *CaptureCommandHandler, ce *CaptureEventHandler) (*message.Router, *cqrs.CommandBus, *cqrs.EventBus)
}{
{
// facade is deprecated, testing backwards compatibility
Name: "facade",
CreateCqrs: func(t *testing.T, cc *CaptureCommandHandler, ce *CaptureEventHandler) (*message.Router, *cqrs.CommandBus, *cqrs.EventBus) {
router, cqrsFacade := createRouterAndFacade(t, cc, ce)
return router, cqrsFacade.CommandBus(), cqrsFacade.EventBus()
},
},
{
Name: "constructors",
CreateCqrs: createCqrsComponents,
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.Name, func(t *testing.T) {
captureCommandHandler := &CaptureCommandHandler{}
captureEventHandler := &CaptureEventHandler{}
router, commandBus, eventBus := tc.CreateCqrs(t, captureCommandHandler, captureEventHandler)
pointerCmd := &TestCommand{ID: watermill.NewULID()}
require.NoError(t, commandBus.Send(context.Background(), pointerCmd))
assert.EqualValues(t, []interface{}{pointerCmd}, captureCommandHandler.HandledCommands())
captureCommandHandler.Reset()
nonPointerCmd := TestCommand{ID: watermill.NewULID()}
require.NoError(t, commandBus.Send(context.Background(), nonPointerCmd))
// command is always unmarshaled to pointer value
assert.EqualValues(t, []interface{}{&nonPointerCmd}, captureCommandHandler.HandledCommands())
captureCommandHandler.Reset()
pointerEvent := &TestEvent{ID: watermill.NewULID()}
require.NoError(t, eventBus.Publish(context.Background(), pointerEvent))
assert.EqualValues(t, []interface{}{pointerEvent}, captureEventHandler.HandledEvents())
captureEventHandler.Reset()
nonPointerEvent := TestEvent{ID: watermill.NewULID()}
require.NoError(t, eventBus.Publish(context.Background(), nonPointerEvent))
// event is always unmarshaled to pointer value
assert.EqualValues(t, []interface{}{&nonPointerEvent}, captureEventHandler.HandledEvents())
captureEventHandler.Reset()
assert.NoError(t, router.Close())
})
}
}
func createCqrsComponents(t *testing.T, commandHandler *CaptureCommandHandler, eventHandler *CaptureEventHandler) (*message.Router, *cqrs.CommandBus, *cqrs.EventBus) {
ts := NewTestServices()
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
eventProcessor, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return params.EventName, nil
},
AckOnUnknownEvent: true,
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
assert.Equal(t, "CaptureEventHandler", params.HandlerName)
assert.Implements(t, new(cqrs.EventHandler), params.EventHandler)
assert.NotNil(t, params.EventHandler)
return ts.EventsPubSub, nil
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = eventProcessor.AddHandlers(eventHandler)
require.NoError(t, err)
eventBus, err := cqrs.NewEventBusWithConfig(
ts.EventsPubSub,
cqrs.EventBusConfig{
GeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {
assert.Equal(t, "cqrs_test.TestEvent", params.EventName)
switch cmd := params.Event.(type) {
case *TestEvent:
assert.NotEmpty(t, cmd.ID)
case TestEvent:
assert.NotEmpty(t, cmd.ID)
default:
assert.Fail(t, "unexpected command type: %T", cmd)
}
assert.NotEmpty(t, params.Event)
return params.EventName, nil
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
router,
cqrs.CommandProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
assert.Equal(t, "cqrs_test.TestCommand", params.CommandName)
assert.Implements(t, new(cqrs.CommandHandler), params.CommandHandler)
assert.NotNil(t, params.CommandHandler)
return params.CommandName, nil
},
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
assert.Equal(t, "CaptureCommandHandler", params.HandlerName)
return ts.CommandsPubSub, nil
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
AckCommandHandlingErrors: false,
},
)
require.NoError(t, err)
err = commandProcessor.AddHandlers(commandHandler)
require.NoError(t, err)
commandBus, err := cqrs.NewCommandBusWithConfig(ts.CommandsPubSub, cqrs.CommandBusConfig{
GeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {
assert.Equal(t, "cqrs_test.TestCommand", params.CommandName)
switch cmd := params.Command.(type) {
case *TestCommand:
assert.NotEmpty(t, cmd.ID)
case TestCommand:
assert.NotEmpty(t, cmd.ID)
default:
assert.Fail(t, "unexpected command type: %T", cmd)
}
assert.NotNil(t, params.Command)
return params.CommandName, nil
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
})
require.NoError(t, err)
go func() {
require.NoError(t, router.Run(context.Background()))
}()
<-router.Running()
return router, commandBus, eventBus
}
func createRouterAndFacade(t *testing.T, commandHandler *CaptureCommandHandler, eventHandler *CaptureEventHandler) (*message.Router, *cqrs.Facade) {
ts := NewTestServices()
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
c, err := cqrs.NewFacade(cqrs.FacadeConfig{
GenerateCommandsTopic: func(commandName string) string {
assert.Equal(t, "cqrs_test.TestCommand", commandName)
return commandName
},
GenerateEventsTopic: func(eventName string) string {
assert.Equal(t, "cqrs_test.TestEvent", eventName)
return eventName
},
CommandHandlers: func(cb *cqrs.CommandBus, eb *cqrs.EventBus) []cqrs.CommandHandler {
require.NotNil(t, cb)
require.NotNil(t, eb)
return []cqrs.CommandHandler{commandHandler}
},
EventHandlers: func(cb *cqrs.CommandBus, eb *cqrs.EventBus) []cqrs.EventHandler {
require.NotNil(t, cb)
require.NotNil(t, eb)
return []cqrs.EventHandler{eventHandler}
},
Router: router,
CommandsPublisher: ts.CommandsPubSub,
CommandsSubscriberConstructor: func(handlerName string) (message.Subscriber, error) {
assert.Equal(t, "CaptureCommandHandler", handlerName)
return ts.CommandsPubSub, nil
},
EventsPublisher: ts.EventsPubSub,
EventsSubscriberConstructor: func(handlerName string) (message.Subscriber, error) {
assert.Equal(t, "CaptureEventHandler", handlerName)
return ts.EventsPubSub, nil
},
Logger: ts.Logger,
CommandEventMarshaler: ts.Marshaler,
})
require.NoError(t, err)
go func() {
require.NoError(t, router.Run(context.Background()))
}()
<-router.Running()
assert.Equal(t, c.CommandEventMarshaler(), ts.Marshaler)
return router, c
}
type TestServices struct {
Logger watermill.LoggerAdapter
CommandsPubSub *gochannel.GoChannel
EventsPubSub *gochannel.GoChannel
Marshaler cqrs.CommandEventMarshaler
}
func NewTestServices() TestServices {
logger := watermill.NewStdLogger(true, true)
return TestServices{
Logger: logger,
CommandsPubSub: gochannel.NewGoChannel(
gochannel.Config{BlockPublishUntilSubscriberAck: true},
logger,
),
EventsPubSub: gochannel.NewGoChannel(
gochannel.Config{BlockPublishUntilSubscriberAck: true},
logger,
),
Marshaler: cqrs.JSONMarshaler{},
}
}
type TestCommand struct {
ID string
}
type CaptureCommandHandler struct {
handledCommands []interface{}
}
func (h CaptureCommandHandler) HandlerName() string {
return "CaptureCommandHandler"
}
func (h CaptureCommandHandler) HandledCommands() []interface{} {
return h.handledCommands
}
func (h *CaptureCommandHandler) Reset() {
h.handledCommands = nil
}
func (CaptureCommandHandler) NewCommand() interface{} {
return &TestCommand{}
}
func (h *CaptureCommandHandler) Handle(ctx context.Context, cmd interface{}) error {
h.handledCommands = append(h.handledCommands, cmd.(*TestCommand))
return nil
}
type TestEvent struct {
ID string
When time.Time
}
type AnotherTestEvent struct {
ID string
}
type CaptureEventHandler struct {
handledEvents []interface{}
}
func (h CaptureEventHandler) HandlerName() string {
return "CaptureEventHandler"
}
func (h CaptureEventHandler) HandledEvents() []interface{} {
return h.handledEvents
}
func (h *CaptureEventHandler) Reset() {
h.handledEvents = nil
}
func (CaptureEventHandler) NewEvent() interface{} {
return &TestEvent{}
}
func (h *CaptureEventHandler) Handle(ctx context.Context, event interface{}) error {
h.handledEvents = append(h.handledEvents, event.(*TestEvent))
return nil
}
type assertPublishTopicPublisher struct {
ExpectedTopic string
T *testing.T
}
func (a assertPublishTopicPublisher) Publish(topic string, messages ...*message.Message) error {
assert.Equal(a.T, a.ExpectedTopic, topic)
return nil
}
func (assertPublishTopicPublisher) Close() error {
return nil
}
type publisherStub struct {
messages map[string]message.Messages
mu sync.Mutex
}
func newPublisherStub() *publisherStub {
return &publisherStub{
messages: make(map[string]message.Messages),
}
}
func (*publisherStub) Close() error {
return nil
}
func (p *publisherStub) Publish(topic string, messages ...*message.Message) error {
p.mu.Lock()
defer p.mu.Unlock()
p.messages[topic] = append(p.messages[topic], messages...)
return nil
}
func TestFacadeConfig_Validate(t *testing.T) {
ts := NewTestServices()
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
validConfig := cqrs.FacadeConfig{
GenerateCommandsTopic: func(commandName string) string {
return commandName
},
GenerateEventsTopic: func(eventName string) string {
return eventName
},
CommandHandlers: func(cb *cqrs.CommandBus, eb *cqrs.EventBus) []cqrs.CommandHandler {
return []cqrs.CommandHandler{}
},
EventHandlers: func(cb *cqrs.CommandBus, eb *cqrs.EventBus) []cqrs.EventHandler {
return []cqrs.EventHandler{}
},
Router: router,
CommandsPublisher: ts.CommandsPubSub,
CommandsSubscriberConstructor: func(handlerName string) (message.Subscriber, error) {
return ts.CommandsPubSub, nil
},
EventsPublisher: ts.EventsPubSub,
EventsSubscriberConstructor: func(handlerName string) (message.Subscriber, error) {
return ts.EventsPubSub, nil
},
Logger: ts.Logger,
CommandEventMarshaler: ts.Marshaler,
}
testCases := []struct {
Name string
Config cqrs.FacadeConfig
Valid bool
}{
{
Name: "valid",
Config: validConfig,
Valid: true,
},
{
Name: "missing_GenerateCommandsTopic",
Config: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {
config.GenerateCommandsTopic = nil
}),
Valid: false,
},
{
Name: "missing_CommandsSubscriberConstructor",
Config: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {
config.CommandsSubscriberConstructor = nil
}),
Valid: false,
},
{
Name: "missing_CommandsPublisher",
Config: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {
config.CommandsPublisher = nil
}),
Valid: false,
},
{
Name: "missing_GenerateEventsTopic",
Config: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {
config.GenerateEventsTopic = nil
}),
Valid: false,
},
{
Name: "missing_GenerateEventsTopic",
Config: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {
config.EventsSubscriberConstructor = nil
}),
Valid: false,
},
{
Name: "missing_EventsPublisher",
Config: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {
config.EventsPublisher = nil
}),
Valid: false,
},
{
Name: "missing_Router",
Config: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {
config.Router = nil
}),
Valid: false,
},
{
Name: "missing_Logger",
Config: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {
config.Logger = nil
}),
Valid: false,
},
{
Name: "missing_CommandEventMarshaler",
Config: transformConfig(validConfig, func(config *cqrs.FacadeConfig) {
config.CommandEventMarshaler = nil
}),
Valid: false,
},
}
for _, c := range testCases {
t.Run(c.Name, func(t *testing.T) {
if c.Valid {
assert.NoError(t, c.Config.Validate())
} else {
assert.Error(t, c.Config.Validate())
}
})
}
}
func transformConfig(config cqrs.FacadeConfig, transformFn func(config *cqrs.FacadeConfig)) cqrs.FacadeConfig {
transformFn(&config)
return config
}

27
components/cqrs/ctx.go Normal file
View File

@@ -0,0 +1,27 @@
package cqrs
import (
"context"
"github.com/ThreeDotsLabs/watermill/message"
)
type ctxKey string
const (
originalMessage ctxKey = "original_message"
)
// OriginalMessageFromCtx returns the original message that was received by the event/command handler.
func OriginalMessageFromCtx(ctx context.Context) *message.Message {
val, ok := ctx.Value(originalMessage).(*message.Message)
if !ok {
return nil
}
return val
}
// CtxWithOriginalMessage returns a new context with the original message attached.
func CtxWithOriginalMessage(ctx context.Context, msg *message.Message) context.Context {
return context.WithValue(ctx, originalMessage, msg)
}

3
components/cqrs/doc.go Normal file
View File

@@ -0,0 +1,3 @@
// Detailed CQRS documentation can be found in https://watermill.io/docs/cqrs/
package cqrs

View File

@@ -0,0 +1,146 @@
package cqrs
import (
"context"
stdErrors "errors"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/pkg/errors"
)
type EventBusConfig struct {
// GeneratePublishTopic is used to generate topic name for publishing event.
GeneratePublishTopic GenerateEventPublishTopicFn
// OnPublish is called before sending the event.
// The *message.Message can be modified.
//
// This option is not required.
OnPublish OnEventSendFn
// Marshaler is used to marshal and unmarshal events.
// It is required.
Marshaler CommandEventMarshaler
// Logger instance used to log.
// If not provided, watermill.NopLogger is used.
Logger watermill.LoggerAdapter
}
func (c *EventBusConfig) setDefaults() {
if c.Logger == nil {
c.Logger = watermill.NopLogger{}
}
}
func (c EventBusConfig) Validate() error {
var err error
if c.Marshaler == nil {
err = stdErrors.Join(err, errors.New("missing Marshaler"))
}
if c.GeneratePublishTopic == nil {
err = stdErrors.Join(err, errors.New("missing GenerateHandlerTopic"))
}
return err
}
type GenerateEventPublishTopicFn func(GenerateEventPublishTopicParams) (string, error)
type GenerateEventPublishTopicParams struct {
EventName string
Event any
}
type OnEventSendFn func(params OnEventSendParams) error
type OnEventSendParams struct {
EventName string
Event any
// Message is never nil and can be modified.
Message *message.Message
}
// EventBus transports events to event handlers.
type EventBus struct {
publisher message.Publisher
config EventBusConfig
}
// NewEventBus creates a new CommandBus.
// Deprecated: use NewEventBusWithConfig instead.
func NewEventBus(
publisher message.Publisher,
generateTopic func(eventName string) string,
marshaler CommandEventMarshaler,
) (*EventBus, error) {
if publisher == nil {
return nil, errors.New("missing publisher")
}
if generateTopic == nil {
return nil, errors.New("missing generateTopic")
}
if marshaler == nil {
return nil, errors.New("missing marshaler")
}
return &EventBus{
publisher: publisher,
config: EventBusConfig{
GeneratePublishTopic: func(params GenerateEventPublishTopicParams) (string, error) {
return generateTopic(params.EventName), nil
},
Marshaler: marshaler,
},
}, nil
}
// NewEventBusWithConfig creates a new EventBus.
func NewEventBusWithConfig(publisher message.Publisher, config EventBusConfig) (*EventBus, error) {
if publisher == nil {
return nil, errors.New("missing publisher")
}
config.setDefaults()
if err := config.Validate(); err != nil {
return nil, errors.Wrap(err, "invalid config")
}
return &EventBus{publisher, config}, nil
}
// Publish sends event to the event bus.
func (c EventBus) Publish(ctx context.Context, event any) error {
msg, err := c.config.Marshaler.Marshal(event)
if err != nil {
return err
}
eventName := c.config.Marshaler.Name(event)
topicName, err := c.config.GeneratePublishTopic(GenerateEventPublishTopicParams{
EventName: eventName,
Event: event,
})
if err != nil {
return errors.Wrap(err, "cannot generate topic")
}
msg.SetContext(ctx)
if c.config.OnPublish != nil {
err := c.config.OnPublish(OnEventSendParams{
EventName: eventName,
Event: event,
Message: msg,
})
if err != nil {
return errors.Wrap(err, "cannot execute OnPublish")
}
}
return c.publisher.Publish(topicName, msg)
}

View File

@@ -0,0 +1,168 @@
package cqrs_test
import (
"context"
"fmt"
"testing"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEventBusConfig_Validate(t *testing.T) {
testCases := []struct {
Name string
ModifyValidConfig func(*cqrs.EventBusConfig)
ExpectedErr error
}{
{
Name: "valid_config",
ModifyValidConfig: nil,
ExpectedErr: nil,
},
{
Name: "missing_GenerateEventPublishTopic",
ModifyValidConfig: func(config *cqrs.EventBusConfig) {
config.GeneratePublishTopic = nil
},
ExpectedErr: fmt.Errorf("missing GenerateHandlerTopic"),
},
{
Name: "missing_marshaler",
ModifyValidConfig: func(config *cqrs.EventBusConfig) {
config.Marshaler = nil
},
ExpectedErr: fmt.Errorf("missing Marshaler"),
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.Name, func(t *testing.T) {
validConfig := cqrs.EventBusConfig{
GeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {
return "", nil
},
Marshaler: cqrs.JSONMarshaler{},
}
if tc.ModifyValidConfig != nil {
tc.ModifyValidConfig(&validConfig)
}
err := validConfig.Validate()
if tc.ExpectedErr == nil {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.ExpectedErr.Error())
}
})
}
}
func TestNewEventBus(t *testing.T) {
pub := newPublisherStub()
generateTopic := func(commandName string) string {
return ""
}
marshaler := cqrs.JSONMarshaler{}
cb, err := cqrs.NewEventBus(pub, generateTopic, marshaler)
assert.NotNil(t, cb)
assert.NoError(t, err)
cb, err = cqrs.NewEventBus(nil, generateTopic, marshaler)
assert.Nil(t, cb)
assert.Error(t, err)
cb, err = cqrs.NewEventBus(pub, nil, marshaler)
assert.Nil(t, cb)
assert.Error(t, err)
cb, err = cqrs.NewEventBus(pub, generateTopic, nil)
assert.Nil(t, cb)
assert.Error(t, err)
}
func TestEventBus_Send_ContextPropagation(t *testing.T) {
publisher := newPublisherStub()
eventBus, err := cqrs.NewEventBus(
publisher,
func(eventName string) string {
return "whatever"
},
cqrs.JSONMarshaler{},
)
require.NoError(t, err)
ctx := context.WithValue(context.Background(), contextKey("key"), "value")
err = eventBus.Publish(ctx, "message")
require.NoError(t, err)
assert.Equal(t, ctx, publisher.messages["whatever"][0].Context())
}
func TestEventBus_Send_topic_name(t *testing.T) {
cb, err := cqrs.NewEventBus(
assertPublishTopicPublisher{ExpectedTopic: "cqrs_test.TestEvent", T: t},
func(commandName string) string {
return commandName
},
cqrs.JSONMarshaler{},
)
require.NoError(t, err)
err = cb.Publish(context.Background(), TestEvent{})
require.NoError(t, err)
}
func TestEventBus_Send_OnPublish(t *testing.T) {
publisher := newPublisherStub()
eb, err := cqrs.NewEventBusWithConfig(
publisher,
cqrs.EventBusConfig{
GeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {
return "whatever", nil
},
Marshaler: cqrs.JSONMarshaler{},
OnPublish: func(params cqrs.OnEventSendParams) error {
params.Message.Metadata.Set("key", "value")
return nil
},
},
)
require.NoError(t, err)
err = eb.Publish(context.Background(), TestEvent{})
require.NoError(t, err)
assert.Equal(t, "value", publisher.messages["whatever"][0].Metadata.Get("key"))
}
func TestEventBus_Send_OnPublish_error(t *testing.T) {
publisher := newPublisherStub()
expectedErr := errors.New("some error")
eb, err := cqrs.NewEventBusWithConfig(
publisher,
cqrs.EventBusConfig{
GeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {
return "whatever", nil
},
Marshaler: cqrs.JSONMarshaler{},
OnPublish: func(params cqrs.OnEventSendParams) error {
return expectedErr
},
},
)
require.NoError(t, err)
err = eb.Publish(context.Background(), TestEvent{})
require.EqualError(t, err, "cannot execute OnPublish: some error")
}

View File

@@ -0,0 +1,73 @@
package cqrs
import (
"context"
)
// EventHandler receives events defined by NewEvent and handles them with its Handle method.
// If using DDD, CommandHandler may modify and persist the aggregate.
// It can also invoke a process manager, a saga or just build a read model.
//
// In contrast to CommandHandler, every Event can have multiple EventHandlers.
//
// One instance of EventHandler is used during handling messages.
// When multiple events are delivered at the same time, Handle method can be executed multiple times at the same time.
// Because of that, Handle method needs to be thread safe!
type EventHandler interface {
// HandlerName is the name used in message.Router while creating handler.
//
// It will be also passed to EventsSubscriberConstructor.
// May be useful, for example, to create a consumer group per each handler.
//
// WARNING: If HandlerName was changed and is used for generating consumer groups,
// it may result with **reconsuming all messages** !!!
HandlerName() string
NewEvent() any
Handle(ctx context.Context, event any) error
}
type genericEventHandler[T any] struct {
handleFunc func(ctx context.Context, event *T) error
handlerName string
}
// NewEventHandler creates a new EventHandler implementation based on provided function
// and event type inferred from function argument.
func NewEventHandler[T any](
handlerName string,
handleFunc func(ctx context.Context, event *T) error,
) EventHandler {
return &genericEventHandler[T]{
handleFunc: handleFunc,
handlerName: handlerName,
}
}
func (c genericEventHandler[T]) HandlerName() string {
return c.handlerName
}
func (c genericEventHandler[T]) NewEvent() any {
tVar := new(T)
return tVar
}
func (c genericEventHandler[T]) Handle(ctx context.Context, e any) error {
event := e.(*T)
return c.handleFunc(ctx, event)
}
type GroupEventHandler interface {
NewEvent() interface{}
Handle(ctx context.Context, event interface{}) error
}
// NewGroupEventHandler creates a new GroupEventHandler implementation based on provided function
// and event type inferred from function argument.
func NewGroupEventHandler[T any](handleFunc func(ctx context.Context, event *T) error) GroupEventHandler {
return &genericEventHandler[T]{
handleFunc: handleFunc,
}
}

View File

@@ -0,0 +1,48 @@
package cqrs_test
import (
"context"
"fmt"
"testing"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
"github.com/stretchr/testify/assert"
)
type SomeEvent struct {
Foo string
}
func TestNewEventHandler(t *testing.T) {
cmdToSend := &SomeEvent{"bar"}
ch := cqrs.NewEventHandler(
"some_handler",
func(ctx context.Context, cmd *SomeEvent) error {
assert.Equal(t, cmdToSend, cmd)
return fmt.Errorf("some error")
},
)
assert.Equal(t, "some_handler", ch.HandlerName())
assert.Equal(t, &SomeEvent{}, ch.NewEvent())
err := ch.Handle(context.Background(), cmdToSend)
assert.EqualError(t, err, "some error")
}
func TestNewGroupEventHandler(t *testing.T) {
cmdToSend := &SomeEvent{"bar"}
ch := cqrs.NewGroupEventHandler(
func(ctx context.Context, cmd *SomeEvent) error {
assert.Equal(t, cmdToSend, cmd)
return fmt.Errorf("some error")
},
)
assert.Equal(t, &SomeEvent{}, ch.NewEvent())
err := ch.Handle(context.Background(), cmdToSend)
assert.EqualError(t, err, "some error")
}

View File

@@ -0,0 +1,380 @@
package cqrs
import (
stdErrors "errors"
"fmt"
"github.com/pkg/errors"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/message"
)
type EventProcessorConfig struct {
// GenerateSubscribeTopic is used to generate topic for subscribing to events.
// If event processor is using handler groups, GenerateSubscribeTopic is used instead.
GenerateSubscribeTopic EventProcessorGenerateSubscribeTopicFn
// SubscriberConstructor is used to create subscriber for EventHandler.
//
// This function is called for every EventHandler instance.
// If you want to re-use one subscriber for multiple handlers, use GroupEventProcessor instead.
SubscriberConstructor EventProcessorSubscriberConstructorFn
// OnHandle is called before handling event.
// OnHandle works in a similar way to middlewares: you can inject additional logic before and after handling an event.
//
// Because of that, you need to explicitly call params.Handler.Handle() to handle the event.
//
// func(params EventProcessorOnHandleParams) (err error) {
// // logic before handle
// // (...)
//
// err := params.Handler.Handle(params.Message.Context(), params.Event)
//
// // logic after handle
// // (...)
//
// return err
// }
//
// This option is not required.
OnHandle EventProcessorOnHandleFn
// AckOnUnknownEvent is used to decide if message should be acked if event has no handler defined.
AckOnUnknownEvent bool
// Marshaler is used to marshal and unmarshal events.
// It is required.
Marshaler CommandEventMarshaler
// Logger instance used to log.
// If not provided, watermill.NopLogger is used.
Logger watermill.LoggerAdapter
// disableRouterAutoAddHandlers is used to keep backwards compatibility.
// it is set when EventProcessor is created by NewEventProcessor.
// Deprecated: please migrate to NewEventProcessorWithConfig.
disableRouterAutoAddHandlers bool
}
func (c *EventProcessorConfig) setDefaults() {
if c.Logger == nil {
c.Logger = watermill.NopLogger{}
}
}
func (c EventProcessorConfig) Validate() error {
var err error
if c.Marshaler == nil {
err = stdErrors.Join(err, errors.New("missing Marshaler"))
}
if c.GenerateSubscribeTopic == nil {
err = stdErrors.Join(err, errors.New("missing GenerateHandlerTopic"))
}
if c.SubscriberConstructor == nil {
err = stdErrors.Join(err, errors.New("missing SubscriberConstructor"))
}
return err
}
type EventProcessorGenerateSubscribeTopicFn func(EventProcessorGenerateSubscribeTopicParams) (string, error)
type EventProcessorGenerateSubscribeTopicParams struct {
EventName string
EventHandler EventHandler
}
type EventProcessorSubscriberConstructorFn func(EventProcessorSubscriberConstructorParams) (message.Subscriber, error)
type EventProcessorSubscriberConstructorParams struct {
EventName string
HandlerName string
EventHandler EventHandler
}
type EventProcessorOnHandleFn func(params EventProcessorOnHandleParams) error
type EventProcessorOnHandleParams struct {
Handler EventHandler
Event any
EventName string
// Message is never nil and can be modified.
Message *message.Message
}
// EventProcessor determines which EventHandler should handle event received from event bus.
type EventProcessor struct {
router *message.Router
handlers []EventHandler
config EventProcessorConfig
}
// NewEventProcessorWithConfig creates a new EventProcessor.
func NewEventProcessorWithConfig(router *message.Router, config EventProcessorConfig) (*EventProcessor, error) {
config.setDefaults()
if err := config.Validate(); err != nil {
return nil, errors.Wrap(err, "invalid config EventProcessor")
}
if router == nil && !config.disableRouterAutoAddHandlers {
return nil, errors.New("missing router")
}
return &EventProcessor{
router: router,
config: config,
}, nil
}
// NewEventProcessor creates a new EventProcessor.
// Deprecated. Use NewEventProcessorWithConfig instead.
func NewEventProcessor(
individualHandlers []EventHandler,
generateTopic func(eventName string) string,
subscriberConstructor EventsSubscriberConstructor,
marshaler CommandEventMarshaler,
logger watermill.LoggerAdapter,
) (*EventProcessor, error) {
if len(individualHandlers) == 0 {
return nil, errors.New("missing handlers")
}
if generateTopic == nil {
return nil, errors.New("nil generateTopic")
}
if subscriberConstructor == nil {
return nil, errors.New("missing subscriberConstructor")
}
if marshaler == nil {
return nil, errors.New("missing marshaler")
}
if logger == nil {
logger = watermill.NopLogger{}
}
eventProcessorConfig := EventProcessorConfig{
AckOnUnknownEvent: true, // this is the previous default behaviour - keeping backwards compatibility
GenerateSubscribeTopic: func(params EventProcessorGenerateSubscribeTopicParams) (string, error) {
return generateTopic(params.EventName), nil
},
SubscriberConstructor: func(params EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return subscriberConstructor(params.HandlerName)
},
Marshaler: marshaler,
Logger: logger,
disableRouterAutoAddHandlers: true,
}
eventProcessorConfig.setDefaults()
ep, err := NewEventProcessorWithConfig(nil, eventProcessorConfig)
if err != nil {
return nil, err
}
for _, handler := range individualHandlers {
if err := ep.AddHandlers(handler); err != nil {
return nil, err
}
}
return ep, nil
}
// EventsSubscriberConstructor creates a subscriber for EventHandler.
// It allows you to create separated customized Subscriber for every command handler.
//
// When handler groups are used, handler group is passed as handlerName.
// Deprecated: please use EventProcessorSubscriberConstructorFn instead.
type EventsSubscriberConstructor func(handlerName string) (message.Subscriber, error)
// AddHandlers adds a new EventHandler to the EventProcessor and adds it to the router.
func (p *EventProcessor) AddHandlers(handlers ...EventHandler) error {
if p.config.disableRouterAutoAddHandlers {
p.handlers = append(p.handlers, handlers...)
return nil
}
for _, handler := range handlers {
if _, err := p.addHandlerToRouter(p.router, handler); err != nil {
return err
}
p.handlers = append(p.handlers, handler)
}
return nil
}
// AddHandler adds a new EventHandler to the EventProcessor and adds it to the router.
func (p *EventProcessor) AddHandler(handler EventHandler) (*message.Handler, error) {
if p.config.disableRouterAutoAddHandlers {
p.handlers = append(p.handlers, handler)
return nil, nil
}
h, err := p.addHandlerToRouter(p.router, handler)
if err != nil {
return nil, err
}
p.handlers = append(p.handlers, handler)
return h, nil
}
// AddHandlersToRouter adds the EventProcessor's handlers to the given router.
// It should be called only once per EventProcessor instance.
//
// It is required to call AddHandlersToRouter only if command processor is created with NewEventProcessor (disableRouterAutoAddHandlers is set to true).
// Deprecated: please migrate to event processor created by NewEventProcessorWithConfig.
func (p EventProcessor) AddHandlersToRouter(r *message.Router) error {
if !p.config.disableRouterAutoAddHandlers {
return errors.New("AddHandlersToRouter should be called only when using deprecated NewEventProcessor")
}
for i := range p.handlers {
handler := p.handlers[i]
if _, err := p.addHandlerToRouter(r, handler); err != nil {
return err
}
}
return nil
}
func (p EventProcessor) addHandlerToRouter(r *message.Router, handler EventHandler) (*message.Handler, error) {
if err := validateEvent(handler.NewEvent()); err != nil {
return nil, errors.Wrapf(err, "invalid event for handler %s", handler.HandlerName())
}
handlerName := handler.HandlerName()
eventName := p.config.Marshaler.Name(handler.NewEvent())
topicName, err := p.config.GenerateSubscribeTopic(EventProcessorGenerateSubscribeTopicParams{
EventName: eventName,
EventHandler: handler,
})
if err != nil {
return nil, errors.Wrapf(err, "cannot generate topic name for handler %s", handlerName)
}
logger := p.config.Logger.With(watermill.LogFields{
"event_handler_name": handlerName,
"topic": topicName,
})
handlerFunc, err := p.routerHandlerFunc(handler, logger)
if err != nil {
return nil, err
}
if p.config.SubscriberConstructor == nil {
return nil, errors.New("missing SubscriberConstructor config option")
}
subscriber, err := p.config.SubscriberConstructor(EventProcessorSubscriberConstructorParams{
EventName: eventName,
HandlerName: handlerName,
EventHandler: handler,
})
if err != nil {
return nil, errors.Wrap(err, "cannot create subscriber for event processor")
}
return addHandlerToRouter(p.config.Logger, r, handlerName, topicName, handlerFunc, subscriber), nil
}
func (p EventProcessor) Handlers() []EventHandler {
return p.handlers
}
func addHandlerToRouter(logger watermill.LoggerAdapter, r *message.Router, handlerName string, topicName string, handlerFunc message.NoPublishHandlerFunc, subscriber message.Subscriber) *message.Handler {
logger = logger.With(watermill.LogFields{
"event_handler_name": handlerName,
"topic": topicName,
})
logger.Debug("Adding CQRS event handler to router", nil)
return r.AddConsumerHandler(
handlerName,
topicName,
subscriber,
handlerFunc,
)
}
func (p EventProcessor) routerHandlerFunc(handler EventHandler, logger watermill.LoggerAdapter) (message.NoPublishHandlerFunc, error) {
initEvent := handler.NewEvent()
expectedEventName := p.config.Marshaler.Name(initEvent)
if err := validateEvent(initEvent); err != nil {
return nil, err
}
return func(msg *message.Message) error {
event := handler.NewEvent()
messageEventName := p.config.Marshaler.NameFromMessage(msg)
if messageEventName != expectedEventName {
if !p.config.AckOnUnknownEvent {
return fmt.Errorf("received unexpected event type %s, expected %s", messageEventName, expectedEventName)
} else {
logger.Trace("Received different event type than expected, ignoring", watermill.LogFields{
"message_uuid": msg.UUID,
"expected_event_type": expectedEventName,
"received_event_type": messageEventName,
})
return nil
}
}
logger.Debug("Handling event", watermill.LogFields{
"message_uuid": msg.UUID,
"received_event_type": messageEventName,
})
ctx := CtxWithOriginalMessage(msg.Context(), msg)
msg.SetContext(ctx)
if err := p.config.Marshaler.Unmarshal(msg, event); err != nil {
return err
}
handle := func(params EventProcessorOnHandleParams) error {
return params.Handler.Handle(ctx, params.Event)
}
if p.config.OnHandle != nil {
handle = p.config.OnHandle
}
err := handle(EventProcessorOnHandleParams{
Handler: handler,
Event: event,
EventName: messageEventName,
Message: msg,
})
if err != nil {
logger.Debug("Error when handling event", watermill.LogFields{"err": err})
return err
}
return nil
}, nil
}
func validateEvent(event interface{}) error {
// EventHandler's NewEvent must return a pointer, because it is used to unmarshal
if err := isPointer(event); err != nil {
return errors.Wrap(err, "command must be a non-nil pointer")
}
return nil
}

View File

@@ -0,0 +1,272 @@
package cqrs
import (
stdErrors "errors"
"fmt"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/pkg/errors"
)
type EventGroupProcessorConfig struct {
// GenerateSubscribeTopic is used to generate topic for subscribing to events for handler groups.
// This option is required for EventProcessor if handler groups are used.
GenerateSubscribeTopic EventGroupProcessorGenerateSubscribeTopicFn
// SubscriberConstructor is used to create subscriber for GroupEventHandler.
// This function is called for every events group once - thanks to that it's possible to have one subscription per group.
// It's useful, when we are processing events from one stream and we want to do it in order.
SubscriberConstructor EventGroupProcessorSubscriberConstructorFn
// OnHandle is called before handling event.
// OnHandle works in a similar way to middlewares: you can inject additional logic before and after handling an event.
//
// Because of that, you need to explicitly call params.Handler.Handle() to handle the event.
//
// func(params EventGroupProcessorOnHandleParams) (err error) {
// // logic before handle
// // (...)
//
// err := params.Handler.Handle(params.Message.Context(), params.Event)
//
// // logic after handle
// // (...)
//
// return err
// }
//
// This option is not required.
OnHandle EventGroupProcessorOnHandleFn
// AckOnUnknownEvent is used to decide if message should be acked if event has no handler defined.
AckOnUnknownEvent bool
// Marshaler is used to marshal and unmarshal events.
// It is required.
Marshaler CommandEventMarshaler
// Logger instance used to log.
// If not provided, watermill.NopLogger is used.
Logger watermill.LoggerAdapter
}
func (c *EventGroupProcessorConfig) setDefaults() {
if c.Logger == nil {
c.Logger = watermill.NopLogger{}
}
}
func (c EventGroupProcessorConfig) Validate() error {
var err error
if c.Marshaler == nil {
err = stdErrors.Join(err, errors.New("missing Marshaler"))
}
if c.GenerateSubscribeTopic == nil {
err = stdErrors.Join(err, errors.New("missing GenerateHandlerGroupTopic"))
}
if c.SubscriberConstructor == nil {
err = stdErrors.Join(err, errors.New("missing SubscriberConstructor"))
}
return err
}
type EventGroupProcessorGenerateSubscribeTopicFn func(EventGroupProcessorGenerateSubscribeTopicParams) (string, error)
type EventGroupProcessorGenerateSubscribeTopicParams struct {
EventGroupName string
EventGroupHandlers []GroupEventHandler
}
type EventGroupProcessorSubscriberConstructorFn func(EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error)
type EventGroupProcessorSubscriberConstructorParams struct {
EventGroupName string
EventGroupHandlers []GroupEventHandler
}
type EventGroupProcessorOnHandleFn func(params EventGroupProcessorOnHandleParams) error
type EventGroupProcessorOnHandleParams struct {
GroupName string
Handler GroupEventHandler
Event any
EventName string
// Message is never nil and can be modified.
Message *message.Message
}
// EventGroupProcessor determines which EventHandler should handle event received from event bus.
// Compared to EventProcessor, EventGroupProcessor allows to have multiple handlers that share the same subscriber instance.
type EventGroupProcessor struct {
router *message.Router
groupEventHandlers map[string][]GroupEventHandler
config EventGroupProcessorConfig
}
// NewEventGroupProcessorWithConfig creates a new EventGroupProcessor.
func NewEventGroupProcessorWithConfig(router *message.Router, config EventGroupProcessorConfig) (*EventGroupProcessor, error) {
config.setDefaults()
if err := config.Validate(); err != nil {
return nil, errors.Wrap(err, "invalid config EventProcessor")
}
if router == nil {
return nil, errors.New("missing router")
}
return &EventGroupProcessor{
router: router,
groupEventHandlers: map[string][]GroupEventHandler{},
config: config,
}, nil
}
// AddHandlersGroup adds a new list of GroupEventHandler to the EventGroupProcessor and adds it to the router.
//
// Compared to AddHandlers, AddHandlersGroup allows to have multiple handlers that share the same subscriber instance.
//
// It's allowed to have multiple handlers for the same event type in one group, but we recommend to not do that.
// Please keep in mind that those handlers will be processed within the same message.
// If first handler succeeds and the second fails, the message will be re-delivered and the first will be re-executed.
//
// Handlers group needs to be unique within the EventProcessor instance.
//
// Handler group name is used as handler's name in router.
func (p *EventGroupProcessor) AddHandlersGroup(groupName string, handlers ...GroupEventHandler) error {
if len(handlers) == 0 {
return errors.New("no handlers provided")
}
if _, ok := p.groupEventHandlers[groupName]; ok {
return fmt.Errorf("event handler group '%s' already exists", groupName)
}
if err := p.addHandlerToRouter(p.router, groupName, handlers); err != nil {
return err
}
p.groupEventHandlers[groupName] = handlers
return nil
}
func (p EventGroupProcessor) addHandlerToRouter(r *message.Router, groupName string, handlersGroup []GroupEventHandler) error {
for i, handler := range handlersGroup {
if err := validateEvent(handler.NewEvent()); err != nil {
return errors.Wrapf(
err,
"invalid event for handler %T (num %d) in group %s",
handler,
i,
groupName,
)
}
}
topicName, err := p.config.GenerateSubscribeTopic(EventGroupProcessorGenerateSubscribeTopicParams{
EventGroupName: groupName,
EventGroupHandlers: handlersGroup,
})
if err != nil {
return errors.Wrapf(err, "cannot generate topic name for handler group %s", groupName)
}
logger := p.config.Logger.With(watermill.LogFields{
"event_handler_group_name": groupName,
"topic": topicName,
})
handlerFunc, err := p.routerHandlerGroupFunc(handlersGroup, groupName, logger)
if err != nil {
return err
}
subscriber, err := p.config.SubscriberConstructor(EventGroupProcessorSubscriberConstructorParams{
EventGroupName: groupName,
EventGroupHandlers: handlersGroup,
})
if err != nil {
return errors.Wrap(err, "cannot create subscriber for event processor")
}
_ = addHandlerToRouter(p.config.Logger, r, groupName, topicName, handlerFunc, subscriber)
return nil
}
func (p EventGroupProcessor) routerHandlerGroupFunc(handlers []GroupEventHandler, groupName string, logger watermill.LoggerAdapter) (message.NoPublishHandlerFunc, error) {
return func(msg *message.Message) error {
messageEventName := p.config.Marshaler.NameFromMessage(msg)
handledAnyEvent := false
for _, handler := range handlers {
initEvent := handler.NewEvent()
expectedEventName := p.config.Marshaler.Name(initEvent)
event := handler.NewEvent()
if messageEventName != expectedEventName {
logger.Trace("Received different event type than expected, ignoring", watermill.LogFields{
"message_uuid": msg.UUID,
"expected_event_type": expectedEventName,
"received_event_type": messageEventName,
})
continue
}
logger.Debug("Handling event", watermill.LogFields{
"message_uuid": msg.UUID,
"received_event_type": messageEventName,
})
ctx := CtxWithOriginalMessage(msg.Context(), msg)
msg.SetContext(ctx)
if err := p.config.Marshaler.Unmarshal(msg, event); err != nil {
return err
}
handle := func(params EventGroupProcessorOnHandleParams) error {
return params.Handler.Handle(ctx, params.Event)
}
if p.config.OnHandle != nil {
handle = p.config.OnHandle
}
err := handle(EventGroupProcessorOnHandleParams{
GroupName: groupName,
Handler: handler,
EventName: messageEventName,
Event: event,
Message: msg,
})
if err != nil {
logger.Debug("Error when handling event", watermill.LogFields{"err": err})
return err
}
handledAnyEvent = true
}
if handledAnyEvent {
return nil
}
if !p.config.AckOnUnknownEvent {
return fmt.Errorf("no handler found for event %s", p.config.Marshaler.NameFromMessage(msg))
} else {
logger.Trace("Received event can't be handled by any handler in handler group", watermill.LogFields{
"message_uuid": msg.UUID,
"received_event_type": messageEventName,
})
return nil
}
}, nil
}

View File

@@ -0,0 +1,457 @@
package cqrs_test
import (
"context"
"fmt"
"sync/atomic"
"testing"
"time"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEventGroupProcessorConfig_Validate(t *testing.T) {
testCases := []struct {
Name string
ModifyValidConfig func(*cqrs.EventGroupProcessorConfig)
ExpectedErr error
}{
{
Name: "valid_config",
ModifyValidConfig: nil,
ExpectedErr: nil,
},
{
Name: "valid_with_group_handlers",
ExpectedErr: nil,
},
{
Name: "missing_GroupSubscriberConstructor",
ModifyValidConfig: func(config *cqrs.EventGroupProcessorConfig) {
config.SubscriberConstructor = nil
},
ExpectedErr: fmt.Errorf("missing SubscriberConstructor"),
},
{
Name: "missing_GenerateHandlerGroupSubscribeTopic",
ModifyValidConfig: func(config *cqrs.EventGroupProcessorConfig) {
config.GenerateSubscribeTopic = nil
},
ExpectedErr: fmt.Errorf("missing GenerateHandlerGroupTopic"),
},
{
Name: "missing_marshaler",
ModifyValidConfig: func(config *cqrs.EventGroupProcessorConfig) {
config.Marshaler = nil
},
ExpectedErr: fmt.Errorf("missing Marshaler"),
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.Name, func(t *testing.T) {
validConfig := cqrs.EventGroupProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: cqrs.JSONMarshaler{},
}
if tc.ModifyValidConfig != nil {
tc.ModifyValidConfig(&validConfig)
}
err := validConfig.Validate()
if tc.ExpectedErr == nil {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.ExpectedErr.Error())
}
})
}
}
func TestNewEventProcessor_OnGroupHandle(t *testing.T) {
ts := NewTestServices()
msg1, err := ts.Marshaler.Marshal(&TestEvent{ID: "1"})
require.NoError(t, err)
msg2, err := ts.Marshaler.Marshal(&TestEvent{ID: "2"})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg1,
msg2,
},
}
handlerCalled := 0
defer func() {
// for msg 1 we are not calling handler - but returning before
assert.Equal(t, 1, handlerCalled)
}()
handler := cqrs.NewEventHandler("test", func(ctx context.Context, cmd *TestEvent) error {
handlerCalled++
return nil
})
onHandleCalled := int64(0)
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
config := cqrs.EventGroupProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
OnHandle: func(params cqrs.EventGroupProcessorOnHandleParams) error {
atomic.AddInt64(&onHandleCalled, 1)
assert.Equal(t, "some_group", params.GroupName)
assert.IsType(t, &TestEvent{}, params.Event)
assert.Equal(t, "cqrs_test.TestEvent", params.EventName)
assert.Equal(t, handler, params.Handler)
if params.Event.(*TestEvent).ID == "1" {
assert.Equal(t, msg1, params.Message)
return errors.New("test error")
} else {
assert.Equal(t, msg2, params.Message)
}
return params.Handler.Handle(params.Message.Context(), params.Event)
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
}
cp, err := cqrs.NewEventGroupProcessorWithConfig(router, config)
require.NoError(t, err)
err = cp.AddHandlersGroup("some_group", handler)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg1.Nacked():
// ok
case <-msg1.Acked():
// ack received
t.Fatal("ack received, message should be nacked")
}
select {
case <-msg2.Acked():
// ok
case <-msg2.Nacked():
// nack received
}
assert.EqualValues(t, 2, onHandleCalled)
}
func TestNewEventProcessor_AckOnUnknownEvent_handler_group(t *testing.T) {
ts := NewTestServices()
msg, err := ts.Marshaler.Marshal(&UnknownEvent{})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg,
},
}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
cp, err := cqrs.NewEventGroupProcessorWithConfig(
router,
cqrs.EventGroupProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
AckOnUnknownEvent: true,
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = cp.AddHandlersGroup(
"foo",
cqrs.NewEventHandler("test", func(ctx context.Context, cmd *TestEvent) error {
return nil
}),
)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg.Acked():
// ok
case <-msg.Nacked():
// ack received
t.Fatal("ack received, message should be nacked")
}
}
func TestNewEventProcessor_AckOnUnknownEvent_disabled_handler_group(t *testing.T) {
ts := NewTestServices()
msg, err := ts.Marshaler.Marshal(&UnknownEvent{})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg,
},
}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
cp, err := cqrs.NewEventGroupProcessorWithConfig(
router,
cqrs.EventGroupProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
AckOnUnknownEvent: false,
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = cp.AddHandlersGroup(
"foo",
cqrs.NewEventHandler("test", func(ctx context.Context, cmd *TestEvent) error {
return nil
}),
)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg.Nacked():
// ok
case <-msg.Acked():
t.Fatal("ack received, message should be nacked")
}
}
func TestEventProcessor_handler_group(t *testing.T) {
ts := NewTestServices()
event1 := &TestEvent{ID: "1"}
msg1, err := ts.Marshaler.Marshal(event1)
require.NoError(t, err)
event2 := &AnotherTestEvent{ID: "2"}
msg2, err := ts.Marshaler.Marshal(event2)
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg1,
msg2,
},
WaitForAckBeforeSendingNext: true,
}
var handlersCalls []int
handlers := []cqrs.GroupEventHandler{
cqrs.NewGroupEventHandler(func(ctx context.Context, event *TestEvent) error {
assert.EqualValues(t, event1, event)
handlersCalls = append(handlersCalls, 1)
return nil
}),
cqrs.NewGroupEventHandler(func(ctx context.Context, event *AnotherTestEvent) error {
assert.EqualValues(t, event2, event)
handlersCalls = append(handlersCalls, 2)
return nil
}),
cqrs.NewGroupEventHandler(func(ctx context.Context, event *AnotherTestEvent) error {
assert.EqualValues(t, event2, event)
handlersCalls = append(handlersCalls, 3)
return nil
}),
}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
eventProcessor, err := cqrs.NewEventGroupProcessorWithConfig(
router,
cqrs.EventGroupProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {
assert.Equal(t, "some_group", params.EventGroupName)
assert.Equal(t, handlers, params.EventGroupHandlers)
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {
assert.Equal(t, "some_group", params.EventGroupName)
assert.Equal(t, handlers, params.EventGroupHandlers)
return mockSub, nil
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = eventProcessor.AddHandlersGroup(
"some_group",
handlers...,
)
require.NoError(t, err)
err = eventProcessor.AddHandlersGroup(
"some_group",
handlers...,
)
require.ErrorContains(t, err, "event handler group 'some_group' already exists")
err = eventProcessor.AddHandlersGroup(
"some_group_2",
)
require.ErrorContains(t, err, "no handlers provided")
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg1.Acked():
// ok
case <-time.After(time.Second):
t.Fatal("message 1 not acked")
}
select {
case <-msg2.Acked():
// ok
case <-time.After(time.Second):
t.Fatal("message 2 not acked")
}
assert.Equal(t, []int{1, 2, 3}, handlersCalls)
}
func TestEventGroupProcessor_original_msg_set_to_ctx(t *testing.T) {
ts := NewTestServices()
msg, err := ts.Marshaler.Marshal(&TestEvent{})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg,
},
}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
cp, err := cqrs.NewEventGroupProcessorWithConfig(
router,
cqrs.EventGroupProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
AckOnUnknownEvent: true,
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
var msgFromCtx *message.Message
err = cp.AddHandlersGroup(
"some_group",
cqrs.NewGroupEventHandler(
func(ctx context.Context, cmd *TestEvent) error {
msgFromCtx = cqrs.OriginalMessageFromCtx(ctx)
return nil
}),
)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg.Acked():
// ok
case <-msg.Nacked():
// nack received
t.Fatal("nack received, message should be acked")
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for ack")
}
require.NotNil(t, msgFromCtx)
assert.Equal(t, msg, msgFromCtx)
}

View File

@@ -0,0 +1,539 @@
package cqrs_test
import (
"context"
"fmt"
"sync/atomic"
"testing"
"time"
"github.com/pkg/errors"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEventProcessorConfig_Validate(t *testing.T) {
testCases := []struct {
Name string
ModifyValidConfig func(*cqrs.EventProcessorConfig)
ExpectedErr error
}{
{
Name: "valid_config",
ModifyValidConfig: nil,
ExpectedErr: nil,
},
{
Name: "missing_GenerateHandlerSubscribeTopic",
ModifyValidConfig: func(config *cqrs.EventProcessorConfig) {
config.GenerateSubscribeTopic = nil
},
ExpectedErr: fmt.Errorf("missing GenerateHandlerTopic"),
},
{
Name: "missing_marshaler",
ModifyValidConfig: func(config *cqrs.EventProcessorConfig) {
config.Marshaler = nil
},
ExpectedErr: fmt.Errorf("missing Marshaler"),
},
{
Name: "missing_subscriber_constructor",
ModifyValidConfig: func(config *cqrs.EventProcessorConfig) {
config.SubscriberConstructor = nil
},
ExpectedErr: fmt.Errorf("missing SubscriberConstructor"),
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.Name, func(t *testing.T) {
validConfig := cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: cqrs.JSONMarshaler{},
}
if tc.ModifyValidConfig != nil {
tc.ModifyValidConfig(&validConfig)
}
err := validConfig.Validate()
if tc.ExpectedErr == nil {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.ExpectedErr.Error())
}
})
}
}
func TestNewEventProcessor(t *testing.T) {
eventConfig := cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: cqrs.JSONMarshaler{},
}
require.NoError(t, eventConfig.Validate())
router, err := message.NewRouter(message.RouterConfig{}, nil)
require.NoError(t, err)
cp, err := cqrs.NewEventProcessorWithConfig(router, eventConfig)
assert.NotNil(t, cp)
assert.NoError(t, err)
eventConfig.SubscriberConstructor = nil
require.Error(t, eventConfig.Validate())
cp, err = cqrs.NewEventProcessorWithConfig(router, eventConfig)
assert.Nil(t, cp)
assert.Error(t, err)
}
type nonPointerEventProcessor struct {
}
func (nonPointerEventProcessor) HandlerName() string {
return "nonPointerEventProcessor"
}
func (nonPointerEventProcessor) NewEvent() interface{} {
return TestEvent{}
}
func (nonPointerEventProcessor) Handle(ctx context.Context, cmd interface{}) error {
panic("not implemented")
}
func TestEventProcessor_non_pointer_event(t *testing.T) {
ts := NewTestServices()
handler := nonPointerEventProcessor{}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
eventProcessor, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = eventProcessor.AddHandlers(handler)
assert.IsType(t, cqrs.NonPointerError{}, errors.Cause(err))
}
type duplicateTestEventHandler1 struct{}
func (h duplicateTestEventHandler1) HandlerName() string {
return "duplicateTestEventHandler1"
}
func (duplicateTestEventHandler1) NewEvent() interface{} {
return &TestEvent{}
}
func (h *duplicateTestEventHandler1) Handle(ctx context.Context, event interface{}) error { return nil }
type duplicateTestEventHandler2 struct{}
func (h duplicateTestEventHandler2) HandlerName() string {
return "duplicateTestEventHandler2"
}
func (duplicateTestEventHandler2) NewEvent() interface{} {
return &TestEvent{}
}
func (h *duplicateTestEventHandler2) Handle(ctx context.Context, event interface{}) error { return nil }
func TestEventProcessor_multiple_same_event_handlers(t *testing.T) {
ts := NewTestServices()
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
eventProcessor, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return nil, nil
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = eventProcessor.AddHandlers(
&duplicateTestEventHandler1{},
&duplicateTestEventHandler2{},
)
require.NoError(t, err)
}
func TestNewEventProcessor_OnHandle(t *testing.T) {
ts := NewTestServices()
msg1, err := ts.Marshaler.Marshal(&TestEvent{ID: "1"})
require.NoError(t, err)
msg2, err := ts.Marshaler.Marshal(&TestEvent{ID: "2"})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg1,
msg2,
},
}
handlerCalled := 0
defer func() {
// for msg 1 we are not calling handler - but returning before
assert.Equal(t, 1, handlerCalled)
}()
handler := cqrs.NewEventHandler("test", func(ctx context.Context, cmd *TestEvent) error {
handlerCalled++
return nil
})
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
onHandleCalled := int64(0)
config := cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
OnHandle: func(params cqrs.EventProcessorOnHandleParams) error {
atomic.AddInt64(&onHandleCalled, 1)
assert.IsType(t, &TestEvent{}, params.Event)
assert.Equal(t, "cqrs_test.TestEvent", params.EventName)
assert.Equal(t, handler, params.Handler)
if params.Event.(*TestEvent).ID == "1" {
assert.Equal(t, msg1, params.Message)
return errors.New("test error")
} else {
assert.Equal(t, msg2, params.Message)
}
return params.Handler.Handle(params.Message.Context(), params.Event)
},
Marshaler: ts.Marshaler,
Logger: ts.Logger,
}
cp, err := cqrs.NewEventProcessorWithConfig(router, config)
require.NoError(t, err)
err = cp.AddHandlers(handler)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg1.Nacked():
// ok
case <-msg1.Acked():
// ack received
t.Fatal("ack received, message should be nacked")
}
select {
case <-msg2.Acked():
// ok
case <-msg2.Nacked():
// nack received
}
assert.EqualValues(t, 2, onHandleCalled)
}
type UnknownEvent struct {
}
func TestNewEventProcessor_AckOnUnknownEvent(t *testing.T) {
ts := NewTestServices()
msg, err := ts.Marshaler.Marshal(&UnknownEvent{})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg,
},
}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
cp, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
AckOnUnknownEvent: true,
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = cp.AddHandlers(
cqrs.NewEventHandler("test", func(ctx context.Context, cmd *TestEvent) error {
return nil
}),
)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg.Acked():
// ok
case <-msg.Nacked():
// ack received
t.Fatal("ack received, message should be nacked")
}
}
func TestNewEventProcessor_AckOnUnknownEvent_disabled(t *testing.T) {
ts := NewTestServices()
msg, err := ts.Marshaler.Marshal(&UnknownEvent{})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg,
},
}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
cp, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
AckOnUnknownEvent: false,
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = cp.AddHandlers(
cqrs.NewEventHandler("test", func(ctx context.Context, cmd *TestEvent) error {
return nil
}),
)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg.Nacked():
// ok
case <-msg.Acked():
// ack received
t.Fatal("ack received, message should be nacked")
}
}
func TestNewEventProcessor_backward_compatibility_of_AckOnUnknownEvent(t *testing.T) {
ts := NewTestServices()
msg, err := ts.Marshaler.Marshal(&UnknownEvent{})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg,
},
}
cp, err := cqrs.NewEventProcessor(
[]cqrs.EventHandler{
cqrs.NewEventHandler("test", func(ctx context.Context, cmd *TestEvent) error {
return nil
}),
},
func(eventName string) string {
return "events"
},
func(handlerName string) (message.Subscriber, error) {
return mockSub, nil
},
ts.Marshaler,
ts.Logger,
)
require.NoError(t, err)
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
err = cp.AddHandlersToRouter(router)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg.Acked():
// ok
case <-msg.Nacked():
// ack received
t.Fatal("ack received, message should be nacked")
}
}
func TestEventProcessor_AddHandlersToRouter_without_disableRouterAutoAddHandlers(t *testing.T) {
ts := NewTestServices()
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
cp, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return ts.EventsPubSub, nil
},
AckOnUnknownEvent: false,
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
err = cp.AddHandlersToRouter(router)
assert.ErrorContains(t, err, "AddHandlersToRouter should be called only when using deprecated NewEventProcessor")
}
func TestEventProcessor_original_msg_set_to_ctx(t *testing.T) {
ts := NewTestServices()
msg, err := ts.Marshaler.Marshal(&TestEvent{})
require.NoError(t, err)
mockSub := &mockSubscriber{
MessagesToSend: []*message.Message{
msg,
},
}
router, err := message.NewRouter(message.RouterConfig{}, ts.Logger)
require.NoError(t, err)
cp, err := cqrs.NewEventProcessorWithConfig(
router,
cqrs.EventProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return mockSub, nil
},
AckOnUnknownEvent: true,
Marshaler: ts.Marshaler,
Logger: ts.Logger,
},
)
require.NoError(t, err)
var msgFromCtx *message.Message
err = cp.AddHandlers(cqrs.NewEventHandler(
"handler", func(ctx context.Context, cmd *TestEvent) error {
msgFromCtx = cqrs.OriginalMessageFromCtx(ctx)
return nil
}),
)
require.NoError(t, err)
go func() {
err := router.Run(context.Background())
assert.NoError(t, err)
}()
<-router.Running()
select {
case <-msg.Acked():
// ok
case <-msg.Nacked():
// nack received
t.Fatal("nack received, message should be acked")
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for ack")
}
require.NotNil(t, msgFromCtx)
assert.Equal(t, msg, msgFromCtx)
}

View File

@@ -0,0 +1,55 @@
package cqrs
import (
"errors"
"fmt"
"github.com/ThreeDotsLabs/watermill/message"
)
// CommandEventMarshaler marshals Commands and Events to Watermill's messages and vice versa.
// Payload of the command needs to be marshaled to []bytes.
type CommandEventMarshaler interface {
// Marshal marshals Command or Event to Watermill's message.
Marshal(v interface{}) (*message.Message, error)
// Unmarshal unmarshals watermill's message to v Command or Event.
Unmarshal(msg *message.Message, v interface{}) (err error)
// Name returns the name of Command or Event.
// Name is used to determine, that received command or event is event which we want to handle.
Name(v interface{}) string
// NameFromMessage returns the name of Command or Event from Watermill's message (generated by Marshal).
//
// When we have Command or Event marshaled to Watermill's message,
// we should use NameFromMessage instead of Name to avoid unnecessary unmarshaling.
NameFromMessage(msg *message.Message) string
}
// CommandEventMarshalerDecorator decorates CommandEventMarshaler with additional functionality.
// It can be used to add additional metadata to the message.
type CommandEventMarshalerDecorator struct {
CommandEventMarshaler
// DecorateFunc is called after marshaling the message.
DecorateFunc func(v any, msg *message.Message) error
}
// Marshal marshals Command or Event to Watermill's message and decorates it.
func (c CommandEventMarshalerDecorator) Marshal(v any) (*message.Message, error) {
msg, err := c.CommandEventMarshaler.Marshal(v)
if err != nil {
return nil, err
}
if c.DecorateFunc == nil {
return nil, errors.New("DecorateFunc is nil")
}
if err := c.DecorateFunc(v, msg); err != nil {
return nil, fmt.Errorf("cannot decorate message: %w", err)
}
return msg, nil
}

View File

@@ -0,0 +1,53 @@
package cqrs
import (
"encoding/json"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/message"
)
type JSONMarshaler struct {
NewUUID func() string
GenerateName func(v interface{}) string
}
func (m JSONMarshaler) Marshal(v interface{}) (*message.Message, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
msg := message.NewMessage(
m.newUUID(),
b,
)
msg.Metadata.Set("name", m.Name(v))
return msg, nil
}
func (m JSONMarshaler) newUUID() string {
if m.NewUUID != nil {
return m.NewUUID()
}
// default
return watermill.NewUUID()
}
func (JSONMarshaler) Unmarshal(msg *message.Message, v interface{}) (err error) {
return json.Unmarshal(msg.Payload, v)
}
func (m JSONMarshaler) Name(cmdOrEvent interface{}) string {
if m.GenerateName != nil {
return m.GenerateName(cmdOrEvent)
}
return FullyQualifiedStructName(cmdOrEvent)
}
func (m JSONMarshaler) NameFromMessage(msg *message.Message) string {
return msg.Metadata.Get("name")
}

View File

@@ -0,0 +1,56 @@
package cqrs_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
)
var jsonEventToMarshal = TestEvent{
ID: watermill.NewULID(),
When: time.Date(2016, time.August, 15, 14, 13, 12, 0, time.UTC),
}
func TestJsonMarshaler(t *testing.T) {
marshaler := cqrs.JSONMarshaler{}
msg, err := marshaler.Marshal(jsonEventToMarshal)
require.NoError(t, err)
eventToUnmarshal := TestEvent{}
err = marshaler.Unmarshal(msg, &eventToUnmarshal)
require.NoError(t, err)
assert.EqualValues(t, jsonEventToMarshal, eventToUnmarshal)
}
func TestJSONMarshaler_Marshal_new_uuid_set(t *testing.T) {
marshaler := cqrs.JSONMarshaler{
NewUUID: func() string {
return "foo"
},
}
msg, err := marshaler.Marshal(jsonEventToMarshal)
require.NoError(t, err)
assert.Equal(t, msg.UUID, "foo")
}
func TestJSONMarshaler_Marshal_generate_name(t *testing.T) {
marshaler := cqrs.JSONMarshaler{
GenerateName: func(v interface{}) string {
return "foo"
},
}
msg, err := marshaler.Marshal(jsonEventToMarshal)
require.NoError(t, err)
assert.Equal(t, msg.Metadata.Get("name"), "foo")
}

View File

@@ -0,0 +1,85 @@
package cqrs
import (
"reflect"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/message"
"google.golang.org/protobuf/proto"
"github.com/pkg/errors"
)
// ProtoMarshaler is the default Protocol Buffers marshaler.
type ProtoMarshaler struct {
NewUUID func() string
GenerateName func(v interface{}) string
}
// NoProtoMessageError is returned when the given value does not implement proto.Message.
type NoProtoMessageError struct {
v interface{}
}
func (e NoProtoMessageError) Error() string {
rv := reflect.ValueOf(e.v)
if rv.Kind() != reflect.Pointer {
return "v is not proto.Message, you must pass pointer value to implement proto.Message"
}
return "v is not proto.Message"
}
// Marshal marshals the given protobuf's message into watermill's Message.
func (m ProtoMarshaler) Marshal(v interface{}) (*message.Message, error) {
protoMsg, ok := v.(proto.Message)
if !ok {
return nil, errors.WithStack(NoProtoMessageError{v})
}
b, err := proto.Marshal(protoMsg)
if err != nil {
return nil, err
}
msg := message.NewMessage(
m.newUUID(),
b,
)
msg.Metadata.Set("name", m.Name(v))
return msg, nil
}
func (m ProtoMarshaler) newUUID() string {
if m.NewUUID != nil {
return m.NewUUID()
}
// default
return watermill.NewUUID()
}
// Unmarshal unmarshals given watermill's Message into protobuf's message.
func (ProtoMarshaler) Unmarshal(msg *message.Message, v interface{}) (err error) {
protoV, ok := v.(proto.Message)
if !ok {
return errors.WithStack(NoProtoMessageError{v})
}
return proto.Unmarshal(msg.Payload, protoV)
}
// Name returns the command or event's name.
func (m ProtoMarshaler) Name(cmdOrEvent interface{}) string {
if m.GenerateName != nil {
return m.GenerateName(cmdOrEvent)
}
return FullyQualifiedStructName(cmdOrEvent)
}
// NameFromMessage returns the metadata name value for a given Message.
func (m ProtoMarshaler) NameFromMessage(msg *message.Message) string {
return msg.Metadata.Get("name")
}

View File

@@ -0,0 +1,483 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc v4.24.4
// source: events.proto
package cqrs_test
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Status int32
const (
Status_STATUS_UNSPECIFIED Status = 0
Status_ACTIVE Status = 1
Status_DELETED Status = 2
)
// Enum value maps for Status.
var (
Status_name = map[int32]string{
0: "STATUS_UNSPECIFIED",
1: "ACTIVE",
2: "DELETED",
}
Status_value = map[string]int32{
"STATUS_UNSPECIFIED": 0,
"ACTIVE": 1,
"DELETED": 2,
}
)
func (x Status) Enum() *Status {
p := new(Status)
*p = x
return p
}
func (x Status) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Status) Descriptor() protoreflect.EnumDescriptor {
return file_events_proto_enumTypes[0].Descriptor()
}
func (Status) Type() protoreflect.EnumType {
return &file_events_proto_enumTypes[0]
}
func (x Status) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Status.Descriptor instead.
func (Status) EnumDescriptor() ([]byte, []int) {
return file_events_proto_rawDescGZIP(), []int{0}
}
type TestProtobufLegacyEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
When *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=when,proto3" json:"when,omitempty"`
}
func (x *TestProtobufLegacyEvent) Reset() {
*x = TestProtobufLegacyEvent{}
if protoimpl.UnsafeEnabled {
mi := &file_events_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TestProtobufLegacyEvent) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TestProtobufLegacyEvent) ProtoMessage() {}
func (x *TestProtobufLegacyEvent) ProtoReflect() protoreflect.Message {
mi := &file_events_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TestProtobufLegacyEvent.ProtoReflect.Descriptor instead.
func (*TestProtobufLegacyEvent) Descriptor() ([]byte, []int) {
return file_events_proto_rawDescGZIP(), []int{0}
}
func (x *TestProtobufLegacyEvent) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *TestProtobufLegacyEvent) GetWhen() *timestamppb.Timestamp {
if x != nil {
return x.When
}
return nil
}
type SubEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Tags []string `protobuf:"bytes,1,rep,name=tags,proto3" json:"tags,omitempty"`
Flags map[string]bool `protobuf:"bytes,2,rep,name=flags,proto3" json:"flags,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
}
func (x *SubEvent) Reset() {
*x = SubEvent{}
if protoimpl.UnsafeEnabled {
mi := &file_events_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SubEvent) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SubEvent) ProtoMessage() {}
func (x *SubEvent) ProtoReflect() protoreflect.Message {
mi := &file_events_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SubEvent.ProtoReflect.Descriptor instead.
func (*SubEvent) Descriptor() ([]byte, []int) {
return file_events_proto_rawDescGZIP(), []int{1}
}
func (x *SubEvent) GetTags() []string {
if x != nil {
return x.Tags
}
return nil
}
func (x *SubEvent) GetFlags() map[string]bool {
if x != nil {
return x.Flags
}
return nil
}
type TestComplexProtobufEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
When *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=when,proto3" json:"when,omitempty"`
// Complex fields to test edge cases
NestedMap map[string]*SubEvent `protobuf:"bytes,4,rep,name=nested_map,json=nestedMap,proto3" json:"nested_map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Events []*SubEvent `protobuf:"bytes,5,rep,name=events,proto3" json:"events,omitempty"`
// Types that are assignable to Result:
//
// *TestComplexProtobufEvent_Success
// *TestComplexProtobufEvent_Error
// *TestComplexProtobufEvent_Fallback
Result isTestComplexProtobufEvent_Result `protobuf_oneof:"result"`
}
func (x *TestComplexProtobufEvent) Reset() {
*x = TestComplexProtobufEvent{}
if protoimpl.UnsafeEnabled {
mi := &file_events_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TestComplexProtobufEvent) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TestComplexProtobufEvent) ProtoMessage() {}
func (x *TestComplexProtobufEvent) ProtoReflect() protoreflect.Message {
mi := &file_events_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TestComplexProtobufEvent.ProtoReflect.Descriptor instead.
func (*TestComplexProtobufEvent) Descriptor() ([]byte, []int) {
return file_events_proto_rawDescGZIP(), []int{2}
}
func (x *TestComplexProtobufEvent) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *TestComplexProtobufEvent) GetData() []byte {
if x != nil {
return x.Data
}
return nil
}
func (x *TestComplexProtobufEvent) GetWhen() *timestamppb.Timestamp {
if x != nil {
return x.When
}
return nil
}
func (x *TestComplexProtobufEvent) GetNestedMap() map[string]*SubEvent {
if x != nil {
return x.NestedMap
}
return nil
}
func (x *TestComplexProtobufEvent) GetEvents() []*SubEvent {
if x != nil {
return x.Events
}
return nil
}
func (m *TestComplexProtobufEvent) GetResult() isTestComplexProtobufEvent_Result {
if m != nil {
return m.Result
}
return nil
}
func (x *TestComplexProtobufEvent) GetSuccess() *SubEvent {
if x, ok := x.GetResult().(*TestComplexProtobufEvent_Success); ok {
return x.Success
}
return nil
}
func (x *TestComplexProtobufEvent) GetError() string {
if x, ok := x.GetResult().(*TestComplexProtobufEvent_Error); ok {
return x.Error
}
return ""
}
func (x *TestComplexProtobufEvent) GetFallback() Status {
if x, ok := x.GetResult().(*TestComplexProtobufEvent_Fallback); ok {
return x.Fallback
}
return Status_STATUS_UNSPECIFIED
}
type isTestComplexProtobufEvent_Result interface {
isTestComplexProtobufEvent_Result()
}
type TestComplexProtobufEvent_Success struct {
Success *SubEvent `protobuf:"bytes,6,opt,name=success,proto3,oneof"`
}
type TestComplexProtobufEvent_Error struct {
Error string `protobuf:"bytes,7,opt,name=error,proto3,oneof"`
}
type TestComplexProtobufEvent_Fallback struct {
Fallback Status `protobuf:"varint,8,opt,name=fallback,proto3,enum=cqrs_test.Status,oneof"`
}
func (*TestComplexProtobufEvent_Success) isTestComplexProtobufEvent_Result() {}
func (*TestComplexProtobufEvent_Error) isTestComplexProtobufEvent_Result() {}
func (*TestComplexProtobufEvent_Fallback) isTestComplexProtobufEvent_Result() {}
var File_events_proto protoreflect.FileDescriptor
var file_events_proto_rawDesc = []byte{
0x0a, 0x0c, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09,
0x63, 0x71, 0x72, 0x73, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73,
0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x59, 0x0a, 0x17, 0x54, 0x65,
0x73, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79,
0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x77, 0x68, 0x65, 0x6e, 0x18, 0x03, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
0x04, 0x77, 0x68, 0x65, 0x6e, 0x22, 0x8e, 0x01, 0x0a, 0x08, 0x53, 0x75, 0x62, 0x45, 0x76, 0x65,
0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09,
0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18,
0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x63, 0x71, 0x72, 0x73, 0x5f, 0x74, 0x65, 0x73,
0x74, 0x2e, 0x53, 0x75, 0x62, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73,
0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x1a, 0x38, 0x0a, 0x0a,
0x46, 0x6c, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05,
0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c,
0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xcb, 0x03, 0x0a, 0x18, 0x54, 0x65, 0x73, 0x74, 0x43,
0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x45, 0x76,
0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2e, 0x0a, 0x04, 0x77, 0x68, 0x65, 0x6e, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x52, 0x04, 0x77, 0x68, 0x65, 0x6e, 0x12, 0x51, 0x0a, 0x0a, 0x6e, 0x65, 0x73, 0x74, 0x65,
0x64, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x71,
0x72, 0x73, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x70,
0x6c, 0x65, 0x78, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x45, 0x76, 0x65, 0x6e, 0x74,
0x2e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,
0x09, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x4d, 0x61, 0x70, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76,
0x65, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x71, 0x72,
0x73, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x75, 0x62, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52,
0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2f, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65,
0x73, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x71, 0x72, 0x73, 0x5f,
0x74, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x75, 0x62, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52,
0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f,
0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
0x12, 0x2f, 0x0a, 0x08, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x08, 0x20, 0x01,
0x28, 0x0e, 0x32, 0x11, 0x2e, 0x63, 0x71, 0x72, 0x73, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x08, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63,
0x6b, 0x1a, 0x51, 0x0a, 0x0e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x4d, 0x61, 0x70, 0x45, 0x6e,
0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x71, 0x72, 0x73, 0x5f, 0x74, 0x65, 0x73, 0x74,
0x2e, 0x53, 0x75, 0x62, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x3a, 0x02, 0x38, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x4a, 0x04,
0x08, 0x17, 0x10, 0x1f, 0x2a, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16,
0x0a, 0x12, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49,
0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45,
0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x02, 0x42,
0x0d, 0x5a, 0x0b, 0x2e, 0x2f, 0x63, 0x71, 0x72, 0x73, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_events_proto_rawDescOnce sync.Once
file_events_proto_rawDescData = file_events_proto_rawDesc
)
func file_events_proto_rawDescGZIP() []byte {
file_events_proto_rawDescOnce.Do(func() {
file_events_proto_rawDescData = protoimpl.X.CompressGZIP(file_events_proto_rawDescData)
})
return file_events_proto_rawDescData
}
var file_events_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_events_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_events_proto_goTypes = []interface{}{
(Status)(0), // 0: cqrs_test.Status
(*TestProtobufLegacyEvent)(nil), // 1: cqrs_test.TestProtobufLegacyEvent
(*SubEvent)(nil), // 2: cqrs_test.SubEvent
(*TestComplexProtobufEvent)(nil), // 3: cqrs_test.TestComplexProtobufEvent
nil, // 4: cqrs_test.SubEvent.FlagsEntry
nil, // 5: cqrs_test.TestComplexProtobufEvent.NestedMapEntry
(*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
}
var file_events_proto_depIdxs = []int32{
6, // 0: cqrs_test.TestProtobufLegacyEvent.when:type_name -> google.protobuf.Timestamp
4, // 1: cqrs_test.SubEvent.flags:type_name -> cqrs_test.SubEvent.FlagsEntry
6, // 2: cqrs_test.TestComplexProtobufEvent.when:type_name -> google.protobuf.Timestamp
5, // 3: cqrs_test.TestComplexProtobufEvent.nested_map:type_name -> cqrs_test.TestComplexProtobufEvent.NestedMapEntry
2, // 4: cqrs_test.TestComplexProtobufEvent.events:type_name -> cqrs_test.SubEvent
2, // 5: cqrs_test.TestComplexProtobufEvent.success:type_name -> cqrs_test.SubEvent
0, // 6: cqrs_test.TestComplexProtobufEvent.fallback:type_name -> cqrs_test.Status
2, // 7: cqrs_test.TestComplexProtobufEvent.NestedMapEntry.value:type_name -> cqrs_test.SubEvent
8, // [8:8] is the sub-list for method output_type
8, // [8:8] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
}
func init() { file_events_proto_init() }
func file_events_proto_init() {
if File_events_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_events_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TestProtobufLegacyEvent); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_events_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SubEvent); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_events_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TestComplexProtobufEvent); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_events_proto_msgTypes[2].OneofWrappers = []interface{}{
(*TestComplexProtobufEvent_Success)(nil),
(*TestComplexProtobufEvent_Error)(nil),
(*TestComplexProtobufEvent_Fallback)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_events_proto_rawDesc,
NumEnums: 1,
NumMessages: 5,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_events_proto_goTypes,
DependencyIndexes: file_events_proto_depIdxs,
EnumInfos: file_events_proto_enumTypes,
MessageInfos: file_events_proto_msgTypes,
}.Build()
File_events_proto = out.File
file_events_proto_rawDesc = nil
file_events_proto_goTypes = nil
file_events_proto_depIdxs = nil
}

View File

@@ -0,0 +1,90 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: testdata/events.proto
package cqrs_test
import (
fmt "fmt"
math "math"
proto "github.com/golang/protobuf/proto"
timestamp "github.com/golang/protobuf/ptypes/timestamp"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type TestProtobufEvent struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
When *timestamp.Timestamp `protobuf:"bytes,3,opt,name=when,proto3" json:"when,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *TestProtobufEvent) Reset() { *m = TestProtobufEvent{} }
func (m *TestProtobufEvent) String() string { return proto.CompactTextString(m) }
func (*TestProtobufEvent) ProtoMessage() {}
func (*TestProtobufEvent) Descriptor() ([]byte, []int) {
return fileDescriptor_37faf0ac8d97ee4c, []int{0}
}
func (m *TestProtobufEvent) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_TestProtobufEvent.Unmarshal(m, b)
}
func (m *TestProtobufEvent) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_TestProtobufEvent.Marshal(b, m, deterministic)
}
func (m *TestProtobufEvent) XXX_Merge(src proto.Message) {
xxx_messageInfo_TestProtobufEvent.Merge(m, src)
}
func (m *TestProtobufEvent) XXX_Size() int {
return xxx_messageInfo_TestProtobufEvent.Size(m)
}
func (m *TestProtobufEvent) XXX_DiscardUnknown() {
xxx_messageInfo_TestProtobufEvent.DiscardUnknown(m)
}
var xxx_messageInfo_TestProtobufEvent proto.InternalMessageInfo
func (m *TestProtobufEvent) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *TestProtobufEvent) GetWhen() *timestamp.Timestamp {
if m != nil {
return m.When
}
return nil
}
func init() {
proto.RegisterType((*TestProtobufEvent)(nil), "cqrs_test.TestProtobufEvent")
}
func init() { proto.RegisterFile("testdata/events.proto", fileDescriptor_37faf0ac8d97ee4c) }
var fileDescriptor_37faf0ac8d97ee4c = []byte{
// 146 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x2d, 0x49, 0x2d, 0x2e,
0x49, 0x49, 0x2c, 0x49, 0xd4, 0x4f, 0x2d, 0x4b, 0xcd, 0x2b, 0x29, 0xd6, 0x2b, 0x28, 0xca, 0x2f,
0xc9, 0x17, 0xe2, 0x4c, 0x2e, 0x2c, 0x2a, 0x8e, 0x07, 0xc9, 0x49, 0xc9, 0xa7, 0xe7, 0xe7, 0xa7,
0xe7, 0xa4, 0xea, 0x83, 0x25, 0x92, 0x4a, 0xd3, 0xf4, 0x4b, 0x32, 0x73, 0x53, 0x8b, 0x4b, 0x12,
0x73, 0x0b, 0x20, 0x6a, 0x95, 0x82, 0xb9, 0x04, 0x43, 0x52, 0x8b, 0x4b, 0x02, 0xa0, 0xf2, 0xae,
0x20, 0x73, 0x84, 0xf8, 0xb8, 0x98, 0x32, 0x53, 0x24, 0x18, 0x15, 0x18, 0x35, 0x38, 0x83, 0x98,
0x32, 0x53, 0x84, 0xf4, 0xb8, 0x58, 0xca, 0x33, 0x52, 0xf3, 0x24, 0x98, 0x15, 0x18, 0x35, 0xb8,
0x8d, 0xa4, 0xf4, 0x20, 0x86, 0xea, 0xc1, 0x0c, 0xd5, 0x0b, 0x81, 0x19, 0x1a, 0x04, 0x56, 0x97,
0xc4, 0x06, 0x96, 0x31, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xff, 0x4e, 0x39, 0x0e, 0xa0, 0x00,
0x00, 0x00,
}

View File

@@ -0,0 +1,127 @@
package cqrs
import (
"fmt"
"runtime/debug"
stderrors "errors"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/gogo/protobuf/proto"
"github.com/pkg/errors"
stdproto "google.golang.org/protobuf/proto"
)
// ProtobufMarshaler a protobuf marshaler using github.com/gogo/protobuf/proto (deprecated).
//
// DEPRECATED: Use ProtoMarshaler instead. This marshaler will not work with newer protobuf files.
// IMPORTANT: This marshaler is backward and forward compatible with ProtoMarshaler.
// ProtobufMarshaler from Watermill versions until v1.4.3 are not forward compatible with ProtoMarshaler.
// Suggested migration steps:
// 1. Update Watermill to v1.4.4 or newer, so all publishers and subscribers will be forward and backward compatible.
// 2. Change all usages of ProtobufMarshaler to ProtoMarshaler.
type ProtobufMarshaler struct {
NewUUID func() string
GenerateName func(v interface{}) string
// DisableStdProtoFallback disables fallback to github.com/golang/protobuf/proto when github.com/gogo/protobuf/proto
// because receiving a message that was marshaled with github.com/golang/protobuf/proto.
// Fallback is enabled by default to enable migration to ProtoMarshaler.
DisableStdProtoFallback bool
}
// Marshal marshals the given protobuf's message into watermill's Message.
func (m ProtobufMarshaler) Marshal(v interface{}) (msg *message.Message, err error) {
defer func() {
// gogo proto can panic on unmarshal (for example, because it received a message from ProtoMarshaler with oneof)
if r := recover(); r != nil {
err = stderrors.Join(err, fmt.Errorf(
"github.com/gogo/protobuf/proto panic (we recommend migrating marshaler to cqrs.ProtoMarshaler to avoid that): %v\n%s",
r,
string(debug.Stack()),
))
}
if err != nil && !m.DisableStdProtoFallback {
_, isStdProtoMsg := v.(stdproto.Message)
if isStdProtoMsg {
msg, err = m.ToProtoMarshaler().Marshal(v)
}
}
}()
protoMsg, ok := v.(proto.Message)
if !ok {
return nil, errors.WithStack(NoProtoMessageError{v})
}
b, err := proto.Marshal(protoMsg)
if err != nil {
return nil, err
}
msg = message.NewMessage(
m.newUUID(),
b,
)
msg.Metadata.Set("name", m.Name(v))
return msg, nil
}
func (m ProtobufMarshaler) newUUID() string {
if m.NewUUID != nil {
return m.NewUUID()
}
// default
return watermill.NewUUID()
}
// Unmarshal unmarshals given watermill's Message into protobuf's message.
func (m ProtobufMarshaler) Unmarshal(msg *message.Message, v interface{}) (err error) {
protoV, ok := v.(proto.Message)
if !ok {
return errors.WithStack(NoProtoMessageError{v})
}
defer func() {
// gogo proto can panic on unmarshal (for example, because it received a message from ProtoMarshaler with oneof)
if r := recover(); r != nil {
err = stderrors.Join(err, fmt.Errorf(
"github.com/gogo/protobuf/proto panic (we recommend migrating marshaler to cqrs.ProtoMarshaler to avoid that): %v\n%s",
r,
string(debug.Stack()),
))
}
if err != nil && !m.DisableStdProtoFallback {
err = m.ToProtoMarshaler().Unmarshal(msg, v)
}
}()
return proto.Unmarshal(msg.Payload, protoV)
}
func (m ProtobufMarshaler) ToProtoMarshaler() ProtoMarshaler {
return ProtoMarshaler{
NewUUID: m.NewUUID,
GenerateName: m.GenerateName,
}
}
// Name returns the command or event's name.
func (m ProtobufMarshaler) Name(cmdOrEvent interface{}) string {
if m.GenerateName != nil {
return m.GenerateName(cmdOrEvent)
}
return FullyQualifiedStructName(cmdOrEvent)
}
// NameFromMessage returns the metadata name value for a given Message.
func (m ProtobufMarshaler) NameFromMessage(msg *message.Message) string {
return msg.Metadata.Get("name")
}

View File

@@ -0,0 +1,142 @@
package cqrs_test
import (
"testing"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProtobufMarshaler_with_fallback(t *testing.T) {
marshaler := cqrs.ProtobufMarshaler{}
assertProtoMarshalUnmarshal(
t,
marshaler,
marshaler,
newProtoTestComplexEvent(),
&TestComplexProtobufEvent{},
"cqrs_test.TestComplexProtobufEvent",
)
legacyEvent, _ := newProtoLegacyTestEvent()
assertProtoMarshalUnmarshal(
t,
marshaler,
marshaler,
legacyEvent,
&TestProtobufEvent{},
"cqrs_test.TestProtobufEvent",
)
}
func TestProtobufMarshaler_without_fallback_legacy_event(t *testing.T) {
legacyEvent, _ := newProtoLegacyTestEvent()
marshaler := cqrs.ProtobufMarshaler{
DisableStdProtoFallback: true,
}
assertProtoMarshalUnmarshal(
t,
marshaler,
marshaler,
legacyEvent,
&TestProtobufEvent{},
"cqrs_test.TestProtobufEvent",
)
}
func TestProtobufMarshaler_Marshal_generated_name(t *testing.T) {
marshaler := cqrs.ProtobufMarshaler{
NewUUID: func() string {
return "foo"
},
}
msg, err := marshaler.Marshal(newProtoTestComplexEvent())
require.NoError(t, err)
assert.Equal(t, msg.UUID, "foo")
}
func TestProtobufMarshaler_catch_panic(t *testing.T) {
marshalerNoFallback := cqrs.ProtobufMarshaler{
DisableStdProtoFallback: true,
}
marshalerWithFallback := cqrs.ProtobufMarshaler{
DisableStdProtoFallback: false,
}
complexEvent := newProtoTestComplexEvent()
msg, err := marshalerNoFallback.Marshal(complexEvent)
assert.Nil(t, msg)
assert.ErrorContains(t, err, "(we recommend migrating marshaler to cqrs.ProtoMarshaler to avoid that)")
assert.ErrorContains(t, err, "invalid memory address or nil pointer dereference")
assert.ErrorContains(t, err, "github.com/gogo/protobuf/proto panic")
assert.ErrorContains(t, err, "runtime/debug.Stack()", "error should contain stack trace")
// let's simulate situation when publishing service uses fallback and consuming service does not
msg, err = marshalerWithFallback.Marshal(complexEvent)
require.NoError(t, err)
err = marshalerNoFallback.Unmarshal(msg, &TestComplexProtobufEvent{})
assert.ErrorContains(t, err, "(we recommend migrating marshaler to cqrs.ProtoMarshaler to avoid that)")
assert.ErrorContains(t, err, "protobuf tag not enough fields in TestComplexProtobufEvent.state")
assert.ErrorContains(t, err, "github.com/gogo/protobuf/proto panic")
assert.ErrorContains(t, err, "runtime/debug.Stack()", "error should contain stack trace")
// marshaler with fallback should handle this message
err = marshalerWithFallback.Unmarshal(msg, &TestComplexProtobufEvent{})
require.NoError(t, err)
}
func TestProtobufMarshaler_compatible_with_ProtoMarshaler(t *testing.T) {
legacyEvent, legacyEventRegenerated := newProtoLegacyTestEvent()
complexEvent := newProtoTestComplexEvent()
deprecatedMarshaler := cqrs.ProtobufMarshaler{}
newMarshaler := cqrs.ProtoMarshaler{}
t.Run("from_deprecated_to_new", func(t *testing.T) {
assertProtoMarshalUnmarshal(
t,
deprecatedMarshaler,
newMarshaler,
complexEvent,
&TestComplexProtobufEvent{},
"cqrs_test.TestComplexProtobufEvent",
)
assertProtoMarshalUnmarshal(
t,
deprecatedMarshaler,
newMarshaler,
legacyEvent,
&TestProtobufLegacyEvent{},
"cqrs_test.TestProtobufEvent",
)
})
t.Run("from_new_to_deprecated", func(t *testing.T) {
assertProtoMarshalUnmarshal(
t,
newMarshaler,
deprecatedMarshaler,
complexEvent,
&TestComplexProtobufEvent{},
"cqrs_test.TestComplexProtobufEvent",
)
assertProtoMarshalUnmarshal(
t,
newMarshaler,
deprecatedMarshaler,
legacyEventRegenerated,
&TestProtobufEvent{},
"cqrs_test.TestProtobufLegacyEvent",
)
})
}

View File

@@ -0,0 +1,114 @@
package cqrs_test
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
)
func TestProtoMarshaler(t *testing.T) {
assertProtoMarshalUnmarshal(
t,
cqrs.ProtoMarshaler{},
cqrs.ProtoMarshaler{},
newProtoTestComplexEvent(),
&TestComplexProtobufEvent{},
"cqrs_test.TestComplexProtobufEvent",
)
}
func TestProtoMarshaler_Marshal_generated_name(t *testing.T) {
marshaler := cqrs.ProtoMarshaler{
NewUUID: func() string {
return "foo"
},
}
msg, err := marshaler.Marshal(newProtoTestComplexEvent())
require.NoError(t, err)
assert.Equal(t, msg.UUID, "foo")
}
// newProtoLegacyTestEvent returns the same event in two different protobuf versions
func newProtoLegacyTestEvent() (*TestProtobufEvent, *TestProtobufLegacyEvent) {
when := timestamppb.New(time.Now())
id := watermill.NewULID()
legacy := &TestProtobufEvent{
Id: id,
When: when,
}
regenerated := &TestProtobufLegacyEvent{
Id: id,
When: when,
}
return legacy, regenerated
}
func newProtoTestComplexEvent() *TestComplexProtobufEvent {
when := timestamppb.New(time.Now())
eventToMarshal := &TestComplexProtobufEvent{
Id: watermill.NewULID(),
Data: []byte("data"),
When: when,
NestedMap: map[string]*SubEvent{
"foo": {
Tags: []string{"tag1", "tag2"},
Flags: map[string]bool{"flag1": true, "flag2": false},
},
},
Events: []*SubEvent{
{
Tags: []string{"tag1", "tag2"},
},
{
Tags: []string{"tag3", "tag4"},
},
},
Result: &TestComplexProtobufEvent_Success{
Success: &SubEvent{
Tags: []string{"tag10"},
Flags: map[string]bool{"flag10": true},
},
},
}
return eventToMarshal
}
func assertProtoMarshalUnmarshal[T1, T2 fmt.Stringer](
t *testing.T,
marshaler cqrs.CommandEventMarshaler,
unmarshaler cqrs.CommandEventMarshaler,
eventToMarshal T1,
eventToUnmarshal T2,
expectedEventName string,
) {
t.Helper()
msg, err := marshaler.Marshal(eventToMarshal)
require.NoError(t, err)
err = unmarshaler.Unmarshal(msg, eventToUnmarshal)
require.NoError(t, err)
eventToMarshalJson, err := json.Marshal(eventToMarshal)
require.NoError(t, err)
eventToUnmarshalJson, err := json.Marshal(eventToUnmarshal)
require.NoError(t, err)
assert.JSONEq(t, string(eventToMarshalJson), string(eventToUnmarshalJson))
assert.Equal(t, expectedEventName, msg.Metadata.Get("name"))
}

58
components/cqrs/name.go Normal file
View File

@@ -0,0 +1,58 @@
package cqrs
import (
"fmt"
"strings"
)
// FullyQualifiedStructName returns object name in format [package].[type name].
// For example, for the struct:
//
// package events
// type UserCreated struct {}
//
// it will return "events.UserCreated".
//
// It ignores if the value is a pointer or not.
func FullyQualifiedStructName(v interface{}) string {
s := fmt.Sprintf("%T", v)
s = strings.TrimLeft(s, "*")
return s
}
// StructName returns struct name in format [type name].
// For example, for the struct:
//
// package events
// type UserCreated struct {}
//
// it will return "UserCreated".
//
// It ignores if the value is a pointer or not.
func StructName(v interface{}) string {
segments := strings.Split(fmt.Sprintf("%T", v), ".")
return segments[len(segments)-1]
}
type namedStruct interface {
Name() string
}
// NamedStruct returns the name from a message implementing the following interface:
//
// type namedStruct interface {
// Name() string
// }
//
// It ignores if the value is a pointer or not.
func NamedStruct(fallback func(v interface{}) string) func(v interface{}) string {
return func(v interface{}) string {
if v, ok := v.(namedStruct); ok {
return v.Name()
}
return fallback(v)
}
}

View File

@@ -0,0 +1,49 @@
package cqrs_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
)
func TestFullyQualifiedStructName(t *testing.T) {
type Object struct{}
assert.Equal(t, "cqrs_test.Object", cqrs.FullyQualifiedStructName(Object{}))
assert.Equal(t, "cqrs_test.Object", cqrs.FullyQualifiedStructName(&Object{}))
}
func BenchmarkFullyQualifiedStructName(b *testing.B) {
type Object struct{}
o := Object{}
for i := 0; i < b.N; i++ {
cqrs.FullyQualifiedStructName(o)
}
}
func TestStructName(t *testing.T) {
type Object struct{}
assert.Equal(t, "Object", cqrs.StructName(Object{}))
assert.Equal(t, "Object", cqrs.StructName(&Object{}))
}
func TestNamedStruct(t *testing.T) {
assert.Equal(t, "named object", cqrs.NamedStruct(cqrs.StructName)(namedObject{}))
assert.Equal(t, "named object", cqrs.NamedStruct(cqrs.StructName)(&namedObject{}))
// Test fallback
type Object struct{}
assert.Equal(t, "Object", cqrs.NamedStruct(cqrs.StructName)(Object{}))
assert.Equal(t, "Object", cqrs.NamedStruct(cqrs.StructName)(&Object{}))
}
type namedObject struct{}
func (namedObject) Name() string {
return "named object"
}

23
components/cqrs/object.go Normal file
View File

@@ -0,0 +1,23 @@
package cqrs
import (
"reflect"
)
func isPointer(v interface{}) error {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Pointer || rv.IsNil() {
return NonPointerError{rv.Type()}
}
return nil
}
type NonPointerError struct {
Type reflect.Type
}
func (e NonPointerError) Error() string {
return "non-pointer command: " + e.Type.String() + ", handler.NewCommand() should return pointer to the command"
}

38
components/cqrs/testdata/events.proto vendored Normal file
View File

@@ -0,0 +1,38 @@
syntax = "proto3";
package cqrs_test;
option go_package = "./cqrs_test";
import "google/protobuf/timestamp.proto";
message TestProtobufLegacyEvent {
string id = 1;
google.protobuf.Timestamp when = 3;
}
enum Status {
STATUS_UNSPECIFIED = 0;
ACTIVE = 1;
DELETED = 2;
}
message SubEvent {
repeated string tags = 1;
map<string, bool> flags = 2;
}
message TestComplexProtobufEvent {
string id = 1;
bytes data = 2;
google.protobuf.Timestamp when = 3;
map<string, SubEvent> nested_map = 4;
repeated SubEvent events = 5;
oneof result {
SubEvent success = 6;
string error = 7;
Status fallback = 8;
}
reserved 23 to 30;
}