diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000..77ef1f31 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,7 @@ +--- +# Codecov configuration. +# https://docs.codecov.com/docs/codecov-yaml + +ignore: + - "examples/**" + - "**/gen/**" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f410de95..0e3dbe12 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,8 @@ jobs: uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.txt + # One coverage file per Go workspace member (see go.work). + # Keep this list in sync with go.work's use directives. + files: ./coverage.txt,./examples/todolist/coverage.txt verbose: true fail_ci_if_error: true diff --git a/.golangci.yaml b/.golangci.yaml index e7968876..83aa24f8 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -128,7 +128,6 @@ linters: paths: - third_party$ - builtin$ - - examples$ issues: max-issues-per-linter: 0 @@ -153,4 +152,3 @@ formatters: paths: - third_party$ - builtin$ - - examples$ diff --git a/Makefile b/Makefile index c0644c27..32dabd9d 100644 --- a/Makefile +++ b/Makefile @@ -9,17 +9,40 @@ endif GOLANGCI_LINT_FLAGS ?= -v GO_TEST_FLAGS ?= -v -cover -covermode=atomic -coverprofile=coverage.txt -coverpkg=./... +# GO_MODULES is the list of directories participating in the Go workspace, +# discovered via `go work edit -json` so go.work stays the single source of +# truth. golangci-lint and go test don't span workspace modules on their own +# (see https://github.com/golang/vscode-go/issues/2666), so we iterate. +GO_MODULES := $(shell go work edit -json | jq -r '.Use[].DiskPath') + +# GOLANGCI_LINT_CONFIG points each nested module's linter at the root config. +# Every module is linted with the same rules we apply to the library. +GOLANGCI_LINT_CONFIG := $(abspath .golangci.yaml) + +# run_in_modules runs a shell command in each Go workspace module. The first +# argument is a human-readable label for logs; the second is the command +# itself, which runs with $$mod pointing at the module directory. +define run_in_modules + set -e; \ + for mod in $(GO_MODULES); do \ + echo "==> $(1) ($$mod)"; \ + ( cd "$$mod" && $(2) ); \ + done +endef + go.lint: - golangci-lint run $(GOLANGCI_LINT_FLAGS) + $(call run_in_modules,golangci-lint run,golangci-lint run --config $(GOLANGCI_LINT_CONFIG) $(GOLANGCI_LINT_FLAGS)) go.test: - go test $(GO_TEST_FLAGS) ./... + $(call run_in_modules,go test,go test $(GO_TEST_FLAGS) ./...) go.test.unit: - go test -short $(GO_TEST_FLAGS) ./... + $(call run_in_modules,go test -short,go test -short $(GO_TEST_FLAGS) ./...) + +go.build: + $(call run_in_modules,go build,go build ./...) go.mod.update: - echo "==> update dependencies recursively" - go get -u ./... - echo "==> run 'go mod tidy'" - go mod tidy + $(call run_in_modules,go get -u + go mod tidy,go get -u ./... && go mod tidy) + echo "==> go work sync" + go work sync diff --git a/README.md b/README.md index a285c465..774074f3 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,14 @@ You can add this library to your project by running: go get -u github.com/get-eventually/go-eventually ``` +## Examples + +End-to-end examples live under [`examples/`](./examples): + +* [`examples/todolist`](./examples/todolist) — a Connect-based TodoList + service demonstrating aggregates, commands, queries, and BDD-style + scenario tests, backed by the in-memory event store. + ## Contributing Thank you for your consideration ❤️ You can head over our [CONTRIBUTING](./CONTRIBUTING.md) page to get started. diff --git a/examples/todolist/README.md b/examples/todolist/README.md new file mode 100644 index 00000000..926c0386 --- /dev/null +++ b/examples/todolist/README.md @@ -0,0 +1,96 @@ +# TodoList example + +A small Connect-based service that exercises `go-eventually`'s DDD / Event +Sourcing primitives end-to-end. It serves as a real-world litmus test for +the library's API: if something feels awkward in this example, it probably +needs rethinking in the library. + +## What this example demonstrates + +- An aggregate root (`todolist.TodoList`) with a child entity + (`todolist.Item`), built on `aggregate.BaseRoot` and + `aggregate.RecordThat`. +- Commands (`todolist.CreateCommand`, `todolist.AddItemCommand`) + implementing `command.Handler[Cmd]`, persisted through an + `aggregate.EventSourcedRepository` backed by `event.NewInMemoryStore`. +- A query (`todolist.GetQuery`) implementing `query.Handler[Q, R]`, + reusing the same repository's `Get`. +- BDD-style test scenarios using `aggregate.Scenario`, `command.Scenario`, + and (implicitly through the command scenarios) the event streaming + plumbing. +- A Connect service exposing the above over HTTP/2 (h2c), with gRPC + health + gRPC reflection wired up. + +Because the repository internally streams events through the new +`message.Stream[event.Persisted]` iterator, any request that triggers +`AddTodoItem` (which loads the existing aggregate) exercises the iterator +end-to-end. + +## Running + +```sh +go run ./examples/todolist +# or from the example dir: +cd examples/todolist && go run . +# Server listens on :8080 by default +``` + +The example is a member of the repository's Go workspace (`go.work` at +repo root). The workspace is what resolves the `go-eventually` import +to the in-repo library code. The example's `go.mod` still carries a +`require github.com/get-eventually/go-eventually v0.4.0` line as a +nominal floor — `GOWORK=off` would fall back to that released version, +which is NOT what you want when evaluating in-progress library changes. + +Hit it with a Connect client, `grpcurl`, or the built-in reflection: + +```sh +grpcurl -plaintext localhost:8080 list +grpcurl -plaintext -d '{"todo_list_id":"...","title":"chores","owner":"me"}' \ + localhost:8080 todolist.v1.TodoListService/CreateTodoList +``` + +## Design choices worth noting + +- **Commands return `google.protobuf.Empty`.** Clients generate IDs and + pass them in; the server acknowledges. Idempotent on retries with the + same ID. +- **Request/response messages colocated with the service** in + `todo_list_service.proto`; domain messages live in their own files. + Small service surface benefits more from colocation than from strict + one-message-per-file splitting. +- **Connect only.** No HTTP/REST gateway, no `google.api.http` + annotations. The Connect protocol itself already speaks gRPC, gRPC-Web, + and Connect-over-HTTP; that's enough surface for an example. +- **In-memory store.** State is lost on restart. The example is about + wiring, not persistence; swap `event.NewInMemoryStore()` in `main.go` + for a `postgres.NewEventStore(...)` to get durability. +- **Error handling is example-grade.** Domain errors are mapped to Connect + codes (`InvalidArgument`, `AlreadyExists`, `NotFound`, `Internal`) and + the full error chain is propagated to the client. A real service would + sanitize messages before they cross the wire. +- **Package by domain, not by layer.** Everything TodoList-related — + aggregate, events, commands, queries, handlers, Connect transport, + proto conversion — lives in a single `internal/todolist` package. Names + like `CreateCommand`, `GetQuery`, `AddItemCommandHandler`, + `ConnectServiceHandler`, and `ToProto` ride on top of the package + prefix to keep call sites terse and the domain boundary obvious at a + glance. + +## Regenerating the protos + +```sh +cd examples/todolist +buf generate +``` + +Committed output lives under `gen/`; regenerate after any proto change. +The buf configuration uses the v2 schema (see `buf.yaml` + `buf.gen.yaml` +at the module root). + +## CI coverage + +`make go.lint` and `make go.test` at the repo root iterate over every +Go workspace member (discovered from `go.work`). This example is +therefore lint-gated and test-gated on every PR — a library change +that breaks the example fails CI. diff --git a/examples/todolist/buf.gen.yaml b/examples/todolist/buf.gen.yaml new file mode 100644 index 00000000..afbb27c9 --- /dev/null +++ b/examples/todolist/buf.gen.yaml @@ -0,0 +1,13 @@ +version: v2 +managed: + enabled: true + override: + - file_option: go_package_prefix + value: github.com/get-eventually/go-eventually/examples/todolist/gen +plugins: + - remote: buf.build/protocolbuffers/go:v1.31.0 + out: gen + opt: paths=source_relative + - remote: buf.build/connectrpc/go:v1.12.0 + out: gen + opt: paths=source_relative diff --git a/examples/todolist/buf.yaml b/examples/todolist/buf.yaml new file mode 100644 index 00000000..5381f418 --- /dev/null +++ b/examples/todolist/buf.yaml @@ -0,0 +1,19 @@ +version: v2 +modules: + - path: proto +lint: + use: + - COMMENTS + - PACKAGE_NO_IMPORT_CYCLE + - STANDARD + - UNARY_RPC + except: + - FIELD_NOT_REQUIRED + rpc_allow_google_protobuf_empty_responses: true + disallow_comment_ignores: true +breaking: + use: + - FILE + except: + - EXTENSION_NO_DELETE + - FIELD_SAME_DEFAULT diff --git a/examples/todolist/gen/todolist/v1/todo_item.pb.go b/examples/todolist/gen/todolist/v1/todo_item.pb.go new file mode 100644 index 00000000..87641f1e --- /dev/null +++ b/examples/todolist/gen/todolist/v1/todo_item.pb.go @@ -0,0 +1,220 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: todolist/v1/todo_item.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) +) + +// TodoItem is a single entry in a TodoList. +type TodoItem struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Unique identifier (UUID) for this item. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Short title of the item. + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // Free-form description of the item. + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // Whether the item is completed. + Completed bool `protobuf:"varint,4,opt,name=completed,proto3" json:"completed,omitempty"` + // Optional due date; zero-valued if not set. + DueDate *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=due_date,json=dueDate,proto3" json:"due_date,omitempty"` + // When the item was added to its list. + 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_item_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_item_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_item_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 +} + +var File_todolist_v1_todo_item_proto protoreflect.FileDescriptor + +var file_todolist_v1_todo_item_proto_rawDesc = []byte{ + 0x0a, 0x1b, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, + 0x64, 0x6f, 0x5f, 0x69, 0x74, 0x65, 0x6d, 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, 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, + 0x49, 0x74, 0x65, 0x6d, 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_item_proto_rawDescOnce sync.Once + file_todolist_v1_todo_item_proto_rawDescData = file_todolist_v1_todo_item_proto_rawDesc +) + +func file_todolist_v1_todo_item_proto_rawDescGZIP() []byte { + file_todolist_v1_todo_item_proto_rawDescOnce.Do(func() { + file_todolist_v1_todo_item_proto_rawDescData = protoimpl.X.CompressGZIP(file_todolist_v1_todo_item_proto_rawDescData) + }) + return file_todolist_v1_todo_item_proto_rawDescData +} + +var file_todolist_v1_todo_item_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_todolist_v1_todo_item_proto_goTypes = []interface{}{ + (*TodoItem)(nil), // 0: todolist.v1.TodoItem + (*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp +} +var file_todolist_v1_todo_item_proto_depIdxs = []int32{ + 1, // 0: todolist.v1.TodoItem.due_date:type_name -> google.protobuf.Timestamp + 1, // 1: todolist.v1.TodoItem.creation_time:type_name -> google.protobuf.Timestamp + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] 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_item_proto_init() } +func file_todolist_v1_todo_item_proto_init() { + if File_todolist_v1_todo_item_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_todolist_v1_todo_item_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 + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_todolist_v1_todo_item_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_todolist_v1_todo_item_proto_goTypes, + DependencyIndexes: file_todolist_v1_todo_item_proto_depIdxs, + MessageInfos: file_todolist_v1_todo_item_proto_msgTypes, + }.Build() + File_todolist_v1_todo_item_proto = out.File + file_todolist_v1_todo_item_proto_rawDesc = nil + file_todolist_v1_todo_item_proto_goTypes = nil + file_todolist_v1_todo_item_proto_depIdxs = nil +} diff --git a/examples/todolist/gen/todolist/v1/todo_list.pb.go b/examples/todolist/gen/todolist/v1/todo_list.pb.go new file mode 100644 index 00000000..593b5597 --- /dev/null +++ b/examples/todolist/gen/todolist/v1/todo_list.pb.go @@ -0,0 +1,211 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.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) +) + +// TodoList is a list of TodoItems belonging to an owner. +type TodoList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Unique identifier (UUID) for this list. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Display title of the list. + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // Owner identifier (application-defined, typically a username). + Owner string `protobuf:"bytes,3,opt,name=owner,proto3" json:"owner,omitempty"` + // When the list was created. + CreationTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=creation_time,json=creationTime,proto3" json:"creation_time,omitempty"` + // Items currently in the list, in insertion order. + 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[0] + 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[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 TodoList.ProtoReflect.Descriptor instead. +func (*TodoList) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_proto_rawDescGZIP(), []int{0} +} + +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, 0x1a, 0x1b, 0x74, 0x6f, 0x64, + 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, 0x64, 0x6f, 0x5f, 0x69, 0x74, + 0x65, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 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, 1) +var file_todolist_v1_todo_list_proto_goTypes = []interface{}{ + (*TodoList)(nil), // 0: todolist.v1.TodoList + (*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp + (*TodoItem)(nil), // 2: todolist.v1.TodoItem +} +var file_todolist_v1_todo_list_proto_depIdxs = []int32{ + 1, // 0: todolist.v1.TodoList.creation_time:type_name -> google.protobuf.Timestamp + 2, // 1: todolist.v1.TodoList.items:type_name -> todolist.v1.TodoItem + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] 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_proto_init() } +func file_todolist_v1_todo_list_proto_init() { + if File_todolist_v1_todo_list_proto != nil { + return + } + file_todolist_v1_todo_item_proto_init() + if !protoimpl.UnsafeEnabled { + file_todolist_v1_todo_list_proto_msgTypes[0].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: 1, + 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_service.pb.go b/examples/todolist/gen/todolist/v1/todo_list_service.pb.go new file mode 100644 index 00000000..d269f8c9 --- /dev/null +++ b/examples/todolist/gen/todolist/v1/todo_list_service.pb.go @@ -0,0 +1,718 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: todolist/v1/todo_list_service.proto + +package todolistv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + 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) +) + +// CreateTodoListRequest creates a new TodoList with the specified ID. +// +// The todo_list_id is client-generated: clients are expected to generate a +// UUID and pass it in the request. This keeps the command idempotent and +// frees the response of a payload. +type CreateTodoListRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Client-generated UUID for the new list. + TodoListId string `protobuf:"bytes,1,opt,name=todo_list_id,json=todoListId,proto3" json:"todo_list_id,omitempty"` + // Display title of the list. + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // Owner identifier. + Owner string `protobuf:"bytes,3,opt,name=owner,proto3" json:"owner,omitempty"` +} + +func (x *CreateTodoListRequest) Reset() { + *x = CreateTodoListRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_todolist_v1_todo_list_service_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_service_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_service_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateTodoListRequest) GetTodoListId() string { + if x != nil { + return x.TodoListId + } + return "" +} + +func (x *CreateTodoListRequest) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *CreateTodoListRequest) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +// GetTodoListRequest fetches a TodoList by its ID. +type GetTodoListRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // UUID of the list to fetch. + 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_service_proto_msgTypes[1] + 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_service_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 GetTodoListRequest.ProtoReflect.Descriptor instead. +func (*GetTodoListRequest) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_service_proto_rawDescGZIP(), []int{1} +} + +func (x *GetTodoListRequest) GetTodoListId() string { + if x != nil { + return x.TodoListId + } + return "" +} + +// GetTodoListResponse contains the requested TodoList. +type GetTodoListResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The requested list. + 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_service_proto_msgTypes[2] + 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_service_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 GetTodoListResponse.ProtoReflect.Descriptor instead. +func (*GetTodoListResponse) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_service_proto_rawDescGZIP(), []int{2} +} + +func (x *GetTodoListResponse) GetTodoList() *TodoList { + if x != nil { + return x.TodoList + } + return nil +} + +// AddTodoItemRequest adds a new TodoItem to an existing TodoList. +// +// The todo_item_id is client-generated, same as in CreateTodoListRequest. +type AddTodoItemRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // UUID of the target list. + TodoListId string `protobuf:"bytes,1,opt,name=todo_list_id,json=todoListId,proto3" json:"todo_list_id,omitempty"` + // Client-generated UUID for the new item. + TodoItemId string `protobuf:"bytes,2,opt,name=todo_item_id,json=todoItemId,proto3" json:"todo_item_id,omitempty"` + // Short title of the item. + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + // Free-form description of the item. + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + // Optional due date; omit for no due date. + DueDate *timestamppb.Timestamp `protobuf:"bytes,5,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_service_proto_msgTypes[3] + 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_service_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 AddTodoItemRequest.ProtoReflect.Descriptor instead. +func (*AddTodoItemRequest) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_service_proto_rawDescGZIP(), []int{3} +} + +func (x *AddTodoItemRequest) GetTodoListId() string { + if x != nil { + return x.TodoListId + } + return "" +} + +func (x *AddTodoItemRequest) GetTodoItemId() string { + if x != nil { + return x.TodoItemId + } + 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 +} + +// MarkTodoItemAsDoneRequest marks an item in a TodoList as completed. +type MarkTodoItemAsDoneRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // UUID of the list that owns the item. + TodoListId string `protobuf:"bytes,1,opt,name=todo_list_id,json=todoListId,proto3" json:"todo_list_id,omitempty"` + // UUID of the item to mark as done. + 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_service_proto_msgTypes[4] + 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_service_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 MarkTodoItemAsDoneRequest.ProtoReflect.Descriptor instead. +func (*MarkTodoItemAsDoneRequest) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_service_proto_rawDescGZIP(), []int{4} +} + +func (x *MarkTodoItemAsDoneRequest) GetTodoListId() string { + if x != nil { + return x.TodoListId + } + return "" +} + +func (x *MarkTodoItemAsDoneRequest) GetTodoItemId() string { + if x != nil { + return x.TodoItemId + } + return "" +} + +// MarkTodoItemAsPendingRequest marks an item in a TodoList as pending +// (i.e., undoes a previous "mark as done"). +type MarkTodoItemAsPendingRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // UUID of the list that owns the item. + TodoListId string `protobuf:"bytes,1,opt,name=todo_list_id,json=todoListId,proto3" json:"todo_list_id,omitempty"` + // UUID of the item to mark as pending. + 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_service_proto_msgTypes[5] + 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_service_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 MarkTodoItemAsPendingRequest.ProtoReflect.Descriptor instead. +func (*MarkTodoItemAsPendingRequest) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_service_proto_rawDescGZIP(), []int{5} +} + +func (x *MarkTodoItemAsPendingRequest) GetTodoListId() string { + if x != nil { + return x.TodoListId + } + return "" +} + +func (x *MarkTodoItemAsPendingRequest) GetTodoItemId() string { + if x != nil { + return x.TodoItemId + } + return "" +} + +// DeleteTodoItemRequest removes an item from a TodoList. +type DeleteTodoItemRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // UUID of the list that owns the item. + TodoListId string `protobuf:"bytes,1,opt,name=todo_list_id,json=todoListId,proto3" json:"todo_list_id,omitempty"` + // UUID of the item to delete. + 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_service_proto_msgTypes[6] + 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_service_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 DeleteTodoItemRequest.ProtoReflect.Descriptor instead. +func (*DeleteTodoItemRequest) Descriptor() ([]byte, []int) { + return file_todolist_v1_todo_list_service_proto_rawDescGZIP(), []int{6} +} + +func (x *DeleteTodoItemRequest) GetTodoListId() string { + if x != nil { + return x.TodoListId + } + return "" +} + +func (x *DeleteTodoItemRequest) GetTodoItemId() string { + if x != nil { + return x.TodoItemId + } + return "" +} + +var File_todolist_v1_todo_list_service_proto protoreflect.FileDescriptor + +var file_todolist_v1_todo_list_service_proto_rawDesc = []byte{ + 0x0a, 0x23, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, + 0x64, 0x6f, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, 0x2e, + 0x76, 0x31, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 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, 0x65, 0x0a, + 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 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, 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, 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, 0xc7, 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, 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, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x03, 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, 0x04, 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, 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, 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, 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, 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, 0x32, 0xf9, 0x03, 0x0a, 0x0f, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4c, 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, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x50, 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, 0x12, 0x46, 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, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x54, 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, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x5a, 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, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x12, 0x4c, 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, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, + 0xca, 0x01, 0x0a, 0x0f, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x6f, 0x64, 0x6f, 0x6c, 0x69, 0x73, 0x74, + 0x2e, 0x76, 0x31, 0x42, 0x14, 0x54, 0x6f, 0x64, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 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_service_proto_rawDescOnce sync.Once + file_todolist_v1_todo_list_service_proto_rawDescData = file_todolist_v1_todo_list_service_proto_rawDesc +) + +func file_todolist_v1_todo_list_service_proto_rawDescGZIP() []byte { + file_todolist_v1_todo_list_service_proto_rawDescOnce.Do(func() { + file_todolist_v1_todo_list_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_todolist_v1_todo_list_service_proto_rawDescData) + }) + return file_todolist_v1_todo_list_service_proto_rawDescData +} + +var file_todolist_v1_todo_list_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_todolist_v1_todo_list_service_proto_goTypes = []interface{}{ + (*CreateTodoListRequest)(nil), // 0: todolist.v1.CreateTodoListRequest + (*GetTodoListRequest)(nil), // 1: todolist.v1.GetTodoListRequest + (*GetTodoListResponse)(nil), // 2: todolist.v1.GetTodoListResponse + (*AddTodoItemRequest)(nil), // 3: todolist.v1.AddTodoItemRequest + (*MarkTodoItemAsDoneRequest)(nil), // 4: todolist.v1.MarkTodoItemAsDoneRequest + (*MarkTodoItemAsPendingRequest)(nil), // 5: todolist.v1.MarkTodoItemAsPendingRequest + (*DeleteTodoItemRequest)(nil), // 6: todolist.v1.DeleteTodoItemRequest + (*TodoList)(nil), // 7: todolist.v1.TodoList + (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 9: google.protobuf.Empty +} +var file_todolist_v1_todo_list_service_proto_depIdxs = []int32{ + 7, // 0: todolist.v1.GetTodoListResponse.todo_list:type_name -> todolist.v1.TodoList + 8, // 1: todolist.v1.AddTodoItemRequest.due_date:type_name -> google.protobuf.Timestamp + 0, // 2: todolist.v1.TodoListService.CreateTodoList:input_type -> todolist.v1.CreateTodoListRequest + 1, // 3: todolist.v1.TodoListService.GetTodoList:input_type -> todolist.v1.GetTodoListRequest + 3, // 4: todolist.v1.TodoListService.AddTodoItem:input_type -> todolist.v1.AddTodoItemRequest + 4, // 5: todolist.v1.TodoListService.MarkTodoItemAsDone:input_type -> todolist.v1.MarkTodoItemAsDoneRequest + 5, // 6: todolist.v1.TodoListService.MarkTodoItemAsPending:input_type -> todolist.v1.MarkTodoItemAsPendingRequest + 6, // 7: todolist.v1.TodoListService.DeleteTodoItem:input_type -> todolist.v1.DeleteTodoItemRequest + 9, // 8: todolist.v1.TodoListService.CreateTodoList:output_type -> google.protobuf.Empty + 2, // 9: todolist.v1.TodoListService.GetTodoList:output_type -> todolist.v1.GetTodoListResponse + 9, // 10: todolist.v1.TodoListService.AddTodoItem:output_type -> google.protobuf.Empty + 9, // 11: todolist.v1.TodoListService.MarkTodoItemAsDone:output_type -> google.protobuf.Empty + 9, // 12: todolist.v1.TodoListService.MarkTodoItemAsPending:output_type -> google.protobuf.Empty + 9, // 13: todolist.v1.TodoListService.DeleteTodoItem:output_type -> google.protobuf.Empty + 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_service_proto_init() } +func file_todolist_v1_todo_list_service_proto_init() { + if File_todolist_v1_todo_list_service_proto != nil { + return + } + file_todolist_v1_todo_list_proto_init() + if !protoimpl.UnsafeEnabled { + file_todolist_v1_todo_list_service_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_service_proto_msgTypes[1].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_service_proto_msgTypes[2].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_service_proto_msgTypes[3].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_service_proto_msgTypes[4].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_service_proto_msgTypes[5].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_service_proto_msgTypes[6].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 + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_todolist_v1_todo_list_service_proto_rawDesc, + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_todolist_v1_todo_list_service_proto_goTypes, + DependencyIndexes: file_todolist_v1_todo_list_service_proto_depIdxs, + MessageInfos: file_todolist_v1_todo_list_service_proto_msgTypes, + }.Build() + File_todolist_v1_todo_list_service_proto = out.File + file_todolist_v1_todo_list_service_proto_rawDesc = nil + file_todolist_v1_todo_list_service_proto_goTypes = nil + file_todolist_v1_todo_list_service_proto_depIdxs = nil +} diff --git a/examples/todolist/gen/todolist/v1/todolistv1connect/todo_list_service.connect.go b/examples/todolist/gen/todolist/v1/todolistv1connect/todo_list_service.connect.go new file mode 100644 index 00000000..9dd71bfb --- /dev/null +++ b/examples/todolist/gen/todolist/v1/todolistv1connect/todo_list_service.connect.go @@ -0,0 +1,253 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: todolist/v1/todo_list_service.proto + +package todolistv1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "github.com/get-eventually/go-eventually/examples/todolist/gen/todolist/v1" + emptypb "google.golang.org/protobuf/types/known/emptypb" + 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.IsAtLeastVersion0_1_0 + +const ( + // TodoListServiceName is the fully-qualified name of the TodoListService service. + TodoListServiceName = "todolist.v1.TodoListService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // TodoListServiceCreateTodoListProcedure is the fully-qualified name of the TodoListService's + // CreateTodoList RPC. + TodoListServiceCreateTodoListProcedure = "/todolist.v1.TodoListService/CreateTodoList" + // TodoListServiceGetTodoListProcedure is the fully-qualified name of the TodoListService's + // GetTodoList RPC. + TodoListServiceGetTodoListProcedure = "/todolist.v1.TodoListService/GetTodoList" + // TodoListServiceAddTodoItemProcedure is the fully-qualified name of the TodoListService's + // AddTodoItem RPC. + TodoListServiceAddTodoItemProcedure = "/todolist.v1.TodoListService/AddTodoItem" + // TodoListServiceMarkTodoItemAsDoneProcedure is the fully-qualified name of the TodoListService's + // MarkTodoItemAsDone RPC. + TodoListServiceMarkTodoItemAsDoneProcedure = "/todolist.v1.TodoListService/MarkTodoItemAsDone" + // TodoListServiceMarkTodoItemAsPendingProcedure is the fully-qualified name of the + // TodoListService's MarkTodoItemAsPending RPC. + TodoListServiceMarkTodoItemAsPendingProcedure = "/todolist.v1.TodoListService/MarkTodoItemAsPending" + // TodoListServiceDeleteTodoItemProcedure is the fully-qualified name of the TodoListService's + // DeleteTodoItem RPC. + TodoListServiceDeleteTodoItemProcedure = "/todolist.v1.TodoListService/DeleteTodoItem" +) + +// TodoListServiceClient is a client for the todolist.v1.TodoListService service. +type TodoListServiceClient interface { + // CreateTodoList creates a new TodoList with a client-generated ID. + CreateTodoList(context.Context, *connect.Request[v1.CreateTodoListRequest]) (*connect.Response[emptypb.Empty], error) + // GetTodoList fetches an existing TodoList by ID. + GetTodoList(context.Context, *connect.Request[v1.GetTodoListRequest]) (*connect.Response[v1.GetTodoListResponse], error) + // AddTodoItem appends a new item to an existing TodoList. + AddTodoItem(context.Context, *connect.Request[v1.AddTodoItemRequest]) (*connect.Response[emptypb.Empty], error) + // MarkTodoItemAsDone marks an existing item as completed. + MarkTodoItemAsDone(context.Context, *connect.Request[v1.MarkTodoItemAsDoneRequest]) (*connect.Response[emptypb.Empty], error) + // MarkTodoItemAsPending reverts a previous "mark as done". + MarkTodoItemAsPending(context.Context, *connect.Request[v1.MarkTodoItemAsPendingRequest]) (*connect.Response[emptypb.Empty], error) + // DeleteTodoItem removes an item from a TodoList. + DeleteTodoItem(context.Context, *connect.Request[v1.DeleteTodoItemRequest]) (*connect.Response[emptypb.Empty], 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.HTTPClient, baseURL string, opts ...connect.ClientOption) TodoListServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &todoListServiceClient{ + createTodoList: connect.NewClient[v1.CreateTodoListRequest, emptypb.Empty]( + httpClient, + baseURL+TodoListServiceCreateTodoListProcedure, + opts..., + ), + getTodoList: connect.NewClient[v1.GetTodoListRequest, v1.GetTodoListResponse]( + httpClient, + baseURL+TodoListServiceGetTodoListProcedure, + opts..., + ), + addTodoItem: connect.NewClient[v1.AddTodoItemRequest, emptypb.Empty]( + httpClient, + baseURL+TodoListServiceAddTodoItemProcedure, + opts..., + ), + markTodoItemAsDone: connect.NewClient[v1.MarkTodoItemAsDoneRequest, emptypb.Empty]( + httpClient, + baseURL+TodoListServiceMarkTodoItemAsDoneProcedure, + opts..., + ), + markTodoItemAsPending: connect.NewClient[v1.MarkTodoItemAsPendingRequest, emptypb.Empty]( + httpClient, + baseURL+TodoListServiceMarkTodoItemAsPendingProcedure, + opts..., + ), + deleteTodoItem: connect.NewClient[v1.DeleteTodoItemRequest, emptypb.Empty]( + httpClient, + baseURL+TodoListServiceDeleteTodoItemProcedure, + opts..., + ), + } +} + +// todoListServiceClient implements TodoListServiceClient. +type todoListServiceClient struct { + createTodoList *connect.Client[v1.CreateTodoListRequest, emptypb.Empty] + getTodoList *connect.Client[v1.GetTodoListRequest, v1.GetTodoListResponse] + addTodoItem *connect.Client[v1.AddTodoItemRequest, emptypb.Empty] + markTodoItemAsDone *connect.Client[v1.MarkTodoItemAsDoneRequest, emptypb.Empty] + markTodoItemAsPending *connect.Client[v1.MarkTodoItemAsPendingRequest, emptypb.Empty] + deleteTodoItem *connect.Client[v1.DeleteTodoItemRequest, emptypb.Empty] +} + +// CreateTodoList calls todolist.v1.TodoListService.CreateTodoList. +func (c *todoListServiceClient) CreateTodoList(ctx context.Context, req *connect.Request[v1.CreateTodoListRequest]) (*connect.Response[emptypb.Empty], error) { + return c.createTodoList.CallUnary(ctx, req) +} + +// GetTodoList calls todolist.v1.TodoListService.GetTodoList. +func (c *todoListServiceClient) GetTodoList(ctx context.Context, req *connect.Request[v1.GetTodoListRequest]) (*connect.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.Request[v1.AddTodoItemRequest]) (*connect.Response[emptypb.Empty], error) { + return c.addTodoItem.CallUnary(ctx, req) +} + +// MarkTodoItemAsDone calls todolist.v1.TodoListService.MarkTodoItemAsDone. +func (c *todoListServiceClient) MarkTodoItemAsDone(ctx context.Context, req *connect.Request[v1.MarkTodoItemAsDoneRequest]) (*connect.Response[emptypb.Empty], error) { + return c.markTodoItemAsDone.CallUnary(ctx, req) +} + +// MarkTodoItemAsPending calls todolist.v1.TodoListService.MarkTodoItemAsPending. +func (c *todoListServiceClient) MarkTodoItemAsPending(ctx context.Context, req *connect.Request[v1.MarkTodoItemAsPendingRequest]) (*connect.Response[emptypb.Empty], error) { + return c.markTodoItemAsPending.CallUnary(ctx, req) +} + +// DeleteTodoItem calls todolist.v1.TodoListService.DeleteTodoItem. +func (c *todoListServiceClient) DeleteTodoItem(ctx context.Context, req *connect.Request[v1.DeleteTodoItemRequest]) (*connect.Response[emptypb.Empty], error) { + return c.deleteTodoItem.CallUnary(ctx, req) +} + +// TodoListServiceHandler is an implementation of the todolist.v1.TodoListService service. +type TodoListServiceHandler interface { + // CreateTodoList creates a new TodoList with a client-generated ID. + CreateTodoList(context.Context, *connect.Request[v1.CreateTodoListRequest]) (*connect.Response[emptypb.Empty], error) + // GetTodoList fetches an existing TodoList by ID. + GetTodoList(context.Context, *connect.Request[v1.GetTodoListRequest]) (*connect.Response[v1.GetTodoListResponse], error) + // AddTodoItem appends a new item to an existing TodoList. + AddTodoItem(context.Context, *connect.Request[v1.AddTodoItemRequest]) (*connect.Response[emptypb.Empty], error) + // MarkTodoItemAsDone marks an existing item as completed. + MarkTodoItemAsDone(context.Context, *connect.Request[v1.MarkTodoItemAsDoneRequest]) (*connect.Response[emptypb.Empty], error) + // MarkTodoItemAsPending reverts a previous "mark as done". + MarkTodoItemAsPending(context.Context, *connect.Request[v1.MarkTodoItemAsPendingRequest]) (*connect.Response[emptypb.Empty], error) + // DeleteTodoItem removes an item from a TodoList. + DeleteTodoItem(context.Context, *connect.Request[v1.DeleteTodoItemRequest]) (*connect.Response[emptypb.Empty], 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.HandlerOption) (string, http.Handler) { + todoListServiceCreateTodoListHandler := connect.NewUnaryHandler( + TodoListServiceCreateTodoListProcedure, + svc.CreateTodoList, + opts..., + ) + todoListServiceGetTodoListHandler := connect.NewUnaryHandler( + TodoListServiceGetTodoListProcedure, + svc.GetTodoList, + opts..., + ) + todoListServiceAddTodoItemHandler := connect.NewUnaryHandler( + TodoListServiceAddTodoItemProcedure, + svc.AddTodoItem, + opts..., + ) + todoListServiceMarkTodoItemAsDoneHandler := connect.NewUnaryHandler( + TodoListServiceMarkTodoItemAsDoneProcedure, + svc.MarkTodoItemAsDone, + opts..., + ) + todoListServiceMarkTodoItemAsPendingHandler := connect.NewUnaryHandler( + TodoListServiceMarkTodoItemAsPendingProcedure, + svc.MarkTodoItemAsPending, + opts..., + ) + todoListServiceDeleteTodoItemHandler := connect.NewUnaryHandler( + TodoListServiceDeleteTodoItemProcedure, + svc.DeleteTodoItem, + opts..., + ) + return "/todolist.v1.TodoListService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case TodoListServiceCreateTodoListProcedure: + todoListServiceCreateTodoListHandler.ServeHTTP(w, r) + case TodoListServiceGetTodoListProcedure: + todoListServiceGetTodoListHandler.ServeHTTP(w, r) + case TodoListServiceAddTodoItemProcedure: + todoListServiceAddTodoItemHandler.ServeHTTP(w, r) + case TodoListServiceMarkTodoItemAsDoneProcedure: + todoListServiceMarkTodoItemAsDoneHandler.ServeHTTP(w, r) + case TodoListServiceMarkTodoItemAsPendingProcedure: + todoListServiceMarkTodoItemAsPendingHandler.ServeHTTP(w, r) + case TodoListServiceDeleteTodoItemProcedure: + todoListServiceDeleteTodoItemHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedTodoListServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedTodoListServiceHandler struct{} + +func (UnimplementedTodoListServiceHandler) CreateTodoList(context.Context, *connect.Request[v1.CreateTodoListRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("todolist.v1.TodoListService.CreateTodoList is not implemented")) +} + +func (UnimplementedTodoListServiceHandler) GetTodoList(context.Context, *connect.Request[v1.GetTodoListRequest]) (*connect.Response[v1.GetTodoListResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("todolist.v1.TodoListService.GetTodoList is not implemented")) +} + +func (UnimplementedTodoListServiceHandler) AddTodoItem(context.Context, *connect.Request[v1.AddTodoItemRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("todolist.v1.TodoListService.AddTodoItem is not implemented")) +} + +func (UnimplementedTodoListServiceHandler) MarkTodoItemAsDone(context.Context, *connect.Request[v1.MarkTodoItemAsDoneRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("todolist.v1.TodoListService.MarkTodoItemAsDone is not implemented")) +} + +func (UnimplementedTodoListServiceHandler) MarkTodoItemAsPending(context.Context, *connect.Request[v1.MarkTodoItemAsPendingRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("todolist.v1.TodoListService.MarkTodoItemAsPending is not implemented")) +} + +func (UnimplementedTodoListServiceHandler) DeleteTodoItem(context.Context, *connect.Request[v1.DeleteTodoItemRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.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 00000000..1d2f3408 --- /dev/null +++ b/examples/todolist/go.mod @@ -0,0 +1,26 @@ +module github.com/get-eventually/go-eventually/examples/todolist + +go 1.26.0 + +require ( + connectrpc.com/connect v1.19.2 + connectrpc.com/grpchealth v1.4.0 + connectrpc.com/grpcreflect v1.3.0 + github.com/get-eventually/go-eventually v0.4.0 + github.com/google/uuid v1.6.0 + github.com/kelseyhightower/envconfig v1.4.0 + golang.org/x/net v0.52.0 + google.golang.org/protobuf v1.36.11 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/stretchr/testify v1.11.1 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/text v0.36.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/todolist/go.sum b/examples/todolist/go.sum new file mode 100644 index 00000000..9fe77cb6 --- /dev/null +++ b/examples/todolist/go.sum @@ -0,0 +1,34 @@ +connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo= +connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +connectrpc.com/grpchealth v1.4.0 h1:MJC96JLelARPgZTiRF9KRfY/2N9OcoQvF2EWX07v2IE= +connectrpc.com/grpchealth v1.4.0/go.mod h1:WhW6m1EzTmq3Ky1FE8EfkIpSDc6TfUx2M2KqZO3ts/Q= +connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc= +connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/get-eventually/go-eventually v0.4.0 h1:YmTYVR1acQO0ZL/KPUiy/J6ETitBiqIvuoZJAGmWbm0= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +google.golang.org/genproto v0.0.0-20260420184626-e10c466a9529 h1:QoMBg0moLIlB/eucPzc+ID5SgPZWuirtjAn3l8nW2Dg= +google.golang.org/genproto v0.0.0-20260420184626-e10c466a9529/go.mod h1:EjLmDZ8liSLBrCTK5vP+bGIxRQHE3ovGvOI0CzGk1PI= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/todolist/internal/todolist/add_item_command.go b/examples/todolist/internal/todolist/add_item_command.go new file mode 100644 index 00000000..8fa1dad9 --- /dev/null +++ b/examples/todolist/internal/todolist/add_item_command.go @@ -0,0 +1,56 @@ +package todolist + +import ( + "context" + "fmt" + "time" + + "github.com/get-eventually/go-eventually/command" +) + +// AddItemCommand is the Command used to add a new Item to an existing TodoList. +type AddItemCommand struct { + TodoListID ID + TodoItemID ItemID + Title string + Description string + DueDate time.Time +} + +// Name implements message.Message. +func (AddItemCommand) Name() string { return "AddTodoListItem" } + +//nolint:exhaustruct // Interface implementation assertion. +var _ command.Handler[AddItemCommand] = AddItemCommandHandler{} + +// AddItemCommandHandler is the command.Handler for AddItemCommand commands. +type AddItemCommandHandler struct { + Clock func() time.Time + Repository Repository +} + +// Handle implements command.Handler. +func (h AddItemCommandHandler) Handle(ctx context.Context, cmd command.Envelope[AddItemCommand]) error { + tl, err := h.Repository.Get(ctx, cmd.Message.TodoListID) + if err != nil { + return fmt.Errorf("todolist.AddItemCommandHandler: failed to get TodoList from repository, %w", err) + } + + now := h.Clock() + + if err := tl.AddItem( + cmd.Message.TodoItemID, + cmd.Message.Title, + cmd.Message.Description, + cmd.Message.DueDate, + now, + ); err != nil { + return fmt.Errorf("todolist.AddItemCommandHandler: failed to add item to TodoList, %w", err) + } + + if err := h.Repository.Save(ctx, tl); err != nil { + return fmt.Errorf("todolist.AddItemCommandHandler: failed to save new TodoList version, %w", err) + } + + return nil +} diff --git a/examples/todolist/internal/todolist/add_item_command_test.go b/examples/todolist/internal/todolist/add_item_command_test.go new file mode 100644 index 00000000..2fae4ca2 --- /dev/null +++ b/examples/todolist/internal/todolist/add_item_command_test.go @@ -0,0 +1,128 @@ +package todolist_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/aggregate" + "github.com/get-eventually/go-eventually/command" + "github.com/get-eventually/go-eventually/event" + "github.com/get-eventually/go-eventually/examples/todolist/internal/todolist" +) + +func TestAddItemCommandHandler(t *testing.T) { + now := time.Now() + commandHandlerFactory := func(es event.Store) todolist.AddItemCommandHandler { + return todolist.AddItemCommandHandler{ + Clock: func() time.Time { return now }, + Repository: aggregate.NewEventSourcedRepository(es, todolist.Type), + } + } + + todoListID := todolist.ID(uuid.New()) + todoItemID := todolist.ItemID(uuid.New()) + + t.Run("it fails when the target TodoList does not exist", func(t *testing.T) { + command.Scenario[todolist.AddItemCommand, todolist.AddItemCommandHandler](). + When(command.ToEnvelope(todolist.AddItemCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + Title: "a todo item that should fail", + Description: "", + DueDate: time.Time{}, + })). + ThenError(aggregate.ErrRootNotFound). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when the same item has already been added", func(t *testing.T) { + command.Scenario[todolist.AddItemCommand, todolist.AddItemCommandHandler](). + Given(event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todoListID, + Title: testListTitle, + Owner: testListOwner, + 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", + Description: "", + DueDate: time.Time{}, + CreationTime: now, + }), + }). + When(command.ToEnvelope(todolist.AddItemCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + Title: "uh oh, this is gonna fail", + Description: "", + DueDate: time.Time{}, + })). + ThenError(todolist.ErrItemAlreadyExists). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when the item title is empty", func(t *testing.T) { + command.Scenario[todolist.AddItemCommand, todolist.AddItemCommandHandler](). + Given(event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todoListID, + Title: testListTitle, + Owner: testListOwner, + CreationTime: now.Add(-2 * time.Minute), + }), + }). + When(command.ToEnvelope(todolist.AddItemCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + Title: "", + Description: "", + DueDate: time.Time{}, + })). + ThenError(todolist.ErrEmptyItemTitle). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it works", func(t *testing.T) { + command.Scenario[todolist.AddItemCommand, todolist.AddItemCommandHandler](). + Given(event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todoListID, + Title: testListTitle, + Owner: testListOwner, + CreationTime: now.Add(-2 * time.Minute), + }), + }). + When(command.ToEnvelope(todolist.AddItemCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + Title: "a todo item that should succeed", + Description: "", + DueDate: time.Time{}, + })). + Then(event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 2, + Envelope: event.ToEnvelope(todolist.ItemWasAdded{ + ID: todoItemID, + Title: "a todo item that should succeed", + Description: "", + DueDate: time.Time{}, + CreationTime: now, + }), + }). + AssertOn(t, commandHandlerFactory) + }) +} diff --git a/examples/todolist/internal/todolist/connect_handler.go b/examples/todolist/internal/todolist/connect_handler.go new file mode 100644 index 00000000..8a9d634a --- /dev/null +++ b/examples/todolist/internal/todolist/connect_handler.go @@ -0,0 +1,239 @@ +package todolist + +import ( + "context" + "errors" + "fmt" + "time" + + connect "connectrpc.com/connect" + "github.com/google/uuid" + emptypb "google.golang.org/protobuf/types/known/emptypb" + + "github.com/get-eventually/go-eventually/aggregate" + "github.com/get-eventually/go-eventually/command" + todolistv1 "github.com/get-eventually/go-eventually/examples/todolist/gen/todolist/v1" + "github.com/get-eventually/go-eventually/examples/todolist/gen/todolist/v1/todolistv1connect" + "github.com/get-eventually/go-eventually/query" +) + +var _ todolistv1connect.TodoListServiceHandler = (*ConnectServiceHandler)(nil) + +// ConnectServiceHandler is the Connect transport for the TodoList service. +// +// Clients generate IDs for new resources and pass them in the request; the +// server responds to commands with google.protobuf.Empty. This keeps +// commands idempotent and free of response-payload coupling. +type ConnectServiceHandler struct { + todolistv1connect.UnimplementedTodoListServiceHandler + + GetQueryHandler GetQueryHandler + CreateCommandHandler CreateCommandHandler + AddItemCommandHandler AddItemCommandHandler + MarkItemAsDoneCommandHandler MarkItemAsDoneCommandHandler + MarkItemAsPendingCommandHandler MarkItemAsPendingCommandHandler + DeleteItemCommandHandler DeleteItemCommandHandler +} + +// parseUUIDField converts a string field into a uuid.UUID, returning an +// InvalidArgument Connect error on failure. +func parseUUIDField(field, value string) (uuid.UUID, error) { + id, err := uuid.Parse(value) + if err != nil { + return uuid.Nil, connect.NewError( + connect.CodeInvalidArgument, + fmt.Errorf("todolist.ConnectServiceHandler: failed to parse %s as uuid, %w", field, err), + ) + } + + return id, nil +} + +// mapCommandError classifies command-handler errors into Connect codes. +// +// The error is included verbatim (via %w) so clients can see the full chain. +// This is example-appropriate; production code would typically surface only +// a stable, sanitized message per code. +func mapCommandError(op string, err error) *connect.Error { + code := connect.CodeInternal + + switch { + case errors.Is(err, ErrEmptyID), + errors.Is(err, ErrEmptyTitle), + errors.Is(err, ErrNoOwnerSpecified), + errors.Is(err, ErrEmptyItemID), + errors.Is(err, ErrEmptyItemTitle): + code = connect.CodeInvalidArgument + + case errors.Is(err, ErrItemAlreadyExists): + code = connect.CodeAlreadyExists + + case errors.Is(err, ErrItemNotFound), + errors.Is(err, aggregate.ErrRootNotFound): + code = connect.CodeNotFound + } + + return connect.NewError(code, fmt.Errorf("%s: %w", op, err)) +} + +// CreateTodoList implements the Connect service handler. +func (srv *ConnectServiceHandler) CreateTodoList( + ctx context.Context, + req *connect.Request[todolistv1.CreateTodoListRequest], +) (*connect.Response[emptypb.Empty], error) { + id, err := parseUUIDField("todo_list_id", req.Msg.TodoListId) + if err != nil { + return nil, err + } + + cmd := command.ToEnvelope(CreateCommand{ + ID: ID(id), + Title: req.Msg.Title, + Owner: req.Msg.Owner, + }) + + if err := srv.CreateCommandHandler.Handle(ctx, cmd); err != nil { + return nil, mapCommandError("todolist.ConnectServiceHandler.CreateTodoList", err) + } + + return connect.NewResponse(&emptypb.Empty{}), nil +} + +// GetTodoList implements the Connect service handler. +func (srv *ConnectServiceHandler) GetTodoList( + ctx context.Context, + req *connect.Request[todolistv1.GetTodoListRequest], +) (*connect.Response[todolistv1.GetTodoListResponse], error) { + id, err := parseUUIDField("todo_list_id", req.Msg.TodoListId) + if err != nil { + return nil, err + } + + q := query.ToEnvelope(GetQuery{ID: ID(id)}) + + tl, err := srv.GetQueryHandler.Handle(ctx, q) + if err != nil { + return nil, mapCommandError("todolist.ConnectServiceHandler.GetTodoList", err) + } + + return connect.NewResponse(&todolistv1.GetTodoListResponse{ + TodoList: ToProto(tl), + }), nil +} + +// AddTodoItem implements the Connect service handler. +func (srv *ConnectServiceHandler) AddTodoItem( + ctx context.Context, + req *connect.Request[todolistv1.AddTodoItemRequest], +) (*connect.Response[emptypb.Empty], error) { + listID, err := parseUUIDField("todo_list_id", req.Msg.TodoListId) + if err != nil { + return nil, err + } + + itemID, err := parseUUIDField("todo_item_id", req.Msg.TodoItemId) + if err != nil { + return nil, err + } + + var dueDate time.Time + if req.Msg.DueDate != nil { + dueDate = req.Msg.DueDate.AsTime() + } + + cmd := command.ToEnvelope(AddItemCommand{ + TodoListID: ID(listID), + TodoItemID: ItemID(itemID), + Title: req.Msg.Title, + Description: req.Msg.Description, + DueDate: dueDate, + }) + + if err := srv.AddItemCommandHandler.Handle(ctx, cmd); err != nil { + return nil, mapCommandError("todolist.ConnectServiceHandler.AddTodoItem", err) + } + + return connect.NewResponse(&emptypb.Empty{}), nil +} + +// parseListAndItemIDs extracts and validates both UUID identifiers that +// appear in every per-item request. +func parseListAndItemIDs(todoListID, todoItemID string) (ID, ItemID, error) { + listID, err := parseUUIDField("todo_list_id", todoListID) + if err != nil { + return ID(uuid.Nil), ItemID(uuid.Nil), err + } + + itemID, err := parseUUIDField("todo_item_id", todoItemID) + if err != nil { + return ID(uuid.Nil), ItemID(uuid.Nil), err + } + + return ID(listID), ItemID(itemID), nil +} + +// MarkTodoItemAsDone implements the Connect service handler. +func (srv *ConnectServiceHandler) MarkTodoItemAsDone( + ctx context.Context, + req *connect.Request[todolistv1.MarkTodoItemAsDoneRequest], +) (*connect.Response[emptypb.Empty], error) { + listID, itemID, err := parseListAndItemIDs(req.Msg.TodoListId, req.Msg.TodoItemId) + if err != nil { + return nil, err + } + + cmd := command.ToEnvelope(MarkItemAsDoneCommand{ + TodoListID: listID, + TodoItemID: itemID, + }) + + if err := srv.MarkItemAsDoneCommandHandler.Handle(ctx, cmd); err != nil { + return nil, mapCommandError("todolist.ConnectServiceHandler.MarkTodoItemAsDone", err) + } + + return connect.NewResponse(&emptypb.Empty{}), nil +} + +// MarkTodoItemAsPending implements the Connect service handler. +func (srv *ConnectServiceHandler) MarkTodoItemAsPending( + ctx context.Context, + req *connect.Request[todolistv1.MarkTodoItemAsPendingRequest], +) (*connect.Response[emptypb.Empty], error) { + listID, itemID, err := parseListAndItemIDs(req.Msg.TodoListId, req.Msg.TodoItemId) + if err != nil { + return nil, err + } + + cmd := command.ToEnvelope(MarkItemAsPendingCommand{ + TodoListID: listID, + TodoItemID: itemID, + }) + + if err := srv.MarkItemAsPendingCommandHandler.Handle(ctx, cmd); err != nil { + return nil, mapCommandError("todolist.ConnectServiceHandler.MarkTodoItemAsPending", err) + } + + return connect.NewResponse(&emptypb.Empty{}), nil +} + +// DeleteTodoItem implements the Connect service handler. +func (srv *ConnectServiceHandler) DeleteTodoItem( + ctx context.Context, + req *connect.Request[todolistv1.DeleteTodoItemRequest], +) (*connect.Response[emptypb.Empty], error) { + listID, itemID, err := parseListAndItemIDs(req.Msg.TodoListId, req.Msg.TodoItemId) + if err != nil { + return nil, err + } + + cmd := command.ToEnvelope(DeleteItemCommand{ + TodoListID: listID, + TodoItemID: itemID, + }) + + if err := srv.DeleteItemCommandHandler.Handle(ctx, cmd); err != nil { + return nil, mapCommandError("todolist.ConnectServiceHandler.DeleteTodoItem", err) + } + + return connect.NewResponse(&emptypb.Empty{}), nil +} diff --git a/examples/todolist/internal/todolist/create_command.go b/examples/todolist/internal/todolist/create_command.go new file mode 100644 index 00000000..3e8e6ac7 --- /dev/null +++ b/examples/todolist/internal/todolist/create_command.go @@ -0,0 +1,44 @@ +package todolist + +import ( + "context" + "fmt" + "time" + + "github.com/get-eventually/go-eventually/command" +) + +// CreateCommand is the Command used to create a new TodoList. +type CreateCommand struct { + ID ID + Title string + Owner string +} + +// Name implements message.Message. +func (CreateCommand) Name() string { return "CreateTodoList" } + +//nolint:exhaustruct // Interface implementation assertion. +var _ command.Handler[CreateCommand] = CreateCommandHandler{} + +// CreateCommandHandler is the Command Handler for CreateCommand commands. +type CreateCommandHandler struct { + Clock func() time.Time + Repository Saver +} + +// Handle implements command.Handler. +func (h CreateCommandHandler) Handle(ctx context.Context, cmd command.Envelope[CreateCommand]) error { + now := h.Clock() + + tl, err := Create(cmd.Message.ID, cmd.Message.Title, cmd.Message.Owner, now) + if err != nil { + return fmt.Errorf("todolist.CreateCommandHandler: failed to create new todolist, %w", err) + } + + if err := h.Repository.Save(ctx, tl); err != nil { + return fmt.Errorf("todolist.CreateCommandHandler: failed to save todolist to repository, %w", err) + } + + return nil +} diff --git a/examples/todolist/internal/todolist/create_command_test.go b/examples/todolist/internal/todolist/create_command_test.go new file mode 100644 index 00000000..3574fb05 --- /dev/null +++ b/examples/todolist/internal/todolist/create_command_test.go @@ -0,0 +1,79 @@ +package todolist_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/aggregate" + "github.com/get-eventually/go-eventually/command" + "github.com/get-eventually/go-eventually/event" + "github.com/get-eventually/go-eventually/examples/todolist/internal/todolist" +) + +func TestCreateCommandHandler(t *testing.T) { + id := uuid.New() + now := time.Now() + clock := func() time.Time { return now } + + commandHandlerFactory := func(s event.Store) todolist.CreateCommandHandler { + return todolist.CreateCommandHandler{ + Clock: clock, + Repository: aggregate.NewEventSourcedRepository(s, todolist.Type), + } + } + + t.Run("it fails when an invalid id has been provided", func(t *testing.T) { + command.Scenario[todolist.CreateCommand, todolist.CreateCommandHandler](). + When(command.ToEnvelope(todolist.CreateCommand{ + 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) { + command.Scenario[todolist.CreateCommand, todolist.CreateCommandHandler](). + When(command.ToEnvelope(todolist.CreateCommand{ + 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) { + command.Scenario[todolist.CreateCommand, todolist.CreateCommandHandler](). + When(command.ToEnvelope(todolist.CreateCommand{ + ID: todolist.ID(id), + Title: "my-title", + Owner: "", + })). + ThenError(todolist.ErrNoOwnerSpecified). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it works", func(t *testing.T) { + command.Scenario[todolist.CreateCommand, todolist.CreateCommandHandler](). + When(command.ToEnvelope(todolist.CreateCommand{ + ID: todolist.ID(id), + Title: "my-title", + Owner: "owner", + })). + Then(event.Persisted{ + StreamID: event.StreamID(todolist.ID(id).String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todolist.ID(id), + Title: "my-title", + Owner: "owner", + CreationTime: now, + }), + }). + AssertOn(t, commandHandlerFactory) + }) +} diff --git a/examples/todolist/internal/todolist/delete_item_command.go b/examples/todolist/internal/todolist/delete_item_command.go new file mode 100644 index 00000000..475e57a9 --- /dev/null +++ b/examples/todolist/internal/todolist/delete_item_command.go @@ -0,0 +1,47 @@ +package todolist + +import ( + "context" + "fmt" + + "github.com/get-eventually/go-eventually/command" +) + +// DeleteItemCommand is the Command used to remove an Item from a TodoList. +type DeleteItemCommand struct { + TodoListID ID + TodoItemID ItemID +} + +// Name implements message.Message. +func (DeleteItemCommand) Name() string { return "DeleteTodoListItem" } + +//nolint:exhaustruct // Interface implementation assertion. +var _ command.Handler[DeleteItemCommand] = DeleteItemCommandHandler{} + +// DeleteItemCommandHandler is the command.Handler for DeleteItemCommand +// commands. +type DeleteItemCommandHandler struct { + Repository Repository +} + +// Handle implements command.Handler. +func (h DeleteItemCommandHandler) Handle( + ctx context.Context, + cmd command.Envelope[DeleteItemCommand], +) error { + tl, err := h.Repository.Get(ctx, cmd.Message.TodoListID) + if err != nil { + return fmt.Errorf("todolist.DeleteItemCommandHandler: failed to get TodoList from repository, %w", err) + } + + if err := tl.DeleteItem(cmd.Message.TodoItemID); err != nil { + return fmt.Errorf("todolist.DeleteItemCommandHandler: failed to delete item, %w", err) + } + + if err := h.Repository.Save(ctx, tl); err != nil { + return fmt.Errorf("todolist.DeleteItemCommandHandler: failed to save new TodoList version, %w", err) + } + + return nil +} diff --git a/examples/todolist/internal/todolist/delete_item_command_test.go b/examples/todolist/internal/todolist/delete_item_command_test.go new file mode 100644 index 00000000..5cee26af --- /dev/null +++ b/examples/todolist/internal/todolist/delete_item_command_test.go @@ -0,0 +1,96 @@ +package todolist_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/aggregate" + "github.com/get-eventually/go-eventually/command" + "github.com/get-eventually/go-eventually/event" + "github.com/get-eventually/go-eventually/examples/todolist/internal/todolist" +) + +func TestDeleteItemCommandHandler(t *testing.T) { + now := time.Now() + commandHandlerFactory := func(es event.Store) todolist.DeleteItemCommandHandler { + return todolist.DeleteItemCommandHandler{ + Repository: aggregate.NewEventSourcedRepository(es, todolist.Type), + } + } + + todoListID := todolist.ID(uuid.New()) + todoItemID := todolist.ItemID(uuid.New()) + + listCreated := event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todoListID, + Title: testListTitle, + Owner: testListOwner, + CreationTime: now.Add(-2 * time.Minute), + }), + } + itemAdded := event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 2, + Envelope: event.ToEnvelope(todolist.ItemWasAdded{ + ID: todoItemID, + Title: "buy groceries", + Description: "", + DueDate: time.Time{}, + CreationTime: now.Add(-time.Minute), + }), + } + + t.Run("it fails when the target TodoList does not exist", func(t *testing.T) { + command.Scenario[todolist.DeleteItemCommand, todolist.DeleteItemCommandHandler](). + When(command.ToEnvelope(todolist.DeleteItemCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + })). + ThenError(aggregate.ErrRootNotFound). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when the item is not in the list", func(t *testing.T) { + command.Scenario[todolist.DeleteItemCommand, todolist.DeleteItemCommandHandler](). + Given(listCreated). + When(command.ToEnvelope(todolist.DeleteItemCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + })). + ThenError(todolist.ErrItemNotFound). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when the item ID is empty", func(t *testing.T) { + command.Scenario[todolist.DeleteItemCommand, todolist.DeleteItemCommandHandler](). + Given(listCreated). + When(command.ToEnvelope(todolist.DeleteItemCommand{ + TodoListID: todoListID, + TodoItemID: todolist.ItemID(uuid.Nil), + })). + ThenError(todolist.ErrEmptyItemID). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it works", func(t *testing.T) { + command.Scenario[todolist.DeleteItemCommand, todolist.DeleteItemCommandHandler](). + Given(listCreated, itemAdded). + When(command.ToEnvelope(todolist.DeleteItemCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + })). + Then(event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 3, + Envelope: event.ToEnvelope(todolist.ItemWasDeleted{ + ID: todoItemID, + }), + }). + AssertOn(t, commandHandlerFactory) + }) +} diff --git a/examples/todolist/internal/todolist/event.go b/examples/todolist/internal/todolist/event.go new file mode 100644 index 00000000..59d5e70a --- /dev/null +++ b/examples/todolist/internal/todolist/event.go @@ -0,0 +1,54 @@ +package todolist + +import "time" + +// WasCreated is the Domain Event issued when a 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". +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/todolist/fixtures_test.go b/examples/todolist/internal/todolist/fixtures_test.go new file mode 100644 index 00000000..dfd86e97 --- /dev/null +++ b/examples/todolist/internal/todolist/fixtures_test.go @@ -0,0 +1,7 @@ +package todolist_test + +// Shared fixture values used across command scenario tests. +const ( + testListTitle = "my list" + testListOwner = "me" +) diff --git a/examples/todolist/internal/todolist/get_query.go b/examples/todolist/internal/todolist/get_query.go new file mode 100644 index 00000000..6ef99982 --- /dev/null +++ b/examples/todolist/internal/todolist/get_query.go @@ -0,0 +1,44 @@ +package todolist + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/query" +) + +// GetQuery is the Domain Query used to return a TodoList view. +type GetQuery struct { + ID ID +} + +// Name implements message.Message. +func (GetQuery) Name() string { return "GetTodoList" } + +//nolint:exhaustruct // Interface implementation assertion. +var _ query.Handler[GetQuery, *TodoList] = GetQueryHandler{} + +// GetQueryHandler handles a GetQuery by returning the TodoList specified +// by the query's ID. +type GetQueryHandler struct { + Getter Getter +} + +// Handle implements query.Handler. +func (h GetQueryHandler) Handle( + ctx context.Context, + q query.Envelope[GetQuery], +) (*TodoList, error) { + if q.Message.ID == ID(uuid.Nil) { + return nil, fmt.Errorf("todolist.GetQueryHandler: invalid query provided, %w", ErrEmptyID) + } + + tl, err := h.Getter.Get(ctx, q.Message.ID) + if err != nil { + return nil, fmt.Errorf("todolist.GetQueryHandler: failed to get TodoList from repository, %w", err) + } + + return tl, nil +} diff --git a/examples/todolist/internal/todolist/item.go b/examples/todolist/internal/todolist/item.go new file mode 100644 index 00000000..d531bd63 --- /dev/null +++ b/examples/todolist/internal/todolist/item.go @@ -0,0 +1,54 @@ +package todolist + +import ( + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/aggregate" + "github.com/get-eventually/go-eventually/event" +) + +// ItemID is the unique identifier type for a Todo Item. +type ItemID uuid.UUID + +// String returns the canonical UUID string representation of the ItemID. +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.Aggregate. +func (item *Item) Apply(evt event.Event) error { + switch evt := evt.(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/todolist/mark_item_as_done_command.go b/examples/todolist/internal/todolist/mark_item_as_done_command.go new file mode 100644 index 00000000..6409d640 --- /dev/null +++ b/examples/todolist/internal/todolist/mark_item_as_done_command.go @@ -0,0 +1,48 @@ +package todolist + +import ( + "context" + "fmt" + + "github.com/get-eventually/go-eventually/command" +) + +// MarkItemAsDoneCommand is the Command used to mark an Item in a TodoList +// as completed. +type MarkItemAsDoneCommand struct { + TodoListID ID + TodoItemID ItemID +} + +// Name implements message.Message. +func (MarkItemAsDoneCommand) Name() string { return "MarkTodoListItemAsDone" } + +//nolint:exhaustruct // Interface implementation assertion. +var _ command.Handler[MarkItemAsDoneCommand] = MarkItemAsDoneCommandHandler{} + +// MarkItemAsDoneCommandHandler is the command.Handler for +// MarkItemAsDoneCommand commands. +type MarkItemAsDoneCommandHandler struct { + Repository Repository +} + +// Handle implements command.Handler. +func (h MarkItemAsDoneCommandHandler) Handle( + ctx context.Context, + cmd command.Envelope[MarkItemAsDoneCommand], +) error { + tl, err := h.Repository.Get(ctx, cmd.Message.TodoListID) + if err != nil { + return fmt.Errorf("todolist.MarkItemAsDoneCommandHandler: failed to get TodoList from repository, %w", err) + } + + if err := tl.MarkItemAsDone(cmd.Message.TodoItemID); err != nil { + return fmt.Errorf("todolist.MarkItemAsDoneCommandHandler: failed to mark item as done, %w", err) + } + + if err := h.Repository.Save(ctx, tl); err != nil { + return fmt.Errorf("todolist.MarkItemAsDoneCommandHandler: failed to save new TodoList version, %w", err) + } + + return nil +} diff --git a/examples/todolist/internal/todolist/mark_item_as_done_command_test.go b/examples/todolist/internal/todolist/mark_item_as_done_command_test.go new file mode 100644 index 00000000..734b5c9d --- /dev/null +++ b/examples/todolist/internal/todolist/mark_item_as_done_command_test.go @@ -0,0 +1,96 @@ +package todolist_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/aggregate" + "github.com/get-eventually/go-eventually/command" + "github.com/get-eventually/go-eventually/event" + "github.com/get-eventually/go-eventually/examples/todolist/internal/todolist" +) + +func TestMarkItemAsDoneCommandHandler(t *testing.T) { + now := time.Now() + commandHandlerFactory := func(es event.Store) todolist.MarkItemAsDoneCommandHandler { + return todolist.MarkItemAsDoneCommandHandler{ + Repository: aggregate.NewEventSourcedRepository(es, todolist.Type), + } + } + + todoListID := todolist.ID(uuid.New()) + todoItemID := todolist.ItemID(uuid.New()) + + listCreated := event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todoListID, + Title: testListTitle, + Owner: testListOwner, + CreationTime: now.Add(-2 * time.Minute), + }), + } + itemAdded := event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 2, + Envelope: event.ToEnvelope(todolist.ItemWasAdded{ + ID: todoItemID, + Title: "buy groceries", + Description: "", + DueDate: time.Time{}, + CreationTime: now.Add(-time.Minute), + }), + } + + t.Run("it fails when the target TodoList does not exist", func(t *testing.T) { + command.Scenario[todolist.MarkItemAsDoneCommand, todolist.MarkItemAsDoneCommandHandler](). + When(command.ToEnvelope(todolist.MarkItemAsDoneCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + })). + ThenError(aggregate.ErrRootNotFound). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when the item is not in the list", func(t *testing.T) { + command.Scenario[todolist.MarkItemAsDoneCommand, todolist.MarkItemAsDoneCommandHandler](). + Given(listCreated). + When(command.ToEnvelope(todolist.MarkItemAsDoneCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + })). + ThenError(todolist.ErrItemNotFound). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when the item ID is empty", func(t *testing.T) { + command.Scenario[todolist.MarkItemAsDoneCommand, todolist.MarkItemAsDoneCommandHandler](). + Given(listCreated). + When(command.ToEnvelope(todolist.MarkItemAsDoneCommand{ + TodoListID: todoListID, + TodoItemID: todolist.ItemID(uuid.Nil), + })). + ThenError(todolist.ErrEmptyItemID). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it works", func(t *testing.T) { + command.Scenario[todolist.MarkItemAsDoneCommand, todolist.MarkItemAsDoneCommandHandler](). + Given(listCreated, itemAdded). + When(command.ToEnvelope(todolist.MarkItemAsDoneCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + })). + Then(event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 3, + Envelope: event.ToEnvelope(todolist.ItemMarkedAsDone{ + ID: todoItemID, + }), + }). + AssertOn(t, commandHandlerFactory) + }) +} diff --git a/examples/todolist/internal/todolist/mark_item_as_pending_command.go b/examples/todolist/internal/todolist/mark_item_as_pending_command.go new file mode 100644 index 00000000..3a279a2d --- /dev/null +++ b/examples/todolist/internal/todolist/mark_item_as_pending_command.go @@ -0,0 +1,48 @@ +package todolist + +import ( + "context" + "fmt" + + "github.com/get-eventually/go-eventually/command" +) + +// MarkItemAsPendingCommand is the Command used to mark an Item in a +// TodoList as pending (i.e. undoing a previous "mark as done"). +type MarkItemAsPendingCommand struct { + TodoListID ID + TodoItemID ItemID +} + +// Name implements message.Message. +func (MarkItemAsPendingCommand) Name() string { return "MarkTodoListItemAsPending" } + +//nolint:exhaustruct // Interface implementation assertion. +var _ command.Handler[MarkItemAsPendingCommand] = MarkItemAsPendingCommandHandler{} + +// MarkItemAsPendingCommandHandler is the command.Handler for +// MarkItemAsPendingCommand commands. +type MarkItemAsPendingCommandHandler struct { + Repository Repository +} + +// Handle implements command.Handler. +func (h MarkItemAsPendingCommandHandler) Handle( + ctx context.Context, + cmd command.Envelope[MarkItemAsPendingCommand], +) error { + tl, err := h.Repository.Get(ctx, cmd.Message.TodoListID) + if err != nil { + return fmt.Errorf("todolist.MarkItemAsPendingCommandHandler: failed to get TodoList from repository, %w", err) + } + + if err := tl.MarkItemAsPending(cmd.Message.TodoItemID); err != nil { + return fmt.Errorf("todolist.MarkItemAsPendingCommandHandler: failed to mark item as pending, %w", err) + } + + if err := h.Repository.Save(ctx, tl); err != nil { + return fmt.Errorf("todolist.MarkItemAsPendingCommandHandler: failed to save new TodoList version, %w", err) + } + + return nil +} diff --git a/examples/todolist/internal/todolist/mark_item_as_pending_command_test.go b/examples/todolist/internal/todolist/mark_item_as_pending_command_test.go new file mode 100644 index 00000000..7a3dca6b --- /dev/null +++ b/examples/todolist/internal/todolist/mark_item_as_pending_command_test.go @@ -0,0 +1,103 @@ +package todolist_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/aggregate" + "github.com/get-eventually/go-eventually/command" + "github.com/get-eventually/go-eventually/event" + "github.com/get-eventually/go-eventually/examples/todolist/internal/todolist" +) + +func TestMarkItemAsPendingCommandHandler(t *testing.T) { + now := time.Now() + commandHandlerFactory := func(es event.Store) todolist.MarkItemAsPendingCommandHandler { + return todolist.MarkItemAsPendingCommandHandler{ + Repository: aggregate.NewEventSourcedRepository(es, todolist.Type), + } + } + + todoListID := todolist.ID(uuid.New()) + todoItemID := todolist.ItemID(uuid.New()) + + listCreated := event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 1, + Envelope: event.ToEnvelope(todolist.WasCreated{ + ID: todoListID, + Title: testListTitle, + Owner: testListOwner, + CreationTime: now.Add(-2 * time.Minute), + }), + } + itemAdded := event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 2, + Envelope: event.ToEnvelope(todolist.ItemWasAdded{ + ID: todoItemID, + Title: "buy groceries", + Description: "", + DueDate: time.Time{}, + CreationTime: now.Add(-time.Minute), + }), + } + itemMarkedAsDone := event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 3, + Envelope: event.ToEnvelope(todolist.ItemMarkedAsDone{ + ID: todoItemID, + }), + } + + t.Run("it fails when the target TodoList does not exist", func(t *testing.T) { + command.Scenario[todolist.MarkItemAsPendingCommand, todolist.MarkItemAsPendingCommandHandler](). + When(command.ToEnvelope(todolist.MarkItemAsPendingCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + })). + ThenError(aggregate.ErrRootNotFound). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when the item is not in the list", func(t *testing.T) { + command.Scenario[todolist.MarkItemAsPendingCommand, todolist.MarkItemAsPendingCommandHandler](). + Given(listCreated). + When(command.ToEnvelope(todolist.MarkItemAsPendingCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + })). + ThenError(todolist.ErrItemNotFound). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it fails when the item ID is empty", func(t *testing.T) { + command.Scenario[todolist.MarkItemAsPendingCommand, todolist.MarkItemAsPendingCommandHandler](). + Given(listCreated). + When(command.ToEnvelope(todolist.MarkItemAsPendingCommand{ + TodoListID: todoListID, + TodoItemID: todolist.ItemID(uuid.Nil), + })). + ThenError(todolist.ErrEmptyItemID). + AssertOn(t, commandHandlerFactory) + }) + + t.Run("it works after an item has been marked as done", func(t *testing.T) { + command.Scenario[todolist.MarkItemAsPendingCommand, todolist.MarkItemAsPendingCommandHandler](). + Given(listCreated, itemAdded, itemMarkedAsDone). + When(command.ToEnvelope(todolist.MarkItemAsPendingCommand{ + TodoListID: todoListID, + TodoItemID: todoItemID, + })). + Then(event.Persisted{ + StreamID: event.StreamID(todoListID.String()), + Version: 4, + Envelope: event.ToEnvelope(todolist.ItemMarkedAsPending{ + ID: todoItemID, + }), + }). + AssertOn(t, commandHandlerFactory) + }) +} diff --git a/examples/todolist/internal/todolist/proto.go b/examples/todolist/internal/todolist/proto.go new file mode 100644 index 00000000..2c183c4f --- /dev/null +++ b/examples/todolist/internal/todolist/proto.go @@ -0,0 +1,37 @@ +package todolist + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + todolistv1 "github.com/get-eventually/go-eventually/examples/todolist/gen/todolist/v1" +) + +// ToProto converts a TodoList into its generated Protobuf counterpart. +func ToProto(tl *TodoList) *todolistv1.TodoList { + result := &todolistv1.TodoList{ + Id: tl.ID.String(), + Title: tl.Title, + Owner: tl.Owner, + CreationTime: timestamppb.New(tl.CreationTime), + Items: make([]*todolistv1.TodoItem, 0, len(tl.Items)), + } + + for _, item := range tl.Items { + pbItem := &todolistv1.TodoItem{ + Id: item.ID.String(), + Title: item.Title, + Description: item.Description, + Completed: item.Completed, + DueDate: nil, + CreationTime: timestamppb.New(item.CreationTime), + } + + if !item.DueDate.IsZero() { + pbItem.DueDate = timestamppb.New(item.DueDate) + } + + result.Items = append(result.Items, pbItem) + } + + return result +} diff --git a/examples/todolist/internal/todolist/repository.go b/examples/todolist/internal/todolist/repository.go new file mode 100644 index 00000000..5fd33645 --- /dev/null +++ b/examples/todolist/internal/todolist/repository.go @@ -0,0 +1,14 @@ +package todolist + +import "github.com/get-eventually/go-eventually/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/todolist/todolist.go b/examples/todolist/internal/todolist/todolist.go new file mode 100644 index 00000000..dce6d584 --- /dev/null +++ b/examples/todolist/internal/todolist/todolist.go @@ -0,0 +1,253 @@ +// Package todolist contains everything related to the TodoList bounded +// context: the aggregate root, its domain events, commands, queries, and +// their handlers. +// +// This package follows the "package by domain" convention: all artifacts +// that belong to the TodoList context live together, regardless of whether +// they are conceptually on the command, query, or domain side. Naming is +// used to keep the boundary clear (e.g. CreateCommand / GetQuery), taking +// advantage of the package prefix to avoid repeating "TodoList" in every +// identifier. +package todolist + +import ( + "errors" + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/aggregate" + "github.com/get-eventually/go-eventually/event" +) + +// ID is the unique identifier for a TodoList. +type ID uuid.UUID + +// String returns the canonical UUID string representation of the ID. +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 errors.New("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.Aggregate. +func (tl *TodoList) Apply(evt event.Event) error { + switch evt := evt.(type) { + case WasCreated: + tl.ID = evt.ID + tl.Title = evt.Title + tl.Owner = evt.Owner + tl.CreationTime = evt.CreationTime + + case ItemWasAdded: + item := &Item{} //nolint:exhaustruct // Applied below. + if err := item.Apply(evt); err != nil { + return fmt.Errorf("todolist.TodoList.Apply: failed to apply item event, %w", err) + } + + tl.Items = append(tl.Items, item) + + case ItemMarkedAsDone: + return tl.applyItemEvent(evt.ID, evt) + + case ItemMarkedAsPending: + return tl.applyItemEvent(evt.ID, evt) + + case ItemWasDeleted: + items := make([]*Item, 0, len(tl.Items)) + + 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/todolist/todolist_test.go b/examples/todolist/internal/todolist/todolist_test.go new file mode 100644 index 00000000..aa7997c5 --- /dev/null +++ b/examples/todolist/internal/todolist/todolist_test.go @@ -0,0 +1,59 @@ +package todolist_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + + "github.com/get-eventually/go-eventually/aggregate" + "github.com/get-eventually/go-eventually/event" + "github.com/get-eventually/go-eventually/examples/todolist/internal/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()) + + aggregate.Scenario(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", + Description: "", + DueDate: time.Time{}, + CreationTime: now, + }), event.ToEnvelope(todolist.ItemMarkedAsDone{ + ID: todoItemID, + }), event.ToEnvelope(todolist.ItemWasDeleted{ + ID: todoItemID, + })). + AssertOn(t) + }) +} diff --git a/examples/todolist/main.go b/examples/todolist/main.go new file mode 100644 index 00000000..8497f29d --- /dev/null +++ b/examples/todolist/main.go @@ -0,0 +1,152 @@ +// Package main is the entrypoint for the TodoList Connect service example. +// +// The service is backed by an in-memory event.Store: state is lost on +// restart. This example is about showcasing how to wire the +// go-eventually building blocks together, not about persistence. +package main + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + connectgrpchealth "connectrpc.com/grpchealth" + connectgrpcreflect "connectrpc.com/grpcreflect" + "github.com/kelseyhightower/envconfig" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + + "github.com/get-eventually/go-eventually/aggregate" + "github.com/get-eventually/go-eventually/event" + "github.com/get-eventually/go-eventually/examples/todolist/gen/todolist/v1/todolistv1connect" + "github.com/get-eventually/go-eventually/examples/todolist/internal/todolist" +) + +type serverConfig struct { + Address string `default:":8080" envconfig:"ADDRESS"` + ReadTimeout time.Duration `default:"10s" envconfig:"READ_TIMEOUT"` + WriteTimeout time.Duration `default:"10s" envconfig:"WRITE_TIMEOUT"` + ShutdownTimeout time.Duration `default:"15s" envconfig:"SHUTDOWN_TIMEOUT"` +} + +type config struct { + Server serverConfig +} + +func parseConfig() (config, error) { + var cfg config + if err := envconfig.Process("", &cfg); err != nil { + return config{}, fmt.Errorf("failed to parse config from env, %w", err) + } + + return cfg, nil +} + +func run() error { //nolint:funlen // Single linear wire-up of the service; splitting hurts readability. + cfg, err := parseConfig() + if err != nil { + return err + } + + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelDebug, + AddSource: false, + ReplaceAttr: nil, + })) + slog.SetDefault(logger) + + // In-memory plumbing: a single Store feeds both the command and query + // sides through an EventSourcedRepository. + eventStore := event.NewInMemoryStore() + todoListRepository := aggregate.NewEventSourcedRepository(eventStore, todolist.Type) + + server := &todolist.ConnectServiceHandler{ + UnimplementedTodoListServiceHandler: todolistv1connect.UnimplementedTodoListServiceHandler{}, + GetQueryHandler: todolist.GetQueryHandler{ + Getter: todoListRepository, + }, + CreateCommandHandler: todolist.CreateCommandHandler{ + Clock: time.Now, + Repository: todoListRepository, + }, + AddItemCommandHandler: todolist.AddItemCommandHandler{ + Clock: time.Now, + Repository: todoListRepository, + }, + MarkItemAsDoneCommandHandler: todolist.MarkItemAsDoneCommandHandler{ + Repository: todoListRepository, + }, + MarkItemAsPendingCommandHandler: todolist.MarkItemAsPendingCommandHandler{ + Repository: todoListRepository, + }, + DeleteItemCommandHandler: todolist.DeleteItemCommandHandler{ + Repository: todoListRepository, + }, + } + + mux := http.NewServeMux() + mux.Handle(todolistv1connect.NewTodoListServiceHandler(server)) + mux.Handle(connectgrpchealth.NewHandler( + connectgrpchealth.NewStaticChecker(todolistv1connect.TodoListServiceName), + )) + mux.Handle(connectgrpcreflect.NewHandlerV1( + connectgrpcreflect.NewStaticReflector(todolistv1connect.TodoListServiceName), + )) + mux.Handle(connectgrpcreflect.NewHandlerV1Alpha( + connectgrpcreflect.NewStaticReflector(todolistv1connect.TodoListServiceName), + )) + + srv := &http.Server{ //nolint:exhaustruct // Stdlib struct with many optional fields; defaults are fine. + Addr: cfg.Server.Address, + Handler: h2c.NewHandler(mux, &http2.Server{}), //nolint:exhaustruct // h2c.Server defaults are fine. + ReadTimeout: cfg.Server.ReadTimeout, + WriteTimeout: cfg.Server.WriteTimeout, + ReadHeaderTimeout: cfg.Server.ReadTimeout, + } + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + serverErrs := make(chan error, 1) + + go func() { + logger.Info("connect server started", slog.String("address", cfg.Server.Address)) + + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + serverErrs <- fmt.Errorf("connect server exited unexpectedly, %w", err) + + return + } + + serverErrs <- nil + }() + + select { + case <-ctx.Done(): + logger.Info("shutdown signal received") + case err := <-serverErrs: + return err + } + + shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.Server.ShutdownTimeout) + defer cancel() + + if err := srv.Shutdown(shutdownCtx); err != nil { + return fmt.Errorf("graceful shutdown failed, %w", err) + } + + return nil +} + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, "todolist:", err) + os.Exit(1) + } +} diff --git a/examples/todolist/proto/todolist/v1/todo_item.proto b/examples/todolist/proto/todolist/v1/todo_item.proto new file mode 100644 index 00000000..cf596213 --- /dev/null +++ b/examples/todolist/proto/todolist/v1/todo_item.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package todolist.v1; + +import "google/protobuf/timestamp.proto"; + +// TodoItem is a single entry in a TodoList. +message TodoItem { + // Unique identifier (UUID) for this item. + string id = 1; + // Short title of the item. + string title = 2; + // Free-form description of the item. + string description = 3; + // Whether the item is completed. + bool completed = 4; + // Optional due date; zero-valued if not set. + google.protobuf.Timestamp due_date = 5; + // When the item was added to its list. + google.protobuf.Timestamp creation_time = 6; +} diff --git a/examples/todolist/proto/todolist/v1/todo_list.proto b/examples/todolist/proto/todolist/v1/todo_list.proto new file mode 100644 index 00000000..42180627 --- /dev/null +++ b/examples/todolist/proto/todolist/v1/todo_list.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package todolist.v1; + +import "google/protobuf/timestamp.proto"; +import "todolist/v1/todo_item.proto"; + +// TodoList is a list of TodoItems belonging to an owner. +message TodoList { + // Unique identifier (UUID) for this list. + string id = 1; + // Display title of the list. + string title = 2; + // Owner identifier (application-defined, typically a username). + string owner = 3; + // When the list was created. + google.protobuf.Timestamp creation_time = 4; + // Items currently in the list, in insertion order. + repeated TodoItem items = 5; +} diff --git a/examples/todolist/proto/todolist/v1/todo_list_service.proto b/examples/todolist/proto/todolist/v1/todo_list_service.proto new file mode 100644 index 00000000..a57020d9 --- /dev/null +++ b/examples/todolist/proto/todolist/v1/todo_list_service.proto @@ -0,0 +1,93 @@ +syntax = "proto3"; + +package todolist.v1; + +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +import "todolist/v1/todo_list.proto"; + +// TodoListService exposes TodoList operations over Connect. +// +// Commands return google.protobuf.Empty; they never produce response +// payloads. Queries return a dedicated response message. +service TodoListService { + // CreateTodoList creates a new TodoList with a client-generated ID. + rpc CreateTodoList(CreateTodoListRequest) returns (google.protobuf.Empty); + // GetTodoList fetches an existing TodoList by ID. + rpc GetTodoList(GetTodoListRequest) returns (GetTodoListResponse); + // AddTodoItem appends a new item to an existing TodoList. + rpc AddTodoItem(AddTodoItemRequest) returns (google.protobuf.Empty); + // MarkTodoItemAsDone marks an existing item as completed. + rpc MarkTodoItemAsDone(MarkTodoItemAsDoneRequest) returns (google.protobuf.Empty); + // MarkTodoItemAsPending reverts a previous "mark as done". + rpc MarkTodoItemAsPending(MarkTodoItemAsPendingRequest) returns (google.protobuf.Empty); + // DeleteTodoItem removes an item from a TodoList. + rpc DeleteTodoItem(DeleteTodoItemRequest) returns (google.protobuf.Empty); +} + +// CreateTodoListRequest creates a new TodoList with the specified ID. +// +// The todo_list_id is client-generated: clients are expected to generate a +// UUID and pass it in the request. This keeps the command idempotent and +// frees the response of a payload. +message CreateTodoListRequest { + // Client-generated UUID for the new list. + string todo_list_id = 1; + // Display title of the list. + string title = 2; + // Owner identifier. + string owner = 3; +} + +// GetTodoListRequest fetches a TodoList by its ID. +message GetTodoListRequest { + // UUID of the list to fetch. + string todo_list_id = 1; +} + +// GetTodoListResponse contains the requested TodoList. +message GetTodoListResponse { + // The requested list. + TodoList todo_list = 1; +} + +// AddTodoItemRequest adds a new TodoItem to an existing TodoList. +// +// The todo_item_id is client-generated, same as in CreateTodoListRequest. +message AddTodoItemRequest { + // UUID of the target list. + string todo_list_id = 1; + // Client-generated UUID for the new item. + string todo_item_id = 2; + // Short title of the item. + string title = 3; + // Free-form description of the item. + string description = 4; + // Optional due date; omit for no due date. + google.protobuf.Timestamp due_date = 5; +} + +// MarkTodoItemAsDoneRequest marks an item in a TodoList as completed. +message MarkTodoItemAsDoneRequest { + // UUID of the list that owns the item. + string todo_list_id = 1; + // UUID of the item to mark as done. + string todo_item_id = 2; +} + +// MarkTodoItemAsPendingRequest marks an item in a TodoList as pending +// (i.e., undoes a previous "mark as done"). +message MarkTodoItemAsPendingRequest { + // UUID of the list that owns the item. + string todo_list_id = 1; + // UUID of the item to mark as pending. + string todo_item_id = 2; +} + +// DeleteTodoItemRequest removes an item from a TodoList. +message DeleteTodoItemRequest { + // UUID of the list that owns the item. + string todo_list_id = 1; + // UUID of the item to delete. + string todo_item_id = 2; +} diff --git a/go.work b/go.work new file mode 100644 index 00000000..4cfefe82 --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.26.0 + +use ( + . + ./examples/todolist +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 00000000..ca8b2add --- /dev/null +++ b/go.work.sum @@ -0,0 +1,283 @@ +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= +cloud.google.com/go/accessapproval v1.11.0/go.mod h1:7bmInw17bQX+ZPi7YmReC3xKymDrMmxXaUnaI6zQOqI= +cloud.google.com/go/accesscontextmanager v1.12.0/go.mod h1:VO15iVnsM0FO9Dt8hSFPgkuHRZjq6LEYZq1szJ27U2k= +cloud.google.com/go/aiplatform v1.124.0/go.mod h1:yWTZiCunYDnyxeWWD14tDo6+BMlvAUCC5VxuxhvbrVI= +cloud.google.com/go/analytics v0.33.0/go.mod h1:V9Qef2N0y8GDqQ9FTlmM2XpDEMYonZJRPSUNGZlPCcc= +cloud.google.com/go/apigateway v1.10.0/go.mod h1:f3Sk8Tdh1Ty5HR7kgbWB6Yu1M82LM+nIr5DTMZnLZWk= +cloud.google.com/go/apigeeconnect v1.10.0/go.mod h1:mYJekCKZHc2ia5yZX5lwtexTn9CzsOfb6+sh/2hi42Q= +cloud.google.com/go/apigeeregistry v0.13.0/go.mod h1:o+j6eA8hYhTWX5gEqMMBVDWY+/QQFrYe/YJBsO19pn0= +cloud.google.com/go/appengine v1.12.0/go.mod h1:JMjrVFg+YgfksZCWbtA3TgbKbPfZZtapB9cGL/5WVnM= +cloud.google.com/go/area120 v0.13.0/go.mod h1:jD1fw9W4xxIZMY68g7PpbCPleoeGddFs5jPcdhfg3+Y= +cloud.google.com/go/artifactregistry v1.23.0/go.mod h1:aMmdtqKVmbuxCCb/NGDJYZHsK6AtqlcyvD05ACzs1n8= +cloud.google.com/go/asset v1.25.0/go.mod h1:+HaDReZQAh/0syAf0uTMeUrMfXikr+KKyDtCdvf7j4M= +cloud.google.com/go/assuredworkloads v1.16.0/go.mod h1:zBnVYn0E+sDW/mhEmcg1R8+8tguXrtBgmfGY0q34kss= +cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/automl v1.18.0/go.mod h1:OkHxjbVDblDafhwuP8yEkz1xcUJhgcbhbsieCW7GaiI= +cloud.google.com/go/baremetalsolution v1.7.0/go.mod h1:o+stutiS8t+HmjNIG92Gkn8H9+5/q27d6lQp7e9GWdg= +cloud.google.com/go/batch v1.17.0/go.mod h1:dpWfhLmLQZqsTBAFYjZA3pS04fCY5ttTenZcWmSeILw= +cloud.google.com/go/beyondcorp v1.5.0/go.mod h1:vujdO0wfsBV2y1egrJxGtwKZr5P5V6bIHKWp1phWHBY= +cloud.google.com/go/bigquery v1.76.0/go.mod h1:J4wuqka/1hEpdJxH2oBrUR0vjTD+r7drGkpcA3yqERM= +cloud.google.com/go/bigtable v1.46.0/go.mod h1:GUM6PdkG3rrDse9kugqvX5+ktwo3ldfLtLi1VFn5Wj4= +cloud.google.com/go/billing v1.24.0/go.mod h1:axqDO1uHegh7u5qngkTfqN1djAeLGsWAFAblERgmgEk= +cloud.google.com/go/binaryauthorization v1.13.0/go.mod h1:+0CndCJPtcHuVCNok+qQskWvbP5Sp5m6eGL8Vpu5mss= +cloud.google.com/go/certificatemanager v1.12.0/go.mod h1:QOA8qRoM6/Ik03+srLnBykenGTy0fk78dnPcx5ZWOW8= +cloud.google.com/go/channel v1.24.0/go.mod h1:04T5Wjq+mHlvEUNzExydnBW1vO64q3Q2Wsblp/dpBxY= +cloud.google.com/go/cloudbuild v1.28.0/go.mod h1:rg52xEmndQQPiC9NV/8sCaVtKxHMU9D9MeU+oE9VGKA= +cloud.google.com/go/clouddms v1.11.0/go.mod h1:aMgrOZ+/EKF/PL+h1sDbS+7fAIYV5rTwD+G/apCeHQk= +cloud.google.com/go/cloudtasks v1.16.0/go.mod h1:3KeCxwtGEyaySL7CR3lMmEa2I4mq1ynXdgmfNiO4RYE= +cloud.google.com/go/compute v1.60.0/go.mod h1:Xm6PbsLgBpAg4va77ljbBdpMjzuU+uPp5Ze2dnZq7lw= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= +cloud.google.com/go/contactcenterinsights v1.20.0/go.mod h1:2Crd36H59Lwkt4gWrLgmnbnF59IIZIa3XYt1gtNqJkQ= +cloud.google.com/go/container v1.49.0/go.mod h1:EvqoT2eXfxLweXXUlhAMGR0sOAB00XPzEjoL01esSDs= +cloud.google.com/go/containeranalysis v0.17.0/go.mod h1:Zq0XHzUIa0oTa7H6aSR8HWqeJnoRI9syUcYJzfozjZQ= +cloud.google.com/go/datacatalog v1.29.0/go.mod h1:MP8V3kNuESnwMk4mB6zdWmw/4KQ5xZ8dyUNVsggqN5I= +cloud.google.com/go/dataflow v0.14.0/go.mod h1:BWhSrIGmsMfuYj3J+nJ2Tw7tplRR6r28kvRiqCD3WlQ= +cloud.google.com/go/dataform v0.17.0/go.mod h1:i1a0zkS751kvrY1IIPpUQZ77H5doxx7cs0AP3hnXTMk= +cloud.google.com/go/datafusion v1.11.0/go.mod h1:MQdANs3I/4gitzY+mTBx27rrQyMiUg8uc2Z4TPLWWfc= +cloud.google.com/go/datalabeling v0.12.0/go.mod h1:DYjvP4RhQ0332YgO22APYlBjCebb+SCaS0e2KApDq/Q= +cloud.google.com/go/dataplex v1.32.0/go.mod h1:sOazL+Bs/PTxiMHQ5yBboBvEW9qPrpGogx3+RAgfIt8= +cloud.google.com/go/dataproc/v2 v2.19.0/go.mod h1:oARVSa38kAHvSuG+cozsrY2sE6UajGuvOOf9vS+ADHI= +cloud.google.com/go/dataqna v0.12.0/go.mod h1:XiVVFTOEJLBSvm3ILbyjXngGQYpjb/66MSksqz/56fs= +cloud.google.com/go/datastore v1.22.0/go.mod h1:aopSX+Whx0lHspWWBj+AjWt68/zjYsPfDe3LjWtqZg8= +cloud.google.com/go/datastream v1.18.0/go.mod h1:uoWTtfP20W8MXuV2DPcl5zqnVsxQ9QEmmBHX858oYTQ= +cloud.google.com/go/deploy v1.30.0/go.mod h1:lUG7maG/NkoTXmQ8G1mtcVymnbizfDJh6ER7vljVa/U= +cloud.google.com/go/dialogflow v1.80.0/go.mod h1:UtuiGOq9gAlTz9u4Vt+q1syMrx9ANQzTk+lC3WDdSOw= +cloud.google.com/go/dlp v1.32.0/go.mod h1:+haQd/n0QTv5BK7wZnCk2qctd5sfKL50jjh9E6N0d/Q= +cloud.google.com/go/documentai v1.46.0/go.mod h1:mGjfbNf0cqCHKgxMZZV7frbfoF9T2hKkU1h88QyOy3c= +cloud.google.com/go/domains v0.13.0/go.mod h1:BjoSVNc+LVwoHMnE2fxTQNzGLSWWb6f3a8VAN6+VjVk= +cloud.google.com/go/edgecontainer v1.7.0/go.mod h1:mZmgXuMGTGI6RUUTXsOZa+F2rFF21v0JPnuX7LQEqBE= +cloud.google.com/go/errorreporting v0.7.0/go.mod h1:V7ojx7z76JITDZNGyDNkIIa9nNEkQzF6Yj+VHl2YF84= +cloud.google.com/go/essentialcontacts v1.10.0/go.mod h1:W8fTL17jP6vmsPHQaCT5rOjWGohEssuqDUroxnjST0A= +cloud.google.com/go/eventarc v1.21.0/go.mod h1:tIJL0hoWtZXVa5MjcAep/4xB+AXz4AbqQV14ogX5VwU= +cloud.google.com/go/filestore v1.13.0/go.mod h1:oD+PvCWu4HqfEdNv65yk2XaLIiP7h4AuAH9Ua5YBRTM= +cloud.google.com/go/firestore v1.22.0/go.mod h1:PaM4i7i7ruALSKmlpHXXZaPObcZw0W7ie5UOPr72iTU= +cloud.google.com/go/functions v1.22.0/go.mod h1:t40GeqBAQNuqKlHCxmV/pxhyYJnImLcvRa3GBv4tAy0= +cloud.google.com/go/gkebackup v1.11.0/go.mod h1:D2MDbHW4V/uKCmS9TnT8hNKX2tPkE/pWp9nSm0TQ9hY= +cloud.google.com/go/gkeconnect v0.15.0/go.mod h1:5iWSBQzMIRLwUHUWVhxxcNK45ZPE8ntyBgE0MkavlqQ= +cloud.google.com/go/gkehub v0.19.0/go.mod h1:xKePlMrI8LpKErzKMWdH/yQv+GDV60ypCNfTTdT+BN0= +cloud.google.com/go/gkemulticloud v1.9.0/go.mod h1:OtfHtgqOgDrXfcdFw8eUkCUI154Q51vvdqZYZV4c4qM= +cloud.google.com/go/gsuiteaddons v1.10.0/go.mod h1:rm/XT7wmwOFGn7jmWtVV65QmZCakzTbHLSojIC4Hskg= +cloud.google.com/go/iam v1.9.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4= +cloud.google.com/go/iap v1.15.0/go.mod h1:b+r+yjrss2WmAEzNrQQjlEdD5E9B8c47mOF7XnqT+z0= +cloud.google.com/go/ids v1.8.0/go.mod h1:uCSFrXfCnRUKBl5PdE/ZqBNp1+vKSKPWpdYGa61WjpQ= +cloud.google.com/go/iot v1.11.0/go.mod h1:62W4n2fe/Ct66NWJEfCB5suZ3XsL5Atx+MxFjScr+9s= +cloud.google.com/go/kms v1.29.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U= +cloud.google.com/go/language v1.17.0/go.mod h1:xSeiVB4UiA9wYmFy2GWjf1Mb1K3uR1Yi/80qoqTxH04= +cloud.google.com/go/lifesciences v0.13.0/go.mod h1:FwS+QkqPdVWl4SmKUCFozFvsTVWTLH13HCKcwR/MR9U= +cloud.google.com/go/logging v1.16.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI= +cloud.google.com/go/longrunning v0.11.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM= +cloud.google.com/go/managedidentities v1.10.0/go.mod h1:rm72jf/v//0NG73VQNZM1JlV2E95uhJymmSXlgi6hMA= +cloud.google.com/go/maps v1.33.0/go.mod h1:HH1V8tduMn+b9oRMCdl3vok98uvHco/wElZXyJQ/9kU= +cloud.google.com/go/mediatranslation v0.12.0/go.mod h1:kjZrowuigFr+Bf1HM1TCtp1a3E3kfG1ovPK5VEuaNAQ= +cloud.google.com/go/memcache v1.14.0/go.mod h1:y/rXhJiieCF742K958dY29fSfM+Y3wh2thRmWspU2Dg= +cloud.google.com/go/metastore v1.17.0/go.mod h1:JGTjGdQ627m2ptDo86XsIKqzzZCk+GG41VEFD7ENsqs= +cloud.google.com/go/monitoring v1.27.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM= +cloud.google.com/go/networkconnectivity v1.24.0/go.mod h1:Uhzfk7NbiY6RNqV9XFvPWRji58+MkTYsTRfQ3EPtrGg= +cloud.google.com/go/networkmanagement v1.26.0/go.mod h1:2YogSU3sD7LvtmWntUAuGARbFQmy3A0En3LrJr69jkU= +cloud.google.com/go/networksecurity v0.14.0/go.mod h1:LMn10eRVf4K85PMF33yRoKAra7VhCOetxFcLDMh9A74= +cloud.google.com/go/notebooks v1.15.0/go.mod h1:NScGIhfQCqLRIlVaUVbm595F6dhqiTl5XS1KaKgitKM= +cloud.google.com/go/optimization v1.10.0/go.mod h1:qCWskZMcynh0GBsUrCP6oPwwnUhbwg5UcXvVM9hzOD8= +cloud.google.com/go/orchestration v1.14.0/go.mod h1:H7MFVP8Z/dtml39nf43sWYPL/2o7J4tdSZAlJrBuqnQ= +cloud.google.com/go/orgpolicy v1.18.0/go.mod h1:9LHqEGx5P5dhansdKTNIEXpM+QbebAIOs66+HUID4aQ= +cloud.google.com/go/osconfig v1.19.0/go.mod h1:BofnHqjjvu6lZQv/hqo2+rLCUiY4O6A9UYwwvVrSBjk= +cloud.google.com/go/oslogin v1.17.0/go.mod h1:3Oa36T3781Mv+yCSVYlfasi7auHjfPFqvNOd1q92umc= +cloud.google.com/go/phishingprotection v0.12.0/go.mod h1:2gyYqwNjePPEocXDkDve3EuJPaRqN/E7fp28K3arR0k= +cloud.google.com/go/policytroubleshooter v1.14.0/go.mod h1:yNuROjN6h+2/TE2JOvBBJMjYIjC6j0UYHq8f2kVHlA4= +cloud.google.com/go/privatecatalog v0.13.0/go.mod h1:av2b5Rv+oG5ORxUqGlCAYO9s4pXjgc6q2qO9nkTcqT8= +cloud.google.com/go/pubsub v1.50.2/go.mod h1:jyCWeZdGFqd4mitSsBERnJcpqaHBsxQoPkNvjj4sp0w= +cloud.google.com/go/pubsub/v2 v2.5.1/go.mod h1:Pd+qeabMX+576vQJhTN7TelE4k6kJh15dLU/ptOQ/UA= +cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI= +cloud.google.com/go/recaptchaenterprise/v2 v2.24.0/go.mod h1:+ntF70/j7qBa6G/pwmYA0mkBcDeTCXV6WDqUL7GObfs= +cloud.google.com/go/recommendationengine v0.12.0/go.mod h1:UP9cN46tDpZ/N57eDYIWeIRHjMOchtiIyjWjV0Dvr3k= +cloud.google.com/go/recommender v1.16.0/go.mod h1:INRBLfBQJCrgPqjBVFht4OjaFq/WhB/c5V1sqBOdX8g= +cloud.google.com/go/redis v1.21.0/go.mod h1:EUlUT24BAL6LsE1f/N9Bg3LhRCfH+LzwLGbst3KuZRw= +cloud.google.com/go/resourcemanager v1.13.0/go.mod h1:ve0VNxPoDU6XxDuEMCjkineb0YzXQXx3mOWwnNckGDE= +cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw= +cloud.google.com/go/retail v1.29.0/go.mod h1:sfq/cT+gfSLuURf/mdVAw5n0pav3hxSP1rT8RfL7Qxk= +cloud.google.com/go/run v1.19.0/go.mod h1:Z5wHbyFirI8XU48EPs5XJf/qmVm1SXZEhuS8EvZOuQU= +cloud.google.com/go/scheduler v1.14.0/go.mod h1:0hsZg0MZJADyke1lutI0FHAYJR8Dtm8oIivXkmpACkA= +cloud.google.com/go/secretmanager v1.19.0/go.mod h1:9OmSuOeiiUicANglrbdKWSnT3gYkRcXuUQDk7dDW0zU= +cloud.google.com/go/security v1.22.0/go.mod h1:XaB3p0SE7v2bBitsLBb1hM6R8/oI/k/IujpXFJalFK0= +cloud.google.com/go/securitycenter v1.42.0/go.mod h1:7BMMbSTAddVfiE+HrC8tKS6SuRkyK7FRPlkpAZBRV3U= +cloud.google.com/go/servicedirectory v1.15.0/go.mod h1:CtgjXS1idj3s9Q6tB68021Rzk8Q6decV6+ldXC1BoBk= +cloud.google.com/go/shell v1.11.0/go.mod h1:TivWrVriy6xQ0wBjNJJridJgODZz8zXUEW2u48kynzY= +cloud.google.com/go/spanner v1.90.0/go.mod h1:8NB5a7qgwIhGD19Ly+vkpKffPL78vIG9RcrgsuREha0= +cloud.google.com/go/speech v1.33.0/go.mod h1:shnf33sZbGnQQZyek1fdLOR5rRKV6D3jsNqpqyijvj8= +cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU= +cloud.google.com/go/storagetransfer v1.16.0/go.mod h1:AbGutEym/KNasoiDpSj/CYbigp5yhgosSgwlhGvQNs4= +cloud.google.com/go/talent v1.11.0/go.mod h1:GSwli9V25WQdzeuJDJWH9TlQmA8lPFn7yKsxowdxW9Y= +cloud.google.com/go/texttospeech v1.19.0/go.mod h1:p/UVJILAo/S5vsJaWZVdDRzNzA7wXIA+hTACvpMeOBk= +cloud.google.com/go/tpu v1.11.0/go.mod h1:F5gT5BL22Dhsr05JLHdMjAjj+wcTn3Xtuu4jvq9yFug= +cloud.google.com/go/trace v1.14.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0= +cloud.google.com/go/translate v1.15.0/go.mod h1:3mErnHTQBu9yeLiL35K0HBBuaM6Vk2fD/vyWFz790VU= +cloud.google.com/go/video v1.30.0/go.mod h1:KxDL728ZzH+FJwtEb9XkiLTETW5bI37hTWbJiRYeXkk= +cloud.google.com/go/videointelligence v1.15.0/go.mod h1:mmX1JpIWzwozaigrdRNjikZc3aFLNHFKh+OFwAdfiW4= +cloud.google.com/go/vision/v2 v2.12.0/go.mod h1:ODlLCajJOq4t8thoi1uVvbnfIfix73HsYWhZuIveagQ= +cloud.google.com/go/vmmigration v1.13.0/go.mod h1:MP6mQ21ru1usBeCbl805Ioz0Fy+yf3qK2kUkhZ69QQY= +cloud.google.com/go/vmwareengine v1.6.0/go.mod h1:e66l90IZhm1yQfYZv+YCWjSNSklQZCRmuEvKL8n3Ua0= +cloud.google.com/go/vpcaccess v1.11.0/go.mod h1:4Uus6E/9FYUtIrwBE1wJ1RosKwb02H6kEd9puJ02TL8= +cloud.google.com/go/webrisk v1.14.0/go.mod h1:VIQw8smiaMOlget/xOk6niTkNJTiQc5skEmCuAksxJc= +cloud.google.com/go/websecurityscanner v1.10.0/go.mod h1:cZSc9HqoFdccL1mqZtPIInOd4R8PBGwI20wdnrz6AO8= +cloud.google.com/go/workflows v1.17.0/go.mod h1:TWsrDGgsJy7xAJ07byzHhKKehEWItJG3BivEHVhGH5g= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest/adal v0.9.16/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/aws/aws-sdk-go v1.49.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go-v2 v1.16.16/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8/go.mod h1:JTnlBSot91steJeti4ryyu/tLd4Sk84O5W22L7O2EQU= +github.com/aws/aws-sdk-go-v2/credentials v1.12.20/go.mod h1:UKY5HyIux08bbNA7Blv4PcXQ8cTkGh7ghHMFklaviR4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.33/go.mod h1:84XgODVR8uRhmOnUkKGUZKqIMxmjmLOR8Uyp7G/TPwc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23/go.mod h1:2DFxAQ9pfIRy0imBCJv+vZ2X6RKxves6fbnEuSry6b4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17/go.mod h1:pRwaTYCJemADaqCbUAxltMoHKata7hmB5PjEXeu0kfg= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14/go.mod h1:AyGgqiKv9ECM6IZeNQtdT8NnMvUb3/2wokeq2Fgryto= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.9/go.mod h1:a9j48l6yL5XINLHLcOKInjdvknN+vWqPBxqeIDw7ktw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18/go.mod h1:NS55eQ4YixUJPTC+INxi2/jCqe1y2Uw3rnh9wEOVJxY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17/go.mod h1:4nYOrY41Lrbk2170/BGkcJKBhws9Pfn8MG3aGqjjeFI= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17/go.mod h1:YqMdV+gEKCQ59NrB7rzrJdALeBIsYiVi8Inj3+KcqHI= +github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11/go.mod h1:fmgDANqTUCxciViKl9hb/zD5LFbvPINFRgWhDbR+vZo= +github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= +github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/dvsekhvalnov/jose2go v1.7.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= +github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= +github.com/get-eventually/go-eventually v0.4.0/go.mod h1:sp7/2JOA7HCwVhhmoYOIq51Re0GLZ6tsxoSs8i8V01I= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= +github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/pierrec/lz4/v4 v4.1.16/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/snowflakedb/gosnowflake v1.6.19/go.mod h1:FM1+PWUdwB9udFDsXdfD58NONC0m+MlOSmQRvimobSM= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= +modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= +modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= +modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= +modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4=