snapshot of https://github.com/ThreeDotsLabs/watermill.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
master / ci (push) Failing after 0s
Some checks failed
master / ci (push) Failing after 0s
This commit is contained in:
168
components/cqrs/command_bus.go
Normal file
168
components/cqrs/command_bus.go
Normal 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
|
||||
}
|
||||
224
components/cqrs/command_bus_test.go
Normal file
224
components/cqrs/command_bus_test.go
Normal 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")
|
||||
}
|
||||
59
components/cqrs/command_handler.go
Normal file
59
components/cqrs/command_handler.go
Normal 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)
|
||||
}
|
||||
32
components/cqrs/command_handler_test.go
Normal file
32
components/cqrs/command_handler_test.go
Normal 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")
|
||||
}
|
||||
382
components/cqrs/command_processor.go
Normal file
382
components/cqrs/command_processor.go
Normal 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
|
||||
}
|
||||
522
components/cqrs/command_processor_test.go
Normal file
522
components/cqrs/command_processor_test.go
Normal 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
199
components/cqrs/cqrs.go
Normal 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
|
||||
}
|
||||
490
components/cqrs/cqrs_test.go
Normal file
490
components/cqrs/cqrs_test.go
Normal 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
27
components/cqrs/ctx.go
Normal 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
3
components/cqrs/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Detailed CQRS documentation can be found in https://watermill.io/docs/cqrs/
|
||||
|
||||
package cqrs
|
||||
146
components/cqrs/event_bus.go
Normal file
146
components/cqrs/event_bus.go
Normal 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)
|
||||
}
|
||||
168
components/cqrs/event_bus_test.go
Normal file
168
components/cqrs/event_bus_test.go
Normal 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")
|
||||
}
|
||||
73
components/cqrs/event_handler.go
Normal file
73
components/cqrs/event_handler.go
Normal 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,
|
||||
}
|
||||
}
|
||||
48
components/cqrs/event_handler_test.go
Normal file
48
components/cqrs/event_handler_test.go
Normal 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")
|
||||
}
|
||||
380
components/cqrs/event_processor.go
Normal file
380
components/cqrs/event_processor.go
Normal 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
|
||||
}
|
||||
272
components/cqrs/event_processor_group.go
Normal file
272
components/cqrs/event_processor_group.go
Normal 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
|
||||
}
|
||||
457
components/cqrs/event_processor_group_test.go
Normal file
457
components/cqrs/event_processor_group_test.go
Normal 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)
|
||||
}
|
||||
539
components/cqrs/event_processor_test.go
Normal file
539
components/cqrs/event_processor_test.go
Normal 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)
|
||||
}
|
||||
55
components/cqrs/marshaler.go
Normal file
55
components/cqrs/marshaler.go
Normal 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
|
||||
}
|
||||
53
components/cqrs/marshaler_json.go
Normal file
53
components/cqrs/marshaler_json.go
Normal 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")
|
||||
}
|
||||
56
components/cqrs/marshaler_json_test.go
Normal file
56
components/cqrs/marshaler_json_test.go
Normal 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")
|
||||
}
|
||||
85
components/cqrs/marshaler_protobuf.go
Normal file
85
components/cqrs/marshaler_protobuf.go
Normal 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")
|
||||
}
|
||||
483
components/cqrs/marshaler_protobuf_events_new_test.go
Normal file
483
components/cqrs/marshaler_protobuf_events_new_test.go
Normal 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
|
||||
}
|
||||
90
components/cqrs/marshaler_protobuf_events_test.go
Normal file
90
components/cqrs/marshaler_protobuf_events_test.go
Normal 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,
|
||||
}
|
||||
127
components/cqrs/marshaler_protobuf_gogo.go
Normal file
127
components/cqrs/marshaler_protobuf_gogo.go
Normal 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")
|
||||
}
|
||||
142
components/cqrs/marshaler_protobuf_gogo_test.go
Normal file
142
components/cqrs/marshaler_protobuf_gogo_test.go
Normal 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",
|
||||
)
|
||||
})
|
||||
}
|
||||
114
components/cqrs/marshaler_protobuf_test.go
Normal file
114
components/cqrs/marshaler_protobuf_test.go
Normal 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
58
components/cqrs/name.go
Normal 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)
|
||||
}
|
||||
}
|
||||
49
components/cqrs/name_test.go
Normal file
49
components/cqrs/name_test.go
Normal 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
23
components/cqrs/object.go
Normal 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
38
components/cqrs/testdata/events.proto
vendored
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user