From af7183ce5d4c57926b94f008355a345d2f8bd74c Mon Sep 17 00:00:00 2001 From: Danilo Cianfrone Date: Fri, 3 Mar 2023 00:01:24 +0100 Subject: [PATCH 01/11] feat(examples): add todolist --- examples/todolist/command/create_todolist.go | 41 +++++++++ .../todolist/command/create_todolist_test.go | 46 ++++++++++ examples/todolist/domain/todolist/event.go | 29 +++++++ examples/todolist/domain/todolist/item.go | 39 +++++++++ .../todolist/domain/todolist/repository.go | 9 ++ examples/todolist/domain/todolist/todolist.go | 86 +++++++++++++++++++ examples/todolist/go.mod | 18 ++++ examples/todolist/go.sum | 29 +++++++ go.work | 2 + 9 files changed, 299 insertions(+) create mode 100644 examples/todolist/command/create_todolist.go create mode 100644 examples/todolist/command/create_todolist_test.go create mode 100644 examples/todolist/domain/todolist/event.go create mode 100644 examples/todolist/domain/todolist/item.go create mode 100644 examples/todolist/domain/todolist/repository.go create mode 100644 examples/todolist/domain/todolist/todolist.go create mode 100644 examples/todolist/go.mod create mode 100644 examples/todolist/go.sum diff --git a/examples/todolist/command/create_todolist.go b/examples/todolist/command/create_todolist.go new file mode 100644 index 00000000..d6952ae2 --- /dev/null +++ b/examples/todolist/command/create_todolist.go @@ -0,0 +1,41 @@ +package command + +import ( + "context" + "fmt" + "time" + + "github.com/get-eventually/go-eventually/core/command" + "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" +) + +type CreateTodoList struct { + ID todolist.ID + Title string + Owner string +} + +func (CreateTodoList) Name() string { return "CreateTodoList" } + +var _ command.Handler[CreateTodoList] = CreateTodoListHandler{} + +type CreateTodoListHandler struct { + Clock func() time.Time + Repository todolist.Saver +} + +// Handle implements command.Handler +func (h CreateTodoListHandler) Handle(ctx context.Context, cmd command.Envelope[CreateTodoList]) error { + now := h.Clock() + + todoList, err := todolist.Create(cmd.Message.ID, cmd.Message.Title, cmd.Message.Owner, now) + if err != nil { + return fmt.Errorf("command.CreateTodoListHandler: failed to create new todolist, %w", err) + } + + if err := h.Repository.Save(ctx, todoList); err != nil { + return fmt.Errorf("command.CreateTodoListHandler: failed to save todolist to repository, %w", err) + } + + return nil +} diff --git a/examples/todolist/command/create_todolist_test.go b/examples/todolist/command/create_todolist_test.go new file mode 100644 index 00000000..d3fe3c3a --- /dev/null +++ b/examples/todolist/command/create_todolist_test.go @@ -0,0 +1,46 @@ +package command_test + +import ( + "testing" + "time" + + "github.com/get-eventually/go-eventually/core/aggregate" + "github.com/get-eventually/go-eventually/core/command" + "github.com/get-eventually/go-eventually/core/event" + "github.com/get-eventually/go-eventually/core/test/scenario" + "github.com/google/uuid" + + appcommand "github.com/get-eventually/go-eventually/examples/todolist/command" + "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" +) + +func TestCreateTodoListHandler(t *testing.T) { + id := uuid.New() + now := time.Now() + clock := func() time.Time { return now } + + t.Run("it works", func(t *testing.T) { + scenario.CommandHandler[appcommand.CreateTodoList, appcommand.CreateTodoListHandler](). + When(command.ToEnvelope(appcommand.CreateTodoList{ + ID: todolist.ID(id), + Title: "my-title", + Owner: "owner", + })). + Then(event.Persisted{ + StreamID: event.StreamID(id.String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todolist.ID(id), + Title: "my-title", + Owner: "owner", + CreationTime: now, + }), + }). + AssertOn(t, func(s event.Store) appcommand.CreateTodoListHandler { + return appcommand.CreateTodoListHandler{ + Clock: clock, + Repository: aggregate.NewEventSourcedRepository(s, todolist.Type), + } + }) + }) +} diff --git a/examples/todolist/domain/todolist/event.go b/examples/todolist/domain/todolist/event.go new file mode 100644 index 00000000..4bf4ee77 --- /dev/null +++ b/examples/todolist/domain/todolist/event.go @@ -0,0 +1,29 @@ +package todolist + +import "time" + +type WasCreated struct { + ID ID + Title string + Owner string + CreationTime time.Time +} + +func (WasCreated) Name() string { return "TodoListWasCreated" } + +type ItemWasAdded struct { + ID ItemID + Title string + Description string + DueDate time.Time +} + +func (ItemWasAdded) Name() string { return "TodoListItemWasAdded" } + +type ItemMarkedAsDone struct{} + +func (ItemMarkedAsDone) Name() string { return "TodoListItemMarkedAsDone" } + +type ItemWasDeleted struct{} + +func (ItemWasDeleted) Name() string { return "TodoListItemWasDeleted" } diff --git a/examples/todolist/domain/todolist/item.go b/examples/todolist/domain/todolist/item.go new file mode 100644 index 00000000..e4b54660 --- /dev/null +++ b/examples/todolist/domain/todolist/item.go @@ -0,0 +1,39 @@ +package todolist + +import ( + "fmt" + "time" + + "github.com/get-eventually/go-eventually/core/aggregate" + "github.com/get-eventually/go-eventually/core/event" + "github.com/google/uuid" +) + +type ItemID uuid.UUID + +func (id ItemID) String() string { return uuid.UUID(id).String() } + +type Item struct { + aggregate.BaseRoot + + id ItemID + title string + description string + completed bool + dueDate time.Time +} + +func (item *Item) Apply(event event.Event) error { + switch evt := event.(type) { + case ItemWasAdded: + item.id = evt.ID + item.title = evt.Title + item.description = evt.Description + item.completed = false + item.dueDate = evt.DueDate + default: + return fmt.Errorf("todolist.Item.Apply: unsupported event, %T", evt) + } + + return nil +} diff --git a/examples/todolist/domain/todolist/repository.go b/examples/todolist/domain/todolist/repository.go new file mode 100644 index 00000000..da0574e6 --- /dev/null +++ b/examples/todolist/domain/todolist/repository.go @@ -0,0 +1,9 @@ +package todolist + +import "github.com/get-eventually/go-eventually/core/aggregate" + +type ( + Getter = aggregate.Getter[ID, *TodoList] + Saver = aggregate.Saver[ID, *TodoList] + Repository = aggregate.Repository[ID, *TodoList] +) diff --git a/examples/todolist/domain/todolist/todolist.go b/examples/todolist/domain/todolist/todolist.go new file mode 100644 index 00000000..66e94f03 --- /dev/null +++ b/examples/todolist/domain/todolist/todolist.go @@ -0,0 +1,86 @@ +package todolist + +import ( + "fmt" + "time" + + "github.com/get-eventually/go-eventually/core/aggregate" + "github.com/get-eventually/go-eventually/core/event" + "github.com/google/uuid" +) + +type ID uuid.UUID + +func (id ID) String() string { return uuid.UUID(id).String() } + +var Type = aggregate.Type[ID, *TodoList]{ + Name: "TodoList", + Factory: func() *TodoList { return new(TodoList) }, +} + +type TodoList struct { + aggregate.BaseRoot + + id ID + title string + owner string + creationTime time.Time + items []*Item +} + +// AggregateID implements aggregate.Root +func (tl *TodoList) AggregateID() ID { + return tl.id +} + +// Apply implements aggregate.Root +func (tl *TodoList) Apply(event event.Event) error { + switch evt := event.(type) { + case WasCreated: + tl.id = evt.ID + tl.title = evt.Title + tl.owner = evt.Owner + tl.creationTime = evt.CreationTime + + case ItemWasAdded: + item := &Item{} + if err := item.Apply(evt); err != nil { + return fmt.Errorf("todolist.TodoList.Apply: failed to apply item event, %w", err) + } + tl.items = append(tl.items, item) + + case ItemMarkedAsDone: + case ItemWasDeleted: + + default: + return fmt.Errorf("todolist.TodoList.Apply: invalid event, %T", evt) + } + + return nil +} + +func Create(id ID, title, owner string, now time.Time) (*TodoList, error) { + if uuid.UUID(id) == uuid.Nil { + return nil, fmt.Errorf("invalid id") + } + + if title == "" { + return nil, fmt.Errorf("empty title") + } + + if owner == "" { + return nil, fmt.Errorf("empty owner") + } + + var todoList TodoList + if err := aggregate.RecordThat[ID](&todoList, event.ToEnvelope(WasCreated{ + ID: id, + Title: title, + Owner: owner, + CreationTime: now, + })); err != nil { + return nil, fmt.Errorf("todolist.Create: failed to apply domain event, %w", err) + } + + return &todoList, nil +} diff --git a/examples/todolist/go.mod b/examples/todolist/go.mod new file mode 100644 index 00000000..6fea1323 --- /dev/null +++ b/examples/todolist/go.mod @@ -0,0 +1,18 @@ +module github.com/get-eventually/go-eventually/examples/todolist + +go 1.18 + +require ( + github.com/get-eventually/go-eventually/core v0.0.0-20230301093954-efadfc924ad7 + github.com/google/uuid v1.3.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/stretchr/testify v1.8.2 // indirect + golang.org/x/sync v0.1.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/todolist/go.sum b/examples/todolist/go.sum new file mode 100644 index 00000000..aadcb8a4 --- /dev/null +++ b/examples/todolist/go.sum @@ -0,0 +1,29 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/get-eventually/go-eventually/core v0.0.0-20230301093954-efadfc924ad7 h1:9Er2kSmSC1K9L1lESfPy6hBF6eiz6HBYJ+0rmiI8Liw= +github.com/get-eventually/go-eventually/core v0.0.0-20230301093954-efadfc924ad7/go.mod h1:3KiUK1ntGvBCcttLtFeNhdf83XkaUscdrGVJcvEISFY= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work b/go.work index 32f49db3..2f115be1 100644 --- a/go.work +++ b/go.work @@ -2,6 +2,7 @@ go 1.18 use ( ./core + ./examples/todolist ./oteleventually ./postgres ./serdes @@ -9,5 +10,6 @@ use ( replace ( github.com/get-eventually/go-eventually/core v0.0.0-20230213095413-67475c43eea4 => ./core + github.com/get-eventually/go-eventually/core v0.0.0-20230301093954-efadfc924ad7 => ./core github.com/get-eventually/go-eventually/serdes v0.0.0-20230227215702-6ac2a4505ce1 => ./serdes ) From 38efa0c1fc6b2e0862394c9ed18162c05946b737 Mon Sep 17 00:00:00 2001 From: Danilo Cianfrone Date: Fri, 3 Mar 2023 15:35:18 +0100 Subject: [PATCH 02/11] chore: use -coverpkg for cross-package coverage --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9b37fde6..0f7418cd 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -GO_TEST_FLAGS := -v -race -coverprofile=coverage.out +GO_TEST_FLAGS := -v -race -covermode=atomic -coverpkg=./... -coverprofile=coverage.out GOLANGCI_LINT_FLAGS ?= .PHONY: run-linter From a5e5d14de54f926be9dbb6e3c8dac7f754d04f46 Mon Sep 17 00:00:00 2001 From: Danilo Cianfrone Date: Fri, 3 Mar 2023 15:39:30 +0100 Subject: [PATCH 03/11] feat(examples/todolist): add test cases --- .../todolist/command/create_todolist_test.go | 72 +++++++++++++++++-- examples/todolist/domain/todolist/event.go | 14 +++- examples/todolist/domain/todolist/item.go | 7 ++ examples/todolist/domain/todolist/todolist.go | 56 ++++++++++++++- 4 files changed, 138 insertions(+), 11 deletions(-) diff --git a/examples/todolist/command/create_todolist_test.go b/examples/todolist/command/create_todolist_test.go index d3fe3c3a..3d84e6a0 100644 --- a/examples/todolist/command/create_todolist_test.go +++ b/examples/todolist/command/create_todolist_test.go @@ -8,6 +8,7 @@ import ( "github.com/get-eventually/go-eventually/core/command" "github.com/get-eventually/go-eventually/core/event" "github.com/get-eventually/go-eventually/core/test/scenario" + "github.com/get-eventually/go-eventually/core/version" "github.com/google/uuid" appcommand "github.com/get-eventually/go-eventually/examples/todolist/command" @@ -19,6 +20,46 @@ func TestCreateTodoListHandler(t *testing.T) { now := time.Now() clock := func() time.Time { return now } + commandHandlerFactory := func(s event.Store) appcommand.CreateTodoListHandler { + return appcommand.CreateTodoListHandler{ + Clock: clock, + Repository: aggregate.NewEventSourcedRepository(s, todolist.Type), + } + } + + t.Run("it fails when an invalid id has been provided", func(t *testing.T) { + scenario.CommandHandler[appcommand.CreateTodoList, appcommand.CreateTodoListHandler](). + When(command.ToEnvelope(appcommand.CreateTodoList{ + ID: todolist.ID(uuid.Nil), + Title: "my-title", + Owner: "owner", + })). + ThenError(todolist.ErrEmptyID). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when a title has not been provided", func(t *testing.T) { + scenario.CommandHandler[appcommand.CreateTodoList, appcommand.CreateTodoListHandler](). + When(command.ToEnvelope(appcommand.CreateTodoList{ + ID: todolist.ID(id), + Title: "", + Owner: "owner", + })). + ThenError(todolist.ErrEmptyTitle). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when an owner has not been provided", func(t *testing.T) { + scenario.CommandHandler[appcommand.CreateTodoList, appcommand.CreateTodoListHandler](). + When(command.ToEnvelope(appcommand.CreateTodoList{ + ID: todolist.ID(id), + Title: "my-title", + Owner: "", + })). + ThenError(todolist.ErrNoOwnerSpecified). + AssertOn(t, commandHandlerFactory) + }) + t.Run("it works", func(t *testing.T) { scenario.CommandHandler[appcommand.CreateTodoList, appcommand.CreateTodoListHandler](). When(command.ToEnvelope(appcommand.CreateTodoList{ @@ -36,11 +77,30 @@ func TestCreateTodoListHandler(t *testing.T) { CreationTime: now, }), }). - AssertOn(t, func(s event.Store) appcommand.CreateTodoListHandler { - return appcommand.CreateTodoListHandler{ - Clock: clock, - Repository: aggregate.NewEventSourcedRepository(s, todolist.Type), - } - }) + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when trying to create a TodoList that exists already", func(t *testing.T) { + scenario.CommandHandler[appcommand.CreateTodoList, appcommand.CreateTodoListHandler](). + Given(event.Persisted{ + StreamID: event.StreamID(id.String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todolist.ID(id), + Title: "my-title", + Owner: "owner", + CreationTime: now, + }), + }). + When(command.ToEnvelope(appcommand.CreateTodoList{ + ID: todolist.ID(id), + Title: "my-title", + Owner: "owner", + })). + ThenError(version.ConflictError{ + Expected: 0, + Actual: 1, + }). + AssertOn(t, commandHandlerFactory) }) } diff --git a/examples/todolist/domain/todolist/event.go b/examples/todolist/domain/todolist/event.go index 4bf4ee77..1cb934b0 100644 --- a/examples/todolist/domain/todolist/event.go +++ b/examples/todolist/domain/todolist/event.go @@ -20,10 +20,20 @@ type ItemWasAdded struct { func (ItemWasAdded) Name() string { return "TodoListItemWasAdded" } -type ItemMarkedAsDone struct{} +type ItemMarkedAsDone struct { + ID ItemID +} func (ItemMarkedAsDone) Name() string { return "TodoListItemMarkedAsDone" } -type ItemWasDeleted struct{} +type ItemMarkedAsPending struct { + ID ItemID +} + +func (ItemMarkedAsPending) Name() string { return "TodoListItemMarkedAsPending" } + +type ItemWasDeleted struct { + ID ItemID +} func (ItemWasDeleted) Name() string { return "TodoListItemWasDeleted" } diff --git a/examples/todolist/domain/todolist/item.go b/examples/todolist/domain/todolist/item.go index e4b54660..e66f17d8 100644 --- a/examples/todolist/domain/todolist/item.go +++ b/examples/todolist/domain/todolist/item.go @@ -31,6 +31,13 @@ func (item *Item) Apply(event event.Event) error { item.description = evt.Description item.completed = false item.dueDate = evt.DueDate + + case ItemMarkedAsDone: + item.completed = true + + case ItemMarkedAsPending: + item.completed = false + default: return fmt.Errorf("todolist.Item.Apply: unsupported event, %T", evt) } diff --git a/examples/todolist/domain/todolist/todolist.go b/examples/todolist/domain/todolist/todolist.go index 66e94f03..9d571b9f 100644 --- a/examples/todolist/domain/todolist/todolist.go +++ b/examples/todolist/domain/todolist/todolist.go @@ -1,6 +1,7 @@ package todolist import ( + "errors" "fmt" "time" @@ -33,6 +34,29 @@ func (tl *TodoList) AggregateID() ID { return tl.id } +func (tl *TodoList) findItemByID(id ItemID) *Item { + for _, item := range tl.items { + if item.id == id { + return item + } + } + + return nil +} + +func (tl *TodoList) applyItemEvent(id ItemID, evt event.Event) error { + item := tl.findItemByID(id) + if item == nil { + return fmt.Errorf("todolist.TodoList.Apply: item not found") + } + + if err := item.Apply(evt); err != nil { + return fmt.Errorf("todolist.TodoList.Apply: failed to apply item event, %w", err) + } + + return nil +} + // Apply implements aggregate.Root func (tl *TodoList) Apply(event event.Event) error { switch evt := event.(type) { @@ -49,8 +73,23 @@ func (tl *TodoList) Apply(event event.Event) error { } tl.items = append(tl.items, item) + case ItemMarkedAsPending: + return tl.applyItemEvent(evt.ID, evt) + case ItemMarkedAsDone: + return tl.applyItemEvent(evt.ID, evt) + case ItemWasDeleted: + var items []*Item + for _, item := range tl.items { + if item.id == evt.ID { + continue + } + + items = append(items, item) + } + + tl.items = items default: return fmt.Errorf("todolist.TodoList.Apply: invalid event, %T", evt) @@ -59,20 +98,31 @@ func (tl *TodoList) Apply(event event.Event) error { return nil } +var ( + ErrEmptyID = errors.New("todolist.TodoList: empty id provided") + ErrEmptyTitle = errors.New("todolist.TodoList: empty title provided") + ErrNoOwnerSpecified = errors.New("todolist.TodoList: no owner specified") +) + func Create(id ID, title, owner string, now time.Time) (*TodoList, error) { + wrapErr := func(err error) error { + return fmt.Errorf("todolist.Create: failed to create new TodoList, %w", err) + } + if uuid.UUID(id) == uuid.Nil { - return nil, fmt.Errorf("invalid id") + return nil, wrapErr(ErrEmptyID) } if title == "" { - return nil, fmt.Errorf("empty title") + return nil, wrapErr(ErrEmptyTitle) } if owner == "" { - return nil, fmt.Errorf("empty owner") + return nil, wrapErr(ErrNoOwnerSpecified) } var todoList TodoList + if err := aggregate.RecordThat[ID](&todoList, event.ToEnvelope(WasCreated{ ID: id, Title: title, From 64ade49f6b8b86aea1cd849288f132c060329c7e Mon Sep 17 00:00:00 2001 From: Danilo Cianfrone Date: Sat, 11 Mar 2023 01:16:33 +0100 Subject: [PATCH 04/11] feat: add AddTodoListItem command --- .../todolist/command/add_todo_list_item.go | 54 ++++++++++++++ .../command/add_todo_list_item_test.go | 70 +++++++++++++++++++ examples/todolist/domain/todolist/event.go | 9 +-- examples/todolist/domain/todolist/item.go | 12 ++-- examples/todolist/domain/todolist/todolist.go | 49 ++++++++++++- 5 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 examples/todolist/command/add_todo_list_item.go create mode 100644 examples/todolist/command/add_todo_list_item_test.go diff --git a/examples/todolist/command/add_todo_list_item.go b/examples/todolist/command/add_todo_list_item.go new file mode 100644 index 00000000..c1db92ee --- /dev/null +++ b/examples/todolist/command/add_todo_list_item.go @@ -0,0 +1,54 @@ +package command + +import ( + "context" + "fmt" + "time" + + "github.com/get-eventually/go-eventually/core/command" + "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" +) + +type AddTodoListItem struct { + TodoListID todolist.ID + TodoItemID todolist.ItemID + Title string + Description string + DueDate time.Time +} + +// Name implements command.Command +func (AddTodoListItem) Name() string { return "AddTodoListItem" } + +var _ command.Handler[AddTodoListItem] = AddTodoListItemHandler{} + +type AddTodoListItemHandler struct { + Clock func() time.Time + Repository todolist.Repository +} + +// Handle implements command.Handler +func (h AddTodoListItemHandler) Handle(ctx context.Context, cmd command.Envelope[AddTodoListItem]) error { + todoList, err := h.Repository.Get(ctx, cmd.Message.TodoListID) + if err != nil { + return fmt.Errorf("command.AddTodoListItem: failed to get TodoList from repository, %w", err) + } + + now := h.Clock() + + if err := todoList.AddItem( + cmd.Message.TodoItemID, + cmd.Message.Title, + cmd.Message.Description, + cmd.Message.DueDate, + now, + ); err != nil { + return fmt.Errorf("command.AddTodoListItem: failed to add item to TodoList, %w", err) + } + + if err := h.Repository.Save(ctx, todoList); err != nil { + return fmt.Errorf("command.AddTodoListItem: failed to save new TodoList version, %w", err) + } + + return nil +} diff --git a/examples/todolist/command/add_todo_list_item_test.go b/examples/todolist/command/add_todo_list_item_test.go new file mode 100644 index 00000000..b8e134a7 --- /dev/null +++ b/examples/todolist/command/add_todo_list_item_test.go @@ -0,0 +1,70 @@ +package command_test + +import ( + "testing" + "time" + + "github.com/get-eventually/go-eventually/core/aggregate" + "github.com/get-eventually/go-eventually/core/command" + "github.com/get-eventually/go-eventually/core/event" + "github.com/get-eventually/go-eventually/core/test/scenario" + appcommand "github.com/get-eventually/go-eventually/examples/todolist/command" + "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" + "github.com/google/uuid" +) + +func TestAddTodoListItem(t *testing.T) { + now := time.Now() + commandHandlerFactory := func(es event.Store) appcommand.AddTodoListItemHandler { + return appcommand.AddTodoListItemHandler{ + Clock: func() time.Time { return now }, + Repository: aggregate.NewEventSourcedRepository(es, todolist.Type), + } + } + + todoListID := todolist.ID(uuid.New()) + todoItemID := todolist.ItemID(uuid.New()) + listTitle := "my list" + listOwner := "me" + + t.Run("it fails when the target TodoList does not exist", func(t *testing.T) { + scenario.CommandHandler[appcommand.AddTodoListItem, appcommand.AddTodoListItemHandler](). + When(command.ToEnvelope(appcommand.AddTodoListItem{ + TodoListID: todoListID, + TodoItemID: todoItemID, + Title: "a todo item that should fail", + })). + ThenError(aggregate.ErrRootNotFound). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it works", func(t *testing.T) { + scenario.CommandHandler[appcommand.AddTodoListItem, appcommand.AddTodoListItemHandler](). + Given(event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todoListID, + Title: listTitle, + Owner: listOwner, + CreationTime: now.Add(-2 * time.Minute), + }), + }). + When(command.ToEnvelope(appcommand.AddTodoListItem{ + TodoListID: todoListID, + TodoItemID: todoItemID, + Title: "a todo item that should succeed", + })). + Then(event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 2, + Envelope: event.ToEnvelope(todolist.ItemWasAdded{ + ID: todoItemID, + Title: "a todo item that should succeed", + CreationTime: now, + }), + }). + AssertOn(t, commandHandlerFactory) + }) + +} diff --git a/examples/todolist/domain/todolist/event.go b/examples/todolist/domain/todolist/event.go index 1cb934b0..194eda32 100644 --- a/examples/todolist/domain/todolist/event.go +++ b/examples/todolist/domain/todolist/event.go @@ -12,10 +12,11 @@ type WasCreated struct { func (WasCreated) Name() string { return "TodoListWasCreated" } type ItemWasAdded struct { - ID ItemID - Title string - Description string - DueDate time.Time + ID ItemID + Title string + Description string + DueDate time.Time + CreationTime time.Time } func (ItemWasAdded) Name() string { return "TodoListItemWasAdded" } diff --git a/examples/todolist/domain/todolist/item.go b/examples/todolist/domain/todolist/item.go index e66f17d8..e55117ec 100644 --- a/examples/todolist/domain/todolist/item.go +++ b/examples/todolist/domain/todolist/item.go @@ -16,11 +16,12 @@ func (id ItemID) String() string { return uuid.UUID(id).String() } type Item struct { aggregate.BaseRoot - id ItemID - title string - description string - completed bool - dueDate time.Time + id ItemID + title string + description string + completed bool + dueDate time.Time + creationTime time.Time } func (item *Item) Apply(event event.Event) error { @@ -31,6 +32,7 @@ func (item *Item) Apply(event event.Event) error { item.description = evt.Description item.completed = false item.dueDate = evt.DueDate + item.creationTime = evt.CreationTime case ItemMarkedAsDone: item.completed = true diff --git a/examples/todolist/domain/todolist/todolist.go b/examples/todolist/domain/todolist/todolist.go index 9d571b9f..e73daf2f 100644 --- a/examples/todolist/domain/todolist/todolist.go +++ b/examples/todolist/domain/todolist/todolist.go @@ -99,9 +99,12 @@ func (tl *TodoList) Apply(event event.Event) error { } var ( - ErrEmptyID = errors.New("todolist.TodoList: empty id provided") - ErrEmptyTitle = errors.New("todolist.TodoList: empty title provided") - ErrNoOwnerSpecified = errors.New("todolist.TodoList: no owner specified") + ErrEmptyID = errors.New("todolist.TodoList: empty id provided") + ErrEmptyTitle = errors.New("todolist.TodoList: empty title provided") + ErrNoOwnerSpecified = errors.New("todolist.TodoList: no owner specified") + ErrEmptyItemID = errors.New("todolist.TodoList: empty item id provided") + ErrEmptyItemTitle = errors.New("todolist.TodoList: empty item title provided") + ErrItemAlreadyExists = errors.New("todolist.TodoList: item already exists") ) func Create(id ID, title, owner string, now time.Time) (*TodoList, error) { @@ -134,3 +137,43 @@ func Create(id ID, title, owner string, now time.Time) (*TodoList, error) { return &todoList, nil } + +func (todoList *TodoList) itemByID(id ItemID) (*Item, bool) { + for _, item := range todoList.items { + if item.id == id { + return item, true + } + } + + return nil, false +} + +func (todoList *TodoList) AddItem(id ItemID, title, description string, dueDate, now time.Time) error { + wrapErr := func(err error) error { + return fmt.Errorf("todolist.AddItem: failed to add new TodoItem to list, %w", err) + } + + if uuid.UUID(id) == uuid.Nil { + return wrapErr(ErrEmptyItemID) + } + + if title == "" { + return wrapErr(ErrEmptyTitle) + } + + if _, ok := todoList.itemByID(id); ok { + return wrapErr(ErrItemAlreadyExists) + } + + if err := aggregate.RecordThat[ID](todoList, event.ToEnvelope(ItemWasAdded{ + ID: id, + Title: title, + Description: description, + DueDate: dueDate, + CreationTime: now, + })); err != nil { + return fmt.Errorf("todolist.AddItem: failed to apply domain event, %w", err) + } + + return nil +} From 6bc0ccb37e17250ce286d628eaaf3737fde63136 Mon Sep 17 00:00:00 2001 From: Danilo Cianfrone Date: Sat, 11 Mar 2023 10:11:38 +0100 Subject: [PATCH 05/11] test: add testcases for AddTodoListItem --- .../command/add_todo_list_item_test.go | 72 ++++++++++++++++++- examples/todolist/domain/todolist/todolist.go | 2 +- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/examples/todolist/command/add_todo_list_item_test.go b/examples/todolist/command/add_todo_list_item_test.go index b8e134a7..b00947e8 100644 --- a/examples/todolist/command/add_todo_list_item_test.go +++ b/examples/todolist/command/add_todo_list_item_test.go @@ -38,6 +38,77 @@ func TestAddTodoListItem(t *testing.T) { AssertOn(t, commandHandlerFactory) }) + t.Run("it fails when the same item has already been added", func(t *testing.T) { + scenario.CommandHandler[appcommand.AddTodoListItem, appcommand.AddTodoListItemHandler](). + Given(event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todoListID, + Title: listTitle, + Owner: listOwner, + CreationTime: now.Add(-2 * time.Minute), + }), + }, event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 2, + Envelope: event.ToEnvelope(todolist.ItemWasAdded{ + ID: todoItemID, + Title: "a todo item that should succeed", + CreationTime: now, + }), + }). + When(command.ToEnvelope(appcommand.AddTodoListItem{ + TodoListID: todoListID, + TodoItemID: todoItemID, + Title: "uh oh, this is gonna fail", + })). + ThenError(todolist.ErrItemAlreadyExists). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when the item id provided is empty", func(t *testing.T) { + scenario.CommandHandler[appcommand.AddTodoListItem, appcommand.AddTodoListItemHandler](). + Given(event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todoListID, + Title: listTitle, + Owner: listOwner, + CreationTime: now.Add(-2 * time.Minute), + }), + }). + When(command.ToEnvelope(appcommand.AddTodoListItem{ + TodoListID: todoListID, + TodoItemID: todolist.ItemID(uuid.Nil), + Title: "i think i forgot to add an id...", + })). + ThenError(todolist.ErrEmptyItemID). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when an empty item title is provided", func(t *testing.T) { + scenario.CommandHandler[appcommand.AddTodoListItem, appcommand.AddTodoListItemHandler](). + Given(event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todoListID, + Title: listTitle, + Owner: listOwner, + CreationTime: now.Add(-2 * time.Minute), + }), + }). + When(command.ToEnvelope(appcommand.AddTodoListItem{ + TodoListID: todoListID, + TodoItemID: todoItemID, + Title: "", + })). + ThenError(todolist.ErrEmptyItemTitle). + AssertOn(t, commandHandlerFactory) + }) + t.Run("it works", func(t *testing.T) { scenario.CommandHandler[appcommand.AddTodoListItem, appcommand.AddTodoListItemHandler](). Given(event.Persisted{ @@ -66,5 +137,4 @@ func TestAddTodoListItem(t *testing.T) { }). AssertOn(t, commandHandlerFactory) }) - } diff --git a/examples/todolist/domain/todolist/todolist.go b/examples/todolist/domain/todolist/todolist.go index e73daf2f..c4438a7b 100644 --- a/examples/todolist/domain/todolist/todolist.go +++ b/examples/todolist/domain/todolist/todolist.go @@ -158,7 +158,7 @@ func (todoList *TodoList) AddItem(id ItemID, title, description string, dueDate, } if title == "" { - return wrapErr(ErrEmptyTitle) + return wrapErr(ErrEmptyItemTitle) } if _, ok := todoList.itemByID(id); ok { From 47e88f5b41e8a191db856879deeff670897c86e1 Mon Sep 17 00:00:00 2001 From: Danilo Cianfrone Date: Tue, 14 Mar 2023 00:15:22 +0100 Subject: [PATCH 06/11] doc(examples/todolist): add documentation --- .../todolist/command/add_todo_list_item.go | 6 +- .../command/add_todo_list_item_test.go | 3 +- examples/todolist/command/create_todolist.go | 5 +- .../todolist/command/create_todolist_test.go | 4 +- examples/todolist/command/doc.go | 3 + examples/todolist/domain/todolist/event.go | 14 +++ examples/todolist/domain/todolist/item.go | 7 +- .../todolist/domain/todolist/repository.go | 9 +- examples/todolist/domain/todolist/todolist.go | 92 +++++++++++++++++-- .../todolist/domain/todolist/todolist_test.go | 57 ++++++++++++ 10 files changed, 183 insertions(+), 17 deletions(-) create mode 100644 examples/todolist/command/doc.go create mode 100644 examples/todolist/domain/todolist/todolist_test.go diff --git a/examples/todolist/command/add_todo_list_item.go b/examples/todolist/command/add_todo_list_item.go index c1db92ee..7a8b3959 100644 --- a/examples/todolist/command/add_todo_list_item.go +++ b/examples/todolist/command/add_todo_list_item.go @@ -9,6 +9,7 @@ import ( "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" ) +// AddTodoListItem the Command used to add a new Item to an existing TodoList. type AddTodoListItem struct { TodoListID todolist.ID TodoItemID todolist.ItemID @@ -17,17 +18,18 @@ type AddTodoListItem struct { DueDate time.Time } -// Name implements command.Command +// Name implements message.Message. func (AddTodoListItem) Name() string { return "AddTodoListItem" } var _ command.Handler[AddTodoListItem] = AddTodoListItemHandler{} +// AddTodoListItemHandler is the command.Handler for AddTodoListItem commands. type AddTodoListItemHandler struct { Clock func() time.Time Repository todolist.Repository } -// Handle implements command.Handler +// Handle implements command.Handler. func (h AddTodoListItemHandler) Handle(ctx context.Context, cmd command.Envelope[AddTodoListItem]) error { todoList, err := h.Repository.Get(ctx, cmd.Message.TodoListID) if err != nil { diff --git a/examples/todolist/command/add_todo_list_item_test.go b/examples/todolist/command/add_todo_list_item_test.go index b00947e8..320baaf9 100644 --- a/examples/todolist/command/add_todo_list_item_test.go +++ b/examples/todolist/command/add_todo_list_item_test.go @@ -4,13 +4,14 @@ import ( "testing" "time" + "github.com/google/uuid" + "github.com/get-eventually/go-eventually/core/aggregate" "github.com/get-eventually/go-eventually/core/command" "github.com/get-eventually/go-eventually/core/event" "github.com/get-eventually/go-eventually/core/test/scenario" appcommand "github.com/get-eventually/go-eventually/examples/todolist/command" "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" - "github.com/google/uuid" ) func TestAddTodoListItem(t *testing.T) { diff --git a/examples/todolist/command/create_todolist.go b/examples/todolist/command/create_todolist.go index d6952ae2..560f69e0 100644 --- a/examples/todolist/command/create_todolist.go +++ b/examples/todolist/command/create_todolist.go @@ -9,22 +9,25 @@ import ( "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" ) +// CreateTodoList is the Command used to create a new TodoList. type CreateTodoList struct { ID todolist.ID Title string Owner string } +// Name implements message.Message. func (CreateTodoList) Name() string { return "CreateTodoList" } var _ command.Handler[CreateTodoList] = CreateTodoListHandler{} +// CreateTodoListHandler is the Command Handler for CreateTodoList commands. type CreateTodoListHandler struct { Clock func() time.Time Repository todolist.Saver } -// Handle implements command.Handler +// Handle implements command.Handler. func (h CreateTodoListHandler) Handle(ctx context.Context, cmd command.Envelope[CreateTodoList]) error { now := h.Clock() diff --git a/examples/todolist/command/create_todolist_test.go b/examples/todolist/command/create_todolist_test.go index 3d84e6a0..faab689d 100644 --- a/examples/todolist/command/create_todolist_test.go +++ b/examples/todolist/command/create_todolist_test.go @@ -4,13 +4,13 @@ import ( "testing" "time" + "github.com/google/uuid" + "github.com/get-eventually/go-eventually/core/aggregate" "github.com/get-eventually/go-eventually/core/command" "github.com/get-eventually/go-eventually/core/event" "github.com/get-eventually/go-eventually/core/test/scenario" "github.com/get-eventually/go-eventually/core/version" - "github.com/google/uuid" - appcommand "github.com/get-eventually/go-eventually/examples/todolist/command" "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" ) diff --git a/examples/todolist/command/doc.go b/examples/todolist/command/doc.go new file mode 100644 index 00000000..bd669911 --- /dev/null +++ b/examples/todolist/command/doc.go @@ -0,0 +1,3 @@ +// Package command contains Application Commands and Command Handlers +// for the TodoList bounded context. +package command diff --git a/examples/todolist/domain/todolist/event.go b/examples/todolist/domain/todolist/event.go index 194eda32..170dc0e6 100644 --- a/examples/todolist/domain/todolist/event.go +++ b/examples/todolist/domain/todolist/event.go @@ -2,6 +2,7 @@ package todolist import "time" +// WasCreated is the Domain Event issued when new TodoList gets created. type WasCreated struct { ID ID Title string @@ -9,8 +10,11 @@ type WasCreated struct { CreationTime time.Time } +// Name implements message.Message. func (WasCreated) Name() string { return "TodoListWasCreated" } +// ItemWasAdded is the Domain Event issued when a new Item gets added +// to an existing TodoList. type ItemWasAdded struct { ID ItemID Title string @@ -19,22 +23,32 @@ type ItemWasAdded struct { CreationTime time.Time } +// Name implements message.Message. func (ItemWasAdded) Name() string { return "TodoListItemWasAdded" } +// ItemMarkedAsDone is the Domain Event issued when an existing Item +// in a TodoList gets marked as "done", or "completed". type ItemMarkedAsDone struct { ID ItemID } +// Name implements message.Message. func (ItemMarkedAsDone) Name() string { return "TodoListItemMarkedAsDone" } +// ItemMarkedAsPending is the Domain Event issued when an existing Item +// in a TodoList gets marked as "pending". type ItemMarkedAsPending struct { ID ItemID } +// Name implements message.Message. func (ItemMarkedAsPending) Name() string { return "TodoListItemMarkedAsPending" } +// ItemWasDeleted is the Domain Event issued when an existing Item +// gets deleted from a TodoList. type ItemWasDeleted struct { ID ItemID } +// Name implements message.Message. func (ItemWasDeleted) Name() string { return "TodoListItemWasDeleted" } diff --git a/examples/todolist/domain/todolist/item.go b/examples/todolist/domain/todolist/item.go index e55117ec..237dd03d 100644 --- a/examples/todolist/domain/todolist/item.go +++ b/examples/todolist/domain/todolist/item.go @@ -4,15 +4,19 @@ import ( "fmt" "time" + "github.com/google/uuid" + "github.com/get-eventually/go-eventually/core/aggregate" "github.com/get-eventually/go-eventually/core/event" - "github.com/google/uuid" ) +// ItemID is the unique identifier type for a Todo Item. type ItemID uuid.UUID func (id ItemID) String() string { return uuid.UUID(id).String() } +// Item represents a Todo Item. +// Items are managed by a TodoList aggregate root instance. type Item struct { aggregate.BaseRoot @@ -24,6 +28,7 @@ type Item struct { creationTime time.Time } +// Apply implements aggregate.Root. func (item *Item) Apply(event event.Event) error { switch evt := event.(type) { case ItemWasAdded: diff --git a/examples/todolist/domain/todolist/repository.go b/examples/todolist/domain/todolist/repository.go index da0574e6..96531717 100644 --- a/examples/todolist/domain/todolist/repository.go +++ b/examples/todolist/domain/todolist/repository.go @@ -3,7 +3,12 @@ package todolist import "github.com/get-eventually/go-eventually/core/aggregate" type ( - Getter = aggregate.Getter[ID, *TodoList] - Saver = aggregate.Saver[ID, *TodoList] + // Getter is a helper type for an aggregate.Getter interface for a TodoList. + Getter = aggregate.Getter[ID, *TodoList] + + // Saver is a helper type for an aggregate.Saver interface for a TodoList. + Saver = aggregate.Saver[ID, *TodoList] + + // Repository is a helper type for an aggregate.Repository interface for a TodoList. Repository = aggregate.Repository[ID, *TodoList] ) diff --git a/examples/todolist/domain/todolist/todolist.go b/examples/todolist/domain/todolist/todolist.go index c4438a7b..9e43aafe 100644 --- a/examples/todolist/domain/todolist/todolist.go +++ b/examples/todolist/domain/todolist/todolist.go @@ -1,3 +1,5 @@ +// Package todolist contains the domain types and implementations +// for the TodoList Aggregate Root. package todolist import ( @@ -5,20 +7,24 @@ import ( "fmt" "time" + "github.com/google/uuid" + "github.com/get-eventually/go-eventually/core/aggregate" "github.com/get-eventually/go-eventually/core/event" - "github.com/google/uuid" ) +// ID is the unique identifier for a TodoList. type ID uuid.UUID func (id ID) String() string { return uuid.UUID(id).String() } +// Type represents the Aggregate Root type for usage with go-eventually utilities. var Type = aggregate.Type[ID, *TodoList]{ Name: "TodoList", Factory: func() *TodoList { return new(TodoList) }, } +// TodoList is a list of different Todo items, that belongs to a specific owner. type TodoList struct { aggregate.BaseRoot @@ -29,7 +35,7 @@ type TodoList struct { items []*Item } -// AggregateID implements aggregate.Root +// AggregateID implements aggregate.Root. func (tl *TodoList) AggregateID() ID { return tl.id } @@ -57,7 +63,7 @@ func (tl *TodoList) applyItemEvent(id ItemID, evt event.Event) error { return nil } -// Apply implements aggregate.Root +// Apply implements aggregate.Root. func (tl *TodoList) Apply(event event.Event) error { switch evt := event.(type) { case WasCreated: @@ -71,6 +77,7 @@ func (tl *TodoList) Apply(event event.Event) error { if err := item.Apply(evt); err != nil { return fmt.Errorf("todolist.TodoList.Apply: failed to apply item event, %w", err) } + tl.items = append(tl.items, item) case ItemMarkedAsPending: @@ -98,6 +105,7 @@ func (tl *TodoList) Apply(event event.Event) error { return nil } +// Errors that can be returned by domain commands on a TodoList instance. var ( ErrEmptyID = errors.New("todolist.TodoList: empty id provided") ErrEmptyTitle = errors.New("todolist.TodoList: empty title provided") @@ -105,8 +113,13 @@ var ( ErrEmptyItemID = errors.New("todolist.TodoList: empty item id provided") ErrEmptyItemTitle = errors.New("todolist.TodoList: empty item title provided") ErrItemAlreadyExists = errors.New("todolist.TodoList: item already exists") + ErrItemNotFound = errors.New("todolist.TodoList: item was not found in list") ) +// Create creates a new TodoList. +// +// Both id, title and owner are required parameters: when empty, the function +// will return an error. func Create(id ID, title, owner string, now time.Time) (*TodoList, error) { wrapErr := func(err error) error { return fmt.Errorf("todolist.Create: failed to create new TodoList, %w", err) @@ -138,8 +151,8 @@ func Create(id ID, title, owner string, now time.Time) (*TodoList, error) { return &todoList, nil } -func (todoList *TodoList) itemByID(id ItemID) (*Item, bool) { - for _, item := range todoList.items { +func (tl *TodoList) itemByID(id ItemID) (*Item, bool) { + for _, item := range tl.items { if item.id == id { return item, true } @@ -148,7 +161,13 @@ func (todoList *TodoList) itemByID(id ItemID) (*Item, bool) { return nil, false } -func (todoList *TodoList) AddItem(id ItemID, title, description string, dueDate, now time.Time) error { +// AddItem adds a new Todo item to an existing list. +// +// Both id and title cannot be empty: if so, the method will return an error. +// +// Moreover, if the specified id is already being used by another Todo item, +// the method will return ErrItemAlreadyExists. +func (tl *TodoList) AddItem(id ItemID, title, description string, dueDate, now time.Time) error { wrapErr := func(err error) error { return fmt.Errorf("todolist.AddItem: failed to add new TodoItem to list, %w", err) } @@ -161,11 +180,11 @@ func (todoList *TodoList) AddItem(id ItemID, title, description string, dueDate, return wrapErr(ErrEmptyItemTitle) } - if _, ok := todoList.itemByID(id); ok { + if _, ok := tl.itemByID(id); ok { return wrapErr(ErrItemAlreadyExists) } - if err := aggregate.RecordThat[ID](todoList, event.ToEnvelope(ItemWasAdded{ + if err := aggregate.RecordThat[ID](tl, event.ToEnvelope(ItemWasAdded{ ID: id, Title: title, Description: description, @@ -177,3 +196,60 @@ func (todoList *TodoList) AddItem(id ItemID, title, description string, dueDate, return nil } + +func (tl *TodoList) recordItemEvent(id ItemID, eventFactory func() event.Envelope) error { + if uuid.UUID(id) == uuid.Nil { + return ErrEmptyItemID + } + + if _, ok := tl.itemByID(id); !ok { + return ErrItemNotFound + } + + return aggregate.RecordThat[ID](tl, eventFactory()) +} + +// MarkItemAsDone marks the Todo item with the specified id as "done". +// +// The method returns an error when the id is empty, or it doesn't point +// to an existing Todo item. +func (tl *TodoList) MarkItemAsDone(id ItemID) error { + err := tl.recordItemEvent(id, func() event.Envelope { + return event.ToEnvelope(ItemMarkedAsDone{ID: id}) + }) + if err != nil { + return fmt.Errorf("todolist.MarkItemAsDone: failed to mark item as done, %w", err) + } + + return nil +} + +// MarkItemAsPending marks the Todo item with the specified id as "pending". +// +// The method returns an error when the id is empty, or it doesn't point +// to an existing Todo item. +func (tl *TodoList) MarkItemAsPending(id ItemID) error { + err := tl.recordItemEvent(id, func() event.Envelope { + return event.ToEnvelope(ItemMarkedAsPending{ID: id}) + }) + if err != nil { + return fmt.Errorf("todolist.MarkItemAsPending: failed to mark item as pending, %w", err) + } + + return nil +} + +// DeleteItem deletes the Todo item with the specified id from the TodoList. +// +// The method returns an error when the id is empty, or it doesn't point +// to an existing Todo item. +func (tl *TodoList) DeleteItem(id ItemID) error { + err := tl.recordItemEvent(id, func() event.Envelope { + return event.ToEnvelope(ItemWasDeleted{ID: id}) + }) + if err != nil { + return fmt.Errorf("todolist.DeleteItem: failed to delete item, %w", err) + } + + return nil +} diff --git a/examples/todolist/domain/todolist/todolist_test.go b/examples/todolist/domain/todolist/todolist_test.go new file mode 100644 index 00000000..9d6c2797 --- /dev/null +++ b/examples/todolist/domain/todolist/todolist_test.go @@ -0,0 +1,57 @@ +package todolist_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/core/event" + "github.com/get-eventually/go-eventually/core/test/scenario" + "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" +) + +func TestTodoList(t *testing.T) { + t.Run("it works", func(t *testing.T) { + now := time.Now() + todoListID := todolist.ID(uuid.New()) + todoItemID := todolist.ItemID(uuid.New()) + + scenario.AggregateRoot(todolist.Type). + When(func() (*todolist.TodoList, error) { + tl, err := todolist.Create(todoListID, "test list", "me", now) + if err != nil { + return nil, err + } + + if err := tl.AddItem(todoItemID, "do something", "", time.Time{}, now); err != nil { + return nil, err + } + + if err := tl.MarkItemAsDone(todoItemID); err != nil { + return nil, err + } + + if err := tl.DeleteItem(todoItemID); err != nil { + return nil, err + } + + return tl, nil + }). + Then(4, event.ToEnvelope(todolist.WasCreated{ + ID: todoListID, + Title: "test list", + Owner: "me", + CreationTime: now, + }), event.ToEnvelope(todolist.ItemWasAdded{ + ID: todoItemID, + Title: "do something", + CreationTime: now, + }), event.ToEnvelope(todolist.ItemMarkedAsDone{ + ID: todoItemID, + }), event.ToEnvelope(todolist.ItemWasDeleted{ + ID: todoItemID, + })). + AssertOn(t) + }) +} From 584df1f09b2e25bd0fdc2289bfc82eeb857e98c2 Mon Sep 17 00:00:00 2001 From: Danilo Cianfrone Date: Thu, 16 Mar 2023 15:04:07 +0100 Subject: [PATCH 07/11] feat(core): add query package --- core/query/doc.go | 3 +++ core/query/query.go | 66 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 core/query/doc.go create mode 100644 core/query/query.go diff --git a/core/query/doc.go b/core/query/doc.go new file mode 100644 index 00000000..f7fd2c21 --- /dev/null +++ b/core/query/doc.go @@ -0,0 +1,3 @@ +// Package query contains types and interfaces for implementing Query Handlers, +// useful to request data or information to be exposed through an API. +package query diff --git a/core/query/query.go b/core/query/query.go new file mode 100644 index 00000000..716a1ba4 --- /dev/null +++ b/core/query/query.go @@ -0,0 +1,66 @@ +package query + +import ( + "context" + + "github.com/get-eventually/go-eventually/core/message" +) + +// Query is a specific kind of Message that represents the a request for information. +type Query message.Message + +// Envelope carries both a Query and some optional Metadata attached to it. +type Envelope[T Query] message.Envelope[T] + +// ToGenericEnvelope returns a GenericEnvelope version of the current Envelope instance. +func (cmd Envelope[T]) ToGenericEnvelope() GenericEnvelope { + return GenericEnvelope{ + Message: cmd.Message, + Metadata: cmd.Metadata, + } +} + +// Handler is the interface that defines a Query Handler, +// a component that receives a specific kind of Query and executes it to return +// the desired output. +type Handler[T Query, R any] interface { + Handle(ctx context.Context, query Envelope[T]) (R, error) +} + +// HandlerFunc is a functional type that implements the Handler interface. +// Useful for testing and stateless Handlers. +type HandlerFunc[T Query, R any] func(context.Context, Envelope[T]) (R, error) + +// Handle handles the provided Query through the functional Handler. +func (fn HandlerFunc[T, R]) Handle(ctx context.Context, cmd Envelope[T]) (R, error) { + return fn(ctx, cmd) +} + +// GenericEnvelope is a Query Envelope that depends solely on the Query interface, +// not a specific generic Query type. +type GenericEnvelope Envelope[Query] + +// FromGenericEnvelope attempts to type-cast a GenericEnvelope instance into +// a strongly-typed Query Envelope. +// +// A boolean guard is returned to signal whether the type-casting was successful +// or not. +func FromGenericEnvelope[T Query](cmd GenericEnvelope) (Envelope[T], bool) { + if v, ok := cmd.Message.(T); ok { + return Envelope[T]{ + Message: v, + Metadata: cmd.Metadata, + }, true + } + + return Envelope[T]{}, false +} + +// ToEnvelope is a convenience function that wraps the provided Query type +// into an Envelope, with no metadata attached to it. +func ToEnvelope[T Query](cmd T) Envelope[T] { + return Envelope[T]{ + Message: cmd, + Metadata: nil, + } +} From cf052c333adbab9b0b309ddbb587e576360026b0 Mon Sep 17 00:00:00 2001 From: Danilo Cianfrone Date: Thu, 16 Mar 2023 15:04:42 +0100 Subject: [PATCH 08/11] feat(examples/todolist): complete the example --- .golangci.yml | 2 +- examples/todolist/buf.gen.yaml | 14 + examples/todolist/config.go | 26 + .../todolist/gen/todolist/v1/todo_list.pb.go | 318 ++++++ .../gen/todolist/v1/todo_list_api.pb.go | 976 ++++++++++++++++++ .../todo_list_api.connect.go | 196 ++++ examples/todolist/go.mod | 13 + examples/todolist/go.sum | 33 +- .../command/add_todo_list_item.go | 2 +- .../command/add_todo_list_item_test.go | 4 +- .../{ => internal}/command/create_todolist.go | 2 +- .../command/create_todolist_test.go | 4 +- .../todolist/{ => internal}/command/doc.go | 0 .../{ => internal}/domain/todolist/event.go | 0 .../{ => internal}/domain/todolist/item.go | 28 +- .../domain/todolist/repository.go | 0 .../domain/todolist/todolist.go | 53 +- .../domain/todolist/todolist_test.go | 2 +- examples/todolist/internal/grpc/todolist.go | 90 ++ .../todolist/internal/protoconv/todolist.go | 35 + .../todolist/internal/query/get_todo_list.go | 39 + examples/todolist/main.go | 87 ++ examples/todolist/proto/buf.lock | 7 + examples/todolist/proto/buf.yaml | 12 + .../proto/todolist/v1/todo_list.proto | 22 + .../proto/todolist/v1/todo_list_api.proto | 88 ++ go.work | 1 + go.work.sum | 3 +- 28 files changed, 2001 insertions(+), 56 deletions(-) create mode 100644 examples/todolist/buf.gen.yaml create mode 100644 examples/todolist/config.go create mode 100644 examples/todolist/gen/todolist/v1/todo_list.pb.go create mode 100644 examples/todolist/gen/todolist/v1/todo_list_api.pb.go create mode 100644 examples/todolist/gen/todolist/v1/todolistv1connect/todo_list_api.connect.go rename examples/todolist/{ => internal}/command/add_todo_list_item.go (94%) rename examples/todolist/{ => internal}/command/add_todo_list_item_test.go (97%) rename examples/todolist/{ => internal}/command/create_todolist.go (92%) rename examples/todolist/{ => internal}/command/create_todolist_test.go (96%) rename examples/todolist/{ => internal}/command/doc.go (100%) rename examples/todolist/{ => internal}/domain/todolist/event.go (100%) rename examples/todolist/{ => internal}/domain/todolist/item.go (68%) rename examples/todolist/{ => internal}/domain/todolist/repository.go (100%) rename examples/todolist/{ => internal}/domain/todolist/todolist.go (91%) rename examples/todolist/{ => internal}/domain/todolist/todolist_test.go (94%) create mode 100644 examples/todolist/internal/grpc/todolist.go create mode 100644 examples/todolist/internal/protoconv/todolist.go create mode 100644 examples/todolist/internal/query/get_todo_list.go create mode 100644 examples/todolist/main.go create mode 100644 examples/todolist/proto/buf.lock create mode 100644 examples/todolist/proto/buf.yaml create mode 100644 examples/todolist/proto/todolist/v1/todo_list.proto create mode 100644 examples/todolist/proto/todolist/v1/todo_list_api.proto diff --git a/.golangci.yml b/.golangci.yml index 544bda16..e97545ba 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -40,7 +40,7 @@ linters-settings: govet: check-shadowing: true lll: - line-length: 120 + line-length: 160 misspell: locale: US nolintlint: diff --git a/examples/todolist/buf.gen.yaml b/examples/todolist/buf.gen.yaml new file mode 100644 index 00000000..6e86608e --- /dev/null +++ b/examples/todolist/buf.gen.yaml @@ -0,0 +1,14 @@ +version: v1 +managed: + enabled: true + go_package_prefix: + default: github.com/get-eventually/go-eventually/examples/todolist/gen + except: + - buf.build/googleapis/googleapis +plugins: + - plugin: buf.build/protocolbuffers/go + out: gen + opt: paths=source_relative + - plugin: buf.build/bufbuild/connect-go + out: gen + opt: paths=source_relative diff --git a/examples/todolist/config.go b/examples/todolist/config.go new file mode 100644 index 00000000..a85b945d --- /dev/null +++ b/examples/todolist/config.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "time" + + "github.com/kelseyhightower/envconfig" +) + +type Config struct { + Server struct { + Address string `default:":8080" required:"true"` + ReadTimeout time.Duration `default:"10s" required:"true"` + WriteTimeout time.Duration `default:"10s" required:"true"` + } +} + +func ParseConfig() (*Config, error) { + var config Config + + if err := envconfig.Process("", &config); err != nil { + return nil, fmt.Errorf("config: failed to parse from env, %v", err) + } + + return &config, nil +} diff --git a/examples/todolist/gen/todolist/v1/todo_list.pb.go b/examples/todolist/gen/todolist/v1/todo_list.pb.go new file mode 100644 index 00000000..c35738e8 --- /dev/null +++ b/examples/todolist/gen/todolist/v1/todo_list.pb.go @@ -0,0 +1,318 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.29.0 +// protoc (unknown) +// source: todolist/v1/todo_list.proto + +package todolistv1 + +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 TodoItem struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Completed bool `protobuf:"varint,4,opt,name=completed,proto3" json:"completed,omitempty"` + DueDate *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=due_date,json=dueDate,proto3" json:"due_date,omitempty"` + CreationTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=creation_time,json=creationTime,proto3" json:"creation_time,omitempty"` +} + +func (x *TodoItem) Reset() { + *x = TodoItem{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TodoItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TodoItem) ProtoMessage() {} + +func (x *TodoItem) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_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 TodoItem.ProtoReflect.Descriptor instead. +func (*TodoItem) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_proto_rawDescGZIP(), []int{0} +} + +func (x *TodoItem) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *TodoItem) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *TodoItem) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *TodoItem) GetCompleted() bool { + if x != nil { + return x.Completed + } + return false +} + +func (x *TodoItem) GetDueDate() *timestamppb.Timestamp { + if x != nil { + return x.DueDate + } + return nil +} + +func (x *TodoItem) GetCreationTime() *timestamppb.Timestamp { + if x != nil { + return x.CreationTime + } + return nil +} + +type TodoList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Owner string `protobuf:"bytes,3,opt,name=owner,proto3" json:"owner,omitempty"` + CreationTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=creation_time,json=creationTime,proto3" json:"creation_time,omitempty"` + Items []*TodoItem `protobuf:"bytes,5,rep,name=items,proto3" json:"items,omitempty"` +} + +func (x *TodoList) Reset() { + *x = TodoList{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TodoList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TodoList) ProtoMessage() {} + +func (x *TodoList) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_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 TodoList.ProtoReflect.Descriptor instead. +func (*TodoList) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_proto_rawDescGZIP(), []int{1} +} + +func (x *TodoList) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *TodoList) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *TodoList) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *TodoList) GetCreationTime() *timestamppb.Timestamp { + if x != nil { + return x.CreationTime + } + return nil +} + +func (x *TodoList) GetItems() []*TodoItem { + if x != nil { + return x.Items + } + return nil +} + +var File_todolist_v1_todo_list_proto protoreflect.FileDescriptor + +var file_todolist_v1_todo_list_proto_rawDesc = []byte{ + 0x0a, 0x1b, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, + 0x64, 0x6f, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x74, + 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 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, 0xe8, 0x01, 0x0a, 0x08, + 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x20, + 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x35, + 0x0a, 0x08, 0x64, 0x75, 0x65, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x05, 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, 0x07, 0x64, 0x75, + 0x65, 0x44, 0x61, 0x74, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 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, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x08, 0x54, 0x6f, 0x64, 0x6f, 0x4c, + 0x69, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, + 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x12, + 0x3f, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x18, 0x04, 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, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x2b, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x6f, + 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x42, 0xc3, 0x01, + 0x0a, 0x0f, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, + 0x31, 0x42, 0x0d, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x50, 0x01, 0x5a, 0x54, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, + 0x65, 0x74, 0x2d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x75, 0x61, 0x6c, 0x6c, 0x79, 0x2f, 0x67, 0x6f, + 0x2d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x75, 0x61, 0x6c, 0x6c, 0x79, 0x2f, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x67, 0x65, + 0x6e, 0x2f, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x74, 0x6f, + 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x54, 0x58, 0x58, 0xaa, 0x02, + 0x0b, 0x54, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0b, 0x54, + 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x17, 0x54, 0x6f, 0x64, + 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0c, 0x54, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x3a, + 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_todolist_v1_todo_list_proto_rawDescOnce sync.Once + file_todolist_v1_todo_list_proto_rawDescData = file_todolist_v1_todo_list_proto_rawDesc +) + +func file_todolist_v1_todo_list_proto_rawDescGZIP() []byte { + file_todolist_v1_todo_list_proto_rawDescOnce.Do(func() { + file_todolist_v1_todo_list_proto_rawDescData = protoimpl.X.CompressGZIP(file_todolist_v1_todo_list_proto_rawDescData) + }) + return file_todolist_v1_todo_list_proto_rawDescData +} + +var file_todolist_v1_todo_list_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_todolist_v1_todo_list_proto_goTypes = []interface{}{ + (*TodoItem)(nil), // 0: todolist.v1.TodoItem + (*TodoList)(nil), // 1: todolist.v1.TodoList + (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp +} +var file_todolist_v1_todo_list_proto_depIdxs = []int32{ + 2, // 0: todolist.v1.TodoItem.due_date:type_name -> google.protobuf.Timestamp + 2, // 1: todolist.v1.TodoItem.creation_time:type_name -> google.protobuf.Timestamp + 2, // 2: todolist.v1.TodoList.creation_time:type_name -> google.protobuf.Timestamp + 0, // 3: todolist.v1.TodoList.items:type_name -> todolist.v1.TodoItem + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_todolist_v1_todo_list_proto_init() } +func file_todolist_v1_todo_list_proto_init() { + if File_todolist_v1_todo_list_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_todolist_v1_todo_list_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TodoItem); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_todolist_v1_todo_list_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TodoList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_todolist_v1_todo_list_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_todolist_v1_todo_list_proto_goTypes, + DependencyIndexes: file_todolist_v1_todo_list_proto_depIdxs, + MessageInfos: file_todolist_v1_todo_list_proto_msgTypes, + }.Build() + File_todolist_v1_todo_list_proto = out.File + file_todolist_v1_todo_list_proto_rawDesc = nil + file_todolist_v1_todo_list_proto_goTypes = nil + file_todolist_v1_todo_list_proto_depIdxs = nil +} diff --git a/examples/todolist/gen/todolist/v1/todo_list_api.pb.go b/examples/todolist/gen/todolist/v1/todo_list_api.pb.go new file mode 100644 index 00000000..159d2f5b --- /dev/null +++ b/examples/todolist/gen/todolist/v1/todo_list_api.pb.go @@ -0,0 +1,976 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.29.0 +// protoc (unknown) +// source: todolist/v1/todo_list_api.proto + +package todolistv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + 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 CreateTodoListRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty"` +} + +func (x *CreateTodoListRequest) Reset() { + *x = CreateTodoListRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateTodoListRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTodoListRequest) ProtoMessage() {} + +func (x *CreateTodoListRequest) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_api_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 CreateTodoListRequest.ProtoReflect.Descriptor instead. +func (*CreateTodoListRequest) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_api_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateTodoListRequest) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *CreateTodoListRequest) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +type CreateTodoListResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TodoListId string `protobuf:"bytes,1,opt,name=todo_list_id,json=todoListId,proto3" json:"todo_list_id,omitempty"` +} + +func (x *CreateTodoListResponse) Reset() { + *x = CreateTodoListResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateTodoListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTodoListResponse) ProtoMessage() {} + +func (x *CreateTodoListResponse) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_api_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 CreateTodoListResponse.ProtoReflect.Descriptor instead. +func (*CreateTodoListResponse) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_api_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateTodoListResponse) GetTodoListId() string { + if x != nil { + return x.TodoListId + } + return "" +} + +type GetTodoListRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TodoListId string `protobuf:"bytes,1,opt,name=todo_list_id,json=todoListId,proto3" json:"todo_list_id,omitempty"` +} + +func (x *GetTodoListRequest) Reset() { + *x = GetTodoListRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetTodoListRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTodoListRequest) ProtoMessage() {} + +func (x *GetTodoListRequest) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_api_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 GetTodoListRequest.ProtoReflect.Descriptor instead. +func (*GetTodoListRequest) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_api_proto_rawDescGZIP(), []int{2} +} + +func (x *GetTodoListRequest) GetTodoListId() string { + if x != nil { + return x.TodoListId + } + return "" +} + +type GetTodoListResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TodoList *TodoList `protobuf:"bytes,1,opt,name=todo_list,json=todoList,proto3" json:"todo_list,omitempty"` +} + +func (x *GetTodoListResponse) Reset() { + *x = GetTodoListResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetTodoListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTodoListResponse) ProtoMessage() {} + +func (x *GetTodoListResponse) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[3] + 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 GetTodoListResponse.ProtoReflect.Descriptor instead. +func (*GetTodoListResponse) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_api_proto_rawDescGZIP(), []int{3} +} + +func (x *GetTodoListResponse) GetTodoList() *TodoList { + if x != nil { + return x.TodoList + } + return nil +} + +type AddTodoItemRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TodoListId string `protobuf:"bytes,1,opt,name=todo_list_id,json=todoListId,proto3" json:"todo_list_id,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + DueDate *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=due_date,json=dueDate,proto3" json:"due_date,omitempty"` +} + +func (x *AddTodoItemRequest) Reset() { + *x = AddTodoItemRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddTodoItemRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddTodoItemRequest) ProtoMessage() {} + +func (x *AddTodoItemRequest) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[4] + 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 AddTodoItemRequest.ProtoReflect.Descriptor instead. +func (*AddTodoItemRequest) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_api_proto_rawDescGZIP(), []int{4} +} + +func (x *AddTodoItemRequest) GetTodoListId() string { + if x != nil { + return x.TodoListId + } + return "" +} + +func (x *AddTodoItemRequest) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *AddTodoItemRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AddTodoItemRequest) GetDueDate() *timestamppb.Timestamp { + if x != nil { + return x.DueDate + } + return nil +} + +type AddTodoItemResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TodoItemId string `protobuf:"bytes,1,opt,name=todo_item_id,json=todoItemId,proto3" json:"todo_item_id,omitempty"` +} + +func (x *AddTodoItemResponse) Reset() { + *x = AddTodoItemResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddTodoItemResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddTodoItemResponse) ProtoMessage() {} + +func (x *AddTodoItemResponse) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[5] + 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 AddTodoItemResponse.ProtoReflect.Descriptor instead. +func (*AddTodoItemResponse) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_api_proto_rawDescGZIP(), []int{5} +} + +func (x *AddTodoItemResponse) GetTodoItemId() string { + if x != nil { + return x.TodoItemId + } + return "" +} + +type MarkTodoItemAsDoneRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TodoListId string `protobuf:"bytes,1,opt,name=todo_list_id,json=todoListId,proto3" json:"todo_list_id,omitempty"` + TodoItemId string `protobuf:"bytes,2,opt,name=todo_item_id,json=todoItemId,proto3" json:"todo_item_id,omitempty"` +} + +func (x *MarkTodoItemAsDoneRequest) Reset() { + *x = MarkTodoItemAsDoneRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MarkTodoItemAsDoneRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MarkTodoItemAsDoneRequest) ProtoMessage() {} + +func (x *MarkTodoItemAsDoneRequest) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[6] + 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 MarkTodoItemAsDoneRequest.ProtoReflect.Descriptor instead. +func (*MarkTodoItemAsDoneRequest) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_api_proto_rawDescGZIP(), []int{6} +} + +func (x *MarkTodoItemAsDoneRequest) GetTodoListId() string { + if x != nil { + return x.TodoListId + } + return "" +} + +func (x *MarkTodoItemAsDoneRequest) GetTodoItemId() string { + if x != nil { + return x.TodoItemId + } + return "" +} + +type MarkTodoItemAsDoneResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *MarkTodoItemAsDoneResponse) Reset() { + *x = MarkTodoItemAsDoneResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MarkTodoItemAsDoneResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MarkTodoItemAsDoneResponse) ProtoMessage() {} + +func (x *MarkTodoItemAsDoneResponse) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[7] + 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 MarkTodoItemAsDoneResponse.ProtoReflect.Descriptor instead. +func (*MarkTodoItemAsDoneResponse) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_api_proto_rawDescGZIP(), []int{7} +} + +type MarkTodoItemAsPendingRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TodoListId string `protobuf:"bytes,1,opt,name=todo_list_id,json=todoListId,proto3" json:"todo_list_id,omitempty"` + TodoItemId string `protobuf:"bytes,2,opt,name=todo_item_id,json=todoItemId,proto3" json:"todo_item_id,omitempty"` +} + +func (x *MarkTodoItemAsPendingRequest) Reset() { + *x = MarkTodoItemAsPendingRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MarkTodoItemAsPendingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MarkTodoItemAsPendingRequest) ProtoMessage() {} + +func (x *MarkTodoItemAsPendingRequest) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[8] + 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 MarkTodoItemAsPendingRequest.ProtoReflect.Descriptor instead. +func (*MarkTodoItemAsPendingRequest) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_api_proto_rawDescGZIP(), []int{8} +} + +func (x *MarkTodoItemAsPendingRequest) GetTodoListId() string { + if x != nil { + return x.TodoListId + } + return "" +} + +func (x *MarkTodoItemAsPendingRequest) GetTodoItemId() string { + if x != nil { + return x.TodoItemId + } + return "" +} + +type MarkTodoItemAsPendingResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *MarkTodoItemAsPendingResponse) Reset() { + *x = MarkTodoItemAsPendingResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MarkTodoItemAsPendingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MarkTodoItemAsPendingResponse) ProtoMessage() {} + +func (x *MarkTodoItemAsPendingResponse) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[9] + 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 MarkTodoItemAsPendingResponse.ProtoReflect.Descriptor instead. +func (*MarkTodoItemAsPendingResponse) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_api_proto_rawDescGZIP(), []int{9} +} + +type DeleteTodoItemRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TodoListId string `protobuf:"bytes,1,opt,name=todo_list_id,json=todoListId,proto3" json:"todo_list_id,omitempty"` + TodoItemId string `protobuf:"bytes,2,opt,name=todo_item_id,json=todoItemId,proto3" json:"todo_item_id,omitempty"` +} + +func (x *DeleteTodoItemRequest) Reset() { + *x = DeleteTodoItemRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteTodoItemRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteTodoItemRequest) ProtoMessage() {} + +func (x *DeleteTodoItemRequest) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[10] + 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 DeleteTodoItemRequest.ProtoReflect.Descriptor instead. +func (*DeleteTodoItemRequest) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_api_proto_rawDescGZIP(), []int{10} +} + +func (x *DeleteTodoItemRequest) GetTodoListId() string { + if x != nil { + return x.TodoListId + } + return "" +} + +func (x *DeleteTodoItemRequest) GetTodoItemId() string { + if x != nil { + return x.TodoItemId + } + return "" +} + +type DeleteTodoItemResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *DeleteTodoItemResponse) Reset() { + *x = DeleteTodoItemResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteTodoItemResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteTodoItemResponse) ProtoMessage() {} + +func (x *DeleteTodoItemResponse) ProtoReflect() protoreflect.Message { + mi := &file_todolist_v1_todo_list_api_proto_msgTypes[11] + 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 DeleteTodoItemResponse.ProtoReflect.Descriptor instead. +func (*DeleteTodoItemResponse) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_api_proto_rawDescGZIP(), []int{11} +} + +var File_todolist_v1_todo_list_api_proto protoreflect.FileDescriptor + +var file_todolist_v1_todo_list_api_proto_rawDesc = []byte{ + 0x0a, 0x1f, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, + 0x64, 0x6f, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x0b, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x1a, 0x1c, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 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, 0x1a, 0x1b, 0x74, + 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, 0x64, 0x6f, 0x5f, + 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x43, 0x0a, 0x15, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, + 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x22, + 0x3a, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x64, + 0x6f, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x74, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x64, 0x22, 0x36, 0x0a, 0x12, 0x47, + 0x65, 0x74, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, + 0x74, 0x49, 0x64, 0x22, 0x49, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x6f, + 0x64, 0x6f, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x6f, 0x64, 0x6f, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x08, 0x74, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x22, 0xa5, + 0x01, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x6c, 0x69, + 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x64, + 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x20, 0x0a, + 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x35, 0x0a, 0x08, 0x64, 0x75, 0x65, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 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, 0x07, 0x64, + 0x75, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x37, 0x0a, 0x13, 0x41, 0x64, 0x64, 0x54, 0x6f, 0x64, + 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, + 0x0c, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x64, 0x22, + 0x5f, 0x0a, 0x19, 0x4d, 0x61, 0x72, 0x6b, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x41, + 0x73, 0x44, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0c, + 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x64, 0x12, 0x20, + 0x0a, 0x0c, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x64, + 0x22, 0x1c, 0x0a, 0x1a, 0x4d, 0x61, 0x72, 0x6b, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, + 0x41, 0x73, 0x44, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x62, + 0x0a, 0x1c, 0x4d, 0x61, 0x72, 0x6b, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x73, + 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, + 0x0a, 0x0c, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x64, + 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, + 0x49, 0x64, 0x22, 0x1f, 0x0a, 0x1d, 0x4d, 0x61, 0x72, 0x6b, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, + 0x65, 0x6d, 0x41, 0x73, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x5b, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x6f, 0x64, + 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0c, + 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x64, 0x12, 0x20, + 0x0a, 0x0c, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x64, + 0x22, 0x18, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, + 0x65, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xff, 0x06, 0x0a, 0x0f, 0x54, + 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, + 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, + 0x12, 0x22, 0x2e, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, 0x64, 0x6f, 0x4c, 0x69, + 0x73, 0x74, 0x73, 0x12, 0x76, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, + 0x73, 0x74, 0x12, 0x1f, 0x2e, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x12, 0x1c, 0x2f, + 0x76, 0x31, 0x2f, 0x74, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x2f, 0x7b, 0x74, 0x6f, + 0x64, 0x6f, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x7f, 0x0a, 0x0b, 0x41, + 0x64, 0x64, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x1f, 0x2e, 0x74, 0x6f, 0x64, + 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x54, 0x6f, 0x64, 0x6f, + 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x6f, + 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x54, 0x6f, 0x64, + 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2d, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x27, 0x3a, 0x01, 0x2a, 0x22, 0x22, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, + 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x2f, 0x7b, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x6c, 0x69, + 0x73, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0xac, 0x01, 0x0a, + 0x12, 0x4d, 0x61, 0x72, 0x6b, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x73, 0x44, + 0x6f, 0x6e, 0x65, 0x12, 0x26, 0x2e, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x73, + 0x44, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x6f, + 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x54, 0x6f, + 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x73, 0x44, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x45, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3f, 0x22, 0x3d, 0x2f, 0x76, + 0x31, 0x2f, 0x74, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x2f, 0x7b, 0x74, 0x6f, 0x64, 0x6f, + 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x2f, + 0x7b, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x6d, + 0x61, 0x72, 0x6b, 0x2d, 0x61, 0x73, 0x2d, 0x64, 0x6f, 0x6e, 0x65, 0x12, 0xb8, 0x01, 0x0a, 0x15, + 0x4d, 0x61, 0x72, 0x6b, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x73, 0x50, 0x65, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x29, 0x2e, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, + 0x41, 0x73, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2a, 0x2e, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, + 0x61, 0x72, 0x6b, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x73, 0x50, 0x65, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x48, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x42, 0x22, 0x40, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, 0x64, 0x6f, 0x4c, 0x69, + 0x73, 0x74, 0x2f, 0x7b, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x69, 0x64, + 0x7d, 0x2f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x2f, 0x7b, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x69, 0x74, + 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x6d, 0x61, 0x72, 0x6b, 0x2d, 0x61, 0x73, 0x2d, 0x70, + 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x93, 0x01, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x22, 0x2e, 0x74, 0x6f, 0x64, 0x6f, + 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x6f, + 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, + 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x54, 0x6f, 0x64, 0x6f, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x38, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x32, 0x2a, 0x30, 0x2f, 0x76, 0x31, 0x2f, + 0x74, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x2f, 0x7b, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x6c, + 0x69, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x2f, 0x7b, 0x74, + 0x6f, 0x64, 0x6f, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x7d, 0x42, 0xc6, 0x01, 0x0a, + 0x0f, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, + 0x42, 0x10, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x54, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x67, 0x65, 0x74, 0x2d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x75, 0x61, 0x6c, 0x6c, 0x79, 0x2f, + 0x67, 0x6f, 0x2d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x75, 0x61, 0x6c, 0x6c, 0x79, 0x2f, 0x65, 0x78, + 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x76, 0x31, 0x3b, + 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x54, 0x58, 0x58, + 0xaa, 0x02, 0x0b, 0x54, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x56, 0x31, 0xca, 0x02, + 0x0b, 0x54, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x17, 0x54, + 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0c, 0x54, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, + 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_todolist_v1_todo_list_api_proto_rawDescOnce sync.Once + file_todolist_v1_todo_list_api_proto_rawDescData = file_todolist_v1_todo_list_api_proto_rawDesc +) + +func file_todolist_v1_todo_list_api_proto_rawDescGZIP() []byte { + file_todolist_v1_todo_list_api_proto_rawDescOnce.Do(func() { + file_todolist_v1_todo_list_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_todolist_v1_todo_list_api_proto_rawDescData) + }) + return file_todolist_v1_todo_list_api_proto_rawDescData +} + +var file_todolist_v1_todo_list_api_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_todolist_v1_todo_list_api_proto_goTypes = []interface{}{ + (*CreateTodoListRequest)(nil), // 0: todolist.v1.CreateTodoListRequest + (*CreateTodoListResponse)(nil), // 1: todolist.v1.CreateTodoListResponse + (*GetTodoListRequest)(nil), // 2: todolist.v1.GetTodoListRequest + (*GetTodoListResponse)(nil), // 3: todolist.v1.GetTodoListResponse + (*AddTodoItemRequest)(nil), // 4: todolist.v1.AddTodoItemRequest + (*AddTodoItemResponse)(nil), // 5: todolist.v1.AddTodoItemResponse + (*MarkTodoItemAsDoneRequest)(nil), // 6: todolist.v1.MarkTodoItemAsDoneRequest + (*MarkTodoItemAsDoneResponse)(nil), // 7: todolist.v1.MarkTodoItemAsDoneResponse + (*MarkTodoItemAsPendingRequest)(nil), // 8: todolist.v1.MarkTodoItemAsPendingRequest + (*MarkTodoItemAsPendingResponse)(nil), // 9: todolist.v1.MarkTodoItemAsPendingResponse + (*DeleteTodoItemRequest)(nil), // 10: todolist.v1.DeleteTodoItemRequest + (*DeleteTodoItemResponse)(nil), // 11: todolist.v1.DeleteTodoItemResponse + (*TodoList)(nil), // 12: todolist.v1.TodoList + (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp +} +var file_todolist_v1_todo_list_api_proto_depIdxs = []int32{ + 12, // 0: todolist.v1.GetTodoListResponse.todo_list:type_name -> todolist.v1.TodoList + 13, // 1: todolist.v1.AddTodoItemRequest.due_date:type_name -> google.protobuf.Timestamp + 0, // 2: todolist.v1.TodoListService.CreateTodoList:input_type -> todolist.v1.CreateTodoListRequest + 2, // 3: todolist.v1.TodoListService.GetTodoList:input_type -> todolist.v1.GetTodoListRequest + 4, // 4: todolist.v1.TodoListService.AddTodoItem:input_type -> todolist.v1.AddTodoItemRequest + 6, // 5: todolist.v1.TodoListService.MarkTodoItemAsDone:input_type -> todolist.v1.MarkTodoItemAsDoneRequest + 8, // 6: todolist.v1.TodoListService.MarkTodoItemAsPending:input_type -> todolist.v1.MarkTodoItemAsPendingRequest + 10, // 7: todolist.v1.TodoListService.DeleteTodoItem:input_type -> todolist.v1.DeleteTodoItemRequest + 1, // 8: todolist.v1.TodoListService.CreateTodoList:output_type -> todolist.v1.CreateTodoListResponse + 3, // 9: todolist.v1.TodoListService.GetTodoList:output_type -> todolist.v1.GetTodoListResponse + 5, // 10: todolist.v1.TodoListService.AddTodoItem:output_type -> todolist.v1.AddTodoItemResponse + 7, // 11: todolist.v1.TodoListService.MarkTodoItemAsDone:output_type -> todolist.v1.MarkTodoItemAsDoneResponse + 9, // 12: todolist.v1.TodoListService.MarkTodoItemAsPending:output_type -> todolist.v1.MarkTodoItemAsPendingResponse + 11, // 13: todolist.v1.TodoListService.DeleteTodoItem:output_type -> todolist.v1.DeleteTodoItemResponse + 8, // [8:14] is the sub-list for method output_type + 2, // [2:8] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_todolist_v1_todo_list_api_proto_init() } +func file_todolist_v1_todo_list_api_proto_init() { + if File_todolist_v1_todo_list_api_proto != nil { + return + } + file_todolist_v1_todo_list_proto_init() + if !protoimpl.UnsafeEnabled { + file_todolist_v1_todo_list_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateTodoListRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_todolist_v1_todo_list_api_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateTodoListResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_todolist_v1_todo_list_api_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetTodoListRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_todolist_v1_todo_list_api_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetTodoListResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_todolist_v1_todo_list_api_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddTodoItemRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_todolist_v1_todo_list_api_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddTodoItemResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_todolist_v1_todo_list_api_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MarkTodoItemAsDoneRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_todolist_v1_todo_list_api_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MarkTodoItemAsDoneResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_todolist_v1_todo_list_api_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MarkTodoItemAsPendingRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_todolist_v1_todo_list_api_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MarkTodoItemAsPendingResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_todolist_v1_todo_list_api_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteTodoItemRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_todolist_v1_todo_list_api_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteTodoItemResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_todolist_v1_todo_list_api_proto_rawDesc, + NumEnums: 0, + NumMessages: 12, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_todolist_v1_todo_list_api_proto_goTypes, + DependencyIndexes: file_todolist_v1_todo_list_api_proto_depIdxs, + MessageInfos: file_todolist_v1_todo_list_api_proto_msgTypes, + }.Build() + File_todolist_v1_todo_list_api_proto = out.File + file_todolist_v1_todo_list_api_proto_rawDesc = nil + file_todolist_v1_todo_list_api_proto_goTypes = nil + file_todolist_v1_todo_list_api_proto_depIdxs = nil +} diff --git a/examples/todolist/gen/todolist/v1/todolistv1connect/todo_list_api.connect.go b/examples/todolist/gen/todolist/v1/todolistv1connect/todo_list_api.connect.go new file mode 100644 index 00000000..e1c7e26c --- /dev/null +++ b/examples/todolist/gen/todolist/v1/todolistv1connect/todo_list_api.connect.go @@ -0,0 +1,196 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: todolist/v1/todo_list_api.proto + +package todolistv1connect + +import ( + context "context" + errors "errors" + connect_go "github.com/bufbuild/connect-go" + v1 "github.com/get-eventually/go-eventually/examples/todolist/gen/todolist/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect_go.IsAtLeastVersion0_1_0 + +const ( + // TodoListServiceName is the fully-qualified name of the TodoListService service. + TodoListServiceName = "todolist.v1.TodoListService" +) + +// TodoListServiceClient is a client for the todolist.v1.TodoListService service. +type TodoListServiceClient interface { + CreateTodoList(context.Context, *connect_go.Request[v1.CreateTodoListRequest]) (*connect_go.Response[v1.CreateTodoListResponse], error) + GetTodoList(context.Context, *connect_go.Request[v1.GetTodoListRequest]) (*connect_go.Response[v1.GetTodoListResponse], error) + AddTodoItem(context.Context, *connect_go.Request[v1.AddTodoItemRequest]) (*connect_go.Response[v1.AddTodoItemResponse], error) + MarkTodoItemAsDone(context.Context, *connect_go.Request[v1.MarkTodoItemAsDoneRequest]) (*connect_go.Response[v1.MarkTodoItemAsDoneResponse], error) + MarkTodoItemAsPending(context.Context, *connect_go.Request[v1.MarkTodoItemAsPendingRequest]) (*connect_go.Response[v1.MarkTodoItemAsPendingResponse], error) + DeleteTodoItem(context.Context, *connect_go.Request[v1.DeleteTodoItemRequest]) (*connect_go.Response[v1.DeleteTodoItemResponse], error) +} + +// NewTodoListServiceClient constructs a client for the todolist.v1.TodoListService service. By +// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, +// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewTodoListServiceClient(httpClient connect_go.HTTPClient, baseURL string, opts ...connect_go.ClientOption) TodoListServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &todoListServiceClient{ + createTodoList: connect_go.NewClient[v1.CreateTodoListRequest, v1.CreateTodoListResponse]( + httpClient, + baseURL+"/todolist.v1.TodoListService/CreateTodoList", + opts..., + ), + getTodoList: connect_go.NewClient[v1.GetTodoListRequest, v1.GetTodoListResponse]( + httpClient, + baseURL+"/todolist.v1.TodoListService/GetTodoList", + opts..., + ), + addTodoItem: connect_go.NewClient[v1.AddTodoItemRequest, v1.AddTodoItemResponse]( + httpClient, + baseURL+"/todolist.v1.TodoListService/AddTodoItem", + opts..., + ), + markTodoItemAsDone: connect_go.NewClient[v1.MarkTodoItemAsDoneRequest, v1.MarkTodoItemAsDoneResponse]( + httpClient, + baseURL+"/todolist.v1.TodoListService/MarkTodoItemAsDone", + opts..., + ), + markTodoItemAsPending: connect_go.NewClient[v1.MarkTodoItemAsPendingRequest, v1.MarkTodoItemAsPendingResponse]( + httpClient, + baseURL+"/todolist.v1.TodoListService/MarkTodoItemAsPending", + opts..., + ), + deleteTodoItem: connect_go.NewClient[v1.DeleteTodoItemRequest, v1.DeleteTodoItemResponse]( + httpClient, + baseURL+"/todolist.v1.TodoListService/DeleteTodoItem", + opts..., + ), + } +} + +// todoListServiceClient implements TodoListServiceClient. +type todoListServiceClient struct { + createTodoList *connect_go.Client[v1.CreateTodoListRequest, v1.CreateTodoListResponse] + getTodoList *connect_go.Client[v1.GetTodoListRequest, v1.GetTodoListResponse] + addTodoItem *connect_go.Client[v1.AddTodoItemRequest, v1.AddTodoItemResponse] + markTodoItemAsDone *connect_go.Client[v1.MarkTodoItemAsDoneRequest, v1.MarkTodoItemAsDoneResponse] + markTodoItemAsPending *connect_go.Client[v1.MarkTodoItemAsPendingRequest, v1.MarkTodoItemAsPendingResponse] + deleteTodoItem *connect_go.Client[v1.DeleteTodoItemRequest, v1.DeleteTodoItemResponse] +} + +// CreateTodoList calls todolist.v1.TodoListService.CreateTodoList. +func (c *todoListServiceClient) CreateTodoList(ctx context.Context, req *connect_go.Request[v1.CreateTodoListRequest]) (*connect_go.Response[v1.CreateTodoListResponse], error) { + return c.createTodoList.CallUnary(ctx, req) +} + +// GetTodoList calls todolist.v1.TodoListService.GetTodoList. +func (c *todoListServiceClient) GetTodoList(ctx context.Context, req *connect_go.Request[v1.GetTodoListRequest]) (*connect_go.Response[v1.GetTodoListResponse], error) { + return c.getTodoList.CallUnary(ctx, req) +} + +// AddTodoItem calls todolist.v1.TodoListService.AddTodoItem. +func (c *todoListServiceClient) AddTodoItem(ctx context.Context, req *connect_go.Request[v1.AddTodoItemRequest]) (*connect_go.Response[v1.AddTodoItemResponse], error) { + return c.addTodoItem.CallUnary(ctx, req) +} + +// MarkTodoItemAsDone calls todolist.v1.TodoListService.MarkTodoItemAsDone. +func (c *todoListServiceClient) MarkTodoItemAsDone(ctx context.Context, req *connect_go.Request[v1.MarkTodoItemAsDoneRequest]) (*connect_go.Response[v1.MarkTodoItemAsDoneResponse], error) { + return c.markTodoItemAsDone.CallUnary(ctx, req) +} + +// MarkTodoItemAsPending calls todolist.v1.TodoListService.MarkTodoItemAsPending. +func (c *todoListServiceClient) MarkTodoItemAsPending(ctx context.Context, req *connect_go.Request[v1.MarkTodoItemAsPendingRequest]) (*connect_go.Response[v1.MarkTodoItemAsPendingResponse], error) { + return c.markTodoItemAsPending.CallUnary(ctx, req) +} + +// DeleteTodoItem calls todolist.v1.TodoListService.DeleteTodoItem. +func (c *todoListServiceClient) DeleteTodoItem(ctx context.Context, req *connect_go.Request[v1.DeleteTodoItemRequest]) (*connect_go.Response[v1.DeleteTodoItemResponse], error) { + return c.deleteTodoItem.CallUnary(ctx, req) +} + +// TodoListServiceHandler is an implementation of the todolist.v1.TodoListService service. +type TodoListServiceHandler interface { + CreateTodoList(context.Context, *connect_go.Request[v1.CreateTodoListRequest]) (*connect_go.Response[v1.CreateTodoListResponse], error) + GetTodoList(context.Context, *connect_go.Request[v1.GetTodoListRequest]) (*connect_go.Response[v1.GetTodoListResponse], error) + AddTodoItem(context.Context, *connect_go.Request[v1.AddTodoItemRequest]) (*connect_go.Response[v1.AddTodoItemResponse], error) + MarkTodoItemAsDone(context.Context, *connect_go.Request[v1.MarkTodoItemAsDoneRequest]) (*connect_go.Response[v1.MarkTodoItemAsDoneResponse], error) + MarkTodoItemAsPending(context.Context, *connect_go.Request[v1.MarkTodoItemAsPendingRequest]) (*connect_go.Response[v1.MarkTodoItemAsPendingResponse], error) + DeleteTodoItem(context.Context, *connect_go.Request[v1.DeleteTodoItemRequest]) (*connect_go.Response[v1.DeleteTodoItemResponse], error) +} + +// NewTodoListServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewTodoListServiceHandler(svc TodoListServiceHandler, opts ...connect_go.HandlerOption) (string, http.Handler) { + mux := http.NewServeMux() + mux.Handle("/todolist.v1.TodoListService/CreateTodoList", connect_go.NewUnaryHandler( + "/todolist.v1.TodoListService/CreateTodoList", + svc.CreateTodoList, + opts..., + )) + mux.Handle("/todolist.v1.TodoListService/GetTodoList", connect_go.NewUnaryHandler( + "/todolist.v1.TodoListService/GetTodoList", + svc.GetTodoList, + opts..., + )) + mux.Handle("/todolist.v1.TodoListService/AddTodoItem", connect_go.NewUnaryHandler( + "/todolist.v1.TodoListService/AddTodoItem", + svc.AddTodoItem, + opts..., + )) + mux.Handle("/todolist.v1.TodoListService/MarkTodoItemAsDone", connect_go.NewUnaryHandler( + "/todolist.v1.TodoListService/MarkTodoItemAsDone", + svc.MarkTodoItemAsDone, + opts..., + )) + mux.Handle("/todolist.v1.TodoListService/MarkTodoItemAsPending", connect_go.NewUnaryHandler( + "/todolist.v1.TodoListService/MarkTodoItemAsPending", + svc.MarkTodoItemAsPending, + opts..., + )) + mux.Handle("/todolist.v1.TodoListService/DeleteTodoItem", connect_go.NewUnaryHandler( + "/todolist.v1.TodoListService/DeleteTodoItem", + svc.DeleteTodoItem, + opts..., + )) + return "/todolist.v1.TodoListService/", mux +} + +// UnimplementedTodoListServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedTodoListServiceHandler struct{} + +func (UnimplementedTodoListServiceHandler) CreateTodoList(context.Context, *connect_go.Request[v1.CreateTodoListRequest]) (*connect_go.Response[v1.CreateTodoListResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("todolist.v1.TodoListService.CreateTodoList is not implemented")) +} + +func (UnimplementedTodoListServiceHandler) GetTodoList(context.Context, *connect_go.Request[v1.GetTodoListRequest]) (*connect_go.Response[v1.GetTodoListResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("todolist.v1.TodoListService.GetTodoList is not implemented")) +} + +func (UnimplementedTodoListServiceHandler) AddTodoItem(context.Context, *connect_go.Request[v1.AddTodoItemRequest]) (*connect_go.Response[v1.AddTodoItemResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("todolist.v1.TodoListService.AddTodoItem is not implemented")) +} + +func (UnimplementedTodoListServiceHandler) MarkTodoItemAsDone(context.Context, *connect_go.Request[v1.MarkTodoItemAsDoneRequest]) (*connect_go.Response[v1.MarkTodoItemAsDoneResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("todolist.v1.TodoListService.MarkTodoItemAsDone is not implemented")) +} + +func (UnimplementedTodoListServiceHandler) MarkTodoItemAsPending(context.Context, *connect_go.Request[v1.MarkTodoItemAsPendingRequest]) (*connect_go.Response[v1.MarkTodoItemAsPendingResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("todolist.v1.TodoListService.MarkTodoItemAsPending is not implemented")) +} + +func (UnimplementedTodoListServiceHandler) DeleteTodoItem(context.Context, *connect_go.Request[v1.DeleteTodoItemRequest]) (*connect_go.Response[v1.DeleteTodoItemResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("todolist.v1.TodoListService.DeleteTodoItem is not implemented")) +} diff --git a/examples/todolist/go.mod b/examples/todolist/go.mod index 6fea1323..8843a8ad 100644 --- a/examples/todolist/go.mod +++ b/examples/todolist/go.mod @@ -3,8 +3,16 @@ module github.com/get-eventually/go-eventually/examples/todolist go 1.18 require ( + github.com/bufbuild/connect-go v1.5.2 + github.com/bufbuild/connect-grpchealth-go v1.0.0 + github.com/bufbuild/connect-grpcreflect-go v1.0.0 github.com/get-eventually/go-eventually/core v0.0.0-20230301093954-efadfc924ad7 github.com/google/uuid v1.3.0 + github.com/kelseyhightower/envconfig v1.4.0 + go.uber.org/zap v1.24.0 + golang.org/x/net v0.7.0 + google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 + google.golang.org/protobuf v1.28.1 ) require ( @@ -13,6 +21,11 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/stretchr/testify v1.8.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect golang.org/x/sync v0.1.0 // indirect + golang.org/x/text v0.7.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/get-eventually/go-eventually/core => ../../core diff --git a/examples/todolist/go.sum b/examples/todolist/go.sum index aadcb8a4..ef953c1a 100644 --- a/examples/todolist/go.sum +++ b/examples/todolist/go.sum @@ -1,14 +1,25 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/bufbuild/connect-go v1.5.2 h1:G4EZd5gF1U1ZhhbVJXplbuUnfKpBZ5j5izqIwu2g2W8= +github.com/bufbuild/connect-go v1.5.2/go.mod h1:GmMJYR6orFqD0Y6ZgX8pwQ8j9baizDrIQMm1/a6LnHk= +github.com/bufbuild/connect-grpchealth-go v1.0.0 h1:33v883tL86jLomQT6R2ZYVYaI2cRkuUXvU30WfbQ/ko= +github.com/bufbuild/connect-grpchealth-go v1.0.0/go.mod h1:6OEb4J3rh5+Wdvt4/muOIfZo1lt9cPU8ggwpsjBaZ3Y= +github.com/bufbuild/connect-grpcreflect-go v1.0.0 h1:zWsLFYqrT1O2sNJFYfTXI5WxbAyiY2dvevvnJHPtV5A= +github.com/bufbuild/connect-grpcreflect-go v1.0.0/go.mod h1:825I20H8bfE9rLnBH/046JSpmm3uwpNYdG4duCARetc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/get-eventually/go-eventually/core v0.0.0-20230301093954-efadfc924ad7 h1:9Er2kSmSC1K9L1lESfPy6hBF6eiz6HBYJ+0rmiI8Liw= -github.com/get-eventually/go-eventually/core v0.0.0-20230301093954-efadfc924ad7/go.mod h1:3KiUK1ntGvBCcttLtFeNhdf83XkaUscdrGVJcvEISFY= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -16,12 +27,30 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 h1:znp6mq/drrY+6khTAlJUDNFFcDGV2ENLYKpMq8SyCds= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/todolist/command/add_todo_list_item.go b/examples/todolist/internal/command/add_todo_list_item.go similarity index 94% rename from examples/todolist/command/add_todo_list_item.go rename to examples/todolist/internal/command/add_todo_list_item.go index 7a8b3959..78a358c2 100644 --- a/examples/todolist/command/add_todo_list_item.go +++ b/examples/todolist/internal/command/add_todo_list_item.go @@ -6,7 +6,7 @@ import ( "time" "github.com/get-eventually/go-eventually/core/command" - "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" + "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" ) // AddTodoListItem the Command used to add a new Item to an existing TodoList. diff --git a/examples/todolist/command/add_todo_list_item_test.go b/examples/todolist/internal/command/add_todo_list_item_test.go similarity index 97% rename from examples/todolist/command/add_todo_list_item_test.go rename to examples/todolist/internal/command/add_todo_list_item_test.go index 320baaf9..4c3e58a4 100644 --- a/examples/todolist/command/add_todo_list_item_test.go +++ b/examples/todolist/internal/command/add_todo_list_item_test.go @@ -10,8 +10,8 @@ import ( "github.com/get-eventually/go-eventually/core/command" "github.com/get-eventually/go-eventually/core/event" "github.com/get-eventually/go-eventually/core/test/scenario" - appcommand "github.com/get-eventually/go-eventually/examples/todolist/command" - "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" + appcommand "github.com/get-eventually/go-eventually/examples/todolist/internal/command" + "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" ) func TestAddTodoListItem(t *testing.T) { diff --git a/examples/todolist/command/create_todolist.go b/examples/todolist/internal/command/create_todolist.go similarity index 92% rename from examples/todolist/command/create_todolist.go rename to examples/todolist/internal/command/create_todolist.go index 560f69e0..cd1efc2c 100644 --- a/examples/todolist/command/create_todolist.go +++ b/examples/todolist/internal/command/create_todolist.go @@ -6,7 +6,7 @@ import ( "time" "github.com/get-eventually/go-eventually/core/command" - "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" + "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" ) // CreateTodoList is the Command used to create a new TodoList. diff --git a/examples/todolist/command/create_todolist_test.go b/examples/todolist/internal/command/create_todolist_test.go similarity index 96% rename from examples/todolist/command/create_todolist_test.go rename to examples/todolist/internal/command/create_todolist_test.go index faab689d..b2858edc 100644 --- a/examples/todolist/command/create_todolist_test.go +++ b/examples/todolist/internal/command/create_todolist_test.go @@ -11,8 +11,8 @@ import ( "github.com/get-eventually/go-eventually/core/event" "github.com/get-eventually/go-eventually/core/test/scenario" "github.com/get-eventually/go-eventually/core/version" - appcommand "github.com/get-eventually/go-eventually/examples/todolist/command" - "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" + appcommand "github.com/get-eventually/go-eventually/examples/todolist/internal/command" + "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" ) func TestCreateTodoListHandler(t *testing.T) { diff --git a/examples/todolist/command/doc.go b/examples/todolist/internal/command/doc.go similarity index 100% rename from examples/todolist/command/doc.go rename to examples/todolist/internal/command/doc.go diff --git a/examples/todolist/domain/todolist/event.go b/examples/todolist/internal/domain/todolist/event.go similarity index 100% rename from examples/todolist/domain/todolist/event.go rename to examples/todolist/internal/domain/todolist/event.go diff --git a/examples/todolist/domain/todolist/item.go b/examples/todolist/internal/domain/todolist/item.go similarity index 68% rename from examples/todolist/domain/todolist/item.go rename to examples/todolist/internal/domain/todolist/item.go index 237dd03d..64ee4510 100644 --- a/examples/todolist/domain/todolist/item.go +++ b/examples/todolist/internal/domain/todolist/item.go @@ -20,30 +20,30 @@ func (id ItemID) String() string { return uuid.UUID(id).String() } type Item struct { aggregate.BaseRoot - id ItemID - title string - description string - completed bool - dueDate time.Time - creationTime time.Time + ID ItemID + Title string + Description string + Completed bool + DueDate time.Time + CreationTime time.Time } // Apply implements aggregate.Root. func (item *Item) Apply(event event.Event) error { switch evt := event.(type) { case ItemWasAdded: - item.id = evt.ID - item.title = evt.Title - item.description = evt.Description - item.completed = false - item.dueDate = evt.DueDate - item.creationTime = evt.CreationTime + item.ID = evt.ID + item.Title = evt.Title + item.Description = evt.Description + item.Completed = false + item.DueDate = evt.DueDate + item.CreationTime = evt.CreationTime case ItemMarkedAsDone: - item.completed = true + item.Completed = true case ItemMarkedAsPending: - item.completed = false + item.Completed = false default: return fmt.Errorf("todolist.Item.Apply: unsupported event, %T", evt) diff --git a/examples/todolist/domain/todolist/repository.go b/examples/todolist/internal/domain/todolist/repository.go similarity index 100% rename from examples/todolist/domain/todolist/repository.go rename to examples/todolist/internal/domain/todolist/repository.go diff --git a/examples/todolist/domain/todolist/todolist.go b/examples/todolist/internal/domain/todolist/todolist.go similarity index 91% rename from examples/todolist/domain/todolist/todolist.go rename to examples/todolist/internal/domain/todolist/todolist.go index 9e43aafe..d7ddb166 100644 --- a/examples/todolist/domain/todolist/todolist.go +++ b/examples/todolist/internal/domain/todolist/todolist.go @@ -28,31 +28,31 @@ var Type = aggregate.Type[ID, *TodoList]{ type TodoList struct { aggregate.BaseRoot - id ID - title string - owner string - creationTime time.Time - items []*Item + ID ID + Title string + Owner string + CreationTime time.Time + Items []*Item } // AggregateID implements aggregate.Root. func (tl *TodoList) AggregateID() ID { - return tl.id + return tl.ID } -func (tl *TodoList) findItemByID(id ItemID) *Item { - for _, item := range tl.items { - if item.id == id { - return item +func (tl *TodoList) itemByID(id ItemID) (*Item, bool) { + for _, item := range tl.Items { + if item.ID == id { + return item, true } } - return nil + return nil, false } func (tl *TodoList) applyItemEvent(id ItemID, evt event.Event) error { - item := tl.findItemByID(id) - if item == nil { + item, ok := tl.itemByID(id) + if !ok { return fmt.Errorf("todolist.TodoList.Apply: item not found") } @@ -67,10 +67,10 @@ func (tl *TodoList) applyItemEvent(id ItemID, evt event.Event) error { func (tl *TodoList) Apply(event event.Event) error { switch evt := event.(type) { case WasCreated: - tl.id = evt.ID - tl.title = evt.Title - tl.owner = evt.Owner - tl.creationTime = evt.CreationTime + tl.ID = evt.ID + tl.Title = evt.Title + tl.Owner = evt.Owner + tl.CreationTime = evt.CreationTime case ItemWasAdded: item := &Item{} @@ -78,7 +78,7 @@ func (tl *TodoList) Apply(event event.Event) error { return fmt.Errorf("todolist.TodoList.Apply: failed to apply item event, %w", err) } - tl.items = append(tl.items, item) + tl.Items = append(tl.Items, item) case ItemMarkedAsPending: return tl.applyItemEvent(evt.ID, evt) @@ -88,15 +88,16 @@ func (tl *TodoList) Apply(event event.Event) error { case ItemWasDeleted: var items []*Item - for _, item := range tl.items { - if item.id == evt.ID { + + for _, item := range tl.Items { + if item.ID == evt.ID { continue } items = append(items, item) } - tl.items = items + tl.Items = items default: return fmt.Errorf("todolist.TodoList.Apply: invalid event, %T", evt) @@ -151,16 +152,6 @@ func Create(id ID, title, owner string, now time.Time) (*TodoList, error) { return &todoList, nil } -func (tl *TodoList) itemByID(id ItemID) (*Item, bool) { - for _, item := range tl.items { - if item.id == id { - return item, true - } - } - - return nil, false -} - // AddItem adds a new Todo item to an existing list. // // Both id and title cannot be empty: if so, the method will return an error. diff --git a/examples/todolist/domain/todolist/todolist_test.go b/examples/todolist/internal/domain/todolist/todolist_test.go similarity index 94% rename from examples/todolist/domain/todolist/todolist_test.go rename to examples/todolist/internal/domain/todolist/todolist_test.go index 9d6c2797..e9dcb4e9 100644 --- a/examples/todolist/domain/todolist/todolist_test.go +++ b/examples/todolist/internal/domain/todolist/todolist_test.go @@ -8,7 +8,7 @@ import ( "github.com/get-eventually/go-eventually/core/event" "github.com/get-eventually/go-eventually/core/test/scenario" - "github.com/get-eventually/go-eventually/examples/todolist/domain/todolist" + "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" ) func TestTodoList(t *testing.T) { diff --git a/examples/todolist/internal/grpc/todolist.go b/examples/todolist/internal/grpc/todolist.go new file mode 100644 index 00000000..d04551b2 --- /dev/null +++ b/examples/todolist/internal/grpc/todolist.go @@ -0,0 +1,90 @@ +package grpc + +import ( + "context" + "errors" + "fmt" + + "github.com/bufbuild/connect-go" + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/core/aggregate" + "github.com/get-eventually/go-eventually/core/command" + "github.com/get-eventually/go-eventually/core/query" + todolistv1 "github.com/get-eventually/go-eventually/examples/todolist/gen/todolist/v1" + "github.com/get-eventually/go-eventually/examples/todolist/gen/todolist/v1/todolistv1connect" + appcommand "github.com/get-eventually/go-eventually/examples/todolist/internal/command" + "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" + "github.com/get-eventually/go-eventually/examples/todolist/internal/protoconv" + appquery "github.com/get-eventually/go-eventually/examples/todolist/internal/query" +) + +type TodoListServiceServer struct { + todolistv1connect.UnimplementedTodoListServiceHandler + + GenerateIDFunc func() uuid.UUID + + GetTodoListHandler appquery.GetTodoListHandler + + CreateTodoListHandler appcommand.CreateTodoListHandler + AddTodoListHandler appcommand.AddTodoListItemHandler +} + +func (srv TodoListServiceServer) GetTodoList( + ctx context.Context, + req *connect.Request[todolistv1.GetTodoListRequest], +) (*connect.Response[todolistv1.GetTodoListResponse], error) { + id, err := uuid.Parse(req.Msg.TodoListId) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("grpc.TodoListServiceServer: failed to parse todoListId param, %v", err)) + } + + q := query.ToEnvelope(appquery.GetTodoList{ + ID: todolist.ID(id), + }) + + switch res, err := srv.GetTodoListHandler.Handle(ctx, q); { + case err == nil: + return connect.NewResponse(&todolistv1.GetTodoListResponse{ + TodoList: protoconv.FromTodoList(res), + }), nil + + case errors.Is(err, aggregate.ErrRootNotFound): + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("grpc:TodoListServiceServer: failed to handle query, %v", err)) + + default: + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("gprc.TodoListServiceServer: failed to handle query, %v", err)) + } +} + +func (srv TodoListServiceServer) CreateTodoList( + ctx context.Context, + req *connect.Request[todolistv1.CreateTodoListRequest], +) (*connect.Response[todolistv1.CreateTodoListResponse], error) { + id := srv.GenerateIDFunc() + + cmd := command.ToEnvelope(appcommand.CreateTodoList{ + ID: todolist.ID(id), + Title: req.Msg.Title, + Owner: req.Msg.Owner, + }) + + switch err := srv.CreateTodoListHandler.Handle(ctx, cmd); { + case err == nil: + return connect.NewResponse(&todolistv1.CreateTodoListResponse{ + TodoListId: id.String(), + }), nil + + case errors.Is(err, todolist.ErrEmptyTitle), errors.Is(err, todolist.ErrNoOwnerSpecified): + return nil, connect.NewError( + connect.CodeInvalidArgument, + fmt.Errorf("grpc.TodoListServiceServer.CreateTodoList: invalid arguments, %v", err), + ) + + default: + return nil, connect.NewError( + connect.CodeInternal, + fmt.Errorf("grpc.TodoListServiceServer.CreateTodoList: failed to handle command, %v", err), + ) + } +} diff --git a/examples/todolist/internal/protoconv/todolist.go b/examples/todolist/internal/protoconv/todolist.go new file mode 100644 index 00000000..02f01acb --- /dev/null +++ b/examples/todolist/internal/protoconv/todolist.go @@ -0,0 +1,35 @@ +package protoconv + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + todolistv1 "github.com/get-eventually/go-eventually/examples/todolist/gen/todolist/v1" + "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" +) + +func FromTodoList(tl *todolist.TodoList) *todolistv1.TodoList { + result := &todolistv1.TodoList{ + Id: tl.ID.String(), + Title: tl.Title, + Owner: tl.Owner, + CreationTime: timestamppb.New(tl.CreationTime), + } + + for _, item := range tl.Items { + ritem := &todolistv1.TodoItem{ + Id: item.ID.String(), + Title: item.Title, + Description: item.Description, + Completed: item.Completed, + CreationTime: timestamppb.New(item.CreationTime), + } + + if !item.DueDate.IsZero() { + ritem.DueDate = timestamppb.New(item.DueDate) + } + + result.Items = append(result.Items, ritem) + } + + return result +} diff --git a/examples/todolist/internal/query/get_todo_list.go b/examples/todolist/internal/query/get_todo_list.go new file mode 100644 index 00000000..dec9c8ad --- /dev/null +++ b/examples/todolist/internal/query/get_todo_list.go @@ -0,0 +1,39 @@ +package query + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/core/query" + "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" +) + +type GetTodoList struct { + ID todolist.ID +} + +func (GetTodoList) Name() string { return "GetTodoList" } + +var _ query.Handler[GetTodoList, *todolist.TodoList] = GetTodoListHandler{} + +type GetTodoListHandler struct { + Getter todolist.Getter +} + +// Handle implements query.Handler. +func (h GetTodoListHandler) Handle(ctx context.Context, query query.Envelope[GetTodoList]) (*todolist.TodoList, error) { + q := query.Message + + if q.ID == todolist.ID(uuid.Nil) { + return nil, fmt.Errorf("query.GetTodoList: invalid query provided, %w", todolist.ErrEmptyID) + } + + tl, err := h.Getter.Get(ctx, q.ID) + if err != nil { + return nil, fmt.Errorf("query.GetTodoList: failed to get TodoList from repository, %w", err) + } + + return tl, nil +} diff --git a/examples/todolist/main.go b/examples/todolist/main.go new file mode 100644 index 00000000..a313e2b1 --- /dev/null +++ b/examples/todolist/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "errors" + "fmt" + "net/http" + "time" + + grpchealth "github.com/bufbuild/connect-grpchealth-go" + grpcreflect "github.com/bufbuild/connect-grpcreflect-go" + "github.com/google/uuid" + "go.uber.org/zap" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + + "github.com/get-eventually/go-eventually/core/aggregate" + "github.com/get-eventually/go-eventually/core/test" + "github.com/get-eventually/go-eventually/examples/todolist/gen/todolist/v1/todolistv1connect" + "github.com/get-eventually/go-eventually/examples/todolist/internal/command" + "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" + "github.com/get-eventually/go-eventually/examples/todolist/internal/grpc" + "github.com/get-eventually/go-eventually/examples/todolist/internal/query" +) + +func run() error { + config, err := ParseConfig() + if err != nil { + return fmt.Errorf("todolist.main: failed to parse config, %v", err) + } + + logger, err := zap.NewDevelopment() + if err != nil { + return fmt.Errorf("todolist.main: failed to initialize logger, %v", err) + } + + //nolint:errcheck // No need for this error to come up if it happens. + defer logger.Sync() + + eventStore := test.NewInMemoryEventStore() + todoListRepository := aggregate.NewEventSourcedRepository(eventStore, todolist.Type) + + todoListServiceServer := &grpc.TodoListServiceServer{ + GenerateIDFunc: uuid.New, + GetTodoListHandler: query.GetTodoListHandler{ + Getter: todoListRepository, + }, + CreateTodoListHandler: command.CreateTodoListHandler{ + Clock: time.Now, + Repository: todoListRepository, + }, + AddTodoListHandler: command.AddTodoListItemHandler{ + Clock: time.Now, + Repository: todoListRepository, + }, + } + + mux := http.NewServeMux() + mux.Handle(todolistv1connect.NewTodoListServiceHandler(todoListServiceServer)) + mux.Handle(grpchealth.NewHandler(grpchealth.NewStaticChecker(todolistv1connect.TodoListServiceName))) + mux.Handle(grpcreflect.NewHandlerV1(grpcreflect.NewStaticReflector(todolistv1connect.TodoListServiceName))) + mux.Handle(grpcreflect.NewHandlerV1Alpha(grpcreflect.NewStaticReflector(todolistv1connect.TodoListServiceName))) + + logger.Sugar().Infow("grpc server started", + "address", config.Server.Address, + ) + + // TODO: implement graceful shutdown + srv := &http.Server{ + Addr: config.Server.Address, + Handler: h2c.NewHandler(mux, &http2.Server{}), + ReadTimeout: config.Server.ReadTimeout, + WriteTimeout: config.Server.WriteTimeout, + } + + err = srv.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("todolist.main: grpc server exited with error, %v", err) + } + + return nil +} + +func main() { + if err := run(); err != nil { + panic(err) + } +} diff --git a/examples/todolist/proto/buf.lock b/examples/todolist/proto/buf.lock new file mode 100644 index 00000000..19abf63f --- /dev/null +++ b/examples/todolist/proto/buf.lock @@ -0,0 +1,7 @@ +# Generated by buf. DO NOT EDIT. +version: v1 +deps: + - remote: buf.build + owner: googleapis + repository: googleapis + commit: 75b4300737fb4efca0831636be94e517 diff --git a/examples/todolist/proto/buf.yaml b/examples/todolist/proto/buf.yaml new file mode 100644 index 00000000..02e45324 --- /dev/null +++ b/examples/todolist/proto/buf.yaml @@ -0,0 +1,12 @@ +version: v1 +deps: + - buf.build/googleapis/googleapis +breaking: + use: + - FILE +lint: + use: + - DEFAULT + # - COMMENTS + - UNARY_RPC + - PACKAGE_NO_IMPORT_CYCLE diff --git a/examples/todolist/proto/todolist/v1/todo_list.proto b/examples/todolist/proto/todolist/v1/todo_list.proto new file mode 100644 index 00000000..3db58497 --- /dev/null +++ b/examples/todolist/proto/todolist/v1/todo_list.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package todolist.v1; + +import "google/protobuf/timestamp.proto"; + +message TodoItem { + string id = 1; + string title = 2; + string description = 3; + bool completed = 4; + google.protobuf.Timestamp due_date = 5; + google.protobuf.Timestamp creation_time = 6; +} + +message TodoList { + string id = 1; + string title = 2; + string owner = 3; + google.protobuf.Timestamp creation_time = 4; + repeated TodoItem items = 5; +} diff --git a/examples/todolist/proto/todolist/v1/todo_list_api.proto b/examples/todolist/proto/todolist/v1/todo_list_api.proto new file mode 100644 index 00000000..44eb499c --- /dev/null +++ b/examples/todolist/proto/todolist/v1/todo_list_api.proto @@ -0,0 +1,88 @@ +syntax = "proto3"; + +package todolist.v1; + +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "todolist/v1/todo_list.proto"; + +service TodoListService { + rpc CreateTodoList(CreateTodoListRequest) returns (CreateTodoListResponse) { + option (google.api.http) = { + post: "/v1/todoLists" + body: "*" + }; + } + + rpc GetTodoList(GetTodoListRequest) returns (GetTodoListResponse) { + option (google.api.http) = {get: "/v1/todoLists/{todo_list_id}"}; + } + + rpc AddTodoItem(AddTodoItemRequest) returns (AddTodoItemResponse) { + option (google.api.http) = { + post: "/v1/todoLists/{todo_list_id}/items" + body: "*" + }; + } + + rpc MarkTodoItemAsDone(MarkTodoItemAsDoneRequest) returns (MarkTodoItemAsDoneResponse) { + option (google.api.http) = {post: "/v1/todoList/{todo_list_id}/items/{todo_item_id}/mark-as-done"}; + } + + rpc MarkTodoItemAsPending(MarkTodoItemAsPendingRequest) returns (MarkTodoItemAsPendingResponse) { + option (google.api.http) = {post: "/v1/todoList/{todo_list_id}/items/{todo_item_id}/mark-as-pending"}; + } + + rpc DeleteTodoItem(DeleteTodoItemRequest) returns (DeleteTodoItemResponse) { + option (google.api.http) = {delete: "/v1/todoList/{todo_list_id}/items/{todo_item_id}"}; + } +} + +message CreateTodoListRequest { + string title = 1; + string owner = 2; +} + +message CreateTodoListResponse { + string todo_list_id = 1; +} + +message GetTodoListRequest { + string todo_list_id = 1; +} + +message GetTodoListResponse { + TodoList todo_list = 1; +} + +message AddTodoItemRequest { + string todo_list_id = 1; + string title = 2; + string description = 3; + google.protobuf.Timestamp due_date = 4; +} + +message AddTodoItemResponse { + string todo_item_id = 1; +} + +message MarkTodoItemAsDoneRequest { + string todo_list_id = 1; + string todo_item_id = 2; +} + +message MarkTodoItemAsDoneResponse {} + +message MarkTodoItemAsPendingRequest { + string todo_list_id = 1; + string todo_item_id = 2; +} + +message MarkTodoItemAsPendingResponse {} + +message DeleteTodoItemRequest { + string todo_list_id = 1; + string todo_item_id = 2; +} + +message DeleteTodoItemResponse {} diff --git a/go.work b/go.work index 2f115be1..bf7a069b 100644 --- a/go.work +++ b/go.work @@ -11,5 +11,6 @@ use ( replace ( github.com/get-eventually/go-eventually/core v0.0.0-20230213095413-67475c43eea4 => ./core github.com/get-eventually/go-eventually/core v0.0.0-20230301093954-efadfc924ad7 => ./core + github.com/get-eventually/go-eventually/core v0.0.0-20230307083130-640eec013300 => ./core github.com/get-eventually/go-eventually/serdes v0.0.0-20230227215702-6ac2a4505ce1 => ./serdes ) diff --git a/go.work.sum b/go.work.sum index 38c27e98..6d34eb36 100644 --- a/go.work.sum +++ b/go.work.sum @@ -5,7 +5,6 @@ github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ= github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= @@ -22,8 +21,10 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4 github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ= go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= From c8f19189072ac162d089f1fcda8b7f5c19c428f9 Mon Sep 17 00:00:00 2001 From: Danilo Cianfrone Date: Thu, 16 Mar 2023 20:38:15 +0100 Subject: [PATCH 09/11] feat(examples/todolist): add main file --- .golangci.yml | 1 + examples/todolist/config.go | 26 -------------------------- examples/todolist/main.go | 21 ++++++++++++++++++++- 3 files changed, 21 insertions(+), 27 deletions(-) delete mode 100644 examples/todolist/config.go diff --git a/.golangci.yml b/.golangci.yml index e97545ba..9b79def9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,6 +6,7 @@ run: - migrations # NOTE: this is relative to postgres module - core/internal/user - postgres/internal/user + - gen/* linters-settings: dupl: diff --git a/examples/todolist/config.go b/examples/todolist/config.go deleted file mode 100644 index a85b945d..00000000 --- a/examples/todolist/config.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "fmt" - "time" - - "github.com/kelseyhightower/envconfig" -) - -type Config struct { - Server struct { - Address string `default:":8080" required:"true"` - ReadTimeout time.Duration `default:"10s" required:"true"` - WriteTimeout time.Duration `default:"10s" required:"true"` - } -} - -func ParseConfig() (*Config, error) { - var config Config - - if err := envconfig.Process("", &config); err != nil { - return nil, fmt.Errorf("config: failed to parse from env, %v", err) - } - - return &config, nil -} diff --git a/examples/todolist/main.go b/examples/todolist/main.go index a313e2b1..8c661410 100644 --- a/examples/todolist/main.go +++ b/examples/todolist/main.go @@ -9,6 +9,7 @@ import ( grpchealth "github.com/bufbuild/connect-grpchealth-go" grpcreflect "github.com/bufbuild/connect-grpcreflect-go" "github.com/google/uuid" + "github.com/kelseyhightower/envconfig" "go.uber.org/zap" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" @@ -22,8 +23,26 @@ import ( "github.com/get-eventually/go-eventually/examples/todolist/internal/query" ) +type config struct { + Server struct { + Address string `default:":8080" required:"true"` + ReadTimeout time.Duration `default:"10s" required:"true"` + WriteTimeout time.Duration `default:"10s" required:"true"` + } +} + +func parseConfig() (*config, error) { + var config config + + if err := envconfig.Process("", &config); err != nil { + return nil, fmt.Errorf("config: failed to parse from env, %v", err) + } + + return &config, nil +} + func run() error { - config, err := ParseConfig() + config, err := parseConfig() if err != nil { return fmt.Errorf("todolist.main: failed to parse config, %v", err) } From eafa06d3654005e15971c674b518cd3e3a91e4de Mon Sep 17 00:00:00 2001 From: Danilo Cianfrone Date: Tue, 21 Mar 2023 22:33:18 +0100 Subject: [PATCH 10/11] fix: linter complaints --- examples/todolist/internal/grpc/todolist.go | 78 +++++++++++++++++-- .../todolist/internal/protoconv/todolist.go | 2 + examples/todolist/internal/query/doc.go | 3 + .../todolist/internal/query/get_todo_list.go | 4 + examples/todolist/main.go | 1 + postgres/aggregate_repository_test.go | 1 - 6 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 examples/todolist/internal/query/doc.go diff --git a/examples/todolist/internal/grpc/todolist.go b/examples/todolist/internal/grpc/todolist.go index d04551b2..527b15f7 100644 --- a/examples/todolist/internal/grpc/todolist.go +++ b/examples/todolist/internal/grpc/todolist.go @@ -1,3 +1,4 @@ +// Package grpc contains the gRPC server implementations for the application. package grpc import ( @@ -19,6 +20,9 @@ import ( appquery "github.com/get-eventually/go-eventually/examples/todolist/internal/query" ) +var _ todolistv1connect.TodoListServiceHandler = TodoListServiceServer{} + +// TodoListServiceServer is the gRPC server implementation for this application. type TodoListServiceServer struct { todolistv1connect.UnimplementedTodoListServiceHandler @@ -30,6 +34,7 @@ type TodoListServiceServer struct { AddTodoListHandler appcommand.AddTodoListItemHandler } +// GetTodoList implements todolistv1connect.TodoListServiceHandler. func (srv TodoListServiceServer) GetTodoList( ctx context.Context, req *connect.Request[todolistv1.GetTodoListRequest], @@ -43,6 +48,13 @@ func (srv TodoListServiceServer) GetTodoList( ID: todolist.ID(id), }) + makeError := func(code connect.Code, err error) *connect.Error { + return connect.NewError( + code, + fmt.Errorf("grpc.TodoListServiceServer.GetTodoList: failed to handle query, %v", err), + ) + } + switch res, err := srv.GetTodoListHandler.Handle(ctx, q); { case err == nil: return connect.NewResponse(&todolistv1.GetTodoListResponse{ @@ -50,13 +62,14 @@ func (srv TodoListServiceServer) GetTodoList( }), nil case errors.Is(err, aggregate.ErrRootNotFound): - return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("grpc:TodoListServiceServer: failed to handle query, %v", err)) + return nil, makeError(connect.CodeNotFound, err) default: - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("gprc.TodoListServiceServer: failed to handle query, %v", err)) + return nil, makeError(connect.CodeInternal, err) } } +// CreateTodoList implements todolistv1connect.TodoListServiceHandler. func (srv TodoListServiceServer) CreateTodoList( ctx context.Context, req *connect.Request[todolistv1.CreateTodoListRequest], @@ -69,6 +82,13 @@ func (srv TodoListServiceServer) CreateTodoList( Owner: req.Msg.Owner, }) + makeError := func(code connect.Code, err error) *connect.Error { + return connect.NewError( + code, + fmt.Errorf("grpc.TodoListServiceServer.CreateTodoList: failed to handle command, %v", err), + ) + } + switch err := srv.CreateTodoListHandler.Handle(ctx, cmd); { case err == nil: return connect.NewResponse(&todolistv1.CreateTodoListResponse{ @@ -76,15 +96,59 @@ func (srv TodoListServiceServer) CreateTodoList( }), nil case errors.Is(err, todolist.ErrEmptyTitle), errors.Is(err, todolist.ErrNoOwnerSpecified): + return nil, makeError(connect.CodeInvalidArgument, err) + + default: + return nil, makeError(connect.CodeInternal, err) + } +} + +// AddTodoItem implements todolistv1connect.TodoListServiceHandler. +func (srv TodoListServiceServer) AddTodoItem( + ctx context.Context, + req *connect.Request[todolistv1.AddTodoItemRequest], +) (*connect.Response[todolistv1.AddTodoItemResponse], error) { + todoListID, err := uuid.Parse(req.Msg.TodoListId) + if err != nil { return nil, connect.NewError( connect.CodeInvalidArgument, - fmt.Errorf("grpc.TodoListServiceServer.CreateTodoList: invalid arguments, %v", err), + fmt.Errorf("grpc.TodoListServiceServer.AddTodoItem: failed to parse todoListId into uuid, %v", err), ) + } - default: - return nil, connect.NewError( - connect.CodeInternal, - fmt.Errorf("grpc.TodoListServiceServer.CreateTodoList: failed to handle command, %v", err), + id := srv.GenerateIDFunc() + + cmd := command.ToEnvelope(appcommand.AddTodoListItem{ + TodoListID: todolist.ID(todoListID), + TodoItemID: todolist.ItemID(id), + Title: req.Msg.Title, + Description: req.Msg.Description, + }) + + if req.Msg.DueDate != nil { + cmd.Message.DueDate = req.Msg.DueDate.AsTime() + } + + makeError := func(code connect.Code, err error) *connect.Error { + return connect.NewError( + code, + fmt.Errorf("grpc.TodoListServiceServer.AddTodoItem: failed to handle command, %v", err), ) } + + switch err := srv.AddTodoListHandler.Handle(ctx, cmd); { + case err == nil: + return connect.NewResponse(&todolistv1.AddTodoItemResponse{ + TodoItemId: id.String(), + }), nil + + case errors.Is(err, todolist.ErrEmptyItemTitle): + return nil, makeError(connect.CodeInvalidArgument, err) + + case errors.Is(err, todolist.ErrItemAlreadyExists): + return nil, makeError(connect.CodeAlreadyExists, err) + + default: + return nil, makeError(connect.CodeInternal, err) + } } diff --git a/examples/todolist/internal/protoconv/todolist.go b/examples/todolist/internal/protoconv/todolist.go index 02f01acb..1f6e000d 100644 --- a/examples/todolist/internal/protoconv/todolist.go +++ b/examples/todolist/internal/protoconv/todolist.go @@ -1,3 +1,4 @@ +// Package protoconv contains methods for conversion from Protobufs to Domain Objects and back. package protoconv import ( @@ -7,6 +8,7 @@ import ( "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" ) +// FromTodoList converts a TodoList aggregate root into its Protobuf counterpart. func FromTodoList(tl *todolist.TodoList) *todolistv1.TodoList { result := &todolistv1.TodoList{ Id: tl.ID.String(), diff --git a/examples/todolist/internal/query/doc.go b/examples/todolist/internal/query/doc.go new file mode 100644 index 00000000..0fc66814 --- /dev/null +++ b/examples/todolist/internal/query/doc.go @@ -0,0 +1,3 @@ +// Package query contains Application Queries and their Query Handlers +// for the TodoList bounded context. +package query diff --git a/examples/todolist/internal/query/get_todo_list.go b/examples/todolist/internal/query/get_todo_list.go index dec9c8ad..87292e2d 100644 --- a/examples/todolist/internal/query/get_todo_list.go +++ b/examples/todolist/internal/query/get_todo_list.go @@ -10,14 +10,18 @@ import ( "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" ) +// GetTodoList is a Domain Query used to return a TodoList view. type GetTodoList struct { ID todolist.ID } +// Name implements message.Message. func (GetTodoList) Name() string { return "GetTodoList" } var _ query.Handler[GetTodoList, *todolist.TodoList] = GetTodoListHandler{} +// GetTodoListHandler handles a GetTodoList query, returning the TodoList aggregate root +// specified by the query. type GetTodoListHandler struct { Getter todolist.Getter } diff --git a/examples/todolist/main.go b/examples/todolist/main.go index 8c661410..4fcfd5c3 100644 --- a/examples/todolist/main.go +++ b/examples/todolist/main.go @@ -1,3 +1,4 @@ +// Package main contains the entrypoint for the TodoList gRPC API application. package main import ( diff --git a/postgres/aggregate_repository_test.go b/postgres/aggregate_repository_test.go index 20cd9ec9..02876a39 100644 --- a/postgres/aggregate_repository_test.go +++ b/postgres/aggregate_repository_test.go @@ -22,7 +22,6 @@ import ( const defaultPostgresURL = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" -//nolint:lll // 121 characters are fine :) func testUserRepository(t *testing.T) func(ctx context.Context, repository aggregate.Repository[uuid.UUID, *user.User]) { return func(ctx context.Context, repository aggregate.Repository[uuid.UUID, *user.User]) { t.Run("it can load and save aggregates from the database", func(t *testing.T) { From 7ad99e8ab49fe889c14fa31e9504d7e1df9b39b0 Mon Sep 17 00:00:00 2001 From: Danilo Cianfrone Date: Wed, 22 Mar 2023 11:49:38 +0100 Subject: [PATCH 11/11] feat: add scenario.QueryHandler --- core/internal/user/get_user.go | 62 +++++++++ core/query/query_test.go | 35 +++++ core/test/scenario/query_handler.go | 170 +++++++++++++++++++++++ core/test/scenario/query_handler_test.go | 91 ++++++++++++ 4 files changed, 358 insertions(+) create mode 100644 core/internal/user/get_user.go create mode 100644 core/query/query_test.go create mode 100644 core/test/scenario/query_handler.go create mode 100644 core/test/scenario/query_handler_test.go diff --git a/core/internal/user/get_user.go b/core/internal/user/get_user.go new file mode 100644 index 00000000..f8bee369 --- /dev/null +++ b/core/internal/user/get_user.go @@ -0,0 +1,62 @@ +package user + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/core/aggregate" + "github.com/get-eventually/go-eventually/core/query" + "github.com/get-eventually/go-eventually/core/version" +) + +var ErrEmptyID = errors.New("user: empty id provided") + +type ViewModel struct { + Version version.Version + ID uuid.UUID + FirstName, LastName string + BirthDate time.Time + Email string +} + +func buildViewModel(u *User) ViewModel { + return ViewModel{ + Version: u.Version(), + ID: u.id, + FirstName: u.firstName, + LastName: u.lastName, + BirthDate: u.birthDate, + Email: u.email, + } +} + +type GetQuery struct { + ID uuid.UUID +} + +func (GetQuery) Name() string { return "GetUser" } + +type GetQueryHandler struct { + Repository aggregate.Getter[uuid.UUID, *User] +} + +func (h GetQueryHandler) Handle(ctx context.Context, q query.Envelope[GetQuery]) (ViewModel, error) { + makeError := func(err error) error { + return fmt.Errorf("user.GetQuery: failed to handle query, %w", err) + } + + if q.Message.ID == uuid.Nil { + return ViewModel{}, makeError(ErrEmptyID) + } + + user, err := h.Repository.Get(ctx, q.Message.ID) + if err != nil { + return ViewModel{}, makeError(err) + } + + return buildViewModel(user), nil +} diff --git a/core/query/query_test.go b/core/query/query_test.go new file mode 100644 index 00000000..047f8d03 --- /dev/null +++ b/core/query/query_test.go @@ -0,0 +1,35 @@ +package query_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/get-eventually/go-eventually/core/query" +) + +var ( + _ query.Query = queryTest1{} + _ query.Query = queryTest2{} +) + +type queryTest1 struct{} + +func (queryTest1) Name() string { return "query_test_1" } + +type queryTest2 struct{} + +func (queryTest2) Name() string { return "query_test_2" } + +func TestGenericEnvelope(t *testing.T) { + query1 := query.ToEnvelope(queryTest1{}) + genericQuery1 := query1.ToGenericEnvelope() + + v1, ok := query.FromGenericEnvelope[queryTest1](genericQuery1) + assert.Equal(t, query1, v1) + assert.True(t, ok) + + v2, ok := query.FromGenericEnvelope[queryTest2](genericQuery1) + assert.Zero(t, v2) + assert.False(t, ok) +} diff --git a/core/test/scenario/query_handler.go b/core/test/scenario/query_handler.go new file mode 100644 index 00000000..26299ffa --- /dev/null +++ b/core/test/scenario/query_handler.go @@ -0,0 +1,170 @@ +package scenario + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/get-eventually/go-eventually/core/event" + "github.com/get-eventually/go-eventually/core/query" + "github.com/get-eventually/go-eventually/core/test" + "github.com/get-eventually/go-eventually/core/version" +) + +// QueryHandlerInit is the entrypoint of the Command Handler scenario API. +// +// A Command Handler scenario can either set the current evaluation context +// by using Given(), or test a "clean-slate" scenario by using When() directly. +type QueryHandlerInit[Q query.Query, R any, T query.Handler[Q, R]] struct{} + +// QueryHandler is a scenario type to test the result of Commands +// being handled by a Command Handler. +// +// Command Handlers in Event-sourced systems produce side effects by means +// of Domain Events. This scenario API helps you with testing the Domain Events +// produced by a Command Handler when handling a specific Command. +func QueryHandler[Q query.Query, R any, T query.Handler[Q, R]]() QueryHandlerInit[Q, R, T] { + return QueryHandlerInit[Q, R, T]{} +} + +// Given sets the Command Handler scenario preconditions. +// +// Domain Events are used in Event-sourced systems to represent a side effect +// that has taken place in the system. In order to set a given state for the +// system to be in while testing a specific Command evaluation, you should +// specify the Domain Events that have happened thus far. +// +// When you're testing Commands with a clean-slate system, you should either specify +// no Domain Events, or skip directly to When(). +func (sc QueryHandlerInit[Q, R, T]) Given(events ...event.Persisted) QueryHandlerGiven[Q, R, T] { + return QueryHandlerGiven[Q, R, T]{ + given: events, + } +} + +// When provides the Command to evaluate. +func (sc QueryHandlerInit[Q, R, T]) When(cmd query.Envelope[Q]) QueryHandlerWhen[Q, R, T] { + return QueryHandlerWhen[Q, R, T]{ + when: cmd, + } +} + +// QueryHandlerGiven is the state of the scenario once +// a set of Domain Events have been provided using Given(), to represent +// the state of the system at the time of evaluating a Command. +type QueryHandlerGiven[Q query.Query, R any, T query.Handler[Q, R]] struct { + given []event.Persisted +} + +// When provides the Command to evaluate. +func (sc QueryHandlerGiven[Q, R, T]) When(cmd query.Envelope[Q]) QueryHandlerWhen[Q, R, T] { + return QueryHandlerWhen[Q, R, T]{ + QueryHandlerGiven: sc, + when: cmd, + } +} + +// QueryHandlerWhen is the state of the scenario once the state of the +// system and the Command to evaluate have been provided. +type QueryHandlerWhen[Q query.Query, R any, T query.Handler[Q, R]] struct { + QueryHandlerGiven[Q, R, T] + + when query.Envelope[Q] +} + +// Then sets a positive expectation on the scenario outcome, to produce +// the Domain Events provided in input. +// +// The list of Domain Events specified should be ordered as the expected +// order of recording by the Command Handler. +func (sc QueryHandlerWhen[Q, R, T]) Then(result R) QueryHandlerThen[Q, R, T] { + return QueryHandlerThen[Q, R, T]{ + QueryHandlerWhen: sc, + then: result, + } +} + +// ThenError sets a negative expectation on the scenario outcome, +// to produce an error value that is similar to the one provided in input. +// +// Error assertion happens using errors.Is(), so the error returned +// by the Command Handler is unwrapped until the cause error to match +// the provided expectation. +func (sc QueryHandlerWhen[Q, R, T]) ThenError(err error) QueryHandlerThen[Q, R, T] { + return QueryHandlerThen[Q, R, T]{ + QueryHandlerWhen: sc, + wantError: true, + thenError: err, + } +} + +// ThenFails sets a negative expectation on the scenario outcome, +// to fail the Command execution with no particular assertion on the error returned. +// +// This is useful when the error returned is not important for the Command +// you're trying to test. +func (sc QueryHandlerWhen[Q, R, T]) ThenFails() QueryHandlerThen[Q, R, T] { + return QueryHandlerThen[Q, R, T]{ + QueryHandlerWhen: sc, + wantError: true, + } +} + +// QueryHandlerThen is the state of the scenario once the preconditions +// and expectations have been fully specified. +type QueryHandlerThen[Q query.Query, R any, T query.Handler[Q, R]] struct { + QueryHandlerWhen[Q, R, T] + + then R + thenError error + wantError bool +} + +// AssertOn performs the specified expectations of the scenario, using the Command Handler +// instance produced by the provided factory function. +// +// A Command Handler should only use a single Aggregate type, to ensure that the +// side effects happen in a well-defined transactional boundary. If your Command Handler +// needs to modify more than one Aggregate, you might be doing something wrong +// in your domain model. +// +// The type of the Aggregate used to evaluate the Command must be specified, +// so that the Event-sourced Repository instance can be provided to the factory function +// to build the desired Command Handler. +func (sc QueryHandlerThen[Q, R, T]) AssertOn( //nolint:gocritic + t *testing.T, + handlerFactory func(event.Store) T, +) { + ctx := context.Background() + store := test.NewInMemoryEventStore() + + for _, event := range sc.given { + _, err := store.Append(ctx, event.StreamID, version.Any, event.Envelope) + if !assert.NoError(t, err) { + return + } + } + + handler := handlerFactory(event.FusedStore{ + Appender: store, + Streamer: store, + }) + + result, err := handler.Handle(context.Background(), sc.when) + + if !sc.wantError { + assert.NoError(t, err) + assert.Equal(t, sc.then, result) + + return + } + + if !assert.Error(t, err) { + return + } + + if sc.thenError != nil { + assert.ErrorIs(t, err, sc.thenError) + } +} diff --git a/core/test/scenario/query_handler_test.go b/core/test/scenario/query_handler_test.go new file mode 100644 index 00000000..6ed0a271 --- /dev/null +++ b/core/test/scenario/query_handler_test.go @@ -0,0 +1,91 @@ +package scenario_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/core/aggregate" + "github.com/get-eventually/go-eventually/core/event" + "github.com/get-eventually/go-eventually/core/internal/user" + "github.com/get-eventually/go-eventually/core/query" + "github.com/get-eventually/go-eventually/core/test/scenario" +) + +func TestQueryHandler(t *testing.T) { + id := uuid.New() + now := time.Now() + + userWasCreatedEvent := user.WasCreated{ + ID: id, + FirstName: "John", + LastName: "Doe", + BirthDate: now, + Email: "john@doe.com", + } + + t.Run("fails when using an invalid id value", func(t *testing.T) { + scenario. + QueryHandler[user.GetQuery, user.ViewModel, user.GetQueryHandler](). + When(query.Envelope[user.GetQuery]{ + Message: user.GetQuery{}, + Metadata: nil, + }). + ThenError(user.ErrEmptyID). + AssertOn(t, func(s event.Store) user.GetQueryHandler { + return user.GetQueryHandler{ + Repository: aggregate.NewEventSourcedRepository(s, user.Type), + } + }) + }) + + t.Run("fails when requesting a user that doesn't exist", func(t *testing.T) { + scenario. + QueryHandler[user.GetQuery, user.ViewModel, user.GetQueryHandler](). + When(query.Envelope[user.GetQuery]{ + Message: user.GetQuery{ + ID: id, + }, + Metadata: nil, + }). + ThenError(aggregate.ErrRootNotFound). + AssertOn(t, func(s event.Store) user.GetQueryHandler { + return user.GetQueryHandler{ + Repository: aggregate.NewEventSourcedRepository(s, user.Type), + } + }) + }) + + t.Run("returns an existing user", func(t *testing.T) { + scenario. + QueryHandler[user.GetQuery, user.ViewModel, user.GetQueryHandler](). + Given(event.Persisted{ + StreamID: event.StreamID(id.String()), + Version: 1, + Envelope: event.Envelope{ + Message: userWasCreatedEvent, + Metadata: nil, + }, + }). + When(query.Envelope[user.GetQuery]{ + Message: user.GetQuery{ + ID: id, + }, + Metadata: nil, + }). + Then(user.ViewModel{ + Version: 1, + ID: id, + FirstName: userWasCreatedEvent.FirstName, + LastName: userWasCreatedEvent.LastName, + BirthDate: userWasCreatedEvent.BirthDate, + Email: userWasCreatedEvent.Email, + }). + AssertOn(t, func(s event.Store) user.GetQueryHandler { + return user.GetQueryHandler{ + Repository: aggregate.NewEventSourcedRepository(s, user.Type), + } + }) + }) +}