diff --git a/.golangci.yml b/.golangci.yml index 544bda1..9b79def 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: @@ -40,7 +41,7 @@ linters-settings: govet: check-shadowing: true lll: - line-length: 120 + line-length: 160 misspell: locale: US nolintlint: diff --git a/Makefile b/Makefile index 9b37fde..0f7418c 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 diff --git a/core/internal/user/get_user.go b/core/internal/user/get_user.go new file mode 100644 index 0000000..f8bee36 --- /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/doc.go b/core/query/doc.go new file mode 100644 index 0000000..f7fd2c2 --- /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 0000000..716a1ba --- /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, + } +} diff --git a/core/query/query_test.go b/core/query/query_test.go new file mode 100644 index 0000000..047f8d0 --- /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 0000000..26299ff --- /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 0000000..6ed0a27 --- /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), + } + }) + }) +} diff --git a/examples/todolist/buf.gen.yaml b/examples/todolist/buf.gen.yaml new file mode 100644 index 0000000..6e86608 --- /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/gen/todolist/v1/todo_list.pb.go b/examples/todolist/gen/todolist/v1/todo_list.pb.go new file mode 100644 index 0000000..c35738e --- /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 0000000..159d2f5 --- /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 0000000..e1c7e26 --- /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 new file mode 100644 index 0000000..8843a8a --- /dev/null +++ b/examples/todolist/go.mod @@ -0,0 +1,31 @@ +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 ( + 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 + 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 new file mode 100644 index 0000000..ef953c1 --- /dev/null +++ b/examples/todolist/go.sum @@ -0,0 +1,58 @@ +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/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= +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.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= +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/examples/todolist/internal/command/add_todo_list_item.go b/examples/todolist/internal/command/add_todo_list_item.go new file mode 100644 index 0000000..78a358c --- /dev/null +++ b/examples/todolist/internal/command/add_todo_list_item.go @@ -0,0 +1,56 @@ +package command + +import ( + "context" + "fmt" + "time" + + "github.com/get-eventually/go-eventually/core/command" + "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" +) + +// AddTodoListItem the Command used to add a new Item to an existing TodoList. +type AddTodoListItem struct { + TodoListID todolist.ID + TodoItemID todolist.ItemID + Title string + Description string + DueDate time.Time +} + +// 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. +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/internal/command/add_todo_list_item_test.go b/examples/todolist/internal/command/add_todo_list_item_test.go new file mode 100644 index 0000000..4c3e58a --- /dev/null +++ b/examples/todolist/internal/command/add_todo_list_item_test.go @@ -0,0 +1,141 @@ +package command_test + +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/internal/command" + "github.com/get-eventually/go-eventually/examples/todolist/internal/domain/todolist" +) + +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 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{ + 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/internal/command/create_todolist.go b/examples/todolist/internal/command/create_todolist.go new file mode 100644 index 0000000..cd1efc2 --- /dev/null +++ b/examples/todolist/internal/command/create_todolist.go @@ -0,0 +1,44 @@ +package command + +import ( + "context" + "fmt" + "time" + + "github.com/get-eventually/go-eventually/core/command" + "github.com/get-eventually/go-eventually/examples/todolist/internal/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. +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/internal/command/create_todolist_test.go b/examples/todolist/internal/command/create_todolist_test.go new file mode 100644 index 0000000..b2858ed --- /dev/null +++ b/examples/todolist/internal/command/create_todolist_test.go @@ -0,0 +1,106 @@ +package command_test + +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" + 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) { + id := uuid.New() + 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{ + 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, 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/internal/command/doc.go b/examples/todolist/internal/command/doc.go new file mode 100644 index 0000000..bd66991 --- /dev/null +++ b/examples/todolist/internal/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/internal/domain/todolist/event.go b/examples/todolist/internal/domain/todolist/event.go new file mode 100644 index 0000000..170dc0e --- /dev/null +++ b/examples/todolist/internal/domain/todolist/event.go @@ -0,0 +1,54 @@ +package todolist + +import "time" + +// WasCreated is the Domain Event issued when new TodoList gets created. +type WasCreated struct { + ID ID + Title string + Owner string + 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 + Description string + DueDate time.Time + 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/internal/domain/todolist/item.go b/examples/todolist/internal/domain/todolist/item.go new file mode 100644 index 0000000..64ee451 --- /dev/null +++ b/examples/todolist/internal/domain/todolist/item.go @@ -0,0 +1,53 @@ +package todolist + +import ( + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/core/aggregate" + "github.com/get-eventually/go-eventually/core/event" +) + +// 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 + + 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 + + case ItemMarkedAsDone: + item.Completed = true + + case ItemMarkedAsPending: + item.Completed = false + + default: + return fmt.Errorf("todolist.Item.Apply: unsupported event, %T", evt) + } + + return nil +} diff --git a/examples/todolist/internal/domain/todolist/repository.go b/examples/todolist/internal/domain/todolist/repository.go new file mode 100644 index 0000000..9653171 --- /dev/null +++ b/examples/todolist/internal/domain/todolist/repository.go @@ -0,0 +1,14 @@ +package todolist + +import "github.com/get-eventually/go-eventually/core/aggregate" + +type ( + // 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/internal/domain/todolist/todolist.go b/examples/todolist/internal/domain/todolist/todolist.go new file mode 100644 index 0000000..d7ddb16 --- /dev/null +++ b/examples/todolist/internal/domain/todolist/todolist.go @@ -0,0 +1,246 @@ +// Package todolist contains the domain types and implementations +// for the TodoList Aggregate Root. +package todolist + +import ( + "errors" + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/core/aggregate" + "github.com/get-eventually/go-eventually/core/event" +) + +// 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 + + ID ID + Title string + Owner string + CreationTime time.Time + Items []*Item +} + +// AggregateID implements aggregate.Root. +func (tl *TodoList) AggregateID() ID { + return tl.ID +} + +func (tl *TodoList) itemByID(id ItemID) (*Item, bool) { + for _, item := range tl.Items { + if item.ID == id { + return item, true + } + } + + return nil, false +} + +func (tl *TodoList) applyItemEvent(id ItemID, evt event.Event) error { + item, ok := tl.itemByID(id) + if !ok { + 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) { + 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 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) + } + + 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") + 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") + 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) + } + + if uuid.UUID(id) == uuid.Nil { + return nil, wrapErr(ErrEmptyID) + } + + if title == "" { + return nil, wrapErr(ErrEmptyTitle) + } + + if owner == "" { + return nil, wrapErr(ErrNoOwnerSpecified) + } + + 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 +} + +// 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) + } + + if uuid.UUID(id) == uuid.Nil { + return wrapErr(ErrEmptyItemID) + } + + if title == "" { + return wrapErr(ErrEmptyItemTitle) + } + + if _, ok := tl.itemByID(id); ok { + return wrapErr(ErrItemAlreadyExists) + } + + if err := aggregate.RecordThat[ID](tl, 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 +} + +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/internal/domain/todolist/todolist_test.go b/examples/todolist/internal/domain/todolist/todolist_test.go new file mode 100644 index 0000000..e9dcb4e --- /dev/null +++ b/examples/todolist/internal/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/internal/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) + }) +} diff --git a/examples/todolist/internal/grpc/todolist.go b/examples/todolist/internal/grpc/todolist.go new file mode 100644 index 0000000..527b15f --- /dev/null +++ b/examples/todolist/internal/grpc/todolist.go @@ -0,0 +1,154 @@ +// Package grpc contains the gRPC server implementations for the application. +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" +) + +var _ todolistv1connect.TodoListServiceHandler = TodoListServiceServer{} + +// TodoListServiceServer is the gRPC server implementation for this application. +type TodoListServiceServer struct { + todolistv1connect.UnimplementedTodoListServiceHandler + + GenerateIDFunc func() uuid.UUID + + GetTodoListHandler appquery.GetTodoListHandler + + CreateTodoListHandler appcommand.CreateTodoListHandler + AddTodoListHandler appcommand.AddTodoListItemHandler +} + +// GetTodoList implements todolistv1connect.TodoListServiceHandler. +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), + }) + + 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{ + TodoList: protoconv.FromTodoList(res), + }), nil + + case errors.Is(err, aggregate.ErrRootNotFound): + return nil, makeError(connect.CodeNotFound, err) + + default: + return nil, makeError(connect.CodeInternal, err) + } +} + +// CreateTodoList implements todolistv1connect.TodoListServiceHandler. +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, + }) + + 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{ + TodoListId: id.String(), + }), 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.AddTodoItem: failed to parse todoListId into uuid, %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 new file mode 100644 index 0000000..1f6e000 --- /dev/null +++ b/examples/todolist/internal/protoconv/todolist.go @@ -0,0 +1,37 @@ +// Package protoconv contains methods for conversion from Protobufs to Domain Objects and back. +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" +) + +// FromTodoList converts a TodoList aggregate root into its Protobuf counterpart. +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/doc.go b/examples/todolist/internal/query/doc.go new file mode 100644 index 0000000..0fc6681 --- /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 new file mode 100644 index 0000000..87292e2 --- /dev/null +++ b/examples/todolist/internal/query/get_todo_list.go @@ -0,0 +1,43 @@ +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" +) + +// 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 +} + +// 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 0000000..4fcfd5c --- /dev/null +++ b/examples/todolist/main.go @@ -0,0 +1,107 @@ +// Package main contains the entrypoint for the TodoList gRPC API application. +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" + "github.com/kelseyhightower/envconfig" + "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" +) + +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() + 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 0000000..19abf63 --- /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 0000000..02e4532 --- /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 0000000..3db5849 --- /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 0000000..44eb499 --- /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 32f49db..bf7a069 100644 --- a/go.work +++ b/go.work @@ -2,6 +2,7 @@ go 1.18 use ( ./core + ./examples/todolist ./oteleventually ./postgres ./serdes @@ -9,5 +10,7 @@ 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 38c27e9..6d34eb3 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= diff --git a/postgres/aggregate_repository_test.go b/postgres/aggregate_repository_test.go index 20cd9ec..02876a3 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) {