From 11fe2046b4ba9fbcedad630c0ef257a20570e12c Mon Sep 17 00:00:00 2001 From: Elliot Jackson <13633636+ElliotMJackson@users.noreply.github.com> Date: Thu, 24 Aug 2023 15:12:36 -0400 Subject: [PATCH 01/20] checkpoint --- README.md | 83 ++++++++++++++++++++++++++++++++++++++++ go.mod | 22 +++++++++++ go.sum | 50 ++++++++++++++++++++++++ interceptor.go | 93 +++++++++++++++++++++++++++++++++++++++++++++ interceptor_test.go | 52 +++++++++++++++++++++++++ 5 files changed, 300 insertions(+) create mode 100644 README.md create mode 100644 go.sum create mode 100644 interceptor.go create mode 100644 interceptor_test.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c6c223 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# ConnectRPC Validate Go + +This repository contains a Go package named `validate` that provides an +interceptor implementation for the ConnectRPC framework. The interceptor is +designed to perform protocol buffer message validation using +the `protovalidate-go` library. The provided interceptor can be used to ensure +that incoming requests and messages adhere to the defined protocol buffer +message structure. + +## Installation + +To use the `validate` package in your Go project, you can add it as a dependency +using `go get`: + +```bash +go get connectrpc.com/validate +``` + +## Usage + +The `validate` package offers an interceptor named `Interceptor` that implements +the `connect.Interceptor` interface. This interceptor is used to validate +incoming messages using the `protovalidate-go` library before passing them on to +the next interceptor or handler. + +### Creating an Interceptor + +To create a new `Interceptor`, you can use the `NewInterceptor` function +provided by the package: + +```go +validator, err := protovalidate.New() // Initialize your protovalidate validator +if err != nil { + // Handle error +} +interceptor := validate.NewInterceptor(validator) +``` + +### Wrapping Unary Functions + +You can wrap a `connect.UnaryFunc` with the interceptor's validation using +the `WrapUnary` method. This ensures that the incoming request is validated +before being processed: + +```go +unaryFunc := func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { +// Your unary function logic +} + +wrappedUnaryFunc := interceptor.WrapUnary(unaryFunc) +``` + +### Wrapping Streaming Clients + +For streaming clients, you can wrap a `connect.StreamingClientFunc` with the +interceptor's validation using the `WrapStreamingClient` method: + +```go +streamingClientFunc := func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { +// Your streaming client logic +} + +wrappedStreamingClientFunc := interceptor.WrapStreamingClient(streamingClientFunc) +``` + +### Wrapping Streaming Handlers + +When dealing with streaming handlers, you can wrap +a `connect.StreamingHandlerFunc` with the interceptor's validation using +the `WrapStreamingHandler` method: + +```go +streamingHandlerFunc := func(ctx context.Context, conn connect.StreamingHandlerConn) error { +// Your streaming handler logic +} + +wrappedStreamingHandlerFunc := interceptor.WrapStreamingHandler(streamingHandlerFunc) +``` + +## License + +This package is distributed under the [MIT License](LICENSE). + diff --git a/go.mod b/go.mod index e1b30c6..daf4dd2 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,25 @@ module connectrpc.com/validate go 1.19 + +require ( + buf.build/gen/go/bufbuild/protovalidate-testing/protocolbuffers/go v1.31.0-20230824200732-8bc04916caea.1 + connectrpc.com/connect v1.11.0 + github.com/bufbuild/protovalidate-go v0.3.0 + github.com/stretchr/testify v1.8.4 + google.golang.org/protobuf v1.31.0 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230824200731-b9b8148056b9.1 // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/cel-go v0.17.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect + golang.org/x/text v0.10.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b7e09d0 --- /dev/null +++ b/go.sum @@ -0,0 +1,50 @@ +buf.build/gen/go/bufbuild/protovalidate-testing/protocolbuffers/go v1.31.0-20230824200732-8bc04916caea.1 h1:wWsaA01KC5hgoW4oHFiGzEqMmgcE7UyU43F8AJrex1U= +buf.build/gen/go/bufbuild/protovalidate-testing/protocolbuffers/go v1.31.0-20230824200732-8bc04916caea.1/go.mod h1:cJ4gQkiW4uPTUTI3+O2862OzMSTnzrFNwMauHLwPDPg= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230824200731-b9b8148056b9.1 h1:9Ea7lsYYvoyqmq79GbCy6POXHrZbC+pHs+6lGNx9IBQ= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230824200731-b9b8148056b9.1/go.mod h1:xafc+XIsTxTy76GJQ1TKgvJWsSugFBqMaN27WhUblew= +connectrpc.com/connect v1.11.0 h1:Av2KQXxSaX4vjqhf5Cl01SX4dqYADQ38eBtr84JSUBk= +connectrpc.com/connect v1.11.0/go.mod h1:3AGaO6RRGMx5IKFfqbe3hvK1NqLosFNP2BxDYTPmNPo= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/bufbuild/protovalidate-go v0.3.0 h1:t9zKgM//9VtPnP0TvyFqWubLQtSbwLwEUVOxgtX9/os= +github.com/bufbuild/protovalidate-go v0.3.0/go.mod h1:4mZkDYMGJlnHHQ9rPOhVEZ4bA13iOJBRLzywxy8f/lo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/cel-go v0.17.4 h1:9556LOjSyIZlgnT0oaCYGq2uk9BM6fzuTXhzYHskonk= +github.com/google/cel-go v0.17.4/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/interceptor.go b/interceptor.go new file mode 100644 index 0000000..dde49f3 --- /dev/null +++ b/interceptor.go @@ -0,0 +1,93 @@ +package validate + +import ( + "context" + "errors" + + "connectrpc.com/connect" + + "github.com/bufbuild/protovalidate-go" + "google.golang.org/protobuf/proto" +) + +// Option interface is currently empty and serves as a placeholder for potential future implementations. +// It allows adding new options without breaking existing code. +type Option interface { + unimplemented() +} + +// Interceptor implements connect.Interceptor. +type Interceptor struct { + validator *protovalidate.Validator +} + +// NewInterceptor returns a new Interceptor. +func NewInterceptor( + validator *protovalidate.Validator, + _ ...Option, +) *Interceptor { + return &Interceptor{ + validator: validator, + } +} + +// WrapUnary returns a new connect.UnaryFunc that wraps the given connect.UnaryFunc. +func (i *Interceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + if err := validate(i.validator, req.Any()); err != nil { + return nil, err + } + return next(ctx, req) + } +} + +// WrapStreamingClient returns a new connect.StreamingClientFunc that wraps the given connect.StreamingClientFunc. +func (i *Interceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { + return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { + return &streamingClientInterceptor{ + validator: i.validator, + StreamingClientConn: next(ctx, spec), + } + } +} + +// WrapStreamingHandler returns a new connect.StreamingHandlerFunc that wraps the given connect.StreamingHandlerFunc. +func (i *Interceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + return func(ctx context.Context, conn connect.StreamingHandlerConn) error { + return next(ctx, &streamingHandlerInterceptor{ + validator: i.validator, + StreamingHandlerConn: conn, + }) + } +} + +// streamingClientInterceptor implements connect.StreamingClientConn. +type streamingClientInterceptor struct { + connect.StreamingClientConn + validator *protovalidate.Validator +} + +func (s *streamingClientInterceptor) Send(msg any) error { + return validate(s.validator, msg) +} + +// streamingHandlerInterceptor implements connect.StreamingHandlerConn. +type streamingHandlerInterceptor struct { + connect.StreamingHandlerConn + validator *protovalidate.Validator +} + +func (s *streamingHandlerInterceptor) Receive(msg any) error { + return validate(s.validator, msg) +} + +func validate(validator *protovalidate.Validator, msg any) error { + message, ok := msg.(proto.Message) + if !ok { + return errors.New("unsupported message type") + } + if err := validator.Validate(message); err != nil { + return err + } + return nil +} diff --git a/interceptor_test.go b/interceptor_test.go new file mode 100644 index 0000000..8e6cb69 --- /dev/null +++ b/interceptor_test.go @@ -0,0 +1,52 @@ +package validate + +import ( + "context" + "testing" + + "buf.build/gen/go/bufbuild/protovalidate-testing/protocolbuffers/go/buf/validate/conformance/cases" + "connectrpc.com/connect" + + "github.com/bufbuild/protovalidate-go" + "github.com/stretchr/testify/require" + + "github.com/stretchr/testify/assert" +) + +func TestInterceptor_WrapUnary(t *testing.T) { + validator, err := protovalidate.New() + require.NoError(t, err) + interceptor := NewInterceptor(validator) + message := &cases.StringConst{Val: "foo"} + mockUnary := func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + return connect.NewResponse(message), nil + } + wrappedUnary := interceptor.WrapUnary(mockUnary) + _, err = wrappedUnary(context.Background(), connect.NewRequest(message)) + assert.NoError(t, err) +} + +type mockStreamingClientConn struct { +} + +func TestStreamingClientInterceptor_Send(t *testing.T) { + validator, err := protovalidate.New() + require.NoError(t, err) + clientConn := &streamingClientInterceptor{ + validator: validator, + } + message := &cases.StringConst{Val: "foo"} + err = clientConn.Send(connect.NewRequest(message)) + assert.NoError(t, err) +} + +func TestStreamingHandlerInterceptor_Receive(t *testing.T) { + validator, err := protovalidate.New() + require.NoError(t, err) + handlerConn := &streamingHandlerInterceptor{ + validator: validator, + } + message := &cases.StringConst{Val: "foo"} + err = handlerConn.Receive(connect.NewRequest(message)) + assert.NoError(t, err) +} From 5b79d937bf283b9feb2a8ac7217d4879733d8c97 Mon Sep 17 00:00:00 2001 From: Elliot Jackson <13633636+elliotmjackson@users.noreply.github.com> Date: Thu, 24 Aug 2023 16:56:54 -0400 Subject: [PATCH 02/20] Add project skeleton (#3) --- .github/CODE_OF_CONDUCT.md | 133 +++++++++++++++++++++ .github/dependabot.yml | 6 + .github/workflows/ci.yaml | 42 +++++++ .github/workflows/pr-title.yaml | 18 +++ .golangci.yml | 53 +++++++++ LICENSE | 201 ++++++++++++++++++++++++++++++++ Makefile | 73 ++++++++++++ interceptor.go | 2 - 8 files changed, 526 insertions(+), 2 deletions(-) create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/pr-title.yaml create mode 100644 .golangci.yml create mode 100644 LICENSE create mode 100644 Makefile diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1900eb4 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +conduct@buf.build. All complaints will be reviewed and investigated promptly +and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..4b10c21 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,42 @@ +name: ci +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + schedule: + - cron: '15 22 * * *' + workflow_dispatch: {} # support manual runs +permissions: + contents: read +jobs: + ci: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: [1.19.x, 1.20.x, 1.21.x] + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 1 + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + - name: Cache + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-validate-go-ci-${{ hashFiles('**/go.sum') }} + restore-keys: ${{ runner.os }}-validate-go-ci- + - name: Test + run: make test + - name: Lint + # Often, lint & gofmt guidelines depend on the Go version. To prevent + # conflicting guidance, run only on the most recent supported version. + # For the same reason, only check generated code on the most recent + # supported version. + if: matrix.go-version == '1.21.x' + run: make checkgenerate && make lint diff --git a/.github/workflows/pr-title.yaml b/.github/workflows/pr-title.yaml new file mode 100644 index 0000000..b114603 --- /dev/null +++ b/.github/workflows/pr-title.yaml @@ -0,0 +1,18 @@ +name: Lint PR Title +# Prevent writing to the repository using the CI token. +# Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions +permissions: + pull-requests: read +on: + pull_request: + # By default, a workflow only runs when a pull_request's activity type is opened, + # synchronize, or reopened. We explicity override here so that PR titles are + # re-linted when the PR text content is edited. + types: + - opened + - edited + - reopened + - synchronize +jobs: + lint: + uses: bufbuild/base-workflows/.github/workflows/pr-title.yaml@main diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..ae5b395 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,53 @@ +run: + skip-dirs-use-default: false +linters-settings: + errcheck: + check-type-assertions: true + forbidigo: + forbid: + - '^fmt\.Print' + - '^log\.' + - '^print$' + - '^println$' + - '^panic$' + godox: + # TODO, OPT, etc. comments are fine to commit. Use FIXME comments for + # temporary hacks, and use godox to prevent committing them. + keywords: [FIXME] + varnamelen: + ignore-decls: + - T any + - i int + - wg sync.WaitGroup +linters: + enable-all: true + disable: + - cyclop # covered by gocyclo + - depguard # unnecessary for small libraries + - deadcode # abandoned + - exhaustivestruct # replaced by exhaustruct + - funlen # rely on code review to limit function length + - gocognit # dubious "cognitive overhead" quantification + - gofumpt # prefer standard gofmt + - goimports # rely on gci instead + - golint # deprecated by Go team + - gomnd # some unnamed constants are okay + - ifshort # deprecated by author + - interfacer # deprecated by author + - ireturn # "accept interfaces, return structs" isn't ironclad + - lll # don't want hard limits for line length + - maintidx # covered by gocyclo + - maligned # readability trumps efficient struct packing + - nlreturn # generous whitespace violates house style + - nosnakecase # deprecated in https://github.com/golangci/golangci-lint/pull/3065 + - scopelint # deprecated by author + - structcheck # abandoned + - testpackage # internal tests are fine + - varcheck # abandoned + - wrapcheck # don't _always_ need to wrap errors + - wsl # generous whitespace violates house style +issues: + exclude: + # Don't ban use of fmt.Errorf to create new errors, but the remaining + # checks from err113 are useful. + - "err113: do not define dynamic errors.*" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f308f8b --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Buf Technologies, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..41eac4c --- /dev/null +++ b/Makefile @@ -0,0 +1,73 @@ +# See https://tech.davis-hansson.com/p/make/ +SHELL := bash +.DELETE_ON_ERROR: +.SHELLFLAGS := -eu -o pipefail -c +.DEFAULT_GOAL := all +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules +MAKEFLAGS += --no-print-directory +BIN := .tmp/bin +export PATH := $(BIN):$(PATH) +export GOBIN := $(abspath $(BIN)) +COPYRIGHT_YEARS := 2023 +LICENSE_IGNORE := --ignore testdata/ + +.PHONY: help +help: ## Describe useful make targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}' + +.PHONY: all +all: ## Build, test, and lint (default) + $(MAKE) test + $(MAKE) lint + +.PHONY: clean +clean: ## Delete intermediate build artifacts + @# -X only removes untracked files, -d recurses into directories, -f actually removes files/dirs + git clean -Xdf + +.PHONY: test +test: build ## Run unit tests + go test -vet=off -race -cover ./... + +.PHONY: build +build: generate ## Build all packages + go build ./... + +.PHONY: generate +generate: $(BIN)/license-header ## Regenerate code and licenses + license-header \ + --license-type apache \ + --copyright-holder "Buf Technologies, Inc." \ + --year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE) + +.PHONY: lint +lint: $(BIN)/golangci-lint ## Lint + go vet ./... + golangci-lint run + +.PHONY: lintfix +lintfix: $(BIN)/golangci-lint ## Automatically fix some lint errors + golangci-lint run --fix + +.PHONY: install +install: ## Install all binaries + go install ./... + +.PHONY: upgrade +upgrade: ## Upgrade dependencies + go get -u -t ./... + go mod tidy -v + +.PHONY: checkgenerate +checkgenerate: + @# Used in CI to verify that `make generate` doesn't produce a diff. + test -z "$$(git status --porcelain | tee /dev/stderr)" + +$(BIN)/license-header: Makefile + @mkdir -p $(@D) + go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v1.26.1 + +$(BIN)/golangci-lint: Makefile + @mkdir -p $(@D) + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.1 diff --git a/interceptor.go b/interceptor.go index dde49f3..675f243 100644 --- a/interceptor.go +++ b/interceptor.go @@ -61,7 +61,6 @@ func (i *Interceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) co } } -// streamingClientInterceptor implements connect.StreamingClientConn. type streamingClientInterceptor struct { connect.StreamingClientConn validator *protovalidate.Validator @@ -71,7 +70,6 @@ func (s *streamingClientInterceptor) Send(msg any) error { return validate(s.validator, msg) } -// streamingHandlerInterceptor implements connect.StreamingHandlerConn. type streamingHandlerInterceptor struct { connect.StreamingHandlerConn validator *protovalidate.Validator From a30e8cac6ed1c461b486d83daec0125e8261f8ae Mon Sep 17 00:00:00 2001 From: Elliot Jackson <13633636+ElliotMJackson@users.noreply.github.com> Date: Thu, 24 Aug 2023 17:50:09 -0400 Subject: [PATCH 03/20] add initial implementation --- .golangci.yml | 7 ++ README.md | 3 +- interceptor.go | 66 ++++++++++++++---- interceptor_test.go | 164 +++++++++++++++++++++++++++++++++++++------- 4 files changed, 198 insertions(+), 42 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index ae5b395..006b47e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -51,3 +51,10 @@ issues: # Don't ban use of fmt.Errorf to create new errors, but the remaining # checks from err113 are useful. - "err113: do not define dynamic errors.*" +# exclude-rules: +# - path: interceptor_test.go +# linters: +# - exhaustruct +# - path: interceptor.go +# linters: +# - exhaustruct diff --git a/README.md b/README.md index 0c6c223..87c1443 100644 --- a/README.md +++ b/README.md @@ -79,5 +79,4 @@ wrappedStreamingHandlerFunc := interceptor.WrapStreamingHandler(streamingHandler ## License -This package is distributed under the [MIT License](LICENSE). - +Offered under the [Apache 2 license](LICENSE). diff --git a/interceptor.go b/interceptor.go index 675f243..8a48b26 100644 --- a/interceptor.go +++ b/interceptor.go @@ -1,27 +1,49 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package validate provides an interceptor implementation for the ConnectRPC framework. +// The interceptor integrates with the protovalidate-go library to validate incoming protobuf messages, +// ensuring adherence to the defined message structure. package validate import ( "context" "errors" + "fmt" "connectrpc.com/connect" - "github.com/bufbuild/protovalidate-go" "google.golang.org/protobuf/proto" ) +var _ connect.Interceptor = &Interceptor{} + // Option interface is currently empty and serves as a placeholder for potential future implementations. // It allows adding new options without breaking existing code. type Option interface { unimplemented() } -// Interceptor implements connect.Interceptor. +// Interceptor implements the connect.Interceptor interface, providing message validation +// for the ConnectRPC framework. It integrates with the protovalidate-go library to ensure +// incoming protocol buffer messages adhere to the defined message structure. type Interceptor struct { validator *protovalidate.Validator } -// NewInterceptor returns a new Interceptor. +// NewInterceptor returns a new instance of the Interceptor. +// It accepts a protovalidate.Validator as a parameter to perform message validation. func NewInterceptor( validator *protovalidate.Validator, _ ...Option, @@ -31,7 +53,7 @@ func NewInterceptor( } } -// WrapUnary returns a new connect.UnaryFunc that wraps the given connect.UnaryFunc. +// WrapUnary implements the connect.Interceptor interface. func (i *Interceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { if err := validate(i.validator, req.Any()); err != nil { @@ -41,7 +63,7 @@ func (i *Interceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { } } -// WrapStreamingClient returns a new connect.StreamingClientFunc that wraps the given connect.StreamingClientFunc. +// WrapStreamingClient implements the connect.Interceptor interface. func (i *Interceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { return &streamingClientInterceptor{ @@ -51,7 +73,7 @@ func (i *Interceptor) WrapStreamingClient(next connect.StreamingClientFunc) conn } } -// WrapStreamingHandler returns a new connect.StreamingHandlerFunc that wraps the given connect.StreamingHandlerFunc. +// WrapStreamingHandler implements the connect.Interceptor interface. func (i *Interceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { return func(ctx context.Context, conn connect.StreamingHandlerConn) error { return next(ctx, &streamingHandlerInterceptor{ @@ -67,7 +89,10 @@ type streamingClientInterceptor struct { } func (s *streamingClientInterceptor) Send(msg any) error { - return validate(s.validator, msg) + if err := validate(s.validator, msg); err != nil { + return err + } + return s.StreamingClientConn.Send(msg) } type streamingHandlerInterceptor struct { @@ -76,16 +101,29 @@ type streamingHandlerInterceptor struct { } func (s *streamingHandlerInterceptor) Receive(msg any) error { - return validate(s.validator, msg) + if err := validate(s.validator, msg); err != nil { + return err + } + return s.StreamingHandlerConn.Receive(msg) } func validate(validator *protovalidate.Validator, msg any) error { - message, ok := msg.(proto.Message) - if !ok { - return errors.New("unsupported message type") - } - if err := validator.Validate(message); err != nil { - return err + switch protoMessage := msg.(type) { + case connect.AnyRequest: + return validate(validator, protoMessage.Any()) + case proto.Message: + if err := validator.Validate(protoMessage); err != nil { + out := connect.NewError(connect.CodeInvalidArgument, err) + var validationErr *protovalidate.ValidationError + if errors.As(err, &validationErr) { + if detail, err := connect.NewErrorDetail(validationErr.ToProto()); err == nil { + out.AddDetail(detail) + } + } + return out + } + default: + return fmt.Errorf("unsupported message type %T", protoMessage) } return nil } diff --git a/interceptor_test.go b/interceptor_test.go index 8e6cb69..bf5995e 100644 --- a/interceptor_test.go +++ b/interceptor_test.go @@ -1,3 +1,17 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package validate import ( @@ -6,47 +20,145 @@ import ( "buf.build/gen/go/bufbuild/protovalidate-testing/protocolbuffers/go/buf/validate/conformance/cases" "connectrpc.com/connect" - "github.com/bufbuild/protovalidate-go" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestInterceptor_WrapUnary(t *testing.T) { - validator, err := protovalidate.New() - require.NoError(t, err) - interceptor := NewInterceptor(validator) - message := &cases.StringConst{Val: "foo"} - mockUnary := func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { - return connect.NewResponse(message), nil + t.Parallel() + var tests = []struct { + name string + message *cases.StringConst + wantErr bool + }{ + { + name: "success", + message: &cases.StringConst{Val: "foo"}, + wantErr: false, + }, + { + name: "fail", + message: &cases.StringConst{Val: "bar"}, + wantErr: true, + }, + } + for _, tt := range tests { + test := tt + t.Run(test.name, func(t *testing.T) { + t.Parallel() + validator, err := protovalidate.New() + require.NoError(t, err) + interceptor := NewInterceptor(validator) + mockUnary := func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + return nil, nil + } + _, err = interceptor.WrapUnary(mockUnary)( + context.Background(), + connect.NewRequest(test.message), + ) + if test.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) } - wrappedUnary := interceptor.WrapUnary(mockUnary) - _, err = wrappedUnary(context.Background(), connect.NewRequest(message)) - assert.NoError(t, err) } +var _ connect.StreamingClientConn = &mockStreamingClientConn{} + type mockStreamingClientConn struct { + connect.StreamingClientConn +} + +func (m *mockStreamingClientConn) Send(_ any) error { + return nil } func TestStreamingClientInterceptor_Send(t *testing.T) { - validator, err := protovalidate.New() - require.NoError(t, err) - clientConn := &streamingClientInterceptor{ - validator: validator, + t.Parallel() + var tests = []struct { + name string + message *cases.StringConst + wantErr bool + }{ + { + name: "success", + message: &cases.StringConst{Val: "foo"}, + wantErr: false, + }, + { + name: "fail", + message: &cases.StringConst{Val: "bar"}, + wantErr: true, + }, } - message := &cases.StringConst{Val: "foo"} - err = clientConn.Send(connect.NewRequest(message)) - assert.NoError(t, err) + for _, tt := range tests { + test := tt + t.Run(test.name, func(t *testing.T) { + t.Parallel() + validator, err := protovalidate.New() + require.NoError(t, err) + + clientConn := streamingClientInterceptor{ + validator: validator, + StreamingClientConn: &mockStreamingClientConn{}, + } + err = clientConn.Send(connect.NewRequest(test.message)) + if test.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +var _ connect.StreamingHandlerConn = &mockStreamingHandlerConn{} + +type mockStreamingHandlerConn struct { + connect.StreamingHandlerConn +} + +func (m *mockStreamingHandlerConn) Receive(_ any) error { + return nil } func TestStreamingHandlerInterceptor_Receive(t *testing.T) { - validator, err := protovalidate.New() - require.NoError(t, err) - handlerConn := &streamingHandlerInterceptor{ - validator: validator, + t.Parallel() + var tests = []struct { + name string + message *cases.StringConst + wantErr bool + }{ + { + name: "success", + message: &cases.StringConst{Val: "foo"}, + wantErr: false, + }, + { + name: "fail", + message: &cases.StringConst{Val: "bar"}, + wantErr: true, + }, + } + for _, tt := range tests { + test := tt + t.Run(test.name, func(t *testing.T) { + t.Parallel() + validator, err := protovalidate.New() + require.NoError(t, err) + handlerConn := &streamingHandlerInterceptor{ + validator: validator, + StreamingHandlerConn: &mockStreamingHandlerConn{}, + } + err = handlerConn.Receive(connect.NewRequest(test.message)) + if test.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) } - message := &cases.StringConst{Val: "foo"} - err = handlerConn.Receive(connect.NewRequest(message)) - assert.NoError(t, err) } From feb82fc20d026d692ca093fac5ba307fad6b5a9a Mon Sep 17 00:00:00 2001 From: Elliot Jackson <13633636+ElliotMJackson@users.noreply.github.com> Date: Fri, 25 Aug 2023 09:25:38 -0400 Subject: [PATCH 04/20] Update README and add stubbed tests --- .golangci.yml | 17 ++--- README.md | 113 ++++++++++++++++---------------- go.mod | 2 + go.sum | 4 ++ interceptor.go | 108 ++++++++++++++++++++---------- interceptor_test.go | 155 ++++++++++++++++++++++++++++---------------- 6 files changed, 242 insertions(+), 157 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 006b47e..cecfb1b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -13,7 +13,7 @@ linters-settings: godox: # TODO, OPT, etc. comments are fine to commit. Use FIXME comments for # temporary hacks, and use godox to prevent committing them. - keywords: [FIXME] + keywords: [ FIXME ] varnamelen: ignore-decls: - T any @@ -51,10 +51,11 @@ issues: # Don't ban use of fmt.Errorf to create new errors, but the remaining # checks from err113 are useful. - "err113: do not define dynamic errors.*" -# exclude-rules: -# - path: interceptor_test.go -# linters: -# - exhaustruct -# - path: interceptor.go -# linters: -# - exhaustruct + exclude-rules: + - path: interceptor_test.go + linters: + - gochecknoglobals + - exhaustruct + - path: interceptor.go + linters: + - exhaustruct diff --git a/README.md b/README.md index 87c1443..891e936 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,83 @@ -# ConnectRPC Validate Go +# ConnectRPC Validation Interceptor -This repository contains a Go package named `validate` that provides an -interceptor implementation for the ConnectRPC framework. The interceptor is -designed to perform protocol buffer message validation using -the `protovalidate-go` library. The provided interceptor can be used to ensure -that incoming requests and messages adhere to the defined protocol buffer -message structure. +[![Build](https://github.com/connectrpc/validate-go/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/connectrpc/validate-go/actions/workflows/ci.yaml) +[![Report Card](https://goreportcard.com/badge/connectrpc.com/validate)](https://goreportcard.com/report/connectrpc.com/validate) +[![GoDoc](https://pkg.go.dev/badge/connectrpc.com/validate.svg)](https://pkg.go.dev/connectrpc.com/validate) + +The `validate` package provides an interceptor implementation for the ConnectRPC +framework. It integrates with the [`protovalidate-go`][protovalidate-go] library +to validate incoming protobuf messages, ensuring adherence to the defined +message structure. This interceptor is a crucial layer in the communication +pipeline, enhancing data integrity and reliability within the ConnectRPC +framework. ## Installation -To use the `validate` package in your Go project, you can add it as a dependency -using `go get`: +To use the `validate` package, you need to have Go installed. You can then +install the package using: -```bash -go get connectrpc.com/validate +```sh +go get -u connectrpc.com/validate ``` ## Usage -The `validate` package offers an interceptor named `Interceptor` that implements -the `connect.Interceptor` interface. This interceptor is used to validate -incoming messages using the `protovalidate-go` library before passing them on to -the next interceptor or handler. - -### Creating an Interceptor +To use the `Interceptor`, follow these steps: -To create a new `Interceptor`, you can use the `NewInterceptor` function -provided by the package: - -```go -validator, err := protovalidate.New() // Initialize your protovalidate validator -if err != nil { - // Handle error -} -interceptor := validate.NewInterceptor(validator) -``` +1. Import the necessary packages: -### Wrapping Unary Functions + ```go + import ( + "connectrpc.com/connect" + "connectrpc.com/validate" + ) + ``` -You can wrap a `connect.UnaryFunc` with the interceptor's validation using -the `WrapUnary` method. This ensures that the incoming request is validated -before being processed: +2. Create a custom validator if needed (optional): -```go -unaryFunc := func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { -// Your unary function logic -} + ```go + validator := protovalidate.New() // Customize the validator as needed + ``` -wrappedUnaryFunc := interceptor.WrapUnary(unaryFunc) -``` + > See [`protovalidate-go`][protovalidate-go] for more information on how to + construct + > a validator. -### Wrapping Streaming Clients +3. Create an instance of the `Interceptor` using `NewInterceptor`: -For streaming clients, you can wrap a `connect.StreamingClientFunc` with the -interceptor's validation using the `WrapStreamingClient` method: + ```go + interceptor, err := validate.NewInterceptor(validate.WithInterceptor(validator)) + if err != nil { + // Handle error + } + ``` -```go -streamingClientFunc := func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { -// Your streaming client logic -} + > If you do not provide a custom validator, the interceptor will create and + use + > a default validator. -wrappedStreamingClientFunc := interceptor.WrapStreamingClient(streamingClientFunc) -``` +4. Apply the interceptor to your ConnectRPC server's handlers: -### Wrapping Streaming Handlers + ```go + path, handler := examplev1connect.NewExampleServiceHandler( + server, + connect.WithInterceptors(interceptor), + ) + ``` -When dealing with streaming handlers, you can wrap -a `connect.StreamingHandlerFunc` with the interceptor's validation using -the `WrapStreamingHandler` method: +By applying the interceptor to your server's handlers, you ensure that incoming +requests are thoroughly validated before being processed. This practice +minimizes the risk of handling invalid or unexpected data, contributing to more +robust and reliable data processing logic. -```go -streamingHandlerFunc := func(ctx context.Context, conn connect.StreamingHandlerConn) error { -// Your streaming handler logic -} +## Ecosystem -wrappedStreamingHandlerFunc := interceptor.WrapStreamingHandler(streamingHandlerFunc) -``` +- [connect-go]: The ConnectRPC framework for Go. +- [protovalidate-go]: A protocol buffer message validator for Go. ## License Offered under the [Apache 2 license](LICENSE). + +[connect-go]: https://github.com/connectrpc/connect-go +[protovalidate-go]: https://github.com/bufbuild/protovalidate-go diff --git a/go.mod b/go.mod index daf4dd2..a087425 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,8 @@ require ( ) require ( + buf.build/gen/go/bufbuild/eliza/connectrpc/go v1.11.0-20230726230109-bf1eaaff2a44.1 // indirect + buf.build/gen/go/bufbuild/eliza/protocolbuffers/go v1.31.0-20230726230109-bf1eaaff2a44.1 // indirect buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230824200731-b9b8148056b9.1 // indirect github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index b7e09d0..7f8570f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +buf.build/gen/go/bufbuild/eliza/connectrpc/go v1.11.0-20230726230109-bf1eaaff2a44.1 h1:qDmGvqEUMAlavAQplT6psGWLCZmkYSTQ9FQIff+P7Kw= +buf.build/gen/go/bufbuild/eliza/connectrpc/go v1.11.0-20230726230109-bf1eaaff2a44.1/go.mod h1:HM9AnJ1yQFbQoMTNOVvmjVeI/NU3BhDviIE3/2yDEPQ= +buf.build/gen/go/bufbuild/eliza/protocolbuffers/go v1.31.0-20230726230109-bf1eaaff2a44.1 h1:NmZA1X/FWAyvcZtcQ2UcCwaeE7LtDbLxjqqMf9G2qvE= +buf.build/gen/go/bufbuild/eliza/protocolbuffers/go v1.31.0-20230726230109-bf1eaaff2a44.1/go.mod h1:mEpu8C3+7iXJhc3ajuUwWLB8FLrhyu9nzdcMqq5ZsXM= buf.build/gen/go/bufbuild/protovalidate-testing/protocolbuffers/go v1.31.0-20230824200732-8bc04916caea.1 h1:wWsaA01KC5hgoW4oHFiGzEqMmgcE7UyU43F8AJrex1U= buf.build/gen/go/bufbuild/protovalidate-testing/protocolbuffers/go v1.31.0-20230824200732-8bc04916caea.1/go.mod h1:cJ4gQkiW4uPTUTI3+O2862OzMSTnzrFNwMauHLwPDPg= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230824200731-b9b8148056b9.1 h1:9Ea7lsYYvoyqmq79GbCy6POXHrZbC+pHs+6lGNx9IBQ= diff --git a/interceptor.go b/interceptor.go index 8a48b26..fbfff35 100644 --- a/interceptor.go +++ b/interceptor.go @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package validate provides an interceptor implementation for the ConnectRPC framework. -// The interceptor integrates with the protovalidate-go library to validate incoming protobuf messages, -// ensuring adherence to the defined message structure. +// Package validate provides an interceptor implementation for the ConnectRPC framework +// that integrates with the protovalidate-go library to validate incoming protobuf messages. +// This interceptor ensures adherence to the defined message structure and can be used +// to enhance the reliability of data communication within the ConnectRPC framework. package validate import ( @@ -27,29 +28,65 @@ import ( "google.golang.org/protobuf/proto" ) -var _ connect.Interceptor = &Interceptor{} - -// Option interface is currently empty and serves as a placeholder for potential future implementations. -// It allows adding new options without breaking existing code. -type Option interface { - unimplemented() -} - -// Interceptor implements the connect.Interceptor interface, providing message validation -// for the ConnectRPC framework. It integrates with the protovalidate-go library to ensure -// incoming protocol buffer messages adhere to the defined message structure. +// Interceptor implements the connect.Interceptor interface and serves as a crucial +// layer in the communication pipeline, by validating incoming requests and ensuring +// they conform to defined protovalidate preconditions. +// +// Default Behaviors: +// - Requests are validated for adherence to the defined message structure. +// - Responses are not validated, focusing validation efforts on incoming data. +// - Errors are raised for incoming messages that are not protocol buffer messages. +// - In case of validation errors, an error detail of the type is attached to provide +// additional context about the validation failure. +// +// It's recommended to use the Interceptor primarily with server-side handlers rather than +// client connections. Placing the Interceptor on handlers ensures that incoming requests +// are thoroughly validated before they are processed, minimizing the risk of handling +// invalid or unexpected data. type Interceptor struct { validator *protovalidate.Validator } -// NewInterceptor returns a new instance of the Interceptor. -// It accepts a protovalidate.Validator as a parameter to perform message validation. -func NewInterceptor( - validator *protovalidate.Validator, - _ ...Option, -) *Interceptor { - return &Interceptor{ - validator: validator, +// Option is a functional option for the Interceptor. +type Option func(*Interceptor) + +// NewInterceptor returns a new instance of the Interceptor. It accepts an optional functional +// option to customize its behavior. If no custom validator is provided, a default validator +// is used for message validation. +// +// Usage: +// +// interceptor, err := NewInterceptor(WithInterceptor(customValidator)) +// if err != nil { +// // Handle error +// } +// +// path, handler := examplev1connect.NewExampleServiceHandler( +// server, +// connect.WithInterceptors(interceptor), +// ) +func NewInterceptor(opts ...Option) (*Interceptor, error) { + out := &Interceptor{} + for _, apply := range opts { + apply(out) + } + + if out.validator == nil { + validator, err := protovalidate.New() + if err != nil { + return nil, err + } + out.validator = validator + } + + return out, nil +} + +// WithInterceptor sets the validator to be used for message validation. +// This option allows customization of the validator used by the Interceptor. +func WithInterceptor(validator *protovalidate.Validator) Option { + return func(i *Interceptor) { + i.validator = validator } } @@ -85,6 +122,7 @@ func (i *Interceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) co type streamingClientInterceptor struct { connect.StreamingClientConn + validator *protovalidate.Validator } @@ -97,6 +135,7 @@ func (s *streamingClientInterceptor) Send(msg any) error { type streamingHandlerInterceptor struct { connect.StreamingHandlerConn + validator *protovalidate.Validator } @@ -108,22 +147,19 @@ func (s *streamingHandlerInterceptor) Receive(msg any) error { } func validate(validator *protovalidate.Validator, msg any) error { - switch protoMessage := msg.(type) { - case connect.AnyRequest: - return validate(validator, protoMessage.Any()) - case proto.Message: - if err := validator.Validate(protoMessage); err != nil { - out := connect.NewError(connect.CodeInvalidArgument, err) - var validationErr *protovalidate.ValidationError - if errors.As(err, &validationErr) { - if detail, err := connect.NewErrorDetail(validationErr.ToProto()); err == nil { - out.AddDetail(detail) - } + protoMessage, ok := msg.(proto.Message) + if !ok { + return fmt.Errorf("message is not a proto.Message: %T", msg) + } + if err := validator.Validate(protoMessage); err != nil { + out := connect.NewError(connect.CodeInvalidArgument, err) + var validationErr *protovalidate.ValidationError + if errors.As(err, &validationErr) { + if detail, err := connect.NewErrorDetail(validationErr.ToProto()); err == nil { + out.AddDetail(detail) } - return out } - default: - return fmt.Errorf("unsupported message type %T", protoMessage) + return out } return nil } diff --git a/interceptor_test.go b/interceptor_test.go index bf5995e..1ade97f 100644 --- a/interceptor_test.go +++ b/interceptor_test.go @@ -16,6 +16,7 @@ package validate import ( "context" + "fmt" "testing" "buf.build/gen/go/bufbuild/protovalidate-testing/protocolbuffers/go/buf/validate/conformance/cases" @@ -25,9 +26,28 @@ import ( "github.com/stretchr/testify/require" ) +func TestNewInterceptor(t *testing.T) { + t.Parallel() + t.Run("success", func(t *testing.T) { + t.Parallel() + interceptor, err := NewInterceptor() + require.NoError(t, err) + assert.NotNil(t, interceptor.validator) + }) + t.Run("success with validator", func(t *testing.T) { + t.Parallel() + validator, err := protovalidate.New() + require.NoError(t, err) + interceptor, err := NewInterceptor(WithInterceptor(validator)) + require.NoError(t, err) + assert.NotNil(t, interceptor.validator) + assert.Equal(t, interceptor.validator, validator) + }) +} + func TestInterceptor_WrapUnary(t *testing.T) { t.Parallel() - var tests = []struct { + tests := []struct { name string message *cases.StringConst wantErr bool @@ -47,9 +67,8 @@ func TestInterceptor_WrapUnary(t *testing.T) { test := tt t.Run(test.name, func(t *testing.T) { t.Parallel() - validator, err := protovalidate.New() + interceptor, err := NewInterceptor() require.NoError(t, err) - interceptor := NewInterceptor(validator) mockUnary := func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { return nil, nil } @@ -66,48 +85,79 @@ func TestInterceptor_WrapUnary(t *testing.T) { } } -var _ connect.StreamingClientConn = &mockStreamingClientConn{} +var _ connect.StreamingClientConn = (*mockStreamingClientConn)(nil) type mockStreamingClientConn struct { connect.StreamingClientConn + + sendFunc func(any) error } -func (m *mockStreamingClientConn) Send(_ any) error { - return nil +func (m *mockStreamingClientConn) Send(in any) error { + return m.sendFunc(in) } -func TestStreamingClientInterceptor_Send(t *testing.T) { - t.Parallel() - var tests = []struct { - name string - message *cases.StringConst - wantErr bool - }{ - { - name: "success", - message: &cases.StringConst{Val: "foo"}, - wantErr: false, +var streamingTests = []struct { + name string + message any + mock func(any) error + wantErr string +}{ + { + name: "success", + message: &cases.StringConst{Val: "foo"}, + mock: func(a any) error { + return nil }, - { - name: "fail", - message: &cases.StringConst{Val: "bar"}, - wantErr: true, + wantErr: "", + }, + { + name: "fail validation", + message: &cases.StringConst{Val: "bar"}, + mock: func(a any) error { + return nil }, - } - for _, tt := range tests { + wantErr: "invalid_argument: validation error:\n - val: value must equal `foo` [string.const]", + }, + { + name: "fail not a proto.Message", + message: struct{ name string }{name: "baz"}, + mock: func(any) error { + return nil + }, + wantErr: "message is not a proto.Message: struct { name string }", + }, + { + name: "pass validation and fail send", + message: &cases.StringConst{Val: "foo"}, + mock: func(any) error { + return fmt.Errorf("send error") + }, + wantErr: "send error", + }, +} + +func TestStreamingClientInterceptor_Send(t *testing.T) { + t.Parallel() + for _, tt := range streamingTests { test := tt t.Run(test.name, func(t *testing.T) { t.Parallel() - validator, err := protovalidate.New() + interceptor, err := NewInterceptor() require.NoError(t, err) - clientConn := streamingClientInterceptor{ - validator: validator, - StreamingClientConn: &mockStreamingClientConn{}, + next := func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { + return &mockStreamingClientConn{ + sendFunc: test.mock, + } } - err = clientConn.Send(connect.NewRequest(test.message)) - if test.wantErr { - assert.Error(t, err) + + client := interceptor.WrapStreamingClient(next) + conn := client(context.Background(), connect.Spec{}) + err = conn.Send(test.message) + if test.wantErr != "" { + require.Error(t, err) + assert.EqualError(t, err, test.wantErr) } else { assert.NoError(t, err) } @@ -115,47 +165,38 @@ func TestStreamingClientInterceptor_Send(t *testing.T) { } } -var _ connect.StreamingHandlerConn = &mockStreamingHandlerConn{} +var _ connect.StreamingHandlerConn = (*mockStreamingHandlerConn)(nil) type mockStreamingHandlerConn struct { connect.StreamingHandlerConn + + receiveFunc func(any) error } -func (m *mockStreamingHandlerConn) Receive(_ any) error { - return nil +func (m *mockStreamingHandlerConn) Receive(in any) error { + return m.receiveFunc(in) } func TestStreamingHandlerInterceptor_Receive(t *testing.T) { t.Parallel() - var tests = []struct { - name string - message *cases.StringConst - wantErr bool - }{ - { - name: "success", - message: &cases.StringConst{Val: "foo"}, - wantErr: false, - }, - { - name: "fail", - message: &cases.StringConst{Val: "bar"}, - wantErr: true, - }, - } - for _, tt := range tests { + for _, tt := range streamingTests { test := tt t.Run(test.name, func(t *testing.T) { t.Parallel() - validator, err := protovalidate.New() + interceptor, err := NewInterceptor() require.NoError(t, err) - handlerConn := &streamingHandlerInterceptor{ - validator: validator, - StreamingHandlerConn: &mockStreamingHandlerConn{}, + + next := func(ctx context.Context, conn connect.StreamingHandlerConn) error { + return conn.Receive(test.message) } - err = handlerConn.Receive(connect.NewRequest(test.message)) - if test.wantErr { - assert.Error(t, err) + conn := &mockStreamingHandlerConn{ + receiveFunc: test.mock, + } + + err = interceptor.WrapStreamingHandler(next)(context.Background(), conn) + if test.wantErr != "" { + require.Error(t, err) + assert.EqualError(t, err, test.wantErr) } else { assert.NoError(t, err) } From 32ce60dd9e4dfb0f14e634324c0ba25cf4a1c578 Mon Sep 17 00:00:00 2001 From: Elliot Jackson <13633636+ElliotMJackson@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:38:22 -0400 Subject: [PATCH 05/20] Add tests to interceptor with complete HTTP stack --- .golangci.yml | 5 +- Makefile | 7 +- README.md | 116 ++-- buf.gen.yaml | 16 + buf.work.yaml | 3 + go.mod | 4 +- go.sum | 4 - interceptor.go | 10 +- interceptor_test.go | 464 ++++++++++++---- internal/gen/connect/ping/v1/ping.pb.go | 514 ++++++++++++++++++ .../ping/v1/pingv1connect/ping.connect.go | 181 ++++++ internal/proto/buf.lock | 8 + internal/proto/buf.yaml | 9 + internal/proto/connect/ping/v1/ping.proto | 82 +++ internal/testserver/in_memory_server.go | 126 +++++ internal/testserver/ping_server.go | 84 +++ 16 files changed, 1452 insertions(+), 181 deletions(-) create mode 100644 buf.gen.yaml create mode 100644 buf.work.yaml create mode 100644 internal/gen/connect/ping/v1/ping.pb.go create mode 100644 internal/gen/connect/ping/v1/pingv1connect/ping.connect.go create mode 100644 internal/proto/buf.lock create mode 100644 internal/proto/buf.yaml create mode 100644 internal/proto/connect/ping/v1/ping.proto create mode 100644 internal/testserver/in_memory_server.go create mode 100644 internal/testserver/ping_server.go diff --git a/.golangci.yml b/.golangci.yml index cecfb1b..29dea8b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -54,8 +54,11 @@ issues: exclude-rules: - path: interceptor_test.go linters: - - gochecknoglobals - exhaustruct + - dupl - path: interceptor.go linters: - exhaustruct + - path: internal/testserver + linters: + - exhaustruct diff --git a/Makefile b/Makefile index 41eac4c..9b03415 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,8 @@ build: generate ## Build all packages go build ./... .PHONY: generate -generate: $(BIN)/license-header ## Regenerate code and licenses +generate: $(BIN)/buf $(BIN)/license-header ## Regenerate code and licenses + buf generate license-header \ --license-type apache \ --copyright-holder "Buf Technologies, Inc." \ @@ -64,6 +65,10 @@ checkgenerate: @# Used in CI to verify that `make generate` doesn't produce a diff. test -z "$$(git status --porcelain | tee /dev/stderr)" +$(BIN)/buf: Makefile + @mkdir -p $(@D) + go install github.com/bufbuild/buf/cmd/buf@v1.26.1 + $(BIN)/license-header: Makefile @mkdir -p $(@D) go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v1.26.1 diff --git a/README.md b/README.md index 891e936..510a9ba 100644 --- a/README.md +++ b/README.md @@ -4,66 +4,72 @@ [![Report Card](https://goreportcard.com/badge/connectrpc.com/validate)](https://goreportcard.com/report/connectrpc.com/validate) [![GoDoc](https://pkg.go.dev/badge/connectrpc.com/validate.svg)](https://pkg.go.dev/connectrpc.com/validate) -The `validate` package provides an interceptor implementation for the ConnectRPC -framework. It integrates with the [`protovalidate-go`][protovalidate-go] library -to validate incoming protobuf messages, ensuring adherence to the defined -message structure. This interceptor is a crucial layer in the communication -pipeline, enhancing data integrity and reliability within the ConnectRPC -framework. +`connectrpc.com/validate` adds support for a protovalidate interceptor to Connect servers. + +[`protovalidate`][protovalidate-go] is a series of libraries designed to validate Protobuf messages at +runtime based on user-defined validation rules. Powered by Google's Common +Expression Language ([CEL][cel-spec]), it provides a +flexible and efficient foundation for defining and evaluating custom validation +rules. The primary goal of [`protovalidate`][protovalidate] is to help developers ensure data +consistency and integrity across the network without requiring generated code. ## Installation -To use the `validate` package, you need to have Go installed. You can then -install the package using: +Add the interceptor to your project with `go get`: -```sh -go get -u connectrpc.com/validate +```bash +go get connectrpc.com/validate ``` -## Usage - -To use the `Interceptor`, follow these steps: - -1. Import the necessary packages: - - ```go - import ( - "connectrpc.com/connect" - "connectrpc.com/validate" - ) - ``` - -2. Create a custom validator if needed (optional): - - ```go - validator := protovalidate.New() // Customize the validator as needed - ``` - - > See [`protovalidate-go`][protovalidate-go] for more information on how to - construct - > a validator. - -3. Create an instance of the `Interceptor` using `NewInterceptor`: - - ```go - interceptor, err := validate.NewInterceptor(validate.WithInterceptor(validator)) - if err != nil { - // Handle error - } - ``` - - > If you do not provide a custom validator, the interceptor will create and - use - > a default validator. - -4. Apply the interceptor to your ConnectRPC server's handlers: - - ```go - path, handler := examplev1connect.NewExampleServiceHandler( - server, - connect.WithInterceptors(interceptor), - ) - ``` +## An Example + +```go +package main + +import ( + "context" + "fmt" + "log" + "net/http" + + "connectrpc.com/connect" + "connectrpc.com/validate" + // Generated from your protobuf schema by protoc-gen-go and + // protoc-gen-connect-go. + pingv1 "connectrpc.com/validate/internal/gen/connect/ping/v1" + "connectrpc.com/validate/internal/gen/connect/ping/v1/pingv1connect" +) + +func main() { + interceptor, err := validate.NewInterceptor() + if err != nil { + log.Fatal(err) + } + + mux := http.NewServeMux() + mux.Handle(pingv1connect.NewPingServiceHandler( + &pingv1connect.UnimplementedPingServiceHandler{}, + connect.WithInterceptors(interceptor), + )) + + http.ListenAndServe("localhost:8080", mux) +} + +func makeRequest() { + client := pingv1connect.NewPingServiceClient( + http.DefaultClient, + "http://localhost:8080", + ) + resp, err := client.Ping( + context.Background(), + connect.NewRequest(&pingv1.PingRequest{}), + ) + if err != nil { + log.Fatal(err) + } + fmt.Println(resp) +} +``` By applying the interceptor to your server's handlers, you ensure that incoming requests are thoroughly validated before being processed. This practice @@ -81,3 +87,5 @@ Offered under the [Apache 2 license](LICENSE). [connect-go]: https://github.com/connectrpc/connect-go [protovalidate-go]: https://github.com/bufbuild/protovalidate-go +[cel-spec]: https://github.com/google/cel-spec +[protovalidate]: https://github.com/bufbuild/protovalidate diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..cb5578b --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,16 @@ +version: v1 +managed: + enabled: true + go_package_prefix: + default: connectrpc.com/validate/internal/gen + except: + - buf.build/bufbuild/protovalidate +plugins: + - plugin: buf.build/connectrpc/go:v1.11.1 + out: internal/gen + opt: + - paths=source_relative + - plugin: buf.build/protocolbuffers/go + out: internal/gen + opt: + - paths=source_relative diff --git a/buf.work.yaml b/buf.work.yaml new file mode 100644 index 0000000..30f1e1f --- /dev/null +++ b/buf.work.yaml @@ -0,0 +1,3 @@ +version: v1 +directories: + - internal/proto diff --git a/go.mod b/go.mod index a087425..68f7336 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( buf.build/gen/go/bufbuild/protovalidate-testing/protocolbuffers/go v1.31.0-20230824200732-8bc04916caea.1 + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230824200731-b9b8148056b9.1 connectrpc.com/connect v1.11.0 github.com/bufbuild/protovalidate-go v0.3.0 github.com/stretchr/testify v1.8.4 @@ -11,9 +12,6 @@ require ( ) require ( - buf.build/gen/go/bufbuild/eliza/connectrpc/go v1.11.0-20230726230109-bf1eaaff2a44.1 // indirect - buf.build/gen/go/bufbuild/eliza/protocolbuffers/go v1.31.0-20230726230109-bf1eaaff2a44.1 // indirect - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230824200731-b9b8148056b9.1 // indirect github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/cel-go v0.17.4 // indirect diff --git a/go.sum b/go.sum index 7f8570f..b7e09d0 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,3 @@ -buf.build/gen/go/bufbuild/eliza/connectrpc/go v1.11.0-20230726230109-bf1eaaff2a44.1 h1:qDmGvqEUMAlavAQplT6psGWLCZmkYSTQ9FQIff+P7Kw= -buf.build/gen/go/bufbuild/eliza/connectrpc/go v1.11.0-20230726230109-bf1eaaff2a44.1/go.mod h1:HM9AnJ1yQFbQoMTNOVvmjVeI/NU3BhDviIE3/2yDEPQ= -buf.build/gen/go/bufbuild/eliza/protocolbuffers/go v1.31.0-20230726230109-bf1eaaff2a44.1 h1:NmZA1X/FWAyvcZtcQ2UcCwaeE7LtDbLxjqqMf9G2qvE= -buf.build/gen/go/bufbuild/eliza/protocolbuffers/go v1.31.0-20230726230109-bf1eaaff2a44.1/go.mod h1:mEpu8C3+7iXJhc3ajuUwWLB8FLrhyu9nzdcMqq5ZsXM= buf.build/gen/go/bufbuild/protovalidate-testing/protocolbuffers/go v1.31.0-20230824200732-8bc04916caea.1 h1:wWsaA01KC5hgoW4oHFiGzEqMmgcE7UyU43F8AJrex1U= buf.build/gen/go/bufbuild/protovalidate-testing/protocolbuffers/go v1.31.0-20230824200732-8bc04916caea.1/go.mod h1:cJ4gQkiW4uPTUTI3+O2862OzMSTnzrFNwMauHLwPDPg= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230824200731-b9b8148056b9.1 h1:9Ea7lsYYvoyqmq79GbCy6POXHrZbC+pHs+6lGNx9IBQ= diff --git a/interceptor.go b/interceptor.go index fbfff35..a4fed69 100644 --- a/interceptor.go +++ b/interceptor.go @@ -56,7 +56,7 @@ type Option func(*Interceptor) // // Usage: // -// interceptor, err := NewInterceptor(WithInterceptor(customValidator)) +// interceptor, err := NewInterceptor(WithValidator(customValidator)) // if err != nil { // // Handle error // } @@ -82,9 +82,9 @@ func NewInterceptor(opts ...Option) (*Interceptor, error) { return out, nil } -// WithInterceptor sets the validator to be used for message validation. +// WithValidator sets the validator to be used for message validation. // This option allows customization of the validator used by the Interceptor. -func WithInterceptor(validator *protovalidate.Validator) Option { +func WithValidator(validator *protovalidate.Validator) Option { return func(i *Interceptor) { i.validator = validator } @@ -140,10 +140,10 @@ type streamingHandlerInterceptor struct { } func (s *streamingHandlerInterceptor) Receive(msg any) error { - if err := validate(s.validator, msg); err != nil { + if err := s.StreamingHandlerConn.Receive(msg); err != nil { return err } - return s.StreamingHandlerConn.Receive(msg) + return validate(s.validator, msg) } func validate(validator *protovalidate.Validator, msg any) error { diff --git a/interceptor_test.go b/interceptor_test.go index 1ade97f..c8e50af 100644 --- a/interceptor_test.go +++ b/interceptor_test.go @@ -12,15 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -package validate +package validate_test import ( "context" + "errors" "fmt" + "net/http" "testing" + "time" - "buf.build/gen/go/bufbuild/protovalidate-testing/protocolbuffers/go/buf/validate/conformance/cases" + validateproto "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "connectrpc.com/connect" + "connectrpc.com/validate" + pingv1 "connectrpc.com/validate/internal/gen/connect/ping/v1" + "connectrpc.com/validate/internal/gen/connect/ping/v1/pingv1connect" + "connectrpc.com/validate/internal/testserver" "github.com/bufbuild/protovalidate-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,175 +37,406 @@ func TestNewInterceptor(t *testing.T) { t.Parallel() t.Run("success", func(t *testing.T) { t.Parallel() - interceptor, err := NewInterceptor() + interceptor, err := validate.NewInterceptor() require.NoError(t, err) - assert.NotNil(t, interceptor.validator) + assert.NotNil(t, interceptor) }) t.Run("success with validator", func(t *testing.T) { t.Parallel() validator, err := protovalidate.New() require.NoError(t, err) - interceptor, err := NewInterceptor(WithInterceptor(validator)) + interceptor, err := validate.NewInterceptor(validate.WithValidator(validator)) require.NoError(t, err) - assert.NotNil(t, interceptor.validator) - assert.Equal(t, interceptor.validator, validator) + assert.NotNil(t, interceptor) }) } func TestInterceptor_WrapUnary(t *testing.T) { t.Parallel() + type args struct { + msg string + code connect.Code + detail *protovalidate.ValidationError + } tests := []struct { name string - message *cases.StringConst - wantErr bool + svc pingv1connect.PingServiceHandler + req *pingv1.PingRequest + want *pingv1.PingResponse + wantErr *args }{ { - name: "success", - message: &cases.StringConst{Val: "foo"}, - wantErr: false, + name: "empty request returns error on required request fields", + req: &pingv1.PingRequest{}, + wantErr: &args{ + msg: "validation error:\n - number: value is required [required]", + code: connect.CodeInvalidArgument, + detail: &protovalidate.ValidationError{ + Violations: []*validateproto.Violation{ + { + FieldPath: "number", + ConstraintId: "required", + Message: "value is required", + }, + }, + }, + }, + }, + { + name: "invalid request returns error with constraint violation", + req: &pingv1.PingRequest{ + Number: 123, + }, + wantErr: &args{ + msg: "validation error:\n - number: value must be greater than 0 and less than 100 [int64.gt_lt]", + code: connect.CodeInvalidArgument, + detail: &protovalidate.ValidationError{ + Violations: []*validateproto.Violation{ + { + FieldPath: "number", + ConstraintId: "int64.gt_lt", + Message: "value must be greater than 0 and less than 100", + }, + }, + }, + }, + }, + { + name: "unrelated server error remains unaffected", + svc: testserver.NewPingServer( + testserver.WithErr( + connect.NewError(connect.CodeInternal, fmt.Errorf("oh no")), + ), + ), + req: &pingv1.PingRequest{ + Number: 50, + }, + wantErr: &args{ + msg: "oh no", + code: connect.CodeInternal, + }, }, { - name: "fail", - message: &cases.StringConst{Val: "bar"}, - wantErr: true, + name: "valid request returns response", + req: &pingv1.PingRequest{ + Number: 50, + }, + want: &pingv1.PingResponse{ + Number: 50, + }, }, } for _, tt := range tests { test := tt t.Run(test.name, func(t *testing.T) { t.Parallel() - interceptor, err := NewInterceptor() - require.NoError(t, err) - mockUnary := func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { - return nil, nil + + if test.svc == nil { + test.svc = testserver.NewPingServer() } - _, err = interceptor.WrapUnary(mockUnary)( - context.Background(), - connect.NewRequest(test.message), + + validator, err := validate.NewInterceptor() + require.NoError(t, err) + + mux := http.NewServeMux() + mux.Handle(pingv1connect.NewPingServiceHandler( + test.svc, + connect.WithInterceptors(validator), + )) + + exampleBookingServer := testserver.NewInMemoryServer(mux) + defer exampleBookingServer.Close() + + client := pingv1connect.NewPingServiceClient( + exampleBookingServer.Client(), + exampleBookingServer.URL(), ) - if test.wantErr { - assert.Error(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + got, err := client.Ping(ctx, connect.NewRequest(test.req)) + if test.wantErr != nil { + require.Error(t, err) + var connectErr *connect.Error + assert.True(t, errors.As(err, &connectErr)) + assert.Equal(t, test.wantErr.msg, connectErr.Message()) + assert.Equal(t, test.wantErr.code, connectErr.Code()) + if test.wantErr.detail != nil { + require.Len(t, connectErr.Details(), 1) + detail, err := connect.NewErrorDetail(test.wantErr.detail.ToProto()) + require.NoError(t, err) + assert.Equal(t, connectErr.Details()[0].Type(), detail.Type()) + } + assert.Nil(t, got) } else { - assert.NoError(t, err) + require.NoError(t, err) + assert.Equal(t, test.want.GetNumber(), got.Msg.GetNumber()) } }) } } -var _ connect.StreamingClientConn = (*mockStreamingClientConn)(nil) - -type mockStreamingClientConn struct { - connect.StreamingClientConn - - sendFunc func(any) error -} - -func (m *mockStreamingClientConn) Send(in any) error { - return m.sendFunc(in) -} - -var streamingTests = []struct { - name string - message any - mock func(any) error - wantErr string -}{ - { - name: "success", - message: &cases.StringConst{Val: "foo"}, - mock: func(a any) error { - return nil +func TestInterceptor_WrapStreamingClient(t *testing.T) { + t.Parallel() + type args struct { + msg string + code connect.Code + detail *protovalidate.ValidationError + closeErr bool + } + tests := []struct { + name string + svc pingv1connect.PingServiceHandler + req *pingv1.SumRequest + want *pingv1.SumResponse + wantErr *args + }{ + { + name: "empty request returns error on required request fields", + req: &pingv1.SumRequest{}, + wantErr: &args{ + msg: "validation error:\n - number: value is required [required]", + code: connect.CodeInvalidArgument, + detail: &protovalidate.ValidationError{ + Violations: []*validateproto.Violation{ + { + FieldPath: "number", + ConstraintId: "required", + Message: "value is required", + }, + }, + }, + }, }, - wantErr: "", - }, - { - name: "fail validation", - message: &cases.StringConst{Val: "bar"}, - mock: func(a any) error { - return nil + { + name: "invalid request returns error with constraint violation", + req: &pingv1.SumRequest{ + Number: 123, + }, + wantErr: &args{ + msg: "validation error:\n - number: value must be greater than 0 and less than 100 [int64.gt_lt]", + code: connect.CodeInvalidArgument, + detail: &protovalidate.ValidationError{ + Violations: []*validateproto.Violation{ + { + FieldPath: "number", + ConstraintId: "int64.gt_lt", + Message: "value must be greater than 0 and less than 100", + }, + }, + }, + }, }, - wantErr: "invalid_argument: validation error:\n - val: value must equal `foo` [string.const]", - }, - { - name: "fail not a proto.Message", - message: struct{ name string }{name: "baz"}, - mock: func(any) error { - return nil + { + name: "unrelated server error remains unaffected", + svc: testserver.NewPingServer( + testserver.WithErr( + connect.NewError(connect.CodeInternal, fmt.Errorf("oh no")), + ), + ), + req: &pingv1.SumRequest{ + Number: 50, + }, + wantErr: &args{ + msg: "oh no", + code: connect.CodeInternal, + closeErr: true, + }, }, - wantErr: "message is not a proto.Message: struct { name string }", - }, - { - name: "pass validation and fail send", - message: &cases.StringConst{Val: "foo"}, - mock: func(any) error { - return fmt.Errorf("send error") + { + name: "valid request returns response", + req: &pingv1.SumRequest{ + Number: 50, + }, + want: &pingv1.SumResponse{ + Sum: 50, + }, }, - wantErr: "send error", - }, -} - -func TestStreamingClientInterceptor_Send(t *testing.T) { - t.Parallel() - for _, tt := range streamingTests { + } + for _, tt := range tests { test := tt t.Run(test.name, func(t *testing.T) { t.Parallel() - interceptor, err := NewInterceptor() - require.NoError(t, err) - next := func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { - return &mockStreamingClientConn{ - sendFunc: test.mock, - } + if test.svc == nil { + test.svc = testserver.NewPingServer() } - client := interceptor.WrapStreamingClient(next) - conn := client(context.Background(), connect.Spec{}) - err = conn.Send(test.message) - if test.wantErr != "" { + validator, err := validate.NewInterceptor() + require.NoError(t, err) + + mux := http.NewServeMux() + mux.Handle(pingv1connect.NewPingServiceHandler( + test.svc, + )) + + exampleBookingServer := testserver.NewInMemoryServer(mux) + defer exampleBookingServer.Close() + + client := pingv1connect.NewPingServiceClient( + exampleBookingServer.Client(), + exampleBookingServer.URL(), + connect.WithInterceptors(validator), + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + stream := client.Sum(ctx) + err = stream.Send(test.req) + resp, closeErr := stream.CloseAndReceive() + if test.wantErr != nil { + if test.wantErr.closeErr { + err = closeErr + } require.Error(t, err) - assert.EqualError(t, err, test.wantErr) + var connectErr *connect.Error + assert.True(t, errors.As(err, &connectErr)) + assert.Equal(t, test.wantErr.msg, connectErr.Message()) + assert.Equal(t, test.wantErr.code, connectErr.Code()) + if test.wantErr.detail != nil { + require.Len(t, connectErr.Details(), 1) + detail, err := connect.NewErrorDetail(test.wantErr.detail.ToProto()) + require.NoError(t, err) + assert.Equal(t, connectErr.Details()[0].Type(), detail.Type()) + } } else { - assert.NoError(t, err) + require.NoError(t, err) + require.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, resp.Msg.GetSum(), test.want.GetSum()) } }) } } -var _ connect.StreamingHandlerConn = (*mockStreamingHandlerConn)(nil) - -type mockStreamingHandlerConn struct { - connect.StreamingHandlerConn - - receiveFunc func(any) error -} - -func (m *mockStreamingHandlerConn) Receive(in any) error { - return m.receiveFunc(in) -} - -func TestStreamingHandlerInterceptor_Receive(t *testing.T) { +func TestInterceptor_WrapStreamingHandler(t *testing.T) { t.Parallel() - for _, tt := range streamingTests { + type args struct { + msg string + code connect.Code + detail *protovalidate.ValidationError + closeErr bool + } + tests := []struct { + name string + svc pingv1connect.PingServiceHandler + req *pingv1.CountUpRequest + want *pingv1.CountUpResponse + wantErr *args + }{ + { + name: "empty request returns error on required request fields", + req: &pingv1.CountUpRequest{}, + wantErr: &args{ + msg: "validation error:\n - number: value is required [required]", + code: connect.CodeInvalidArgument, + detail: &protovalidate.ValidationError{ + Violations: []*validateproto.Violation{ + { + FieldPath: "number", + ConstraintId: "required", + Message: "value is required", + }, + }, + }, + }, + }, + { + name: "invalid request returns error with constraint violation", + req: &pingv1.CountUpRequest{ + Number: 123, + }, + wantErr: &args{ + msg: "validation error:\n - number: value must be greater than 0 and less than 100 [int64.gt_lt]", + code: connect.CodeInvalidArgument, + detail: &protovalidate.ValidationError{ + Violations: []*validateproto.Violation{ + { + FieldPath: "number", + ConstraintId: "int64.gt_lt", + Message: "value must be greater than 0 and less than 100", + }, + }, + }, + }, + }, + { + name: "unrelated server error remains unaffected", + svc: testserver.NewPingServer( + testserver.WithErr( + connect.NewError(connect.CodeInternal, fmt.Errorf("oh no")), + ), + ), + req: &pingv1.CountUpRequest{ + Number: 50, + }, + wantErr: &args{ + msg: "oh no", + code: connect.CodeInternal, + closeErr: true, + }, + }, + { + name: "valid request returns response", + req: &pingv1.CountUpRequest{ + Number: 50, + }, + want: &pingv1.CountUpResponse{ + Number: 1, + }, + }, + } + for _, tt := range tests { test := tt t.Run(test.name, func(t *testing.T) { t.Parallel() - interceptor, err := NewInterceptor() - require.NoError(t, err) - next := func(ctx context.Context, conn connect.StreamingHandlerConn) error { - return conn.Receive(test.message) - } - conn := &mockStreamingHandlerConn{ - receiveFunc: test.mock, + if test.svc == nil { + test.svc = testserver.NewPingServer() } + validator, err := validate.NewInterceptor() + require.NoError(t, err) + mux := http.NewServeMux() + mux.Handle(pingv1connect.NewPingServiceHandler( + test.svc, + connect.WithInterceptors(validator), + )) - err = interceptor.WrapStreamingHandler(next)(context.Background(), conn) - if test.wantErr != "" { + exampleBookingServer := testserver.NewInMemoryServer(mux) + defer exampleBookingServer.Close() + + client := pingv1connect.NewPingServiceClient( + exampleBookingServer.Client(), + exampleBookingServer.URL(), + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + stream, err := client.CountUp(ctx, connect.NewRequest(test.req)) + require.NoError(t, err) + receive := stream.Receive() + got := stream.Msg() + err = stream.Err() + if test.wantErr != nil { require.Error(t, err) - assert.EqualError(t, err, test.wantErr) + var connectErr *connect.Error + assert.True(t, errors.As(err, &connectErr)) + assert.Equal(t, test.wantErr.msg, connectErr.Message()) + assert.Equal(t, test.wantErr.code, connectErr.Code()) + if test.wantErr.detail != nil { + require.Len(t, connectErr.Details(), 1) + detail, err := connect.NewErrorDetail(test.wantErr.detail.ToProto()) + require.NoError(t, err) + assert.Equal(t, connectErr.Details()[0].Type(), detail.Type()) + } } else { - assert.NoError(t, err) + require.NoError(t, err) + assert.True(t, receive) + assert.NotNil(t, got) + assert.Equal(t, test.want.GetNumber(), got.GetNumber()) } }) } diff --git a/internal/gen/connect/ping/v1/ping.pb.go b/internal/gen/connect/ping/v1/ping.pb.go new file mode 100644 index 0000000..0fd5140 --- /dev/null +++ b/internal/gen/connect/ping/v1/ping.pb.go @@ -0,0 +1,514 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The canonical location for this file is +// https://github.com/connectrpc/validate-go/blob/main/internal/proto/connect/ping/v1/ping.proto. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: connect/ping/v1/ping.proto + +// The connect.ping.v1 package contains an echo service designed to test the +// validate-go implementation. + +package pingv1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PingRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` +} + +func (x *PingRequest) Reset() { + *x = PingRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_connect_ping_v1_ping_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingRequest) ProtoMessage() {} + +func (x *PingRequest) ProtoReflect() protoreflect.Message { + mi := &file_connect_ping_v1_ping_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 PingRequest.ProtoReflect.Descriptor instead. +func (*PingRequest) Descriptor() ([]byte, []int) { + return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{0} +} + +func (x *PingRequest) GetNumber() int64 { + if x != nil { + return x.Number + } + return 0 +} + +type PingResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` +} + +func (x *PingResponse) Reset() { + *x = PingResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_connect_ping_v1_ping_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingResponse) ProtoMessage() {} + +func (x *PingResponse) ProtoReflect() protoreflect.Message { + mi := &file_connect_ping_v1_ping_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 PingResponse.ProtoReflect.Descriptor instead. +func (*PingResponse) Descriptor() ([]byte, []int) { + return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{1} +} + +func (x *PingResponse) GetNumber() int64 { + if x != nil { + return x.Number + } + return 0 +} + +type SumRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` +} + +func (x *SumRequest) Reset() { + *x = SumRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_connect_ping_v1_ping_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SumRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SumRequest) ProtoMessage() {} + +func (x *SumRequest) ProtoReflect() protoreflect.Message { + mi := &file_connect_ping_v1_ping_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 SumRequest.ProtoReflect.Descriptor instead. +func (*SumRequest) Descriptor() ([]byte, []int) { + return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{2} +} + +func (x *SumRequest) GetNumber() int64 { + if x != nil { + return x.Number + } + return 0 +} + +type SumResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Sum int64 `protobuf:"varint,1,opt,name=sum,proto3" json:"sum,omitempty"` +} + +func (x *SumResponse) Reset() { + *x = SumResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_connect_ping_v1_ping_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SumResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SumResponse) ProtoMessage() {} + +func (x *SumResponse) ProtoReflect() protoreflect.Message { + mi := &file_connect_ping_v1_ping_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 SumResponse.ProtoReflect.Descriptor instead. +func (*SumResponse) Descriptor() ([]byte, []int) { + return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{3} +} + +func (x *SumResponse) GetSum() int64 { + if x != nil { + return x.Sum + } + return 0 +} + +type CountUpRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` +} + +func (x *CountUpRequest) Reset() { + *x = CountUpRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_connect_ping_v1_ping_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CountUpRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CountUpRequest) ProtoMessage() {} + +func (x *CountUpRequest) ProtoReflect() protoreflect.Message { + mi := &file_connect_ping_v1_ping_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 CountUpRequest.ProtoReflect.Descriptor instead. +func (*CountUpRequest) Descriptor() ([]byte, []int) { + return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{4} +} + +func (x *CountUpRequest) GetNumber() int64 { + if x != nil { + return x.Number + } + return 0 +} + +type CountUpResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` +} + +func (x *CountUpResponse) Reset() { + *x = CountUpResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_connect_ping_v1_ping_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CountUpResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CountUpResponse) ProtoMessage() {} + +func (x *CountUpResponse) ProtoReflect() protoreflect.Message { + mi := &file_connect_ping_v1_ping_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 CountUpResponse.ProtoReflect.Descriptor instead. +func (*CountUpResponse) Descriptor() ([]byte, []int) { + return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{5} +} + +func (x *CountUpResponse) GetNumber() int64 { + if x != nil { + return x.Number + } + return 0 +} + +var File_connect_ping_v1_ping_proto protoreflect.FileDescriptor + +var file_connect_ping_v1_ping_proto_rawDesc = []byte{ + 0x0a, 0x1a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x2f, 0x76, + 0x31, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x63, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x1a, 0x1b, 0x62, + 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x33, 0x0a, 0x0b, 0x50, 0x69, + 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x06, 0x6e, 0x75, 0x6d, + 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x0c, 0xba, 0x48, 0x09, 0xc8, 0x01, + 0x01, 0x22, 0x04, 0x10, 0x64, 0x20, 0x00, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, + 0x34, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x24, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, + 0x0c, 0xba, 0x48, 0x09, 0xc8, 0x01, 0x01, 0x22, 0x04, 0x10, 0x64, 0x20, 0x00, 0x52, 0x06, 0x6e, + 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x32, 0x0a, 0x0a, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x42, 0x0c, 0xba, 0x48, 0x09, 0xc8, 0x01, 0x01, 0x22, 0x04, 0x10, 0x64, 0x20, + 0x00, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x1f, 0x0a, 0x0b, 0x53, 0x75, 0x6d, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x75, 0x6d, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x73, 0x75, 0x6d, 0x22, 0x36, 0x0a, 0x0e, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x06, + 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x0c, 0xba, 0x48, + 0x09, 0xc8, 0x01, 0x01, 0x22, 0x04, 0x10, 0x64, 0x20, 0x00, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, + 0x65, 0x72, 0x22, 0x29, 0x0a, 0x0f, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x32, 0xec, 0x01, + 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, + 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, + 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, + 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x03, 0x53, 0x75, 0x6d, 0x12, 0x1b, 0x2e, 0x63, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, + 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x6d, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x12, 0x50, 0x0a, 0x07, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x55, 0x70, 0x12, 0x1f, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, + 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x42, 0xbb, 0x01, 0x0a, + 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, + 0x67, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x50, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, + 0x01, 0x5a, 0x3b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, + 0x70, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x69, 0x6e, 0x67, 0x76, 0x31, 0xa2, 0x02, + 0x03, 0x43, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x50, + 0x69, 0x6e, 0x67, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x5c, 0x50, 0x69, 0x6e, 0x67, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1b, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x5c, 0x50, 0x69, 0x6e, 0x67, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x3a, 0x3a, 0x50, 0x69, 0x6e, 0x67, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_connect_ping_v1_ping_proto_rawDescOnce sync.Once + file_connect_ping_v1_ping_proto_rawDescData = file_connect_ping_v1_ping_proto_rawDesc +) + +func file_connect_ping_v1_ping_proto_rawDescGZIP() []byte { + file_connect_ping_v1_ping_proto_rawDescOnce.Do(func() { + file_connect_ping_v1_ping_proto_rawDescData = protoimpl.X.CompressGZIP(file_connect_ping_v1_ping_proto_rawDescData) + }) + return file_connect_ping_v1_ping_proto_rawDescData +} + +var file_connect_ping_v1_ping_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_connect_ping_v1_ping_proto_goTypes = []interface{}{ + (*PingRequest)(nil), // 0: connect.ping.v1.PingRequest + (*PingResponse)(nil), // 1: connect.ping.v1.PingResponse + (*SumRequest)(nil), // 2: connect.ping.v1.SumRequest + (*SumResponse)(nil), // 3: connect.ping.v1.SumResponse + (*CountUpRequest)(nil), // 4: connect.ping.v1.CountUpRequest + (*CountUpResponse)(nil), // 5: connect.ping.v1.CountUpResponse +} +var file_connect_ping_v1_ping_proto_depIdxs = []int32{ + 0, // 0: connect.ping.v1.PingService.Ping:input_type -> connect.ping.v1.PingRequest + 2, // 1: connect.ping.v1.PingService.Sum:input_type -> connect.ping.v1.SumRequest + 4, // 2: connect.ping.v1.PingService.CountUp:input_type -> connect.ping.v1.CountUpRequest + 1, // 3: connect.ping.v1.PingService.Ping:output_type -> connect.ping.v1.PingResponse + 3, // 4: connect.ping.v1.PingService.Sum:output_type -> connect.ping.v1.SumResponse + 5, // 5: connect.ping.v1.PingService.CountUp:output_type -> connect.ping.v1.CountUpResponse + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_connect_ping_v1_ping_proto_init() } +func file_connect_ping_v1_ping_proto_init() { + if File_connect_ping_v1_ping_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_connect_ping_v1_ping_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PingRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_connect_ping_v1_ping_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PingResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_connect_ping_v1_ping_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SumRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_connect_ping_v1_ping_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SumResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_connect_ping_v1_ping_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CountUpRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_connect_ping_v1_ping_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CountUpResponse); 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_connect_ping_v1_ping_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_connect_ping_v1_ping_proto_goTypes, + DependencyIndexes: file_connect_ping_v1_ping_proto_depIdxs, + MessageInfos: file_connect_ping_v1_ping_proto_msgTypes, + }.Build() + File_connect_ping_v1_ping_proto = out.File + file_connect_ping_v1_ping_proto_rawDesc = nil + file_connect_ping_v1_ping_proto_goTypes = nil + file_connect_ping_v1_ping_proto_depIdxs = nil +} diff --git a/internal/gen/connect/ping/v1/pingv1connect/ping.connect.go b/internal/gen/connect/ping/v1/pingv1connect/ping.connect.go new file mode 100644 index 0000000..de6bbc3 --- /dev/null +++ b/internal/gen/connect/ping/v1/pingv1connect/ping.connect.go @@ -0,0 +1,181 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The canonical location for this file is +// https://github.com/connectrpc/validate-go/blob/main/internal/proto/connect/ping/v1/ping.proto. + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: connect/ping/v1/ping.proto + +// The connect.ping.v1 package contains an echo service designed to test the +// validate-go implementation. +package pingv1connect + +import ( + connect "connectrpc.com/connect" + v1 "connectrpc.com/validate/internal/gen/connect/ping/v1" + context "context" + errors "errors" + 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 ( + // PingServiceName is the fully-qualified name of the PingService service. + PingServiceName = "connect.ping.v1.PingService" +) + +// 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 ( + // PingServicePingProcedure is the fully-qualified name of the PingService's Ping RPC. + PingServicePingProcedure = "/connect.ping.v1.PingService/Ping" + // PingServiceSumProcedure is the fully-qualified name of the PingService's Sum RPC. + PingServiceSumProcedure = "/connect.ping.v1.PingService/Sum" + // PingServiceCountUpProcedure is the fully-qualified name of the PingService's CountUp RPC. + PingServiceCountUpProcedure = "/connect.ping.v1.PingService/CountUp" +) + +// PingServiceClient is a client for the connect.ping.v1.PingService service. +type PingServiceClient interface { + // Ping sends a ping to the server to determine if it's reachable. + Ping(context.Context, *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) + // Sum calculates the sum of the numbers sent on the stream. + Sum(context.Context) *connect.ClientStreamForClient[v1.SumRequest, v1.SumResponse] + // CountUp returns a stream of the numbers up to the given request. + CountUp(context.Context, *connect.Request[v1.CountUpRequest]) (*connect.ServerStreamForClient[v1.CountUpResponse], error) +} + +// NewPingServiceClient constructs a client for the connect.ping.v1.PingService 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 NewPingServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PingServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &pingServiceClient{ + ping: connect.NewClient[v1.PingRequest, v1.PingResponse]( + httpClient, + baseURL+PingServicePingProcedure, + opts..., + ), + sum: connect.NewClient[v1.SumRequest, v1.SumResponse]( + httpClient, + baseURL+PingServiceSumProcedure, + opts..., + ), + countUp: connect.NewClient[v1.CountUpRequest, v1.CountUpResponse]( + httpClient, + baseURL+PingServiceCountUpProcedure, + opts..., + ), + } +} + +// pingServiceClient implements PingServiceClient. +type pingServiceClient struct { + ping *connect.Client[v1.PingRequest, v1.PingResponse] + sum *connect.Client[v1.SumRequest, v1.SumResponse] + countUp *connect.Client[v1.CountUpRequest, v1.CountUpResponse] +} + +// Ping calls connect.ping.v1.PingService.Ping. +func (c *pingServiceClient) Ping(ctx context.Context, req *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) { + return c.ping.CallUnary(ctx, req) +} + +// Sum calls connect.ping.v1.PingService.Sum. +func (c *pingServiceClient) Sum(ctx context.Context) *connect.ClientStreamForClient[v1.SumRequest, v1.SumResponse] { + return c.sum.CallClientStream(ctx) +} + +// CountUp calls connect.ping.v1.PingService.CountUp. +func (c *pingServiceClient) CountUp(ctx context.Context, req *connect.Request[v1.CountUpRequest]) (*connect.ServerStreamForClient[v1.CountUpResponse], error) { + return c.countUp.CallServerStream(ctx, req) +} + +// PingServiceHandler is an implementation of the connect.ping.v1.PingService service. +type PingServiceHandler interface { + // Ping sends a ping to the server to determine if it's reachable. + Ping(context.Context, *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) + // Sum calculates the sum of the numbers sent on the stream. + Sum(context.Context, *connect.ClientStream[v1.SumRequest]) (*connect.Response[v1.SumResponse], error) + // CountUp returns a stream of the numbers up to the given request. + CountUp(context.Context, *connect.Request[v1.CountUpRequest], *connect.ServerStream[v1.CountUpResponse]) error +} + +// NewPingServiceHandler 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 NewPingServiceHandler(svc PingServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + pingServicePingHandler := connect.NewUnaryHandler( + PingServicePingProcedure, + svc.Ping, + opts..., + ) + pingServiceSumHandler := connect.NewClientStreamHandler( + PingServiceSumProcedure, + svc.Sum, + opts..., + ) + pingServiceCountUpHandler := connect.NewServerStreamHandler( + PingServiceCountUpProcedure, + svc.CountUp, + opts..., + ) + return "/connect.ping.v1.PingService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case PingServicePingProcedure: + pingServicePingHandler.ServeHTTP(w, r) + case PingServiceSumProcedure: + pingServiceSumHandler.ServeHTTP(w, r) + case PingServiceCountUpProcedure: + pingServiceCountUpHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedPingServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedPingServiceHandler struct{} + +func (UnimplementedPingServiceHandler) Ping(context.Context, *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("connect.ping.v1.PingService.Ping is not implemented")) +} + +func (UnimplementedPingServiceHandler) Sum(context.Context, *connect.ClientStream[v1.SumRequest]) (*connect.Response[v1.SumResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("connect.ping.v1.PingService.Sum is not implemented")) +} + +func (UnimplementedPingServiceHandler) CountUp(context.Context, *connect.Request[v1.CountUpRequest], *connect.ServerStream[v1.CountUpResponse]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("connect.ping.v1.PingService.CountUp is not implemented")) +} diff --git a/internal/proto/buf.lock b/internal/proto/buf.lock new file mode 100644 index 0000000..18b34be --- /dev/null +++ b/internal/proto/buf.lock @@ -0,0 +1,8 @@ +# Generated by buf. DO NOT EDIT. +version: v1 +deps: + - remote: buf.build + owner: bufbuild + repository: protovalidate + commit: b9b8148056b94f6898cc669574bae125 + digest: shake256:5660d7a38dd2ff9a7b8a6304bca6fe798dc6bcd7ecb06c4ce8ebdc0649c2fe969356b90a445a391195fdeae461fbbd9a917dab7687e090465addcb2dbb285b36 diff --git a/internal/proto/buf.yaml b/internal/proto/buf.yaml new file mode 100644 index 0000000..c40753e --- /dev/null +++ b/internal/proto/buf.yaml @@ -0,0 +1,9 @@ +version: v1 +deps: + - buf.build/bufbuild/protovalidate +breaking: + use: + - FILE +lint: + use: + - DEFAULT diff --git a/internal/proto/connect/ping/v1/ping.proto b/internal/proto/connect/ping/v1/ping.proto new file mode 100644 index 0000000..913de84 --- /dev/null +++ b/internal/proto/connect/ping/v1/ping.proto @@ -0,0 +1,82 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The canonical location for this file is +// https://github.com/connectrpc/validate-go/blob/main/internal/proto/connect/ping/v1/ping.proto. +syntax = "proto3"; + +// The connect.ping.v1 package contains an echo service designed to test the +// validate-go implementation. +package connect.ping.v1; + +import "buf/validate/validate.proto"; + +message PingRequest { + int64 number = 1 [ + (buf.validate.field).required = true, + (buf.validate.field).int64 = { + gt: 0, + lt: 100 + } + ]; +} + +message PingResponse { + int64 number = 1 [ + (buf.validate.field).required = true, + (buf.validate.field).int64 = { + gt: 0, + lt: 100 + } + ]; +} + +message SumRequest { + int64 number = 1 [ + (buf.validate.field).required = true, + (buf.validate.field).int64 = { + gt: 0, + lt: 100 + } + ]; +} + +message SumResponse { + int64 sum = 1; +} + +message CountUpRequest { + int64 number = 1 [ + (buf.validate.field).required = true, + (buf.validate.field).int64 = { + gt: 0, + lt: 100 + } + ]; +} + +message CountUpResponse { + int64 number = 1; +} + +service PingService { + // Ping sends a ping to the server to determine if it's reachable. + rpc Ping(PingRequest) returns (PingResponse) {} + // Sum calculates the sum of the numbers sent on the stream. + rpc Sum(stream SumRequest) returns (SumResponse) {} + // CountUp returns a stream of the numbers up to the given request. + rpc CountUp(CountUpRequest) returns (stream CountUpResponse) {} +// // CumSum determines the cumulative sum of all the numbers sent on the stream. +// rpc CumSum(stream CumSumRequest) returns (stream CumSumResponse) {} +} diff --git a/internal/testserver/in_memory_server.go b/internal/testserver/in_memory_server.go new file mode 100644 index 0000000..9248cab --- /dev/null +++ b/internal/testserver/in_memory_server.go @@ -0,0 +1,126 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testserver + +import ( + "context" + "errors" + "net" + "net/http" + "net/http/httptest" + "sync" +) + +// InMemoryServer is an HTTP server that uses in-memory pipes instead of TCP. +// It supports HTTP/2 and has TLS enabled. +// +// The Go Playground panics if we try to start a TCP-backed server. If you're +// not familiar with the Playground's behavior, it looks like our examples are +// broken. This server lets us write examples that work in the playground +// without abstracting over HTTP. +type InMemoryServer struct { + server *httptest.Server + listener *memoryListener +} + +// NewInMemoryServer constructs and starts an InMemoryServer. +func NewInMemoryServer(handler http.Handler) *InMemoryServer { + lis := &memoryListener{ + conns: make(chan net.Conn), + closed: make(chan struct{}), + } + server := httptest.NewUnstartedServer(handler) + server.Listener = lis + server.EnableHTTP2 = true + server.StartTLS() + return &InMemoryServer{ + server: server, + listener: lis, + } +} + +// Client returns an HTTP client configured to trust the server's TLS +// certificate and use HTTP/2 over an in-memory pipe. Automatic HTTP-level gzip +// compression is disabled. It closes its idle connections when the server is +// closed. +func (s *InMemoryServer) Client() *http.Client { + client := s.server.Client() + if transport, ok := client.Transport.(*http.Transport); ok { + transport.DialContext = s.listener.DialContext + transport.DisableCompression = true + } + return client +} + +// URL is the server's URL. +func (s *InMemoryServer) URL() string { + return s.server.URL +} + +// Close shuts down the server, blocking until all outstanding requests have +// completed. +func (s *InMemoryServer) Close() { + s.server.Close() +} + +type memoryListener struct { + conns chan net.Conn + once sync.Once + closed chan struct{} +} + +// Accept implements net.Listener. +func (l *memoryListener) Accept() (net.Conn, error) { + select { + case conn := <-l.conns: + return conn, nil + case <-l.closed: + return nil, errors.New("listener closed") + } +} + +// Close implements net.Listener. +func (l *memoryListener) Close() error { + l.once.Do(func() { + close(l.closed) + }) + return nil +} + +// Addr implements net.Listener. +func (l *memoryListener) Addr() net.Addr { + return &memoryAddr{} +} + +// DialContext is the type expected by http.Transport.DialContext. +func (l *memoryListener) DialContext(_ context.Context, _, _ string) (net.Conn, error) { + select { + case <-l.closed: + return nil, errors.New("listener closed") + default: + } + server, client := net.Pipe() + l.conns <- server + return client, nil +} + +type memoryAddr struct{} + +// Network implements net.Addr. +func (*memoryAddr) Network() string { return "memory" } + +// String implements io.Stringer, returning a value that matches the +// certificates used by net/http/httptest. +func (*memoryAddr) String() string { return "example.com" } diff --git a/internal/testserver/ping_server.go b/internal/testserver/ping_server.go new file mode 100644 index 0000000..e2d9a5d --- /dev/null +++ b/internal/testserver/ping_server.go @@ -0,0 +1,84 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testserver + +import ( + "context" + + "connectrpc.com/connect" + pingv1 "connectrpc.com/validate/internal/gen/connect/ping/v1" + "connectrpc.com/validate/internal/gen/connect/ping/v1/pingv1connect" +) + +type pingServer struct { + pingv1connect.UnimplementedPingServiceHandler + + err error +} + +type Option func(*pingServer) + +func NewPingServer(opts ...Option) pingv1connect.PingServiceHandler { + out := &pingServer{} + for _, apply := range opts { + apply(out) + } + return out +} + +func WithErr(err error) Option { + return func(p *pingServer) { + p.err = err + } +} + +func (p *pingServer) Ping(_ context.Context, req *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { + if p.err != nil { + return nil, p.err + } + return connect.NewResponse(&pingv1.PingResponse{ + Number: req.Msg.GetNumber(), + }), nil +} + +func (p *pingServer) Sum(_ context.Context, stream *connect.ClientStream[pingv1.SumRequest]) (*connect.Response[pingv1.SumResponse], error) { + if p.err != nil { + return nil, p.err + } + var sum int64 + for stream.Receive() { + sum += stream.Msg().Number + } + if stream.Err() != nil { + return nil, stream.Err() + } + return connect.NewResponse(&pingv1.SumResponse{Sum: sum}), nil +} + +func (p *pingServer) CountUp( + _ context.Context, + request *connect.Request[pingv1.CountUpRequest], + stream *connect.ServerStream[pingv1.CountUpResponse], +) error { + if p.err != nil { + return p.err + } + for i := int64(1); i <= request.Msg.Number; i++ { + if err := stream.Send(&pingv1.CountUpResponse{Number: i}); err != nil { + return err + } + } + return nil +} From 215e8557c8ed414fea2a754e8a2417dd17f89e1c Mon Sep 17 00:00:00 2001 From: Elliot Jackson <13633636+elliotmjackson@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:56:50 -0400 Subject: [PATCH 06/20] Apply suggestions from code review --- buf.gen.yaml | 2 +- interceptor.go | 11 ++++++----- internal/proto/connect/ping/v1/ping.proto | 10 +--------- internal/testserver/ping_server.go | 6 +----- 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/buf.gen.yaml b/buf.gen.yaml index cb5578b..8d7c3e8 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -6,7 +6,7 @@ managed: except: - buf.build/bufbuild/protovalidate plugins: - - plugin: buf.build/connectrpc/go:v1.11.1 + - plugin: buf.build/connectrpc/go out: internal/gen opt: - paths=source_relative diff --git a/interceptor.go b/interceptor.go index a4fed69..70cd69e 100644 --- a/interceptor.go +++ b/interceptor.go @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package validate provides an interceptor implementation for the ConnectRPC framework -// that integrates with the protovalidate-go library to validate incoming protobuf messages. -// This interceptor ensures adherence to the defined message structure and can be used -// to enhance the reliability of data communication within the ConnectRPC framework. +// Package validate provides an interceptor implementation for the Connect that integrates +// with protovalidate to validate incoming protobuf messages against predefined constraints. +// This interceptor ensures adherence to constraints defined on the proto file without the need +// for extra generated code. Used this interceptor to automatically validate request messages +// and enhance the reliability of data communication. package validate import ( @@ -39,7 +40,7 @@ import ( // - In case of validation errors, an error detail of the type is attached to provide // additional context about the validation failure. // -// It's recommended to use the Interceptor primarily with server-side handlers rather than +// It's recommended to use the Interceptor with server-side handlers rather than // client connections. Placing the Interceptor on handlers ensures that incoming requests // are thoroughly validated before they are processed, minimizing the risk of handling // invalid or unexpected data. diff --git a/internal/proto/connect/ping/v1/ping.proto b/internal/proto/connect/ping/v1/ping.proto index 913de84..907a2a4 100644 --- a/internal/proto/connect/ping/v1/ping.proto +++ b/internal/proto/connect/ping/v1/ping.proto @@ -33,13 +33,7 @@ message PingRequest { } message PingResponse { - int64 number = 1 [ - (buf.validate.field).required = true, - (buf.validate.field).int64 = { - gt: 0, - lt: 100 - } - ]; + int64 number = 1; } message SumRequest { @@ -77,6 +71,4 @@ service PingService { rpc Sum(stream SumRequest) returns (SumResponse) {} // CountUp returns a stream of the numbers up to the given request. rpc CountUp(CountUpRequest) returns (stream CountUpResponse) {} -// // CumSum determines the cumulative sum of all the numbers sent on the stream. -// rpc CumSum(stream CumSumRequest) returns (stream CumSumResponse) {} } diff --git a/internal/testserver/ping_server.go b/internal/testserver/ping_server.go index e2d9a5d..221a189 100644 --- a/internal/testserver/ping_server.go +++ b/internal/testserver/ping_server.go @@ -67,11 +67,7 @@ func (p *pingServer) Sum(_ context.Context, stream *connect.ClientStream[pingv1. return connect.NewResponse(&pingv1.SumResponse{Sum: sum}), nil } -func (p *pingServer) CountUp( - _ context.Context, - request *connect.Request[pingv1.CountUpRequest], - stream *connect.ServerStream[pingv1.CountUpResponse], -) error { +func (p *pingServer) CountUp(_ context.Context, request *connect.Request[pingv1.CountUpRequest], stream *connect.ServerStream[pingv1.CountUpResponse]) error { if p.err != nil { return p.err } From 4aa759db148ad90d3d32edd1e00b20c5e75bdf89 Mon Sep 17 00:00:00 2001 From: Elliot Jackson <13633636+ElliotMJackson@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:57:16 -0400 Subject: [PATCH 07/20] update generated code --- internal/gen/connect/ping/v1/ping.pb.go | 85 ++++++++++++------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/internal/gen/connect/ping/v1/ping.pb.go b/internal/gen/connect/ping/v1/ping.pb.go index 0fd5140..1273dff 100644 --- a/internal/gen/connect/ping/v1/ping.pb.go +++ b/internal/gen/connect/ping/v1/ping.pb.go @@ -334,49 +334,48 @@ var file_connect_ping_v1_ping_proto_rawDesc = []byte{ 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x0c, 0xba, 0x48, 0x09, 0xc8, 0x01, 0x01, 0x22, 0x04, 0x10, 0x64, 0x20, 0x00, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, - 0x34, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x24, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, - 0x0c, 0xba, 0x48, 0x09, 0xc8, 0x01, 0x01, 0x22, 0x04, 0x10, 0x64, 0x20, 0x00, 0x52, 0x06, 0x6e, - 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x32, 0x0a, 0x0a, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x42, 0x0c, 0xba, 0x48, 0x09, 0xc8, 0x01, 0x01, 0x22, 0x04, 0x10, 0x64, 0x20, - 0x00, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x1f, 0x0a, 0x0b, 0x53, 0x75, 0x6d, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x75, 0x6d, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x73, 0x75, 0x6d, 0x22, 0x36, 0x0a, 0x0e, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x06, - 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x0c, 0xba, 0x48, - 0x09, 0xc8, 0x01, 0x01, 0x22, 0x04, 0x10, 0x64, 0x20, 0x00, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, - 0x65, 0x72, 0x22, 0x29, 0x0a, 0x0f, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x32, 0xec, 0x01, - 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, - 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, - 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, - 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x03, 0x53, 0x75, 0x6d, 0x12, 0x1b, 0x2e, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, - 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x6d, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x12, 0x50, 0x0a, 0x07, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x55, 0x70, 0x12, 0x1f, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, - 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x42, 0xbb, 0x01, 0x0a, - 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, - 0x67, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x50, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, - 0x01, 0x5a, 0x3b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, - 0x70, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x69, 0x6e, 0x67, 0x76, 0x31, 0xa2, 0x02, - 0x03, 0x43, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x50, - 0x69, 0x6e, 0x67, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x5c, 0x50, 0x69, 0x6e, 0x67, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1b, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x5c, 0x50, 0x69, 0x6e, 0x67, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x3a, 0x3a, 0x50, 0x69, 0x6e, 0x67, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x26, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x32, 0x0a, 0x0a, 0x53, 0x75, 0x6d, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x0c, 0xba, 0x48, 0x09, 0xc8, 0x01, 0x01, 0x22, 0x04, 0x10, + 0x64, 0x20, 0x00, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x1f, 0x0a, 0x0b, 0x53, + 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x75, + 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x73, 0x75, 0x6d, 0x22, 0x36, 0x0a, 0x0e, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, + 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x0c, + 0xba, 0x48, 0x09, 0xc8, 0x01, 0x01, 0x22, 0x04, 0x10, 0x64, 0x20, 0x00, 0x52, 0x06, 0x6e, 0x75, + 0x6d, 0x62, 0x65, 0x72, 0x22, 0x29, 0x0a, 0x0f, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x32, + 0xec, 0x01, 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x45, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, + 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x03, 0x53, 0x75, 0x6d, 0x12, 0x1b, 0x2e, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x6d, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x12, 0x50, 0x0a, 0x07, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x12, 0x1f, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, + 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x42, 0xbb, + 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, + 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x50, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x50, 0x01, 0x5a, 0x3b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x69, 0x6e, 0x67, 0x76, 0x31, + 0xa2, 0x02, 0x03, 0x43, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x5c, 0x50, 0x69, 0x6e, 0x67, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1b, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x5c, 0x50, 0x69, 0x6e, 0x67, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x3a, 0x3a, 0x50, 0x69, 0x6e, 0x67, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( From 9212a3cccbc5e990f6b69c5d2a812120993634e2 Mon Sep 17 00:00:00 2001 From: Elliot Jackson <13633636+ElliotMJackson@users.noreply.github.com> Date: Tue, 29 Aug 2023 17:01:32 -0400 Subject: [PATCH 08/20] fix lint errors --- interceptor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interceptor.go b/interceptor.go index 70cd69e..835a0ec 100644 --- a/interceptor.go +++ b/interceptor.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package validate provides an interceptor implementation for the Connect that integrates +// Package validate provides an interceptor implementation for the Connect that integrates // with protovalidate to validate incoming protobuf messages against predefined constraints. // This interceptor ensures adherence to constraints defined on the proto file without the need // for extra generated code. Used this interceptor to automatically validate request messages From 795a3e4d27699deb29afa4a952e11d674b52b33a Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Thu, 31 Aug 2023 15:53:07 -0700 Subject: [PATCH 09/20] Rename files to match package --- interceptor.go | 166 ------------------------ validate.go | 151 +++++++++++++++++++++ interceptor_test.go => validate_test.go | 0 3 files changed, 151 insertions(+), 166 deletions(-) delete mode 100644 interceptor.go rename interceptor_test.go => validate_test.go (100%) diff --git a/interceptor.go b/interceptor.go deleted file mode 100644 index 835a0ec..0000000 --- a/interceptor.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package validate provides an interceptor implementation for the Connect that integrates -// with protovalidate to validate incoming protobuf messages against predefined constraints. -// This interceptor ensures adherence to constraints defined on the proto file without the need -// for extra generated code. Used this interceptor to automatically validate request messages -// and enhance the reliability of data communication. -package validate - -import ( - "context" - "errors" - "fmt" - - "connectrpc.com/connect" - "github.com/bufbuild/protovalidate-go" - "google.golang.org/protobuf/proto" -) - -// Interceptor implements the connect.Interceptor interface and serves as a crucial -// layer in the communication pipeline, by validating incoming requests and ensuring -// they conform to defined protovalidate preconditions. -// -// Default Behaviors: -// - Requests are validated for adherence to the defined message structure. -// - Responses are not validated, focusing validation efforts on incoming data. -// - Errors are raised for incoming messages that are not protocol buffer messages. -// - In case of validation errors, an error detail of the type is attached to provide -// additional context about the validation failure. -// -// It's recommended to use the Interceptor with server-side handlers rather than -// client connections. Placing the Interceptor on handlers ensures that incoming requests -// are thoroughly validated before they are processed, minimizing the risk of handling -// invalid or unexpected data. -type Interceptor struct { - validator *protovalidate.Validator -} - -// Option is a functional option for the Interceptor. -type Option func(*Interceptor) - -// NewInterceptor returns a new instance of the Interceptor. It accepts an optional functional -// option to customize its behavior. If no custom validator is provided, a default validator -// is used for message validation. -// -// Usage: -// -// interceptor, err := NewInterceptor(WithValidator(customValidator)) -// if err != nil { -// // Handle error -// } -// -// path, handler := examplev1connect.NewExampleServiceHandler( -// server, -// connect.WithInterceptors(interceptor), -// ) -func NewInterceptor(opts ...Option) (*Interceptor, error) { - out := &Interceptor{} - for _, apply := range opts { - apply(out) - } - - if out.validator == nil { - validator, err := protovalidate.New() - if err != nil { - return nil, err - } - out.validator = validator - } - - return out, nil -} - -// WithValidator sets the validator to be used for message validation. -// This option allows customization of the validator used by the Interceptor. -func WithValidator(validator *protovalidate.Validator) Option { - return func(i *Interceptor) { - i.validator = validator - } -} - -// WrapUnary implements the connect.Interceptor interface. -func (i *Interceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { - return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { - if err := validate(i.validator, req.Any()); err != nil { - return nil, err - } - return next(ctx, req) - } -} - -// WrapStreamingClient implements the connect.Interceptor interface. -func (i *Interceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { - return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { - return &streamingClientInterceptor{ - validator: i.validator, - StreamingClientConn: next(ctx, spec), - } - } -} - -// WrapStreamingHandler implements the connect.Interceptor interface. -func (i *Interceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { - return func(ctx context.Context, conn connect.StreamingHandlerConn) error { - return next(ctx, &streamingHandlerInterceptor{ - validator: i.validator, - StreamingHandlerConn: conn, - }) - } -} - -type streamingClientInterceptor struct { - connect.StreamingClientConn - - validator *protovalidate.Validator -} - -func (s *streamingClientInterceptor) Send(msg any) error { - if err := validate(s.validator, msg); err != nil { - return err - } - return s.StreamingClientConn.Send(msg) -} - -type streamingHandlerInterceptor struct { - connect.StreamingHandlerConn - - validator *protovalidate.Validator -} - -func (s *streamingHandlerInterceptor) Receive(msg any) error { - if err := s.StreamingHandlerConn.Receive(msg); err != nil { - return err - } - return validate(s.validator, msg) -} - -func validate(validator *protovalidate.Validator, msg any) error { - protoMessage, ok := msg.(proto.Message) - if !ok { - return fmt.Errorf("message is not a proto.Message: %T", msg) - } - if err := validator.Validate(protoMessage); err != nil { - out := connect.NewError(connect.CodeInvalidArgument, err) - var validationErr *protovalidate.ValidationError - if errors.As(err, &validationErr) { - if detail, err := connect.NewErrorDetail(validationErr.ToProto()); err == nil { - out.AddDetail(detail) - } - } - return out - } - return nil -} diff --git a/validate.go b/validate.go index 263a22d..835a0ec 100644 --- a/validate.go +++ b/validate.go @@ -12,4 +12,155 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package validate provides an interceptor implementation for the Connect that integrates +// with protovalidate to validate incoming protobuf messages against predefined constraints. +// This interceptor ensures adherence to constraints defined on the proto file without the need +// for extra generated code. Used this interceptor to automatically validate request messages +// and enhance the reliability of data communication. package validate + +import ( + "context" + "errors" + "fmt" + + "connectrpc.com/connect" + "github.com/bufbuild/protovalidate-go" + "google.golang.org/protobuf/proto" +) + +// Interceptor implements the connect.Interceptor interface and serves as a crucial +// layer in the communication pipeline, by validating incoming requests and ensuring +// they conform to defined protovalidate preconditions. +// +// Default Behaviors: +// - Requests are validated for adherence to the defined message structure. +// - Responses are not validated, focusing validation efforts on incoming data. +// - Errors are raised for incoming messages that are not protocol buffer messages. +// - In case of validation errors, an error detail of the type is attached to provide +// additional context about the validation failure. +// +// It's recommended to use the Interceptor with server-side handlers rather than +// client connections. Placing the Interceptor on handlers ensures that incoming requests +// are thoroughly validated before they are processed, minimizing the risk of handling +// invalid or unexpected data. +type Interceptor struct { + validator *protovalidate.Validator +} + +// Option is a functional option for the Interceptor. +type Option func(*Interceptor) + +// NewInterceptor returns a new instance of the Interceptor. It accepts an optional functional +// option to customize its behavior. If no custom validator is provided, a default validator +// is used for message validation. +// +// Usage: +// +// interceptor, err := NewInterceptor(WithValidator(customValidator)) +// if err != nil { +// // Handle error +// } +// +// path, handler := examplev1connect.NewExampleServiceHandler( +// server, +// connect.WithInterceptors(interceptor), +// ) +func NewInterceptor(opts ...Option) (*Interceptor, error) { + out := &Interceptor{} + for _, apply := range opts { + apply(out) + } + + if out.validator == nil { + validator, err := protovalidate.New() + if err != nil { + return nil, err + } + out.validator = validator + } + + return out, nil +} + +// WithValidator sets the validator to be used for message validation. +// This option allows customization of the validator used by the Interceptor. +func WithValidator(validator *protovalidate.Validator) Option { + return func(i *Interceptor) { + i.validator = validator + } +} + +// WrapUnary implements the connect.Interceptor interface. +func (i *Interceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + if err := validate(i.validator, req.Any()); err != nil { + return nil, err + } + return next(ctx, req) + } +} + +// WrapStreamingClient implements the connect.Interceptor interface. +func (i *Interceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { + return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { + return &streamingClientInterceptor{ + validator: i.validator, + StreamingClientConn: next(ctx, spec), + } + } +} + +// WrapStreamingHandler implements the connect.Interceptor interface. +func (i *Interceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + return func(ctx context.Context, conn connect.StreamingHandlerConn) error { + return next(ctx, &streamingHandlerInterceptor{ + validator: i.validator, + StreamingHandlerConn: conn, + }) + } +} + +type streamingClientInterceptor struct { + connect.StreamingClientConn + + validator *protovalidate.Validator +} + +func (s *streamingClientInterceptor) Send(msg any) error { + if err := validate(s.validator, msg); err != nil { + return err + } + return s.StreamingClientConn.Send(msg) +} + +type streamingHandlerInterceptor struct { + connect.StreamingHandlerConn + + validator *protovalidate.Validator +} + +func (s *streamingHandlerInterceptor) Receive(msg any) error { + if err := s.StreamingHandlerConn.Receive(msg); err != nil { + return err + } + return validate(s.validator, msg) +} + +func validate(validator *protovalidate.Validator, msg any) error { + protoMessage, ok := msg.(proto.Message) + if !ok { + return fmt.Errorf("message is not a proto.Message: %T", msg) + } + if err := validator.Validate(protoMessage); err != nil { + out := connect.NewError(connect.CodeInvalidArgument, err) + var validationErr *protovalidate.ValidationError + if errors.As(err, &validationErr) { + if detail, err := connect.NewErrorDetail(validationErr.ToProto()); err == nil { + out.AddDetail(detail) + } + } + return out + } + return nil +} diff --git a/interceptor_test.go b/validate_test.go similarity index 100% rename from interceptor_test.go rename to validate_test.go From d927bb96ab66d3067f63011d7628dc1d81666821 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 1 Sep 2023 00:18:47 -0700 Subject: [PATCH 10/20] Re-word README --- README.md | 154 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 117 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 510a9ba..8a233cc 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,78 @@ -# ConnectRPC Validation Interceptor +# Validate [![Build](https://github.com/connectrpc/validate-go/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/connectrpc/validate-go/actions/workflows/ci.yaml) [![Report Card](https://goreportcard.com/badge/connectrpc.com/validate)](https://goreportcard.com/report/connectrpc.com/validate) [![GoDoc](https://pkg.go.dev/badge/connectrpc.com/validate.svg)](https://pkg.go.dev/connectrpc.com/validate) -`connectrpc.com/validate` adds support for a protovalidate interceptor to Connect servers. +`connectrpc.com/validate` provides a [Connect][connect-go] interceptor that +takes the tedium out of data validation. Rather than hand-writing repetitive +documentation and code — verifing that `User.email` is valid, or that +`User.age` falls within reasonable bounds — you can instead encode those +constraints into your Protobuf schemas and automatically enforce them at +runtime. -[`protovalidate`][protovalidate-go] is a series of libraries designed to validate Protobuf messages at -runtime based on user-defined validation rules. Powered by Google's Common -Expression Language ([CEL][cel-spec]), it provides a -flexible and efficient foundation for defining and evaluating custom validation -rules. The primary goal of [`protovalidate`][protovalidate] is to help developers ensure data -consistency and integrity across the network without requiring generated code. +Under the hood, this package is powered by [protovalidate][protovalidate-go] +and the [Common Expression Language][cel-spec]. Together, they make validation +flexible, efficient, and consistent across languages _without_ additional code +generation. ## Installation -Add the interceptor to your project with `go get`: - ```bash go get connectrpc.com/validate ``` -## An Example +## A small example + +Curious what all this looks like in practice? First, let's define a schema for +our user service: + +```protobuf +syntax = "proto3"; + +package example.user.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +message User { + // Simple constraints, like checking that an email address is valid, are + // predefined. + string email = 1 [(buf.validate.field).string.email = true]; + + // For more complex use cases, like comparing fields against each other, we + // can write a CEL expression. + google.protobuf.Timestamp birth_date = 2; + google.protobuf.Timestamp signup_date = 3; + + option (buf.validate.message).cel = { + id: "user.signup_date", + message: "signup date must be on or after birth date", + expression: "this.signup_date >= this.birth_date" + }; +} + +message CreateUserRequest { + User user = 1; +} + +message CreateUserResponse { + User user = 1; +} + +service UserService { + rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {} +} +``` + +Notice that simple constraints, like checking email addresses, are short and +declarative. When we need a more elaborate constraint, we can write a custom +CEL expression, customize the error message, and much more. (See [the +main protovalidate repository][protovalidate] for more examples.) + +After implementing `UserService`, we can add a validating interceptor with just +one option: + ```go package main @@ -34,10 +85,8 @@ import ( "connectrpc.com/connect" "connectrpc.com/validate" - // Generated from your protobuf schema by protoc-gen-go and - // protoc-gen-connect-go. - pingv1 "connectrpc.com/validate/internal/gen/connect/ping/v1" - "connectrpc.com/validate/internal/gen/connect/ping/v1/pingv1connect" + userv1 "connectrpc.com/validate/internal/gen/example/user/v1" + "connectrpc.com/validate/internal/gen/validate/example/v1/userv1connect" ) func main() { @@ -47,39 +96,66 @@ func main() { } mux := http.NewServeMux() - mux.Handle(pingv1connect.NewPingServiceHandler( + mux.Handle(user1connect.NewPingServiceHandler( &pingv1connect.UnimplementedPingServiceHandler{}, connect.WithInterceptors(interceptor), )) http.ListenAndServe("localhost:8080", mux) } - -func makeRequest() { - client := pingv1connect.NewPingServiceClient( - http.DefaultClient, - "http://localhost:8080", - ) - resp, err := client.Ping( - context.Background(), - connect.NewRequest(&pingv1.PingRequest{}), - ) - if err != nil { - log.Fatal(err) - } - fmt.Println(resp) -} ``` -By applying the interceptor to your server's handlers, you ensure that incoming -requests are thoroughly validated before being processed. This practice -minimizes the risk of handling invalid or unexpected data, contributing to more -robust and reliable data processing logic. +With the `validate.Interceptor` applied, our `UserService` implementation can +assume that all requests have already been validated — no need for +hand-written boilerplate! + +## FAQ + +**Does this interceptor work with Connect clients?** + +Yes: it validates request messages before sending them to the server. But +unless you're _sure_ that your clients always have an up-to-date schema, it's +better to let the server handle validation. + +**How do clients know which fields are invalid?** + +If the request message fails validation, the interceptor returns an error coded +with `connect.CodeInvalidArgument`. It also adds a [detailed representation of the +validation error(s)][violations] as an [error detail][connect-error-detail]. + +**How should schemas import protovalidate's options?** + +Because this interceptor uses [protovalidate][protovalidate-go], it doesn't +need any generated code for validation. However, any Protobuf schemas with +constraints must import [`buf/validate/validate.proto`][validate.proto]. It's +easiest to import this file directly from the [Buf Schema +Registry][bsr]: this repository contains an [example +schema](internal/proto/example/user/v1/user.proto) with constraints, +[buf.yaml](internal/proto/buf.yaml) and [buf.gen.yaml](buf.gen.yaml) +configuration files, and `make generate` [recipe](Makefile). ## Ecosystem -- [connect-go]: The ConnectRPC framework for Go. -- [protovalidate-go]: A protocol buffer message validator for Go. +* [connect-go]: the Connect runtime +* [protovalidate-go]: the underlying Protobuf validation library +* [protovalidate]: schemas and documentation for the constraint language +* [CEL][cel-spec]: the Common Expression Language + +## Status: Unstable + +This module is unstable. Expect breaking changes as we iterate toward a stable +release. + +It supports: + +* The three most recent major releases of Go. Keep in mind that [only the last + two releases receive security patches][go-support-policy]. +* [APIv2] of Protocol Buffers in Go (`google.golang.org/protobuf`). + +Within those parameters, this project follows semantic versioning. Once we tag +a stable release, we will _not_ make breaking changes without incrementing the +major version. + ## License @@ -89,3 +165,7 @@ Offered under the [Apache 2 license](LICENSE). [protovalidate-go]: https://github.com/bufbuild/protovalidate-go [cel-spec]: https://github.com/google/cel-spec [protovalidate]: https://github.com/bufbuild/protovalidate +[violations]: https://pkg.go.dev/buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate#Violations +[connect-error-detail]: https://pkg.go.dev/connectrpc.com/connect#ErrorDetail +[bsr]: https://buf.build +[validate.proto]: https://github.com/bufbuild/protovalidate/blob/main/proto/protovalidate/buf/validate/validate.proto From 9af428589d5b3dfd181f0dfd9cd5403ec99f223b Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 1 Sep 2023 00:19:28 -0700 Subject: [PATCH 11/20] Ensure correct versions of protobuf plugins Rather than using whatever plugin version is latest in the BSR, build `protoc-gen-go` and `protoc-gen-connect-go` locally. This guarantees that we're using versions compatible with the Protobuf and Connect runtimes in go.mod. --- Makefile | 23 ++++++++++++++++++----- buf.gen.yaml | 10 ++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 9b03415..b8fb166 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,8 @@ build: generate ## Build all packages go build ./... .PHONY: generate -generate: $(BIN)/buf $(BIN)/license-header ## Regenerate code and licenses +generate: $(BIN)/buf $(BIN)/license-header $(BIN)/protoc-gen-go $(BIN)/protoc-gen-connect-go ## Regenerate code and licenses + rm -rf internal/gen buf generate license-header \ --license-type apache \ @@ -43,13 +44,16 @@ generate: $(BIN)/buf $(BIN)/license-header ## Regenerate code and licenses --year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE) .PHONY: lint -lint: $(BIN)/golangci-lint ## Lint +lint: $(BIN)/golangci-lint $(BIN)/buf ## Lint go vet ./... golangci-lint run + buf lint + buf format -d --exit-code .PHONY: lintfix -lintfix: $(BIN)/golangci-lint ## Automatically fix some lint errors +lintfix: $(BIN)/golangci-lint $(BIN)/buf ## Automatically fix some lint errors golangci-lint run --fix + buf format -w .PHONY: install install: ## Install all binaries @@ -57,8 +61,7 @@ install: ## Install all binaries .PHONY: upgrade upgrade: ## Upgrade dependencies - go get -u -t ./... - go mod tidy -v + go get -u -t ./... && go mod tidy -v .PHONY: checkgenerate checkgenerate: @@ -76,3 +79,13 @@ $(BIN)/license-header: Makefile $(BIN)/golangci-lint: Makefile @mkdir -p $(@D) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.1 + +$(BIN)/protoc-gen-connect-go: Makefile go.mod + @mkdir -p $(@D) + @# The version of protoc-gen-connect-go is determined by the version in go.mod + go install connectrpc.com/connect/cmd/protoc-gen-connect-go + +$(BIN)/protoc-gen-go: Makefile go.mod + @mkdir -p $(@D) + @# The version of protoc-gen-go is determined by the version in go.mod + go install google.golang.org/protobuf/cmd/protoc-gen-go diff --git a/buf.gen.yaml b/buf.gen.yaml index 8d7c3e8..5a56d93 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -6,11 +6,9 @@ managed: except: - buf.build/bufbuild/protovalidate plugins: - - plugin: buf.build/connectrpc/go + - plugin: go out: internal/gen - opt: - - paths=source_relative - - plugin: buf.build/protocolbuffers/go + opt: paths=source_relative + - plugin: connect-go out: internal/gen - opt: - - paths=source_relative + opt: paths=source_relative From 6f5739c7c699ea6d67d958c4808ee2180b6dd6e4 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 1 Sep 2023 00:23:48 -0700 Subject: [PATCH 12/20] Reword GoDoc --- validate.go | 126 +++++++++++++++++++++++++--------------------------- 1 file changed, 61 insertions(+), 65 deletions(-) diff --git a/validate.go b/validate.go index 835a0ec..a947e0d 100644 --- a/validate.go +++ b/validate.go @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package validate provides an interceptor implementation for the Connect that integrates -// with protovalidate to validate incoming protobuf messages against predefined constraints. -// This interceptor ensures adherence to constraints defined on the proto file without the need -// for extra generated code. Used this interceptor to automatically validate request messages -// and enhance the reliability of data communication. +// Package validate provides a [connect.Interceptor] that validates Protobuf +// messages against constraints specified in the schema. Because the +// interceptor is powered by [protovalidate], validation is flexible, +// efficient, and consistent across languages - without additional code +// generation. package validate import ( @@ -29,69 +29,61 @@ import ( "google.golang.org/protobuf/proto" ) -// Interceptor implements the connect.Interceptor interface and serves as a crucial -// layer in the communication pipeline, by validating incoming requests and ensuring -// they conform to defined protovalidate preconditions. -// -// Default Behaviors: -// - Requests are validated for adherence to the defined message structure. -// - Responses are not validated, focusing validation efforts on incoming data. -// - Errors are raised for incoming messages that are not protocol buffer messages. -// - In case of validation errors, an error detail of the type is attached to provide -// additional context about the validation failure. -// -// It's recommended to use the Interceptor with server-side handlers rather than -// client connections. Placing the Interceptor on handlers ensures that incoming requests -// are thoroughly validated before they are processed, minimizing the risk of handling -// invalid or unexpected data. -type Interceptor struct { - validator *protovalidate.Validator +// An Option configures an [Interceptor]. +type Option interface { + apply(*Interceptor) } -// Option is a functional option for the Interceptor. -type Option func(*Interceptor) +// WithValidator configures the [Interceptor] to use a customized +// [protovalidate.Validator]. See [protovalidate.ValidatorOption] for the range +// of available customizations. +func WithValidator(validator *protovalidate.Validator) Option { + return optionFunc(func(i *Interceptor) { + i.validator = validator + }) +} -// NewInterceptor returns a new instance of the Interceptor. It accepts an optional functional -// option to customize its behavior. If no custom validator is provided, a default validator -// is used for message validation. +// Interceptor is a [connect.Interceptor] that ensures that RPC request +// messages match the constraints expressed in their Protobuf schemas. It does +// not validate response messages. // -// Usage: +// By default, Interceptors use a validator that lazily compiles constraints +// and works with any Protobuf message. This is a simple, widely-applicable +// configuration: after compiling and caching the constraints for a Protobuf +// message type once, validation is very efficient. To customize the validator, +// use [WithValidator] and [protovalidate.ValidatorOption]. // -// interceptor, err := NewInterceptor(WithValidator(customValidator)) -// if err != nil { -// // Handle error -// } +// RPCs with invalid request messages short-circuit with an error. The error +// always uses [connect.CodeInvalidArgument] and has a [detailed representation +// of the error] attached as a [connect.ErrorDetail]. // -// path, handler := examplev1connect.NewExampleServiceHandler( -// server, -// connect.WithInterceptors(interceptor), -// ) +// This interceptor is primarily intended for use on handlers. Client-side use +// is possible, but discouraged unless the client always has an up-to-date +// schema. +type Interceptor struct { + validator *protovalidate.Validator +} + +// NewInterceptor builds an Interceptor. The default configuration is +// appropriate for most use cases. func NewInterceptor(opts ...Option) (*Interceptor, error) { - out := &Interceptor{} - for _, apply := range opts { - apply(out) + var interceptor Interceptor + for _, opt := range opts { + opt.apply(&interceptor) } - if out.validator == nil { + if interceptor.validator == nil { validator, err := protovalidate.New() if err != nil { - return nil, err + return nil, fmt.Errorf("construct validator: %w", err) } - out.validator = validator + interceptor.validator = validator } - return out, nil -} - -// WithValidator sets the validator to be used for message validation. -// This option allows customization of the validator used by the Interceptor. -func WithValidator(validator *protovalidate.Validator) Option { - return func(i *Interceptor) { - i.validator = validator - } + return &interceptor, nil } -// WrapUnary implements the connect.Interceptor interface. +// WrapUnary implements connect.Interceptor. func (i *Interceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { if err := validate(i.validator, req.Any()); err != nil { @@ -101,7 +93,7 @@ func (i *Interceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { } } -// WrapStreamingClient implements the connect.Interceptor interface. +// WrapStreamingClient implements connect.Interceptor. func (i *Interceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { return &streamingClientInterceptor{ @@ -111,7 +103,7 @@ func (i *Interceptor) WrapStreamingClient(next connect.StreamingClientFunc) conn } } -// WrapStreamingHandler implements the connect.Interceptor interface. +// WrapStreamingHandler implements connect.Interceptor. func (i *Interceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { return func(ctx context.Context, conn connect.StreamingHandlerConn) error { return next(ctx, &streamingHandlerInterceptor{ @@ -147,20 +139,24 @@ func (s *streamingHandlerInterceptor) Receive(msg any) error { return validate(s.validator, msg) } +type optionFunc func(*Interceptor) + +func (f optionFunc) apply(i *Interceptor) { f(i) } + func validate(validator *protovalidate.Validator, msg any) error { - protoMessage, ok := msg.(proto.Message) + protoMsg, ok := msg.(proto.Message) if !ok { - return fmt.Errorf("message is not a proto.Message: %T", msg) + return fmt.Errorf("expected proto.Message, got %T", msg) + } + err := validator.Validate(protoMsg) + if err == nil { + return nil } - if err := validator.Validate(protoMessage); err != nil { - out := connect.NewError(connect.CodeInvalidArgument, err) - var validationErr *protovalidate.ValidationError - if errors.As(err, &validationErr) { - if detail, err := connect.NewErrorDetail(validationErr.ToProto()); err == nil { - out.AddDetail(detail) - } + connectErr := connect.NewError(connect.CodeInvalidArgument, err) + if validationErr := new(protovalidate.ValidationError); errors.As(err, &validationErr) { + if detail, err := connect.NewErrorDetail(validationErr.ToProto()); err == nil { + connectErr.AddDetail(detail) } - return out } - return nil + return connectErr } From c3c6a68a06ad633896afcd5f97082b76504c3310 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 1 Sep 2023 00:25:22 -0700 Subject: [PATCH 13/20] Switch example proto to UserService Switch to using a user service as our example. It offers a little more space for complex validation, and using it in tests gives us confidence that the README code snippet is correct. --- internal/gen/connect/ping/v1/ping.pb.go | 513 ------------------ .../ping/v1/pingv1connect/ping.connect.go | 181 ------ internal/gen/example/user/v1/user.pb.go | 346 ++++++++++++ .../user/v1/userv1connect/user.connect.go | 118 ++++ internal/proto/connect/ping/v1/ping.proto | 74 --- internal/proto/example/user/v1/user.proto | 45 ++ internal/testserver/in_memory_server.go | 126 ----- internal/testserver/ping_server.go | 80 --- 8 files changed, 509 insertions(+), 974 deletions(-) delete mode 100644 internal/gen/connect/ping/v1/ping.pb.go delete mode 100644 internal/gen/connect/ping/v1/pingv1connect/ping.connect.go create mode 100644 internal/gen/example/user/v1/user.pb.go create mode 100644 internal/gen/example/user/v1/userv1connect/user.connect.go delete mode 100644 internal/proto/connect/ping/v1/ping.proto create mode 100644 internal/proto/example/user/v1/user.proto delete mode 100644 internal/testserver/in_memory_server.go delete mode 100644 internal/testserver/ping_server.go diff --git a/internal/gen/connect/ping/v1/ping.pb.go b/internal/gen/connect/ping/v1/ping.pb.go deleted file mode 100644 index 1273dff..0000000 --- a/internal/gen/connect/ping/v1/ping.pb.go +++ /dev/null @@ -1,513 +0,0 @@ -// Copyright 2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// The canonical location for this file is -// https://github.com/connectrpc/validate-go/blob/main/internal/proto/connect/ping/v1/ping.proto. - -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.31.0 -// protoc (unknown) -// source: connect/ping/v1/ping.proto - -// The connect.ping.v1 package contains an echo service designed to test the -// validate-go implementation. - -package pingv1 - -import ( - _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type PingRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` -} - -func (x *PingRequest) Reset() { - *x = PingRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_connect_ping_v1_ping_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *PingRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PingRequest) ProtoMessage() {} - -func (x *PingRequest) ProtoReflect() protoreflect.Message { - mi := &file_connect_ping_v1_ping_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 PingRequest.ProtoReflect.Descriptor instead. -func (*PingRequest) Descriptor() ([]byte, []int) { - return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{0} -} - -func (x *PingRequest) GetNumber() int64 { - if x != nil { - return x.Number - } - return 0 -} - -type PingResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` -} - -func (x *PingResponse) Reset() { - *x = PingResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_connect_ping_v1_ping_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *PingResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PingResponse) ProtoMessage() {} - -func (x *PingResponse) ProtoReflect() protoreflect.Message { - mi := &file_connect_ping_v1_ping_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 PingResponse.ProtoReflect.Descriptor instead. -func (*PingResponse) Descriptor() ([]byte, []int) { - return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{1} -} - -func (x *PingResponse) GetNumber() int64 { - if x != nil { - return x.Number - } - return 0 -} - -type SumRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` -} - -func (x *SumRequest) Reset() { - *x = SumRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_connect_ping_v1_ping_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *SumRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SumRequest) ProtoMessage() {} - -func (x *SumRequest) ProtoReflect() protoreflect.Message { - mi := &file_connect_ping_v1_ping_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 SumRequest.ProtoReflect.Descriptor instead. -func (*SumRequest) Descriptor() ([]byte, []int) { - return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{2} -} - -func (x *SumRequest) GetNumber() int64 { - if x != nil { - return x.Number - } - return 0 -} - -type SumResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Sum int64 `protobuf:"varint,1,opt,name=sum,proto3" json:"sum,omitempty"` -} - -func (x *SumResponse) Reset() { - *x = SumResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_connect_ping_v1_ping_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *SumResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SumResponse) ProtoMessage() {} - -func (x *SumResponse) ProtoReflect() protoreflect.Message { - mi := &file_connect_ping_v1_ping_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 SumResponse.ProtoReflect.Descriptor instead. -func (*SumResponse) Descriptor() ([]byte, []int) { - return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{3} -} - -func (x *SumResponse) GetSum() int64 { - if x != nil { - return x.Sum - } - return 0 -} - -type CountUpRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` -} - -func (x *CountUpRequest) Reset() { - *x = CountUpRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_connect_ping_v1_ping_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *CountUpRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CountUpRequest) ProtoMessage() {} - -func (x *CountUpRequest) ProtoReflect() protoreflect.Message { - mi := &file_connect_ping_v1_ping_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 CountUpRequest.ProtoReflect.Descriptor instead. -func (*CountUpRequest) Descriptor() ([]byte, []int) { - return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{4} -} - -func (x *CountUpRequest) GetNumber() int64 { - if x != nil { - return x.Number - } - return 0 -} - -type CountUpResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` -} - -func (x *CountUpResponse) Reset() { - *x = CountUpResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_connect_ping_v1_ping_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *CountUpResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CountUpResponse) ProtoMessage() {} - -func (x *CountUpResponse) ProtoReflect() protoreflect.Message { - mi := &file_connect_ping_v1_ping_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 CountUpResponse.ProtoReflect.Descriptor instead. -func (*CountUpResponse) Descriptor() ([]byte, []int) { - return file_connect_ping_v1_ping_proto_rawDescGZIP(), []int{5} -} - -func (x *CountUpResponse) GetNumber() int64 { - if x != nil { - return x.Number - } - return 0 -} - -var File_connect_ping_v1_ping_proto protoreflect.FileDescriptor - -var file_connect_ping_v1_ping_proto_rawDesc = []byte{ - 0x0a, 0x1a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x2f, 0x76, - 0x31, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x1a, 0x1b, 0x62, - 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, - 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x33, 0x0a, 0x0b, 0x50, 0x69, - 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x06, 0x6e, 0x75, 0x6d, - 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x0c, 0xba, 0x48, 0x09, 0xc8, 0x01, - 0x01, 0x22, 0x04, 0x10, 0x64, 0x20, 0x00, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, - 0x26, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x32, 0x0a, 0x0a, 0x53, 0x75, 0x6d, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x0c, 0xba, 0x48, 0x09, 0xc8, 0x01, 0x01, 0x22, 0x04, 0x10, - 0x64, 0x20, 0x00, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x1f, 0x0a, 0x0b, 0x53, - 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x75, - 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x73, 0x75, 0x6d, 0x22, 0x36, 0x0a, 0x0e, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, - 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x0c, - 0xba, 0x48, 0x09, 0xc8, 0x01, 0x01, 0x22, 0x04, 0x10, 0x64, 0x20, 0x00, 0x52, 0x06, 0x6e, 0x75, - 0x6d, 0x62, 0x65, 0x72, 0x22, 0x29, 0x0a, 0x0f, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x32, - 0xec, 0x01, 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, - 0x45, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, - 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x03, 0x53, 0x75, 0x6d, 0x12, 0x1b, 0x2e, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, - 0x53, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x6d, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x12, 0x50, 0x0a, 0x07, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x70, 0x12, 0x1f, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x55, - 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x42, 0xbb, - 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, - 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x50, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x50, 0x01, 0x5a, 0x3b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x69, 0x6e, 0x67, 0x76, 0x31, - 0xa2, 0x02, 0x03, 0x43, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x5c, 0x50, 0x69, 0x6e, 0x67, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1b, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x5c, 0x50, 0x69, 0x6e, 0x67, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x3a, 0x3a, 0x50, 0x69, 0x6e, 0x67, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_connect_ping_v1_ping_proto_rawDescOnce sync.Once - file_connect_ping_v1_ping_proto_rawDescData = file_connect_ping_v1_ping_proto_rawDesc -) - -func file_connect_ping_v1_ping_proto_rawDescGZIP() []byte { - file_connect_ping_v1_ping_proto_rawDescOnce.Do(func() { - file_connect_ping_v1_ping_proto_rawDescData = protoimpl.X.CompressGZIP(file_connect_ping_v1_ping_proto_rawDescData) - }) - return file_connect_ping_v1_ping_proto_rawDescData -} - -var file_connect_ping_v1_ping_proto_msgTypes = make([]protoimpl.MessageInfo, 6) -var file_connect_ping_v1_ping_proto_goTypes = []interface{}{ - (*PingRequest)(nil), // 0: connect.ping.v1.PingRequest - (*PingResponse)(nil), // 1: connect.ping.v1.PingResponse - (*SumRequest)(nil), // 2: connect.ping.v1.SumRequest - (*SumResponse)(nil), // 3: connect.ping.v1.SumResponse - (*CountUpRequest)(nil), // 4: connect.ping.v1.CountUpRequest - (*CountUpResponse)(nil), // 5: connect.ping.v1.CountUpResponse -} -var file_connect_ping_v1_ping_proto_depIdxs = []int32{ - 0, // 0: connect.ping.v1.PingService.Ping:input_type -> connect.ping.v1.PingRequest - 2, // 1: connect.ping.v1.PingService.Sum:input_type -> connect.ping.v1.SumRequest - 4, // 2: connect.ping.v1.PingService.CountUp:input_type -> connect.ping.v1.CountUpRequest - 1, // 3: connect.ping.v1.PingService.Ping:output_type -> connect.ping.v1.PingResponse - 3, // 4: connect.ping.v1.PingService.Sum:output_type -> connect.ping.v1.SumResponse - 5, // 5: connect.ping.v1.PingService.CountUp:output_type -> connect.ping.v1.CountUpResponse - 3, // [3:6] is the sub-list for method output_type - 0, // [0:3] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_connect_ping_v1_ping_proto_init() } -func file_connect_ping_v1_ping_proto_init() { - if File_connect_ping_v1_ping_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_connect_ping_v1_ping_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PingRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_connect_ping_v1_ping_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PingResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_connect_ping_v1_ping_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SumRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_connect_ping_v1_ping_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SumResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_connect_ping_v1_ping_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CountUpRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_connect_ping_v1_ping_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CountUpResponse); 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_connect_ping_v1_ping_proto_rawDesc, - NumEnums: 0, - NumMessages: 6, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_connect_ping_v1_ping_proto_goTypes, - DependencyIndexes: file_connect_ping_v1_ping_proto_depIdxs, - MessageInfos: file_connect_ping_v1_ping_proto_msgTypes, - }.Build() - File_connect_ping_v1_ping_proto = out.File - file_connect_ping_v1_ping_proto_rawDesc = nil - file_connect_ping_v1_ping_proto_goTypes = nil - file_connect_ping_v1_ping_proto_depIdxs = nil -} diff --git a/internal/gen/connect/ping/v1/pingv1connect/ping.connect.go b/internal/gen/connect/ping/v1/pingv1connect/ping.connect.go deleted file mode 100644 index de6bbc3..0000000 --- a/internal/gen/connect/ping/v1/pingv1connect/ping.connect.go +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright 2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// The canonical location for this file is -// https://github.com/connectrpc/validate-go/blob/main/internal/proto/connect/ping/v1/ping.proto. - -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: connect/ping/v1/ping.proto - -// The connect.ping.v1 package contains an echo service designed to test the -// validate-go implementation. -package pingv1connect - -import ( - connect "connectrpc.com/connect" - v1 "connectrpc.com/validate/internal/gen/connect/ping/v1" - context "context" - errors "errors" - 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 ( - // PingServiceName is the fully-qualified name of the PingService service. - PingServiceName = "connect.ping.v1.PingService" -) - -// 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 ( - // PingServicePingProcedure is the fully-qualified name of the PingService's Ping RPC. - PingServicePingProcedure = "/connect.ping.v1.PingService/Ping" - // PingServiceSumProcedure is the fully-qualified name of the PingService's Sum RPC. - PingServiceSumProcedure = "/connect.ping.v1.PingService/Sum" - // PingServiceCountUpProcedure is the fully-qualified name of the PingService's CountUp RPC. - PingServiceCountUpProcedure = "/connect.ping.v1.PingService/CountUp" -) - -// PingServiceClient is a client for the connect.ping.v1.PingService service. -type PingServiceClient interface { - // Ping sends a ping to the server to determine if it's reachable. - Ping(context.Context, *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) - // Sum calculates the sum of the numbers sent on the stream. - Sum(context.Context) *connect.ClientStreamForClient[v1.SumRequest, v1.SumResponse] - // CountUp returns a stream of the numbers up to the given request. - CountUp(context.Context, *connect.Request[v1.CountUpRequest]) (*connect.ServerStreamForClient[v1.CountUpResponse], error) -} - -// NewPingServiceClient constructs a client for the connect.ping.v1.PingService 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 NewPingServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PingServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - return &pingServiceClient{ - ping: connect.NewClient[v1.PingRequest, v1.PingResponse]( - httpClient, - baseURL+PingServicePingProcedure, - opts..., - ), - sum: connect.NewClient[v1.SumRequest, v1.SumResponse]( - httpClient, - baseURL+PingServiceSumProcedure, - opts..., - ), - countUp: connect.NewClient[v1.CountUpRequest, v1.CountUpResponse]( - httpClient, - baseURL+PingServiceCountUpProcedure, - opts..., - ), - } -} - -// pingServiceClient implements PingServiceClient. -type pingServiceClient struct { - ping *connect.Client[v1.PingRequest, v1.PingResponse] - sum *connect.Client[v1.SumRequest, v1.SumResponse] - countUp *connect.Client[v1.CountUpRequest, v1.CountUpResponse] -} - -// Ping calls connect.ping.v1.PingService.Ping. -func (c *pingServiceClient) Ping(ctx context.Context, req *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) { - return c.ping.CallUnary(ctx, req) -} - -// Sum calls connect.ping.v1.PingService.Sum. -func (c *pingServiceClient) Sum(ctx context.Context) *connect.ClientStreamForClient[v1.SumRequest, v1.SumResponse] { - return c.sum.CallClientStream(ctx) -} - -// CountUp calls connect.ping.v1.PingService.CountUp. -func (c *pingServiceClient) CountUp(ctx context.Context, req *connect.Request[v1.CountUpRequest]) (*connect.ServerStreamForClient[v1.CountUpResponse], error) { - return c.countUp.CallServerStream(ctx, req) -} - -// PingServiceHandler is an implementation of the connect.ping.v1.PingService service. -type PingServiceHandler interface { - // Ping sends a ping to the server to determine if it's reachable. - Ping(context.Context, *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) - // Sum calculates the sum of the numbers sent on the stream. - Sum(context.Context, *connect.ClientStream[v1.SumRequest]) (*connect.Response[v1.SumResponse], error) - // CountUp returns a stream of the numbers up to the given request. - CountUp(context.Context, *connect.Request[v1.CountUpRequest], *connect.ServerStream[v1.CountUpResponse]) error -} - -// NewPingServiceHandler 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 NewPingServiceHandler(svc PingServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - pingServicePingHandler := connect.NewUnaryHandler( - PingServicePingProcedure, - svc.Ping, - opts..., - ) - pingServiceSumHandler := connect.NewClientStreamHandler( - PingServiceSumProcedure, - svc.Sum, - opts..., - ) - pingServiceCountUpHandler := connect.NewServerStreamHandler( - PingServiceCountUpProcedure, - svc.CountUp, - opts..., - ) - return "/connect.ping.v1.PingService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case PingServicePingProcedure: - pingServicePingHandler.ServeHTTP(w, r) - case PingServiceSumProcedure: - pingServiceSumHandler.ServeHTTP(w, r) - case PingServiceCountUpProcedure: - pingServiceCountUpHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedPingServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedPingServiceHandler struct{} - -func (UnimplementedPingServiceHandler) Ping(context.Context, *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("connect.ping.v1.PingService.Ping is not implemented")) -} - -func (UnimplementedPingServiceHandler) Sum(context.Context, *connect.ClientStream[v1.SumRequest]) (*connect.Response[v1.SumResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("connect.ping.v1.PingService.Sum is not implemented")) -} - -func (UnimplementedPingServiceHandler) CountUp(context.Context, *connect.Request[v1.CountUpRequest], *connect.ServerStream[v1.CountUpResponse]) error { - return connect.NewError(connect.CodeUnimplemented, errors.New("connect.ping.v1.PingService.CountUp is not implemented")) -} diff --git a/internal/gen/example/user/v1/user.pb.go b/internal/gen/example/user/v1/user.pb.go new file mode 100644 index 0000000..30dab8b --- /dev/null +++ b/internal/gen/example/user/v1/user.pb.go @@ -0,0 +1,346 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: example/user/v1/user.proto + +package userv1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type User struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + BirthDate *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=birth_date,json=birthDate,proto3" json:"birth_date,omitempty"` + SignupDate *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=signup_date,json=signupDate,proto3" json:"signup_date,omitempty"` +} + +func (x *User) Reset() { + *x = User{} + if protoimpl.UnsafeEnabled { + mi := &file_example_user_v1_user_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_example_user_v1_user_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 User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_example_user_v1_user_proto_rawDescGZIP(), []int{0} +} + +func (x *User) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *User) GetBirthDate() *timestamppb.Timestamp { + if x != nil { + return x.BirthDate + } + return nil +} + +func (x *User) GetSignupDate() *timestamppb.Timestamp { + if x != nil { + return x.SignupDate + } + return nil +} + +type CreateUserRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` +} + +func (x *CreateUserRequest) Reset() { + *x = CreateUserRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_example_user_v1_user_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserRequest) ProtoMessage() {} + +func (x *CreateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_example_user_v1_user_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 CreateUserRequest.ProtoReflect.Descriptor instead. +func (*CreateUserRequest) Descriptor() ([]byte, []int) { + return file_example_user_v1_user_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateUserRequest) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +type CreateUserResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` +} + +func (x *CreateUserResponse) Reset() { + *x = CreateUserResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_example_user_v1_user_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserResponse) ProtoMessage() {} + +func (x *CreateUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_example_user_v1_user_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 CreateUserResponse.ProtoReflect.Descriptor instead. +func (*CreateUserResponse) Descriptor() ([]byte, []int) { + return file_example_user_v1_user_proto_rawDescGZIP(), []int{2} +} + +func (x *CreateUserResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +var File_example_user_v1_user_proto protoreflect.FileDescriptor + +var file_example_user_v1_user_proto_rawDesc = []byte{ + 0x0a, 0x1a, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, + 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x65, 0x78, + 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x1a, 0x1b, 0x62, + 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x65, 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, 0x22, 0x87, 0x02, 0x0a, 0x04, + 0x55, 0x73, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x60, 0x01, 0x52, 0x05, 0x65, 0x6d, + 0x61, 0x69, 0x6c, 0x12, 0x39, 0x0a, 0x0a, 0x62, 0x69, 0x72, 0x74, 0x68, 0x5f, 0x64, 0x61, 0x74, + 0x65, 0x18, 0x02, 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, 0x09, 0x62, 0x69, 0x72, 0x74, 0x68, 0x44, 0x61, 0x74, 0x65, 0x12, 0x3b, + 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x75, 0x70, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x75, 0x70, 0x44, 0x61, 0x74, 0x65, 0x3a, 0x68, 0xba, 0x48, 0x65, + 0x1a, 0x63, 0x0a, 0x10, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x75, 0x70, 0x5f, + 0x64, 0x61, 0x74, 0x65, 0x12, 0x2a, 0x73, 0x69, 0x67, 0x6e, 0x75, 0x70, 0x20, 0x64, 0x61, 0x74, + 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x20, 0x6f, 0x72, 0x20, + 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x62, 0x69, 0x72, 0x74, 0x68, 0x20, 0x64, 0x61, 0x74, 0x65, + 0x1a, 0x23, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x75, 0x70, 0x5f, 0x64, 0x61, + 0x74, 0x65, 0x20, 0x3e, 0x3d, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x62, 0x69, 0x72, 0x74, 0x68, + 0x5f, 0x64, 0x61, 0x74, 0x65, 0x22, 0x3e, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, + 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x04, 0x75, 0x73, + 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x3f, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, + 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x75, + 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, + 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x32, 0x66, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x57, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, + 0x73, 0x65, 0x72, 0x12, 0x22, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x75, 0x73, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xbb, + 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x50, 0x01, 0x5a, 0x3b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, + 0xa2, 0x02, 0x03, 0x45, 0x55, 0x58, 0xaa, 0x02, 0x0f, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x2e, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0f, 0x45, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x5c, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1b, 0x45, 0x78, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x5c, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x45, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x3a, 0x3a, 0x55, 0x73, 0x65, 0x72, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_example_user_v1_user_proto_rawDescOnce sync.Once + file_example_user_v1_user_proto_rawDescData = file_example_user_v1_user_proto_rawDesc +) + +func file_example_user_v1_user_proto_rawDescGZIP() []byte { + file_example_user_v1_user_proto_rawDescOnce.Do(func() { + file_example_user_v1_user_proto_rawDescData = protoimpl.X.CompressGZIP(file_example_user_v1_user_proto_rawDescData) + }) + return file_example_user_v1_user_proto_rawDescData +} + +var file_example_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_example_user_v1_user_proto_goTypes = []interface{}{ + (*User)(nil), // 0: example.user.v1.User + (*CreateUserRequest)(nil), // 1: example.user.v1.CreateUserRequest + (*CreateUserResponse)(nil), // 2: example.user.v1.CreateUserResponse + (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp +} +var file_example_user_v1_user_proto_depIdxs = []int32{ + 3, // 0: example.user.v1.User.birth_date:type_name -> google.protobuf.Timestamp + 3, // 1: example.user.v1.User.signup_date:type_name -> google.protobuf.Timestamp + 0, // 2: example.user.v1.CreateUserRequest.user:type_name -> example.user.v1.User + 0, // 3: example.user.v1.CreateUserResponse.user:type_name -> example.user.v1.User + 1, // 4: example.user.v1.UserService.CreateUser:input_type -> example.user.v1.CreateUserRequest + 2, // 5: example.user.v1.UserService.CreateUser:output_type -> example.user.v1.CreateUserResponse + 5, // [5:6] is the sub-list for method output_type + 4, // [4:5] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_example_user_v1_user_proto_init() } +func file_example_user_v1_user_proto_init() { + if File_example_user_v1_user_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_example_user_v1_user_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*User); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_example_user_v1_user_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateUserRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_example_user_v1_user_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateUserResponse); 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_example_user_v1_user_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_example_user_v1_user_proto_goTypes, + DependencyIndexes: file_example_user_v1_user_proto_depIdxs, + MessageInfos: file_example_user_v1_user_proto_msgTypes, + }.Build() + File_example_user_v1_user_proto = out.File + file_example_user_v1_user_proto_rawDesc = nil + file_example_user_v1_user_proto_goTypes = nil + file_example_user_v1_user_proto_depIdxs = nil +} diff --git a/internal/gen/example/user/v1/userv1connect/user.connect.go b/internal/gen/example/user/v1/userv1connect/user.connect.go new file mode 100644 index 0000000..04d6e19 --- /dev/null +++ b/internal/gen/example/user/v1/userv1connect/user.connect.go @@ -0,0 +1,118 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: example/user/v1/user.proto + +package userv1connect + +import ( + connect "connectrpc.com/connect" + v1 "connectrpc.com/validate/internal/gen/example/user/v1" + context "context" + errors "errors" + 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 ( + // UserServiceName is the fully-qualified name of the UserService service. + UserServiceName = "example.user.v1.UserService" +) + +// 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 ( + // UserServiceCreateUserProcedure is the fully-qualified name of the UserService's CreateUser RPC. + UserServiceCreateUserProcedure = "/example.user.v1.UserService/CreateUser" +) + +// UserServiceClient is a client for the example.user.v1.UserService service. +type UserServiceClient interface { + CreateUser(context.Context, *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.CreateUserResponse], error) +} + +// NewUserServiceClient constructs a client for the example.user.v1.UserService 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 NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) UserServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &userServiceClient{ + createUser: connect.NewClient[v1.CreateUserRequest, v1.CreateUserResponse]( + httpClient, + baseURL+UserServiceCreateUserProcedure, + opts..., + ), + } +} + +// userServiceClient implements UserServiceClient. +type userServiceClient struct { + createUser *connect.Client[v1.CreateUserRequest, v1.CreateUserResponse] +} + +// CreateUser calls example.user.v1.UserService.CreateUser. +func (c *userServiceClient) CreateUser(ctx context.Context, req *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.CreateUserResponse], error) { + return c.createUser.CallUnary(ctx, req) +} + +// UserServiceHandler is an implementation of the example.user.v1.UserService service. +type UserServiceHandler interface { + CreateUser(context.Context, *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.CreateUserResponse], error) +} + +// NewUserServiceHandler 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 NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + userServiceCreateUserHandler := connect.NewUnaryHandler( + UserServiceCreateUserProcedure, + svc.CreateUser, + opts..., + ) + return "/example.user.v1.UserService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case UserServiceCreateUserProcedure: + userServiceCreateUserHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedUserServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedUserServiceHandler struct{} + +func (UnimplementedUserServiceHandler) CreateUser(context.Context, *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.CreateUserResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("example.user.v1.UserService.CreateUser is not implemented")) +} diff --git a/internal/proto/connect/ping/v1/ping.proto b/internal/proto/connect/ping/v1/ping.proto deleted file mode 100644 index 907a2a4..0000000 --- a/internal/proto/connect/ping/v1/ping.proto +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// The canonical location for this file is -// https://github.com/connectrpc/validate-go/blob/main/internal/proto/connect/ping/v1/ping.proto. -syntax = "proto3"; - -// The connect.ping.v1 package contains an echo service designed to test the -// validate-go implementation. -package connect.ping.v1; - -import "buf/validate/validate.proto"; - -message PingRequest { - int64 number = 1 [ - (buf.validate.field).required = true, - (buf.validate.field).int64 = { - gt: 0, - lt: 100 - } - ]; -} - -message PingResponse { - int64 number = 1; -} - -message SumRequest { - int64 number = 1 [ - (buf.validate.field).required = true, - (buf.validate.field).int64 = { - gt: 0, - lt: 100 - } - ]; -} - -message SumResponse { - int64 sum = 1; -} - -message CountUpRequest { - int64 number = 1 [ - (buf.validate.field).required = true, - (buf.validate.field).int64 = { - gt: 0, - lt: 100 - } - ]; -} - -message CountUpResponse { - int64 number = 1; -} - -service PingService { - // Ping sends a ping to the server to determine if it's reachable. - rpc Ping(PingRequest) returns (PingResponse) {} - // Sum calculates the sum of the numbers sent on the stream. - rpc Sum(stream SumRequest) returns (SumResponse) {} - // CountUp returns a stream of the numbers up to the given request. - rpc CountUp(CountUpRequest) returns (stream CountUpResponse) {} -} diff --git a/internal/proto/example/user/v1/user.proto b/internal/proto/example/user/v1/user.proto new file mode 100644 index 0000000..2e29ddb --- /dev/null +++ b/internal/proto/example/user/v1/user.proto @@ -0,0 +1,45 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package example.user.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +message User { + string email = 1 [(buf.validate.field).string.email = true]; + google.protobuf.Timestamp birth_date = 2; + google.protobuf.Timestamp signup_date = 3; + + option (buf.validate.message).cel = { + id: "user.signup_date", + message: "signup date must be on or after birth date", + expression: "this.signup_date >= this.birth_date" + }; +} + +message CreateUserRequest { + User user = 1; +} + +message CreateUserResponse { + User user = 1; +} + +service UserService { + rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {} +} + diff --git a/internal/testserver/in_memory_server.go b/internal/testserver/in_memory_server.go deleted file mode 100644 index 9248cab..0000000 --- a/internal/testserver/in_memory_server.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package testserver - -import ( - "context" - "errors" - "net" - "net/http" - "net/http/httptest" - "sync" -) - -// InMemoryServer is an HTTP server that uses in-memory pipes instead of TCP. -// It supports HTTP/2 and has TLS enabled. -// -// The Go Playground panics if we try to start a TCP-backed server. If you're -// not familiar with the Playground's behavior, it looks like our examples are -// broken. This server lets us write examples that work in the playground -// without abstracting over HTTP. -type InMemoryServer struct { - server *httptest.Server - listener *memoryListener -} - -// NewInMemoryServer constructs and starts an InMemoryServer. -func NewInMemoryServer(handler http.Handler) *InMemoryServer { - lis := &memoryListener{ - conns: make(chan net.Conn), - closed: make(chan struct{}), - } - server := httptest.NewUnstartedServer(handler) - server.Listener = lis - server.EnableHTTP2 = true - server.StartTLS() - return &InMemoryServer{ - server: server, - listener: lis, - } -} - -// Client returns an HTTP client configured to trust the server's TLS -// certificate and use HTTP/2 over an in-memory pipe. Automatic HTTP-level gzip -// compression is disabled. It closes its idle connections when the server is -// closed. -func (s *InMemoryServer) Client() *http.Client { - client := s.server.Client() - if transport, ok := client.Transport.(*http.Transport); ok { - transport.DialContext = s.listener.DialContext - transport.DisableCompression = true - } - return client -} - -// URL is the server's URL. -func (s *InMemoryServer) URL() string { - return s.server.URL -} - -// Close shuts down the server, blocking until all outstanding requests have -// completed. -func (s *InMemoryServer) Close() { - s.server.Close() -} - -type memoryListener struct { - conns chan net.Conn - once sync.Once - closed chan struct{} -} - -// Accept implements net.Listener. -func (l *memoryListener) Accept() (net.Conn, error) { - select { - case conn := <-l.conns: - return conn, nil - case <-l.closed: - return nil, errors.New("listener closed") - } -} - -// Close implements net.Listener. -func (l *memoryListener) Close() error { - l.once.Do(func() { - close(l.closed) - }) - return nil -} - -// Addr implements net.Listener. -func (l *memoryListener) Addr() net.Addr { - return &memoryAddr{} -} - -// DialContext is the type expected by http.Transport.DialContext. -func (l *memoryListener) DialContext(_ context.Context, _, _ string) (net.Conn, error) { - select { - case <-l.closed: - return nil, errors.New("listener closed") - default: - } - server, client := net.Pipe() - l.conns <- server - return client, nil -} - -type memoryAddr struct{} - -// Network implements net.Addr. -func (*memoryAddr) Network() string { return "memory" } - -// String implements io.Stringer, returning a value that matches the -// certificates used by net/http/httptest. -func (*memoryAddr) String() string { return "example.com" } diff --git a/internal/testserver/ping_server.go b/internal/testserver/ping_server.go deleted file mode 100644 index 221a189..0000000 --- a/internal/testserver/ping_server.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package testserver - -import ( - "context" - - "connectrpc.com/connect" - pingv1 "connectrpc.com/validate/internal/gen/connect/ping/v1" - "connectrpc.com/validate/internal/gen/connect/ping/v1/pingv1connect" -) - -type pingServer struct { - pingv1connect.UnimplementedPingServiceHandler - - err error -} - -type Option func(*pingServer) - -func NewPingServer(opts ...Option) pingv1connect.PingServiceHandler { - out := &pingServer{} - for _, apply := range opts { - apply(out) - } - return out -} - -func WithErr(err error) Option { - return func(p *pingServer) { - p.err = err - } -} - -func (p *pingServer) Ping(_ context.Context, req *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { - if p.err != nil { - return nil, p.err - } - return connect.NewResponse(&pingv1.PingResponse{ - Number: req.Msg.GetNumber(), - }), nil -} - -func (p *pingServer) Sum(_ context.Context, stream *connect.ClientStream[pingv1.SumRequest]) (*connect.Response[pingv1.SumResponse], error) { - if p.err != nil { - return nil, p.err - } - var sum int64 - for stream.Receive() { - sum += stream.Msg().Number - } - if stream.Err() != nil { - return nil, stream.Err() - } - return connect.NewResponse(&pingv1.SumResponse{Sum: sum}), nil -} - -func (p *pingServer) CountUp(_ context.Context, request *connect.Request[pingv1.CountUpRequest], stream *connect.ServerStream[pingv1.CountUpResponse]) error { - if p.err != nil { - return p.err - } - for i := int64(1); i <= request.Msg.Number; i++ { - if err := stream.Send(&pingv1.CountUpResponse{Number: i}); err != nil { - return err - } - } - return nil -} From f16a1b9e973fc23d338cbe86e1b2b846ffb4f34f Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 1 Sep 2023 00:28:10 -0700 Subject: [PATCH 14/20] Amend CoC --- .github/CODE_OF_CONDUCT.md | 134 +------------------------------------ 1 file changed, 2 insertions(+), 132 deletions(-) diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 1900eb4..628cbec 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -1,133 +1,3 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -conduct@buf.build. All complaints will be reviewed and investigated promptly -and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. - -Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. - -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available -at [https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org -[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq -[translations]: https://www.contributor-covenant.org/translations +## Community Code of Conduct +Connect follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). From b0cfffe851e85a88bf298773b780f2d508f8228f Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 1 Sep 2023 00:30:23 -0700 Subject: [PATCH 15/20] Add CONTRIBUTING.md --- .github/CONTRIBUTING.md | 74 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/CONTRIBUTING.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..0b7889b --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,74 @@ +Contributing +============ + +We'd love your help making Connect better! + +If you'd like to add new exported APIs, please [open an issue][open-issue] +describing your proposal — discussing API changes ahead of time makes +pull request review much smoother. In your issue, pull request, and any other +communications, please remember to treat your fellow contributors with +respect! + +Note that you'll need to sign the [Contributor License Agreement][cla] before +we can accept any of your contributions. If necessary, a bot will remind you to +accept the CLA when you open your pull request. + +## Setup + +[Fork][fork], then clone the repository: + +``` +mkdir -p $GOPATH/src/connectrpc.com +cd $GOPATH/src/connectrpc.com +git clone git@github.com:your_github_username/validate-go.git validate +cd validate +git remote add upstream https://github.com/connectrpc/validate-go.git +git fetch upstream +``` + +Make sure that the tests and the linters pass (you'll need `bash` and the +latest stable Go release installed): + +``` +make +``` + +## Making Changes + +Start by creating a new branch for your changes: + +``` +cd $GOPATH/src/connectrpc.com/validate +git checkout main +git fetch upstream +git rebase upstream/main +git checkout -b cool_new_feature +``` + +Make your changes, then ensure that `make` still passes. (Unless you're +changing the Protobuf schemas, you can use the standard `go build ./...` and +`go test ./...` while you're coding.) When you're satisfied with your changes, +push them to your fork. + +``` +git commit -a +git push origin cool_new_feature +``` + +Then use the GitHub UI to open a pull request. + +At this point, you're waiting on us to review your changes. We *try* to respond +to issues and pull requests within a few business days, and we may suggest some +improvements or alternatives. Once your changes are approved, one of the +project maintainers will merge them. + +We're much more likely to approve your changes if you: + +* Add tests for new functionality. +* Write a [good commit message][commit-message]. +* Maintain backward compatibility. + +[fork]: https://github.com/connectrpc/validate-go/fork +[open-issue]: https://github.com/connectrpc/validate-go/issues/new +[cla]: https://cla-assistant.io/connectrpc/validate-go +[commit-message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html From 3216dbde2e3118d5b179172d52f26b287c963506 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Tue, 5 Sep 2023 21:37:33 -0700 Subject: [PATCH 16/20] Specify embedded fields first --- validate.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/validate.go b/validate.go index a947e0d..deafddf 100644 --- a/validate.go +++ b/validate.go @@ -97,8 +97,8 @@ func (i *Interceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { func (i *Interceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { return &streamingClientInterceptor{ - validator: i.validator, StreamingClientConn: next(ctx, spec), + validator: i.validator, } } } @@ -107,8 +107,8 @@ func (i *Interceptor) WrapStreamingClient(next connect.StreamingClientFunc) conn func (i *Interceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { return func(ctx context.Context, conn connect.StreamingHandlerConn) error { return next(ctx, &streamingHandlerInterceptor{ - validator: i.validator, StreamingHandlerConn: conn, + validator: i.validator, }) } } From fce00fcbf0f2225ee7edf49b70231a3503ea07aa Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Tue, 5 Sep 2023 22:21:32 -0700 Subject: [PATCH 17/20] Ignore coverage.out --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 987d6a1..333a388 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.pprof *.svg cover.out +coverage.out From 470c003ef38612cef9c2e2ed04a69ab8c1799a63 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Tue, 5 Sep 2023 22:21:50 -0700 Subject: [PATCH 18/20] Add calculator service for tests --- .../example/calculator/v1/calculator.pb.go | 247 ++++++++++++++++++ .../calculatorv1connect/calculator.connect.go | 120 +++++++++ .../example/calculator/v1/calculator.proto | 32 +++ 3 files changed, 399 insertions(+) create mode 100644 internal/gen/example/calculator/v1/calculator.pb.go create mode 100644 internal/gen/example/calculator/v1/calculatorv1connect/calculator.connect.go create mode 100644 internal/proto/example/calculator/v1/calculator.proto diff --git a/internal/gen/example/calculator/v1/calculator.pb.go b/internal/gen/example/calculator/v1/calculator.pb.go new file mode 100644 index 0000000..18bdea3 --- /dev/null +++ b/internal/gen/example/calculator/v1/calculator.pb.go @@ -0,0 +1,247 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: example/calculator/v1/calculator.proto + +package calculatorv1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CumSumRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` +} + +func (x *CumSumRequest) Reset() { + *x = CumSumRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_example_calculator_v1_calculator_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CumSumRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CumSumRequest) ProtoMessage() {} + +func (x *CumSumRequest) ProtoReflect() protoreflect.Message { + mi := &file_example_calculator_v1_calculator_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 CumSumRequest.ProtoReflect.Descriptor instead. +func (*CumSumRequest) Descriptor() ([]byte, []int) { + return file_example_calculator_v1_calculator_proto_rawDescGZIP(), []int{0} +} + +func (x *CumSumRequest) GetNumber() int64 { + if x != nil { + return x.Number + } + return 0 +} + +type CumSumResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Sum int64 `protobuf:"varint,1,opt,name=sum,proto3" json:"sum,omitempty"` +} + +func (x *CumSumResponse) Reset() { + *x = CumSumResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_example_calculator_v1_calculator_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CumSumResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CumSumResponse) ProtoMessage() {} + +func (x *CumSumResponse) ProtoReflect() protoreflect.Message { + mi := &file_example_calculator_v1_calculator_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 CumSumResponse.ProtoReflect.Descriptor instead. +func (*CumSumResponse) Descriptor() ([]byte, []int) { + return file_example_calculator_v1_calculator_proto_rawDescGZIP(), []int{1} +} + +func (x *CumSumResponse) GetSum() int64 { + if x != nil { + return x.Sum + } + return 0 +} + +var File_example_calculator_v1_calculator_proto protoreflect.FileDescriptor + +var file_example_calculator_v1_calculator_proto_rawDesc = []byte{ + 0x0a, 0x26, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, + 0x61, 0x74, 0x6f, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, + 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x15, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x2e, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x1a, + 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x30, 0x0a, 0x0d, + 0x43, 0x75, 0x6d, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, + 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, + 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x22, + 0x0a, 0x0e, 0x43, 0x75, 0x6d, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x10, 0x0a, 0x03, 0x73, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x73, + 0x75, 0x6d, 0x32, 0x70, 0x0a, 0x11, 0x43, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x06, 0x43, 0x75, 0x6d, 0x53, 0x75, + 0x6d, 0x12, 0x24, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x61, 0x6c, 0x63, + 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x6d, 0x53, 0x75, 0x6d, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x2e, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x75, 0x6d, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x28, 0x01, 0x30, 0x01, 0x42, 0xeb, 0x01, 0x0a, 0x19, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x78, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x2e, + 0x76, 0x31, 0x42, 0x0f, 0x43, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x47, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x2f, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x2f, 0x76, + 0x31, 0x3b, 0x63, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x76, 0x31, 0xa2, 0x02, + 0x03, 0x45, 0x43, 0x58, 0xaa, 0x02, 0x15, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x43, + 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x15, 0x45, + 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x5c, 0x43, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x6f, + 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x21, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x5c, 0x43, + 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x17, 0x45, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x3a, 0x3a, 0x43, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x3a, 0x3a, + 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_example_calculator_v1_calculator_proto_rawDescOnce sync.Once + file_example_calculator_v1_calculator_proto_rawDescData = file_example_calculator_v1_calculator_proto_rawDesc +) + +func file_example_calculator_v1_calculator_proto_rawDescGZIP() []byte { + file_example_calculator_v1_calculator_proto_rawDescOnce.Do(func() { + file_example_calculator_v1_calculator_proto_rawDescData = protoimpl.X.CompressGZIP(file_example_calculator_v1_calculator_proto_rawDescData) + }) + return file_example_calculator_v1_calculator_proto_rawDescData +} + +var file_example_calculator_v1_calculator_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_example_calculator_v1_calculator_proto_goTypes = []interface{}{ + (*CumSumRequest)(nil), // 0: example.calculator.v1.CumSumRequest + (*CumSumResponse)(nil), // 1: example.calculator.v1.CumSumResponse +} +var file_example_calculator_v1_calculator_proto_depIdxs = []int32{ + 0, // 0: example.calculator.v1.CalculatorService.CumSum:input_type -> example.calculator.v1.CumSumRequest + 1, // 1: example.calculator.v1.CalculatorService.CumSum:output_type -> example.calculator.v1.CumSumResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_example_calculator_v1_calculator_proto_init() } +func file_example_calculator_v1_calculator_proto_init() { + if File_example_calculator_v1_calculator_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_example_calculator_v1_calculator_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CumSumRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_example_calculator_v1_calculator_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CumSumResponse); 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_example_calculator_v1_calculator_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_example_calculator_v1_calculator_proto_goTypes, + DependencyIndexes: file_example_calculator_v1_calculator_proto_depIdxs, + MessageInfos: file_example_calculator_v1_calculator_proto_msgTypes, + }.Build() + File_example_calculator_v1_calculator_proto = out.File + file_example_calculator_v1_calculator_proto_rawDesc = nil + file_example_calculator_v1_calculator_proto_goTypes = nil + file_example_calculator_v1_calculator_proto_depIdxs = nil +} diff --git a/internal/gen/example/calculator/v1/calculatorv1connect/calculator.connect.go b/internal/gen/example/calculator/v1/calculatorv1connect/calculator.connect.go new file mode 100644 index 0000000..0d195a6 --- /dev/null +++ b/internal/gen/example/calculator/v1/calculatorv1connect/calculator.connect.go @@ -0,0 +1,120 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: example/calculator/v1/calculator.proto + +package calculatorv1connect + +import ( + connect "connectrpc.com/connect" + v1 "connectrpc.com/validate/internal/gen/example/calculator/v1" + context "context" + errors "errors" + 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 ( + // CalculatorServiceName is the fully-qualified name of the CalculatorService service. + CalculatorServiceName = "example.calculator.v1.CalculatorService" +) + +// 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 ( + // CalculatorServiceCumSumProcedure is the fully-qualified name of the CalculatorService's CumSum + // RPC. + CalculatorServiceCumSumProcedure = "/example.calculator.v1.CalculatorService/CumSum" +) + +// CalculatorServiceClient is a client for the example.calculator.v1.CalculatorService service. +type CalculatorServiceClient interface { + CumSum(context.Context) *connect.BidiStreamForClient[v1.CumSumRequest, v1.CumSumResponse] +} + +// NewCalculatorServiceClient constructs a client for the example.calculator.v1.CalculatorService +// 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 NewCalculatorServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) CalculatorServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &calculatorServiceClient{ + cumSum: connect.NewClient[v1.CumSumRequest, v1.CumSumResponse]( + httpClient, + baseURL+CalculatorServiceCumSumProcedure, + opts..., + ), + } +} + +// calculatorServiceClient implements CalculatorServiceClient. +type calculatorServiceClient struct { + cumSum *connect.Client[v1.CumSumRequest, v1.CumSumResponse] +} + +// CumSum calls example.calculator.v1.CalculatorService.CumSum. +func (c *calculatorServiceClient) CumSum(ctx context.Context) *connect.BidiStreamForClient[v1.CumSumRequest, v1.CumSumResponse] { + return c.cumSum.CallBidiStream(ctx) +} + +// CalculatorServiceHandler is an implementation of the example.calculator.v1.CalculatorService +// service. +type CalculatorServiceHandler interface { + CumSum(context.Context, *connect.BidiStream[v1.CumSumRequest, v1.CumSumResponse]) error +} + +// NewCalculatorServiceHandler 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 NewCalculatorServiceHandler(svc CalculatorServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + calculatorServiceCumSumHandler := connect.NewBidiStreamHandler( + CalculatorServiceCumSumProcedure, + svc.CumSum, + opts..., + ) + return "/example.calculator.v1.CalculatorService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case CalculatorServiceCumSumProcedure: + calculatorServiceCumSumHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedCalculatorServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedCalculatorServiceHandler struct{} + +func (UnimplementedCalculatorServiceHandler) CumSum(context.Context, *connect.BidiStream[v1.CumSumRequest, v1.CumSumResponse]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("example.calculator.v1.CalculatorService.CumSum is not implemented")) +} diff --git a/internal/proto/example/calculator/v1/calculator.proto b/internal/proto/example/calculator/v1/calculator.proto new file mode 100644 index 0000000..5dadedb --- /dev/null +++ b/internal/proto/example/calculator/v1/calculator.proto @@ -0,0 +1,32 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package example.calculator.v1; + +import "buf/validate/validate.proto"; + +message CumSumRequest { + int64 number = 1 [(buf.validate.field).int64.gt = 0]; +} + +message CumSumResponse { + int64 sum = 1; +} + +service CalculatorService { + rpc CumSum(stream CumSumRequest) returns (stream CumSumResponse) {} +} + From e3504697e382350d7dc5b98d6f5cf2792a98afae Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Tue, 5 Sep 2023 22:23:36 -0700 Subject: [PATCH 19/20] Refactor tests --- validate_test.go | 533 +++++++++++++++++++---------------------------- 1 file changed, 220 insertions(+), 313 deletions(-) diff --git a/validate_test.go b/validate_test.go index c8e50af..9233007 100644 --- a/validate_test.go +++ b/validate_test.go @@ -17,427 +17,334 @@ package validate_test import ( "context" "errors" - "fmt" + "io" "net/http" + "net/http/httptest" "testing" "time" - validateproto "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + validatepb "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "connectrpc.com/connect" "connectrpc.com/validate" - pingv1 "connectrpc.com/validate/internal/gen/connect/ping/v1" - "connectrpc.com/validate/internal/gen/connect/ping/v1/pingv1connect" - "connectrpc.com/validate/internal/testserver" + calculatorv1 "connectrpc.com/validate/internal/gen/example/calculator/v1" + "connectrpc.com/validate/internal/gen/example/calculator/v1/calculatorv1connect" + userv1 "connectrpc.com/validate/internal/gen/example/user/v1" + "connectrpc.com/validate/internal/gen/example/user/v1/userv1connect" "github.com/bufbuild/protovalidate-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestNewInterceptor(t *testing.T) { +func TestInterceptorUnary(t *testing.T) { t.Parallel() - t.Run("success", func(t *testing.T) { - t.Parallel() - interceptor, err := validate.NewInterceptor() - require.NoError(t, err) - assert.NotNil(t, interceptor) - }) - t.Run("success with validator", func(t *testing.T) { - t.Parallel() - validator, err := protovalidate.New() - require.NoError(t, err) - interceptor, err := validate.NewInterceptor(validate.WithValidator(validator)) - require.NoError(t, err) - assert.NotNil(t, interceptor) - }) -} - -func TestInterceptor_WrapUnary(t *testing.T) { - t.Parallel() - type args struct { - msg string - code connect.Code - detail *protovalidate.ValidationError - } tests := []struct { - name string - svc pingv1connect.PingServiceHandler - req *pingv1.PingRequest - want *pingv1.PingResponse - wantErr *args + name string + svc func(context.Context, *connect.Request[userv1.CreateUserRequest]) (*connect.Response[userv1.CreateUserResponse], error) + req userv1.CreateUserRequest + wantCode connect.Code + wantPath string // field path, from error details }{ { - name: "empty request returns error on required request fields", - req: &pingv1.PingRequest{}, - wantErr: &args{ - msg: "validation error:\n - number: value is required [required]", - code: connect.CodeInvalidArgument, - detail: &protovalidate.ValidationError{ - Violations: []*validateproto.Violation{ - { - FieldPath: "number", - ConstraintId: "required", - Message: "value is required", - }, - }, - }, - }, - }, - { - name: "invalid request returns error with constraint violation", - req: &pingv1.PingRequest{ - Number: 123, - }, - wantErr: &args{ - msg: "validation error:\n - number: value must be greater than 0 and less than 100 [int64.gt_lt]", - code: connect.CodeInvalidArgument, - detail: &protovalidate.ValidationError{ - Violations: []*validateproto.Violation{ - { - FieldPath: "number", - ConstraintId: "int64.gt_lt", - Message: "value must be greater than 0 and less than 100", - }, - }, - }, + name: "valid", + svc: createUser, + req: userv1.CreateUserRequest{ + User: &userv1.User{Email: "someone@example.com"}, }, }, { - name: "unrelated server error remains unaffected", - svc: testserver.NewPingServer( - testserver.WithErr( - connect.NewError(connect.CodeInternal, fmt.Errorf("oh no")), - ), - ), - req: &pingv1.PingRequest{ - Number: 50, - }, - wantErr: &args{ - msg: "oh no", - code: connect.CodeInternal, + name: "invalid", + req: userv1.CreateUserRequest{ + User: &userv1.User{Email: "foo"}, }, + wantCode: connect.CodeInvalidArgument, + wantPath: "user.email", }, { - name: "valid request returns response", - req: &pingv1.PingRequest{ - Number: 50, + name: "underlying_error", + svc: func(_ context.Context, req *connect.Request[userv1.CreateUserRequest]) (*connect.Response[userv1.CreateUserResponse], error) { + return nil, connect.NewError(connect.CodeInternal, errors.New("oh no")) }, - want: &pingv1.PingResponse{ - Number: 50, + req: userv1.CreateUserRequest{ + User: &userv1.User{Email: "someone@example.com"}, }, + wantCode: connect.CodeInternal, }, } for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { + tt := tt + t.Run(tt.name, func(t *testing.T) { t.Parallel() - if test.svc == nil { - test.svc = testserver.NewPingServer() - } - validator, err := validate.NewInterceptor() require.NoError(t, err) mux := http.NewServeMux() - mux.Handle(pingv1connect.NewPingServiceHandler( - test.svc, + mux.Handle(userv1connect.UserServiceCreateUserProcedure, connect.NewUnaryHandler( + userv1connect.UserServiceCreateUserProcedure, + tt.svc, connect.WithInterceptors(validator), )) + srv := startHTTPServer(t, mux) - exampleBookingServer := testserver.NewInMemoryServer(mux) - defer exampleBookingServer.Close() + got, err := userv1connect.NewUserServiceClient(srv.Client(), srv.URL). + CreateUser(context.Background(), connect.NewRequest(&tt.req)) - client := pingv1connect.NewPingServiceClient( - exampleBookingServer.Client(), - exampleBookingServer.URL(), - ) - - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - - got, err := client.Ping(ctx, connect.NewRequest(test.req)) - if test.wantErr != nil { + if tt.wantCode > 0 { require.Error(t, err) var connectErr *connect.Error - assert.True(t, errors.As(err, &connectErr)) - assert.Equal(t, test.wantErr.msg, connectErr.Message()) - assert.Equal(t, test.wantErr.code, connectErr.Code()) - if test.wantErr.detail != nil { - require.Len(t, connectErr.Details(), 1) - detail, err := connect.NewErrorDetail(test.wantErr.detail.ToProto()) + require.True(t, errors.As(err, &connectErr)) + assert.Equal(t, tt.wantCode, connectErr.Code()) + if tt.wantPath != "" { + details := connectErr.Details() + require.Len(t, details, 1) + detail, err := details[0].Value() require.NoError(t, err) - assert.Equal(t, connectErr.Details()[0].Type(), detail.Type()) + violations, ok := detail.(*validatepb.Violations) + require.True(t, ok) + require.Len(t, violations.Violations, 1) + require.Equal(t, tt.wantPath, violations.Violations[0].FieldPath) } - assert.Nil(t, got) } else { require.NoError(t, err) - assert.Equal(t, test.want.GetNumber(), got.Msg.GetNumber()) + assert.NotZero(t, got.Msg) } }) } } -func TestInterceptor_WrapStreamingClient(t *testing.T) { +func TestInterceptorStreamingHandler(t *testing.T) { t.Parallel() - type args struct { - msg string - code connect.Code - detail *protovalidate.ValidationError - closeErr bool - } tests := []struct { - name string - svc pingv1connect.PingServiceHandler - req *pingv1.SumRequest - want *pingv1.SumResponse - wantErr *args + name string + svc func(context.Context, *connect.BidiStream[calculatorv1.CumSumRequest, calculatorv1.CumSumResponse]) error + req calculatorv1.CumSumRequest + wantCode connect.Code + wantPath string // field path, from error details }{ { - name: "empty request returns error on required request fields", - req: &pingv1.SumRequest{}, - wantErr: &args{ - msg: "validation error:\n - number: value is required [required]", - code: connect.CodeInvalidArgument, - detail: &protovalidate.ValidationError{ - Violations: []*validateproto.Violation{ - { - FieldPath: "number", - ConstraintId: "required", - Message: "value is required", - }, - }, - }, - }, + name: "invalid", + svc: cumSumSuccess, + req: calculatorv1.CumSumRequest{Number: 0}, + wantCode: connect.CodeInvalidArgument, + wantPath: "number", }, { - name: "invalid request returns error with constraint violation", - req: &pingv1.SumRequest{ - Number: 123, - }, - wantErr: &args{ - msg: "validation error:\n - number: value must be greater than 0 and less than 100 [int64.gt_lt]", - code: connect.CodeInvalidArgument, - detail: &protovalidate.ValidationError{ - Violations: []*validateproto.Violation{ - { - FieldPath: "number", - ConstraintId: "int64.gt_lt", - Message: "value must be greater than 0 and less than 100", - }, - }, - }, - }, + name: "valid", + svc: cumSumSuccess, + req: calculatorv1.CumSumRequest{Number: 1}, }, { - name: "unrelated server error remains unaffected", - svc: testserver.NewPingServer( - testserver.WithErr( - connect.NewError(connect.CodeInternal, fmt.Errorf("oh no")), - ), - ), - req: &pingv1.SumRequest{ - Number: 50, - }, - wantErr: &args{ - msg: "oh no", - code: connect.CodeInternal, - closeErr: true, - }, - }, - { - name: "valid request returns response", - req: &pingv1.SumRequest{ - Number: 50, - }, - want: &pingv1.SumResponse{ - Sum: 50, - }, + name: "underlying_error", + svc: cumSumError, + req: calculatorv1.CumSumRequest{Number: 1}, + wantCode: connect.CodeInternal, }, } for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { + tt := tt + t.Run(tt.name, func(t *testing.T) { t.Parallel() - if test.svc == nil { - test.svc = testserver.NewPingServer() - } - validator, err := validate.NewInterceptor() require.NoError(t, err) mux := http.NewServeMux() - mux.Handle(pingv1connect.NewPingServiceHandler( - test.svc, + mux.Handle(calculatorv1connect.CalculatorServiceCumSumProcedure, connect.NewBidiStreamHandler( + calculatorv1connect.CalculatorServiceCumSumProcedure, + tt.svc, + connect.WithInterceptors(validator), )) + srv := httptest.NewUnstartedServer(mux) + srv.EnableHTTP2 = true + srv.StartTLS() + t.Cleanup(srv.Close) - exampleBookingServer := testserver.NewInMemoryServer(mux) - defer exampleBookingServer.Close() - - client := pingv1connect.NewPingServiceClient( - exampleBookingServer.Client(), - exampleBookingServer.URL(), - connect.WithInterceptors(validator), - ) + client := calculatorv1connect.NewCalculatorServiceClient(srv.Client(), srv.URL) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + t.Cleanup(cancel) + stream := client.CumSum(ctx) + t.Cleanup(func() { + assert.NoError(t, stream.CloseResponse()) + }) + t.Cleanup(func() { + assert.NoError(t, stream.CloseRequest()) + }) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + err = stream.Send(&tt.req) + require.NoError(t, err) + time.Sleep(time.Second) + got, err := stream.Receive() - stream := client.Sum(ctx) - err = stream.Send(test.req) - resp, closeErr := stream.CloseAndReceive() - if test.wantErr != nil { - if test.wantErr.closeErr { - err = closeErr - } + if tt.wantCode > 0 { require.Error(t, err) var connectErr *connect.Error assert.True(t, errors.As(err, &connectErr)) - assert.Equal(t, test.wantErr.msg, connectErr.Message()) - assert.Equal(t, test.wantErr.code, connectErr.Code()) - if test.wantErr.detail != nil { - require.Len(t, connectErr.Details(), 1) - detail, err := connect.NewErrorDetail(test.wantErr.detail.ToProto()) + assert.Equal(t, tt.wantCode, connectErr.Code()) + if tt.wantPath != "" { + details := connectErr.Details() + require.Len(t, details, 1) + detail, err := details[0].Value() require.NoError(t, err) - assert.Equal(t, connectErr.Details()[0].Type(), detail.Type()) + violations, ok := detail.(*validatepb.Violations) + require.True(t, ok) + require.Len(t, violations.Violations, 1) + require.Equal(t, tt.wantPath, violations.Violations[0].FieldPath) } } else { require.NoError(t, err) - require.NoError(t, err) - assert.NotNil(t, resp) - assert.Equal(t, resp.Msg.GetSum(), test.want.GetSum()) + require.NotZero(t, got.Sum) } }) } } -func TestInterceptor_WrapStreamingHandler(t *testing.T) { +func TestInterceptorStreamingClient(t *testing.T) { t.Parallel() - type args struct { - msg string - code connect.Code - detail *protovalidate.ValidationError - closeErr bool - } tests := []struct { - name string - svc pingv1connect.PingServiceHandler - req *pingv1.CountUpRequest - want *pingv1.CountUpResponse - wantErr *args + name string + svc func(context.Context, *connect.BidiStream[calculatorv1.CumSumRequest, calculatorv1.CumSumResponse]) error + req calculatorv1.CumSumRequest + wantCode connect.Code + wantPath string // field path, from error details + wantReceiveCode connect.Code // code for error calling Receive() }{ { - name: "empty request returns error on required request fields", - req: &pingv1.CountUpRequest{}, - wantErr: &args{ - msg: "validation error:\n - number: value is required [required]", - code: connect.CodeInvalidArgument, - detail: &protovalidate.ValidationError{ - Violations: []*validateproto.Violation{ - { - FieldPath: "number", - ConstraintId: "required", - Message: "value is required", - }, - }, - }, - }, + name: "invalid", + svc: cumSumSuccess, + req: calculatorv1.CumSumRequest{Number: 0}, + wantCode: connect.CodeInvalidArgument, + wantPath: "number", }, { - name: "invalid request returns error with constraint violation", - req: &pingv1.CountUpRequest{ - Number: 123, - }, - wantErr: &args{ - msg: "validation error:\n - number: value must be greater than 0 and less than 100 [int64.gt_lt]", - code: connect.CodeInvalidArgument, - detail: &protovalidate.ValidationError{ - Violations: []*validateproto.Violation{ - { - FieldPath: "number", - ConstraintId: "int64.gt_lt", - Message: "value must be greater than 0 and less than 100", - }, - }, - }, - }, + name: "valid", + svc: cumSumSuccess, + req: calculatorv1.CumSumRequest{Number: 1}, }, { - name: "unrelated server error remains unaffected", - svc: testserver.NewPingServer( - testserver.WithErr( - connect.NewError(connect.CodeInternal, fmt.Errorf("oh no")), - ), - ), - req: &pingv1.CountUpRequest{ - Number: 50, - }, - wantErr: &args{ - msg: "oh no", - code: connect.CodeInternal, - closeErr: true, - }, - }, - { - name: "valid request returns response", - req: &pingv1.CountUpRequest{ - Number: 50, - }, - want: &pingv1.CountUpResponse{ - Number: 1, - }, + name: "underlying_error", + svc: cumSumError, + req: calculatorv1.CumSumRequest{Number: 1}, + wantReceiveCode: connect.CodeInternal, }, } for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { + tt := tt + t.Run(tt.name, func(t *testing.T) { t.Parallel() - if test.svc == nil { - test.svc = testserver.NewPingServer() - } validator, err := validate.NewInterceptor() require.NoError(t, err) + mux := http.NewServeMux() - mux.Handle(pingv1connect.NewPingServiceHandler( - test.svc, - connect.WithInterceptors(validator), + mux.Handle(calculatorv1connect.CalculatorServiceCumSumProcedure, connect.NewBidiStreamHandler( + calculatorv1connect.CalculatorServiceCumSumProcedure, + tt.svc, )) + srv := httptest.NewUnstartedServer(mux) + srv.EnableHTTP2 = true + srv.StartTLS() + t.Cleanup(srv.Close) - exampleBookingServer := testserver.NewInMemoryServer(mux) - defer exampleBookingServer.Close() - - client := pingv1connect.NewPingServiceClient( - exampleBookingServer.Client(), - exampleBookingServer.URL(), + client := calculatorv1connect.NewCalculatorServiceClient( + srv.Client(), + srv.URL, + connect.WithInterceptors(validator), ) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + t.Cleanup(cancel) + stream := client.CumSum(ctx) + t.Cleanup(func() { + assert.NoError(t, stream.CloseResponse()) + }) + t.Cleanup(func() { + assert.NoError(t, stream.CloseRequest()) + }) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - stream, err := client.CountUp(ctx, connect.NewRequest(test.req)) - require.NoError(t, err) - receive := stream.Receive() - got := stream.Msg() - err = stream.Err() - if test.wantErr != nil { + err = stream.Send(&tt.req) + if tt.wantCode > 0 { require.Error(t, err) var connectErr *connect.Error assert.True(t, errors.As(err, &connectErr)) - assert.Equal(t, test.wantErr.msg, connectErr.Message()) - assert.Equal(t, test.wantErr.code, connectErr.Code()) - if test.wantErr.detail != nil { - require.Len(t, connectErr.Details(), 1) - detail, err := connect.NewErrorDetail(test.wantErr.detail.ToProto()) + t.Log(connectErr) + assert.Equal(t, tt.wantCode, connectErr.Code()) + if tt.wantPath != "" { + details := connectErr.Details() + require.Len(t, details, 1) + detail, err := details[0].Value() require.NoError(t, err) - assert.Equal(t, connectErr.Details()[0].Type(), detail.Type()) + violations, ok := detail.(*validatepb.Violations) + require.True(t, ok) + require.Len(t, violations.Violations, 1) + require.Equal(t, tt.wantPath, violations.Violations[0].FieldPath) } } else { require.NoError(t, err) - assert.True(t, receive) - assert.NotNil(t, got) - assert.Equal(t, test.want.GetNumber(), got.GetNumber()) + got, receiveErr := stream.Receive() + if tt.wantReceiveCode > 0 { + require.Equal(t, connect.CodeOf(receiveErr), tt.wantReceiveCode) + } else { + require.NoError(t, receiveErr) + require.NotZero(t, got.Sum) + } } }) } } + +func TestWithValidator(t *testing.T) { + t.Parallel() + validator, err := protovalidate.New(protovalidate.WithDisableLazy(true)) + require.NoError(t, err) + interceptor, err := validate.NewInterceptor(validate.WithValidator(validator)) + require.NoError(t, err) + + mux := http.NewServeMux() + mux.Handle(userv1connect.UserServiceCreateUserProcedure, connect.NewUnaryHandler( + userv1connect.UserServiceCreateUserProcedure, + createUser, + connect.WithInterceptors(interceptor), + )) + srv := startHTTPServer(t, mux) + + req := connect.NewRequest(&userv1.CreateUserRequest{ + User: &userv1.User{Email: "someone@example.com"}, + }) + _, err = userv1connect.NewUserServiceClient(srv.Client(), srv.URL). + CreateUser(context.Background(), req) + require.Error(t, err) + require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) +} + +func startHTTPServer(tb testing.TB, h http.Handler) *httptest.Server { + tb.Helper() + srv := httptest.NewUnstartedServer(h) + srv.EnableHTTP2 = true + srv.Start() + tb.Cleanup(srv.Close) + return srv +} + +func createUser(_ context.Context, req *connect.Request[userv1.CreateUserRequest]) (*connect.Response[userv1.CreateUserResponse], error) { + return connect.NewResponse(&userv1.CreateUserResponse{User: req.Msg.User}), nil +} + +func cumSumSuccess(_ context.Context, stream *connect.BidiStream[calculatorv1.CumSumRequest, calculatorv1.CumSumResponse]) error { + var sum int64 + for { + req, err := stream.Receive() + if errors.Is(err, io.EOF) { + return nil + } else if err != nil { + return err + } + sum += req.Number + if err := stream.Send(&calculatorv1.CumSumResponse{Sum: sum}); err != nil { + return err + } + } + return nil +} + +func cumSumError(_ context.Context, stream *connect.BidiStream[calculatorv1.CumSumRequest, calculatorv1.CumSumResponse]) error { + return connect.NewError(connect.CodeInternal, errors.New("boom")) +} From 8567e994a4952dc6629f9bc00b1bc2acb5391b85 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Tue, 5 Sep 2023 22:37:30 -0700 Subject: [PATCH 20/20] Fix lint --- .golangci.yml | 15 +--- .../example/calculator/v1/calculator.proto | 1 - internal/proto/example/user/v1/user.proto | 1 - validate_test.go | 85 +++++++++---------- 4 files changed, 46 insertions(+), 56 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 29dea8b..bbd8713 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -3,6 +3,10 @@ run: linters-settings: errcheck: check-type-assertions: true + exhaustruct: + include: + # No zero values for param structs. + - 'connectrpc\.com/connect\..*[pP]arams' forbidigo: forbid: - '^fmt\.Print' @@ -51,14 +55,3 @@ issues: # Don't ban use of fmt.Errorf to create new errors, but the remaining # checks from err113 are useful. - "err113: do not define dynamic errors.*" - exclude-rules: - - path: interceptor_test.go - linters: - - exhaustruct - - dupl - - path: interceptor.go - linters: - - exhaustruct - - path: internal/testserver - linters: - - exhaustruct diff --git a/internal/proto/example/calculator/v1/calculator.proto b/internal/proto/example/calculator/v1/calculator.proto index 5dadedb..58392df 100644 --- a/internal/proto/example/calculator/v1/calculator.proto +++ b/internal/proto/example/calculator/v1/calculator.proto @@ -29,4 +29,3 @@ message CumSumResponse { service CalculatorService { rpc CumSum(stream CumSumRequest) returns (stream CumSumResponse) {} } - diff --git a/internal/proto/example/user/v1/user.proto b/internal/proto/example/user/v1/user.proto index 2e29ddb..c25c03d 100644 --- a/internal/proto/example/user/v1/user.proto +++ b/internal/proto/example/user/v1/user.proto @@ -42,4 +42,3 @@ message CreateUserResponse { service UserService { rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {} } - diff --git a/validate_test.go b/validate_test.go index 9233007..a13ce59 100644 --- a/validate_test.go +++ b/validate_test.go @@ -40,20 +40,20 @@ func TestInterceptorUnary(t *testing.T) { tests := []struct { name string svc func(context.Context, *connect.Request[userv1.CreateUserRequest]) (*connect.Response[userv1.CreateUserResponse], error) - req userv1.CreateUserRequest + req *userv1.CreateUserRequest wantCode connect.Code wantPath string // field path, from error details }{ { name: "valid", svc: createUser, - req: userv1.CreateUserRequest{ + req: &userv1.CreateUserRequest{ User: &userv1.User{Email: "someone@example.com"}, }, }, { name: "invalid", - req: userv1.CreateUserRequest{ + req: &userv1.CreateUserRequest{ User: &userv1.User{Email: "foo"}, }, wantCode: connect.CodeInvalidArgument, @@ -64,15 +64,15 @@ func TestInterceptorUnary(t *testing.T) { svc: func(_ context.Context, req *connect.Request[userv1.CreateUserRequest]) (*connect.Response[userv1.CreateUserResponse], error) { return nil, connect.NewError(connect.CodeInternal, errors.New("oh no")) }, - req: userv1.CreateUserRequest{ + req: &userv1.CreateUserRequest{ User: &userv1.User{Email: "someone@example.com"}, }, wantCode: connect.CodeInternal, }, } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { t.Parallel() validator, err := validate.NewInterceptor() @@ -81,20 +81,20 @@ func TestInterceptorUnary(t *testing.T) { mux := http.NewServeMux() mux.Handle(userv1connect.UserServiceCreateUserProcedure, connect.NewUnaryHandler( userv1connect.UserServiceCreateUserProcedure, - tt.svc, + test.svc, connect.WithInterceptors(validator), )) srv := startHTTPServer(t, mux) got, err := userv1connect.NewUserServiceClient(srv.Client(), srv.URL). - CreateUser(context.Background(), connect.NewRequest(&tt.req)) + CreateUser(context.Background(), connect.NewRequest(test.req)) - if tt.wantCode > 0 { + if test.wantCode > 0 { require.Error(t, err) var connectErr *connect.Error require.True(t, errors.As(err, &connectErr)) - assert.Equal(t, tt.wantCode, connectErr.Code()) - if tt.wantPath != "" { + assert.Equal(t, test.wantCode, connectErr.Code()) + if test.wantPath != "" { details := connectErr.Details() require.Len(t, details, 1) detail, err := details[0].Value() @@ -102,7 +102,7 @@ func TestInterceptorUnary(t *testing.T) { violations, ok := detail.(*validatepb.Violations) require.True(t, ok) require.Len(t, violations.Violations, 1) - require.Equal(t, tt.wantPath, violations.Violations[0].FieldPath) + require.Equal(t, test.wantPath, violations.Violations[0].FieldPath) } } else { require.NoError(t, err) @@ -117,32 +117,32 @@ func TestInterceptorStreamingHandler(t *testing.T) { tests := []struct { name string svc func(context.Context, *connect.BidiStream[calculatorv1.CumSumRequest, calculatorv1.CumSumResponse]) error - req calculatorv1.CumSumRequest + req *calculatorv1.CumSumRequest wantCode connect.Code wantPath string // field path, from error details }{ { name: "invalid", svc: cumSumSuccess, - req: calculatorv1.CumSumRequest{Number: 0}, + req: &calculatorv1.CumSumRequest{Number: 0}, wantCode: connect.CodeInvalidArgument, wantPath: "number", }, { name: "valid", svc: cumSumSuccess, - req: calculatorv1.CumSumRequest{Number: 1}, + req: &calculatorv1.CumSumRequest{Number: 1}, }, { name: "underlying_error", svc: cumSumError, - req: calculatorv1.CumSumRequest{Number: 1}, + req: &calculatorv1.CumSumRequest{Number: 1}, wantCode: connect.CodeInternal, }, } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { t.Parallel() validator, err := validate.NewInterceptor() @@ -151,7 +151,7 @@ func TestInterceptorStreamingHandler(t *testing.T) { mux := http.NewServeMux() mux.Handle(calculatorv1connect.CalculatorServiceCumSumProcedure, connect.NewBidiStreamHandler( calculatorv1connect.CalculatorServiceCumSumProcedure, - tt.svc, + test.svc, connect.WithInterceptors(validator), )) srv := httptest.NewUnstartedServer(mux) @@ -170,17 +170,17 @@ func TestInterceptorStreamingHandler(t *testing.T) { assert.NoError(t, stream.CloseRequest()) }) - err = stream.Send(&tt.req) + err = stream.Send(test.req) require.NoError(t, err) time.Sleep(time.Second) got, err := stream.Receive() - if tt.wantCode > 0 { + if test.wantCode > 0 { require.Error(t, err) var connectErr *connect.Error assert.True(t, errors.As(err, &connectErr)) - assert.Equal(t, tt.wantCode, connectErr.Code()) - if tt.wantPath != "" { + assert.Equal(t, test.wantCode, connectErr.Code()) + if test.wantPath != "" { details := connectErr.Details() require.Len(t, details, 1) detail, err := details[0].Value() @@ -188,7 +188,7 @@ func TestInterceptorStreamingHandler(t *testing.T) { violations, ok := detail.(*validatepb.Violations) require.True(t, ok) require.Len(t, violations.Violations, 1) - require.Equal(t, tt.wantPath, violations.Violations[0].FieldPath) + require.Equal(t, test.wantPath, violations.Violations[0].FieldPath) } } else { require.NoError(t, err) @@ -203,7 +203,7 @@ func TestInterceptorStreamingClient(t *testing.T) { tests := []struct { name string svc func(context.Context, *connect.BidiStream[calculatorv1.CumSumRequest, calculatorv1.CumSumResponse]) error - req calculatorv1.CumSumRequest + req *calculatorv1.CumSumRequest wantCode connect.Code wantPath string // field path, from error details wantReceiveCode connect.Code // code for error calling Receive() @@ -211,25 +211,25 @@ func TestInterceptorStreamingClient(t *testing.T) { { name: "invalid", svc: cumSumSuccess, - req: calculatorv1.CumSumRequest{Number: 0}, + req: &calculatorv1.CumSumRequest{Number: 0}, wantCode: connect.CodeInvalidArgument, wantPath: "number", }, { name: "valid", svc: cumSumSuccess, - req: calculatorv1.CumSumRequest{Number: 1}, + req: &calculatorv1.CumSumRequest{Number: 1}, }, { name: "underlying_error", svc: cumSumError, - req: calculatorv1.CumSumRequest{Number: 1}, + req: &calculatorv1.CumSumRequest{Number: 1}, wantReceiveCode: connect.CodeInternal, }, } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { t.Parallel() validator, err := validate.NewInterceptor() @@ -238,7 +238,7 @@ func TestInterceptorStreamingClient(t *testing.T) { mux := http.NewServeMux() mux.Handle(calculatorv1connect.CalculatorServiceCumSumProcedure, connect.NewBidiStreamHandler( calculatorv1connect.CalculatorServiceCumSumProcedure, - tt.svc, + test.svc, )) srv := httptest.NewUnstartedServer(mux) srv.EnableHTTP2 = true @@ -260,14 +260,14 @@ func TestInterceptorStreamingClient(t *testing.T) { assert.NoError(t, stream.CloseRequest()) }) - err = stream.Send(&tt.req) - if tt.wantCode > 0 { + err = stream.Send(test.req) + if test.wantCode > 0 { require.Error(t, err) var connectErr *connect.Error assert.True(t, errors.As(err, &connectErr)) t.Log(connectErr) - assert.Equal(t, tt.wantCode, connectErr.Code()) - if tt.wantPath != "" { + assert.Equal(t, test.wantCode, connectErr.Code()) + if test.wantPath != "" { details := connectErr.Details() require.Len(t, details, 1) detail, err := details[0].Value() @@ -275,13 +275,13 @@ func TestInterceptorStreamingClient(t *testing.T) { violations, ok := detail.(*validatepb.Violations) require.True(t, ok) require.Len(t, violations.Violations, 1) - require.Equal(t, tt.wantPath, violations.Violations[0].FieldPath) + require.Equal(t, test.wantPath, violations.Violations[0].FieldPath) } } else { require.NoError(t, err) got, receiveErr := stream.Receive() - if tt.wantReceiveCode > 0 { - require.Equal(t, connect.CodeOf(receiveErr), tt.wantReceiveCode) + if test.wantReceiveCode > 0 { + require.Equal(t, connect.CodeOf(receiveErr), test.wantReceiveCode) } else { require.NoError(t, receiveErr) require.NotZero(t, got.Sum) @@ -342,9 +342,8 @@ func cumSumSuccess(_ context.Context, stream *connect.BidiStream[calculatorv1.Cu return err } } - return nil } -func cumSumError(_ context.Context, stream *connect.BidiStream[calculatorv1.CumSumRequest, calculatorv1.CumSumResponse]) error { +func cumSumError(_ context.Context, _ *connect.BidiStream[calculatorv1.CumSumRequest, calculatorv1.CumSumResponse]) error { return connect.NewError(connect.CodeInternal, errors.New("boom")) }