├── .github ├── dependabot.yml ├── workflows │ └── ci.yaml └── CODE_OF_CONDUCT.md ├── buf.yaml ├── .gitignore ├── buf.lock ├── buf.gen.yaml ├── go.mod ├── main_windows.go ├── main_unix.go ├── pluginrpc.go ├── env.go ├── procedure_test.go ├── proto.go ├── codec.go ├── internal └── example │ ├── cmd │ ├── echo-list-client │ │ └── main.go │ ├── echo-request-client │ │ └── main.go │ ├── echo-error-client │ │ └── main.go │ └── echo-plugin │ │ └── main.go │ ├── proto │ └── pluginrpc │ │ └── example │ │ └── v1 │ │ └── example.proto │ └── gen │ └── pluginrpc │ └── example │ └── v1 │ ├── examplev1pluginrpc │ └── example.pluginrpc.go │ └── example.pb.go ├── format.go ├── .golangci.yml ├── spec_test.go ├── go.sum ├── RELEASE.md ├── server_registrar.go ├── exit_error.go ├── main.go ├── Makefile ├── wire.go ├── spec.go ├── server.go ├── flags.go ├── runner.go ├── procedure.go ├── handler.go ├── error.go ├── code.go ├── README.md ├── pluginrpc_test.go ├── client.go ├── LICENSE └── cmd └── protoc-gen-pluginrpc-go └── main.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /buf.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | modules: 3 | - path: internal/example/proto 4 | deps: 5 | - buf.build/pluginrpc/pluginrpc 6 | - buf.build/bufbuild/protovalidate 7 | lint: 8 | use: 9 | - DEFAULT 10 | - UNARY_RPC 11 | - COMMENTS 12 | breaking: 13 | use: 14 | - WIRE_JSON 15 | ignore_unstable_packages: true 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.tmp/ 2 | *.pprof 3 | *.svg 4 | cover.out 5 | 6 | /cmd/protoc-gen-pluginrpc-go/protoc-gen-pluginrpc-go 7 | /internal/example/cmd/echo-error-client/echo-error-client 8 | /internal/example/cmd/echo-list-client/echo-list-client 9 | /internal/example/cmd/echo-plugin/echo-plugin 10 | /internal/example/cmd/echo-request-client/echo-request-client 11 | -------------------------------------------------------------------------------- /buf.lock: -------------------------------------------------------------------------------- 1 | # Generated by buf. DO NOT EDIT. 2 | version: v2 3 | deps: 4 | - name: buf.build/bufbuild/protovalidate 5 | commit: a6c49f84cc0f4e038680d390392e2ab0 6 | digest: b5:e968392e88ff7915adcbd1635d670b45bff8836ec2415d81fc559ca5470a695dbdc30030bad8bc5764647c731079e9e7bba0023ea25c4e4a1672a7d2561d4a19 7 | - name: buf.build/pluginrpc/pluginrpc 8 | commit: fe6866f2f15c4402b9e7c0644abf54af 9 | digest: b5:2b5d01b72819d116755f27f08048ff97a2aab106cd4b12c6b60d31000f8a94830de4eac662c4b58123f1eebfc4005c6fa70cca82a29f425e8d6793e4e6bc81fd 10 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | inputs: 3 | - directory: internal/example/proto 4 | managed: 5 | enabled: true 6 | override: 7 | - file_option: go_package_prefix 8 | value: pluginrpc.com/pluginrpc/internal/example/gen 9 | disable: 10 | - file_option: go_package_prefix 11 | module: buf.build/pluginrpc/pluginrpc 12 | - file_option: go_package_prefix 13 | module: buf.build/bufbuild/protovalidate 14 | plugins: 15 | - local: protoc-gen-go 16 | out: internal/example/gen 17 | opt: paths=source_relative 18 | - local: protoc-gen-pluginrpc-go 19 | out: internal/example/gen 20 | opt: paths=source_relative 21 | clean: true 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module pluginrpc.com/pluginrpc 2 | 3 | go 1.21 4 | 5 | toolchain go1.23.0 6 | 7 | require ( 8 | buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.34.2-20240828222655-5345c0a56177.2 9 | github.com/mattn/go-isatty v0.0.20 10 | github.com/spf13/pflag v1.0.5 11 | github.com/stretchr/testify v1.9.0 12 | google.golang.org/protobuf v1.34.2 13 | ) 14 | 15 | require ( 16 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240717164558-a6c49f84cc0f.2 // indirect 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | golang.org/x/sys v0.25.0 // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /main_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build windows 16 | 17 | package pluginrpc 18 | 19 | import "os" 20 | 21 | // extraInterruptSignals are signals beyond os.Interrupt that we want to be handled 22 | // as interrupts. 23 | // 24 | // For unix-like platforms, this adds syscall.SIGTERM. 25 | var extraInterruptSignals = []os.Signal{} 26 | -------------------------------------------------------------------------------- /main_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build unix || js || wasip1 16 | 17 | package pluginrpc 18 | 19 | import ( 20 | "os" 21 | "syscall" 22 | ) 23 | 24 | // extraInterruptSignals are signals beyond os.Interrupt that we want to be handled 25 | // as interrupts. 26 | // 27 | // For unix-like platforms, this adds syscall.SIGTERM. 28 | var extraInterruptSignals = []os.Signal{ 29 | syscall.SIGTERM, 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: [main] 5 | tags: ['v*'] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: '15 22 * * *' 10 | workflow_dispatch: {} # support manual runs 11 | permissions: 12 | contents: read 13 | jobs: 14 | ci: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | go-version: [1.22.x, 1.23.x] 19 | steps: 20 | - name: Checkout Code 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 1 24 | - name: Install Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | - name: Test 29 | run: make test 30 | - name: Lint 31 | # Often, lint & gofmt guidelines depend on the Go version. To prevent 32 | # conflicting guidance, run only on the most recent supported version. 33 | # For the same reason, only check generated code on the most recent 34 | # supported version. 35 | if: matrix.go-version == '1.23.x' 36 | run: make checkgenerate && make lint 37 | -------------------------------------------------------------------------------- /pluginrpc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pluginrpc implements an RPC framework for plugins. 16 | package pluginrpc // import "pluginrpc.com/pluginrpc" 17 | 18 | const ( 19 | // Version is the semantic version of the pluginrpc module. 20 | Version = "0.6.0-dev" 21 | 22 | // IsAtLeastVersion0_1_0 is used in compile-time handshake's with pluginrpc's generated code. 23 | IsAtLeastVersion0_1_0 = true 24 | // IsAtLeastVersion0_4_0 is used in compile-time handshake's with pluginrpc's generated code. 25 | IsAtLeastVersion0_4_0 = true 26 | ) 27 | -------------------------------------------------------------------------------- /env.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "io" 19 | "os" 20 | ) 21 | 22 | // OSEnv is an Env using os.Args, os.Stdin, os.Stdout, and os.Stderr. 23 | var OSEnv = Env{ 24 | Args: os.Args[1:], 25 | Stdin: os.Stdin, 26 | Stdout: os.Stdout, 27 | Stderr: os.Stderr, 28 | } 29 | 30 | // Env specifies an environment used to invoke a plugin. 31 | // 32 | // This abstracts away args, stdin, stdout, and stderr. Envs are used 33 | // by Runners and Servers. 34 | type Env struct { 35 | Args []string 36 | Stdin io.Reader 37 | Stdout io.Writer 38 | Stderr io.Writer 39 | } 40 | -------------------------------------------------------------------------------- /procedure_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestProcedureBasic(t *testing.T) { 24 | t.Parallel() 25 | 26 | procedure, err := NewProcedure("/foo/bar") 27 | require.NoError(t, err) 28 | require.Equal(t, "/foo/bar", procedure.Path()) 29 | require.Empty(t, procedure.Args()) 30 | 31 | procedure, err = NewProcedure("/foo/bar", ProcedureWithArgs("foo", "bar")) 32 | require.NoError(t, err) 33 | require.Equal(t, "/foo/bar", procedure.Path()) 34 | require.Equal(t, []string{"foo", "bar"}, procedure.Args()) 35 | 36 | _, err = NewProcedure("foo/bar") 37 | require.Error(t, err) 38 | _, err = NewProcedure("\\foo\\bar") 39 | require.Error(t, err) 40 | _, err = NewProcedure("/foo/bar", ProcedureWithArgs("f")) 41 | require.Error(t, err) 42 | } 43 | -------------------------------------------------------------------------------- /proto.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "fmt" 19 | 20 | "google.golang.org/protobuf/proto" 21 | ) 22 | 23 | // toProtoMessage casts the value into a proto.Message, returning an error 24 | // if value is not a proto.Message. 25 | // 26 | // We use anys in our code instead of proto.Message for forwards-compatibility; right 27 | // now, we expect jsonpb-encoded values over the wire, but we could easily extend pluginrpc 28 | // to allow for different codecs, and we could add a Codec interface to this library. Since 29 | // everything needs to be a proto.Message right now, this isn't a problem. 30 | func toProtoMessage(value any) (proto.Message, error) { 31 | if value == nil { 32 | return nil, nil 33 | } 34 | message, ok := value.(proto.Message) 35 | if !ok { 36 | return nil, fmt.Errorf("expected proto.Message, got %T", value) 37 | } 38 | return message, nil 39 | } 40 | -------------------------------------------------------------------------------- /codec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "fmt" 19 | 20 | "google.golang.org/protobuf/encoding/protojson" 21 | "google.golang.org/protobuf/proto" 22 | ) 23 | 24 | var ( 25 | binaryCodec = &codec{ 26 | Marshal: proto.Marshal, 27 | Unmarshal: proto.Unmarshal, 28 | } 29 | jsonCodec = &codec{ 30 | Marshal: protojson.MarshalOptions{UseProtoNames: true}.Marshal, 31 | Unmarshal: protojson.Unmarshal, 32 | } 33 | 34 | formatToCodec = map[Format]*codec{ 35 | FormatBinary: binaryCodec, 36 | FormatJSON: jsonCodec, 37 | } 38 | ) 39 | 40 | type codec struct { 41 | Marshal func(message proto.Message) ([]byte, error) 42 | Unmarshal func(data []byte, message proto.Message) error 43 | } 44 | 45 | func codecForFormat(format Format) (*codec, error) { 46 | codec, ok := formatToCodec[format] 47 | if !ok { 48 | return nil, fmt.Errorf("unknown Format: %v", format) 49 | } 50 | return codec, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/example/cmd/echo-list-client/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package main implements a client that calls the EchoList RPC on the 16 | // echo-plugin plugin. 17 | // 18 | // This will echo the list produded by EchoList. 19 | package main 20 | 21 | import ( 22 | "context" 23 | "os" 24 | "strings" 25 | 26 | "pluginrpc.com/pluginrpc" 27 | "pluginrpc.com/pluginrpc/internal/example/gen/pluginrpc/example/v1/examplev1pluginrpc" 28 | ) 29 | 30 | func main() { 31 | if err := run(); err != nil { 32 | if errString := err.Error(); errString != "" { 33 | _, _ = os.Stderr.Write([]byte(errString + "\n")) 34 | } 35 | os.Exit(pluginrpc.WrapExitError(err).ExitCode()) 36 | } 37 | } 38 | 39 | func run() error { 40 | client := pluginrpc.NewClient(pluginrpc.NewExecRunner("echo-plugin")) 41 | echoServiceClient, err := examplev1pluginrpc.NewEchoServiceClient(client) 42 | if err != nil { 43 | return err 44 | } 45 | response, err := echoServiceClient.EchoList(context.Background(), nil) 46 | if err != nil { 47 | return err 48 | } 49 | _, err = os.Stdout.Write([]byte(strings.Join(response.GetList(), "\n") + "\n")) 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /internal/example/cmd/echo-request-client/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package main implements a client that calls the EchoRequest RPC on the 16 | // echo-plugin plugin. 17 | // 18 | // This will echo back any args given to this client. 19 | package main 20 | 21 | import ( 22 | "context" 23 | "os" 24 | "strings" 25 | 26 | "pluginrpc.com/pluginrpc" 27 | examplev1 "pluginrpc.com/pluginrpc/internal/example/gen/pluginrpc/example/v1" 28 | "pluginrpc.com/pluginrpc/internal/example/gen/pluginrpc/example/v1/examplev1pluginrpc" 29 | ) 30 | 31 | func main() { 32 | if err := run(); err != nil { 33 | if errString := err.Error(); errString != "" { 34 | _, _ = os.Stderr.Write([]byte(errString + "\n")) 35 | } 36 | os.Exit(pluginrpc.WrapExitError(err).ExitCode()) 37 | } 38 | } 39 | 40 | func run() error { 41 | client := pluginrpc.NewClient(pluginrpc.NewExecRunner("echo-plugin")) 42 | echoServiceClient, err := examplev1pluginrpc.NewEchoServiceClient(client) 43 | if err != nil { 44 | return err 45 | } 46 | response, err := echoServiceClient.EchoRequest( 47 | context.Background(), 48 | &examplev1.EchoRequestRequest{ 49 | Message: strings.Join(os.Args[1:], " "), 50 | }, 51 | ) 52 | if err != nil { 53 | return err 54 | } 55 | _, err = os.Stdout.Write([]byte(response.GetMessage() + "\n")) 56 | return err 57 | } 58 | -------------------------------------------------------------------------------- /internal/example/cmd/echo-error-client/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package main implements a client that calls the EchoError RPC on the 16 | // echo-plugin plugin. 17 | // 18 | // This will parse the first arg as an error Code, and all further args will 19 | // comprise the error message. 20 | package main 21 | 22 | import ( 23 | "context" 24 | "os" 25 | "strconv" 26 | "strings" 27 | 28 | pluginrpcv1 "buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go/pluginrpc/v1" 29 | "pluginrpc.com/pluginrpc" 30 | examplev1 "pluginrpc.com/pluginrpc/internal/example/gen/pluginrpc/example/v1" 31 | "pluginrpc.com/pluginrpc/internal/example/gen/pluginrpc/example/v1/examplev1pluginrpc" 32 | ) 33 | 34 | func main() { 35 | if err := run(); err != nil { 36 | if errString := err.Error(); errString != "" { 37 | _, _ = os.Stderr.Write([]byte(errString + "\n")) 38 | } 39 | os.Exit(pluginrpc.WrapExitError(err).ExitCode()) 40 | } 41 | } 42 | 43 | func run() error { 44 | client := pluginrpc.NewClient(pluginrpc.NewExecRunner("echo-plugin")) 45 | echoServiceClient, err := examplev1pluginrpc.NewEchoServiceClient(client) 46 | if err != nil { 47 | return err 48 | } 49 | code, err := strconv.ParseInt(os.Args[1], 10, 32) 50 | if err != nil { 51 | return err 52 | } 53 | _, err = echoServiceClient.EchoError( 54 | context.Background(), 55 | &examplev1.EchoErrorRequest{ 56 | Code: pluginrpcv1.Code(code), 57 | Message: strings.Join(os.Args[2:], " "), 58 | }, 59 | ) 60 | return err 61 | } 62 | -------------------------------------------------------------------------------- /internal/example/proto/pluginrpc/example/v1/example.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package pluginrpc.example.v1; 18 | 19 | import "pluginrpc/v1/pluginrpc.proto"; 20 | 21 | // The service that defines echo operations. 22 | service EchoService { 23 | // Echo the request back. 24 | rpc EchoRequest(EchoRequestRequest) returns (EchoRequestResponse); 25 | // Echo the error specified back as an error. 26 | rpc EchoError(EchoErrorRequest) returns (EchoErrorResponse); 27 | // Echo a static list ["foo", "bar"] back given an empty request. 28 | rpc EchoList(EchoListRequest) returns (EchoListResponse); 29 | } 30 | 31 | // A request to echo the given message. 32 | message EchoRequestRequest { 33 | // The message to echo back. 34 | string message = 1; 35 | } 36 | 37 | // A response to echo. 38 | message EchoRequestResponse { 39 | // The echoed message. 40 | string message = 1; 41 | } 42 | 43 | // An error to echo back. 44 | message EchoErrorRequest { 45 | // The error code to return as part of the error. 46 | pluginrpc.v1.Code code = 1; 47 | // The error message to return as part of the error. 48 | string message = 2; 49 | } 50 | 51 | // A blank response. 52 | message EchoErrorResponse {} 53 | 54 | // A request to echo a static list back The request is purposefully 55 | // empty to demonstrate how pluginrpc works with empty requests. 56 | message EchoListRequest {} 57 | 58 | // A response that will always contain the list ["foo", "bar"]. 59 | message EchoListResponse { 60 | // The list that will always be ["foo", "bar"]. 61 | repeated string list = 1; 62 | } 63 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | // Format is the serialization mechanism of the body of Requests, Responses and Specs. 23 | type Format uint32 24 | 25 | const ( 26 | // FormatBinary is the binary format. 27 | FormatBinary Format = 1 28 | // FormatJSON is the JSON format. 29 | FormatJSON Format = 2 30 | 31 | minFormat = FormatBinary 32 | maxFormat = FormatJSON 33 | 34 | formatBinaryString = "binary" 35 | formatJSONString = "json" 36 | ) 37 | 38 | var ( 39 | // AllFormats are all Formsts. 40 | AllFormats = []Format{ 41 | FormatJSON, 42 | FormatBinary, 43 | } 44 | ) 45 | 46 | // String implements fmt.Stringer. 47 | func (f Format) String() string { 48 | switch f { 49 | case FormatBinary: 50 | return formatBinaryString 51 | case FormatJSON: 52 | return formatJSONString 53 | } 54 | return fmt.Sprintf("format_%d", f) 55 | } 56 | 57 | // FormatForString returns the Format for the given string. 58 | // 59 | // Returns 0 if the Format is unknown or s is empty. 60 | func FormatForString(s string) Format { 61 | switch strings.ToLower(strings.TrimSpace(s)) { 62 | case formatBinaryString: 63 | return FormatBinary 64 | case formatJSONString: 65 | return FormatJSON 66 | default: 67 | return 0 68 | } 69 | } 70 | 71 | // *** PRIVATE *** 72 | 73 | func validateFormat(format Format) error { 74 | if !isValidFormat(format) { 75 | return fmt.Errorf("unknown Format: %v", format) 76 | } 77 | return nil 78 | } 79 | 80 | func isValidFormat(format Format) bool { 81 | return format >= minFormat && format <= maxFormat 82 | } 83 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | errcheck: 3 | check-type-assertions: true 4 | forbidigo: 5 | forbid: 6 | - '^fmt\.Print' 7 | - '^log\.' 8 | - '^print$' 9 | - '^println$' 10 | - '^panic$' 11 | godox: 12 | # TODO, OPT, etc. comments are fine to commit. Use FIXME comments for 13 | # temporary hacks, and use godox to prevent committing them. 14 | keywords: [FIXME] 15 | varnamelen: 16 | ignore-decls: 17 | - T any 18 | - i int 19 | - wg sync.WaitGroup 20 | - sb strings.Builder 21 | linters: 22 | enable-all: true 23 | disable: 24 | - cyclop # covered by gocyclo 25 | - depguard # unnecessary for small libraries 26 | - err113 # dubious dynamic errors warnings 27 | - exhaustruct # many exceptions 28 | - funlen # rely on code review to limit function length 29 | - gochecknoglobals # many exceptions 30 | - gocognit # dubious "cognitive overhead" quantification 31 | - gofumpt # prefer standard gofmt 32 | - goimports # rely on gci instead 33 | - gomnd # some unnamed constants are okay 34 | - ireturn # "accept interfaces, return structs" isn't ironclad 35 | - lll # don't want hard limits for line length 36 | - maintidx # covered by gocyclo 37 | - nilnil # we allow this 38 | - nlreturn # generous whitespace violates house style 39 | - tagalign # false positives 40 | - testpackage # internal tests are fine 41 | - thelper # we want to print out the whole stack 42 | - wrapcheck # don't _always_ need to wrap errors 43 | - wsl # generous whitespace violates house style 44 | issues: 45 | exclude-dirs-use-default: false 46 | exclude-rules: 47 | - linters: 48 | - varnamelen 49 | - goconst 50 | path: cmd/protoc-gen-pluginrpc-go/main.go 51 | - linters: 52 | - varnamelen 53 | path: pluginrpc_test.go 54 | - linters: 55 | - gosec 56 | path: runner.go 57 | - linters: 58 | - gocritic 59 | path: server.go 60 | - linters: 61 | - nestif 62 | path: wire.go 63 | -------------------------------------------------------------------------------- /spec_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestMergeSpecsSuccess(t *testing.T) { 24 | t.Parallel() 25 | 26 | procedure1, err := NewProcedure("/foo/bar") 27 | require.NoError(t, err) 28 | procedure2, err := NewProcedure("/foo/baz") 29 | require.NoError(t, err) 30 | spec1, err := NewSpec(procedure1) 31 | require.NoError(t, err) 32 | spec2, err := NewSpec(procedure2) 33 | require.NoError(t, err) 34 | spec, err := MergeSpecs(spec1, spec2) 35 | require.NoError(t, err) 36 | require.Equal( 37 | t, 38 | []Procedure{procedure1, procedure2}, 39 | spec.Procedures(), 40 | ) 41 | } 42 | 43 | func TestMergeSpecsErrorOverlappingPaths(t *testing.T) { 44 | t.Parallel() 45 | 46 | procedure1, err := NewProcedure("/foo/bar") 47 | require.NoError(t, err) 48 | procedure2, err := NewProcedure("/foo/bar") 49 | require.NoError(t, err) 50 | spec1, err := NewSpec(procedure1) 51 | require.NoError(t, err) 52 | spec2, err := NewSpec(procedure2) 53 | require.NoError(t, err) 54 | _, err = MergeSpecs(spec1, spec2) 55 | require.Error(t, err) 56 | } 57 | 58 | func TestMergeSpecsErrorOverlappingArgs(t *testing.T) { 59 | t.Parallel() 60 | 61 | procedure1, err := NewProcedure("/foo/bar", ProcedureWithArgs("foo", "bar")) 62 | require.NoError(t, err) 63 | procedure2, err := NewProcedure("/foo/baz", ProcedureWithArgs("foo", "bar")) 64 | require.NoError(t, err) 65 | spec1, err := NewSpec(procedure1) 66 | require.NoError(t, err) 67 | spec2, err := NewSpec(procedure2) 68 | require.NoError(t, err) 69 | _, err = MergeSpecs(spec1, spec2) 70 | require.Error(t, err) 71 | } 72 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240717164558-a6c49f84cc0f.2 h1:SZRVx928rbYZ6hEKUIN+vtGDkl7uotABRWGY4OAg5gM= 2 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240717164558-a6c49f84cc0f.2/go.mod h1:ylS4c28ACSI59oJrOdW4pHS4n0Hw4TgSPHn8rpHl4Yw= 3 | buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.34.2-20240828222655-5345c0a56177.2 h1:oSi+Adw4xvIjXrW8eY8QGR3sBdfWeY5HN/RefnRt52M= 4 | buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.34.2-20240828222655-5345c0a56177.2/go.mod h1:GjH0gjlY/ns16X8d6eaXV2W+6IFwsO5Ly9WVnzyd1E0= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 8 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 9 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 10 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 14 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 15 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 16 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 17 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 18 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 19 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 20 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 21 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 22 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 23 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 28 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | This document outlines how to create a release of pluginrpc-go. 4 | 5 | 1. Clone the repo, ensuring you have the latest main. 6 | 7 | 2. On a new branch, open [pluginrpc.go](pluginrpc.go) and change the `Version` constant to an 8 | appropriate [semantic version](https://semver.org/). To select the correct version, look at the 9 | version number of the [latest release] and the changes that are included in this new release. 10 | 11 | - If there are only bug fixes and no new features, remove the `-dev` suffix, set MINOR number to be 12 | equal to the [latest release], and set the PATCH number to be 1 more than the PATCH number of the 13 | [latest release]. 14 | - If there are features being released, remove the `-dev` suffix, set the MINOR number to be 1 more 15 | than the MINOR number of the [latest release], and set the PATCH number to `0`. In the common 16 | case, the diff here will just be to remove the `-dev` suffix. 17 | 18 | ```patch 19 | -const Version = "1.14.0-dev" 20 | +const Version = "1.14.0" 21 | ``` 22 | 23 | 3. Check for any changes in 24 | [cmd/protoc-gen-pluginrpc-go/main.go](cmd/protoc-gen-pluginrpc-go/main.go) that require a version 25 | restriction. A constant `IsAtLeastVersionX_Y_Z` should be defined in [pluginrpc.go](pluginrpc.go) 26 | if generated code has begun to use a new API. Make sure the generated code references this 27 | constant. If a new constant has been added since the last release, ensure that the name of the 28 | constant matches the version being released. 29 | 30 | 4. Open a PR titled "Prepare for vX.Y.Z" and a description tagging all current maintainers. Once 31 | it's reviewed and CI passes, merge it. 32 | 33 | _Make sure no new commits are merged until the release is complete._ 34 | 35 | 5. Review all commits in the new release and for each PR check an appropriate label is used and edit 36 | the title to be meaningful to end users. 37 | 38 | 6. Using the Github UI, create a new release. 39 | 40 | - Under “Choose a tag”, type in “vX.Y.Z” to create a new tag for the release upon publish. 41 | - Target the main branch. 42 | - Title the Release “vX.Y.Z”. 43 | - Click “set as latest release”. 44 | - Set the last version as the “Previous tag”. 45 | - Edit the release notes. 46 | 47 | 7. Publish the release. 48 | 49 | 8. On a new branch, open [pluginrpc.go](pluginrpc.go) and change the `Version` to increment the 50 | minor tag and append the `-dev` suffix. Use the next minor release - we never anticipate bugs and 51 | patch releases. 52 | 53 | ```patch 54 | -const Version = "1.14.0" 55 | +const Version = "1.15.0-dev" 56 | ``` 57 | 58 | 9. Open a PR titled "Back to development" Once it's reviewed and CI passes, merge it. 59 | 60 | [latest release]: https://github.com/pluginrpc/pluginrpc-go/releases/latest 61 | -------------------------------------------------------------------------------- /server_registrar.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "sync" 22 | ) 23 | 24 | // ServerRegistrar is used to registered paths when constructing a server. 25 | // 26 | // By splitting out registration from the Server interface, we allow the Server to be immutable. 27 | // 28 | // Generally, ServerRegistrars are called by `Register.*Server` functions from generated code. 29 | type ServerRegistrar interface { 30 | // Register registers the given handle function for the given path. 31 | // 32 | // Paths must be unique. 33 | Register(path string, handleFunc func(context.Context, HandleEnv, ...HandleOption) error) 34 | 35 | pathToHandleFunc() (map[string]func(context.Context, HandleEnv, ...HandleOption) error, error) 36 | 37 | isServerRegistrar() 38 | } 39 | 40 | // NewServerRegistrar returns a new ServerRegistrar. 41 | func NewServerRegistrar() ServerRegistrar { 42 | return newServerRegistrar() 43 | } 44 | 45 | // *** PRIVATE *** 46 | 47 | type serverRegistrar struct { 48 | pathToHandleFuncMap map[string]func(context.Context, HandleEnv, ...HandleOption) error 49 | errs []error 50 | read bool 51 | lock sync.Mutex 52 | } 53 | 54 | func newServerRegistrar() *serverRegistrar { 55 | return &serverRegistrar{ 56 | pathToHandleFuncMap: make(map[string]func(context.Context, HandleEnv, ...HandleOption) error), 57 | } 58 | } 59 | 60 | func (s *serverRegistrar) Register(path string, handleFunc func(context.Context, HandleEnv, ...HandleOption) error) { 61 | s.lock.Lock() 62 | defer s.lock.Unlock() 63 | 64 | if s.read { 65 | s.errs = append(s.errs, errors.New("server registrar already used")) 66 | return 67 | } 68 | 69 | if _, ok := s.pathToHandleFuncMap[path]; ok { 70 | s.errs = append(s.errs, fmt.Errorf("path %q already registered", path)) 71 | return 72 | } 73 | s.pathToHandleFuncMap[path] = handleFunc 74 | } 75 | 76 | func (s *serverRegistrar) pathToHandleFunc() (map[string]func(context.Context, HandleEnv, ...HandleOption) error, error) { 77 | s.lock.Lock() 78 | defer s.lock.Unlock() 79 | 80 | s.read = true 81 | 82 | if len(s.errs) > 0 { 83 | return nil, errors.Join(s.errs...) 84 | } 85 | 86 | return s.pathToHandleFuncMap, nil 87 | } 88 | 89 | func (*serverRegistrar) isServerRegistrar() {} 90 | -------------------------------------------------------------------------------- /internal/example/cmd/echo-plugin/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package main implements an example plugin. 16 | // 17 | // A plugin is the "server-side" of pluginrpc. This is a binary that implements 18 | // the pluginrpc protocol. 19 | package main 20 | 21 | import ( 22 | "context" 23 | "errors" 24 | 25 | "pluginrpc.com/pluginrpc" 26 | examplev1 "pluginrpc.com/pluginrpc/internal/example/gen/pluginrpc/example/v1" 27 | "pluginrpc.com/pluginrpc/internal/example/gen/pluginrpc/example/v1/examplev1pluginrpc" 28 | ) 29 | 30 | func main() { 31 | pluginrpc.Main(newServer) 32 | } 33 | 34 | func newServer() (pluginrpc.Server, error) { 35 | spec, err := examplev1pluginrpc.EchoServiceSpecBuilder{ 36 | // Note that EchoList does not have optional args and will default to path being the only arg. 37 | // 38 | // This means that the following commands will invoke their respective procedures: 39 | // 40 | // echo-plugin echo request 41 | // echo-plugin /pluginrpc.example.v1.EchoService/EchoList 42 | // echo-plugin echo error 43 | EchoRequest: []pluginrpc.ProcedureOption{pluginrpc.ProcedureWithArgs("echo", "request")}, 44 | EchoError: []pluginrpc.ProcedureOption{pluginrpc.ProcedureWithArgs("echo", "error")}, 45 | }.Build() 46 | if err != nil { 47 | return nil, err 48 | } 49 | serverRegistrar := pluginrpc.NewServerRegistrar() 50 | echoServiceServer := examplev1pluginrpc.NewEchoServiceServer(pluginrpc.NewHandler(spec), echoServiceHandler{}) 51 | examplev1pluginrpc.RegisterEchoServiceServer(serverRegistrar, echoServiceServer) 52 | return pluginrpc.NewServer( 53 | spec, 54 | serverRegistrar, 55 | pluginrpc.ServerWithDoc("An example plugin that implements the EchoService."), 56 | ) 57 | } 58 | 59 | type echoServiceHandler struct{} 60 | 61 | func (echoServiceHandler) EchoRequest(_ context.Context, request *examplev1.EchoRequestRequest) (*examplev1.EchoRequestResponse, error) { 62 | return &examplev1.EchoRequestResponse{Message: request.GetMessage()}, nil 63 | } 64 | 65 | func (echoServiceHandler) EchoList(context.Context, *examplev1.EchoListRequest) (*examplev1.EchoListResponse, error) { 66 | return &examplev1.EchoListResponse{List: []string{"foo", "bar"}}, nil 67 | } 68 | 69 | func (echoServiceHandler) EchoError(_ context.Context, request *examplev1.EchoErrorRequest) (*examplev1.EchoErrorResponse, error) { 70 | return nil, pluginrpc.NewError(pluginrpc.Code(request.GetCode()), errors.New(request.GetMessage())) 71 | } 72 | -------------------------------------------------------------------------------- /exit_error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "strconv" 21 | "strings" 22 | ) 23 | 24 | const exitCodeInternal = 1 25 | 26 | // ExitError is an process exit error with an exit code. 27 | // 28 | // Runners return ExitErrors to indicate the exit code of the process. 29 | type ExitError struct { 30 | exitCode int 31 | underlying error 32 | } 33 | 34 | // NewExitError returns a new ExitError. 35 | // 36 | // An ExitError will never have an exit code of 0 when returned from this function. 37 | func NewExitError(exitCode int, underlying error) *ExitError { 38 | return validateExitError( 39 | &ExitError{ 40 | exitCode: exitCode, 41 | underlying: underlying, 42 | }, 43 | ) 44 | } 45 | 46 | // WrapExitError wraps the given error as a *ExitError. 47 | // 48 | // If the given error is nil, this returns nil. 49 | // If the given error is already a *ExitError, this is returned. 50 | // 51 | // An ExitError will never have a exit code of 0 when returned from this function. 52 | func WrapExitError(err error) *ExitError { 53 | if err == nil { 54 | return nil 55 | } 56 | exitError := &ExitError{} 57 | if errors.As(err, &exitError) { 58 | return validateExitError(exitError) 59 | } 60 | return NewExitError(exitCodeInternal, err) 61 | } 62 | 63 | // ExitCode returns the exit code. 64 | // 65 | // If e is nil, this returns 0. 66 | func (e *ExitError) ExitCode() int { 67 | if e == nil { 68 | return 0 69 | } 70 | return e.exitCode 71 | } 72 | 73 | // Error implements error. 74 | // 75 | // If e is nil, this returns the empty string. 76 | func (e *ExitError) Error() string { 77 | if e == nil { 78 | return "" 79 | } 80 | var sb strings.Builder 81 | _, _ = sb.WriteString(`Exited with code `) 82 | _, _ = sb.WriteString(strconv.Itoa(e.exitCode)) 83 | if e.underlying != nil { 84 | _, _ = sb.WriteString(`: `) 85 | _, _ = sb.WriteString(e.underlying.Error()) 86 | } 87 | return sb.String() 88 | } 89 | 90 | // Unwrap implements error. 91 | // 92 | // If e is nil, this returns nil. 93 | func (e *ExitError) Unwrap() error { 94 | if e == nil { 95 | return nil 96 | } 97 | return e.underlying 98 | } 99 | 100 | // *** PRIVATE *** 101 | 102 | func validateExitError(exitError *ExitError) *ExitError { 103 | if exitError.ExitCode() == 0 { 104 | return newInvalidCodeExitError(exitError) 105 | } 106 | return exitError 107 | } 108 | 109 | func newInvalidCodeExitError(exitError *ExitError) *ExitError { 110 | return &ExitError{ 111 | exitCode: exitCodeInternal, 112 | underlying: fmt.Errorf("ExitError created with code %d: %w", exitError.ExitCode(), exitError), 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "context" 19 | "os" 20 | "os/signal" 21 | ) 22 | 23 | var interruptSignals = append( 24 | []os.Signal{ 25 | os.Interrupt, 26 | }, 27 | extraInterruptSignals..., 28 | ) 29 | 30 | // Main is a convenience function that will run the server within a main 31 | // function with the proper semantics. 32 | // 33 | // All registration should already be complete before passing the Server to this function. 34 | // 35 | // func main() { 36 | // pluginrpc.Main(newServer) 37 | // } 38 | // 39 | // func newServer() (pluginrpc.Server, error) { 40 | // spec, err := examplev1pluginrpc.EchoServiceSpecBuilder{ 41 | // EchoRequest: []pluginrpc.ProcedureOption{pluginrpc.ProcedureWithArgs("echo", "request")}, 42 | // EchoError: []pluginrpc.ProcedureOption{pluginrpc.ProcedureWithArgs("echo", "error")}, 43 | // }.Build() 44 | // if err != nil { 45 | // return nil, err 46 | // } 47 | // serverRegistrar := pluginrpc.NewServerRegistrar() 48 | // echoServiceServer := examplev1pluginrpc.NewEchoServiceServer(pluginrpc.NewHandler(spec), echoServiceHandler{}) 49 | // examplev1pluginrpc.RegisterEchoServiceServer(serverRegistrar, echoServiceServer) 50 | // return pluginrpc.NewServer(spec, serverRegistrar) 51 | // } 52 | func Main(newServer func() (Server, error), _ ...MainOption) { 53 | ctx, cancel := withCancelInterruptSignal(context.Background()) 54 | defer cancel() 55 | server, err := newServer() 56 | handleServerMainError(err) 57 | handleServerMainError(server.Serve(ctx, OSEnv)) 58 | } 59 | 60 | // MainOption is an option for Main. 61 | type MainOption func(*mainOptions) 62 | 63 | // *** PRIVATE *** 64 | 65 | func handleServerMainError(err error) { 66 | if err != nil { 67 | if errString := err.Error(); errString != "" { 68 | _, _ = os.Stderr.Write([]byte(errString + "\n")) 69 | } 70 | os.Exit(WrapExitError(err).ExitCode()) 71 | } 72 | } 73 | 74 | // withCancelInterruptSignal returns a context that is cancelled if interrupt signals are sent. 75 | func withCancelInterruptSignal(ctx context.Context) (context.Context, context.CancelFunc) { 76 | interruptSignalC, closer := newInterruptSignalChannel() 77 | ctx, cancel := context.WithCancel(ctx) 78 | go func() { 79 | <-interruptSignalC 80 | closer() 81 | cancel() 82 | }() 83 | return ctx, cancel 84 | } 85 | 86 | // newInterruptSignalChannel returns a new channel for interrupt signals. 87 | // 88 | // Call the returned function to cancel sending to this channel. 89 | func newInterruptSignalChannel() (<-chan os.Signal, func()) { 90 | signalC := make(chan os.Signal, 1) 91 | signal.Notify(signalC, interruptSignals...) 92 | return signalC, func() { 93 | signal.Stop(signalC) 94 | close(signalC) 95 | } 96 | } 97 | 98 | type mainOptions struct{} 99 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # See https://tech.davis-hansson.com/p/make/ 2 | SHELL := bash 3 | .DELETE_ON_ERROR: 4 | .SHELLFLAGS := -eu -o pipefail -c 5 | .DEFAULT_GOAL := all 6 | MAKEFLAGS += --warn-undefined-variables 7 | MAKEFLAGS += --no-builtin-rules 8 | MAKEFLAGS += --no-print-directory 9 | BIN := .tmp/bin 10 | export PATH := $(abspath $(BIN)):$(PATH) 11 | export GOBIN := $(abspath $(BIN)) 12 | COPYRIGHT_YEARS := 2024 13 | LICENSE_IGNORE := --ignore /testdata/ 14 | 15 | BUF_VERSION := v1.42.0 16 | GO_MOD_GOTOOLCHAIN := go1.23.1 17 | GOLANGCI_LINT_VERSION := v1.60.1 18 | # https://github.com/golangci/golangci-lint/issues/4837 19 | GOLANGCI_LINT_GOTOOLCHAIN := $(GO_MOD_GOTOOLCHAIN) 20 | 21 | .PHONY: help 22 | help: ## Describe useful make targets 23 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}' 24 | 25 | .PHONY: all 26 | all: ## Build, test, and lint (default) 27 | $(MAKE) test 28 | $(MAKE) lint 29 | 30 | .PHONY: clean 31 | clean: ## Delete intermediate build artifacts 32 | @# -X only removes untracked files, -d recurses into directories, -f actually removes files/dirs 33 | git clean -Xdf 34 | 35 | .PHONY: test 36 | test: build $(BIN)/echo-plugin ## Run unit tests 37 | go test -vet=off -race -cover ./... 38 | 39 | .PHONY: build 40 | build: generate ## Build all packages 41 | go build ./... 42 | 43 | .PHONY: install 44 | install: ## Install all binaries 45 | go install ./... 46 | 47 | .PHONY: lint 48 | lint: $(BIN)/golangci-lint ## Lint 49 | go vet ./... 50 | GOTOOLCHAIN=$(GOLANGCI_LINT_GOTOOLCHAIN) golangci-lint run --modules-download-mode=readonly --timeout=3m0s 51 | 52 | .PHONY: lintfix 53 | lintfix: $(BIN)/golangci-lint $(BIN)/buf ## Automatically fix some lint errors 54 | GOTOOLCHAIN=$(GOLANGCI_LINT_GOTOOLCHAIN) golangci-lint run --fix --modules-download-mode=readonly --timeout=3m0s 55 | 56 | .PHONY: generate 57 | generate: $(BIN)/buf $(BIN)/protoc-gen-go $(BIN)/protoc-gen-pluginrpc-go $(BIN)/license-header ## Regenerate code and licenses 58 | buf generate 59 | license-header \ 60 | --license-type apache \ 61 | --copyright-holder "Buf Technologies, Inc." \ 62 | --year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE) 63 | 64 | .PHONY: upgrade 65 | upgrade: ## Upgrade dependencies 66 | go mod edit -toolchain=$(GO_MOD_GOTOOLCHAIN) 67 | go get -u -t ./... 68 | go mod tidy -v 69 | 70 | .PHONY: checkgenerate 71 | checkgenerate: 72 | @# Used in CI to verify that `make generate` doesn't produce a diff. 73 | test -z "$$(git status --porcelain | tee /dev/stderr)" 74 | 75 | $(BIN)/buf: Makefile 76 | @mkdir -p $(@D) 77 | go install github.com/bufbuild/buf/cmd/buf@$(BUF_VERSION) 78 | 79 | $(BIN)/license-header: Makefile 80 | @mkdir -p $(@D) 81 | go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@$(BUF_VERSION) 82 | 83 | $(BIN)/golangci-lint: Makefile 84 | @mkdir -p $(@D) 85 | GOTOOLCHAIN=$(GOLANGCI_LINT_GOTOOLCHAIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) 86 | 87 | $(BIN)/protoc-gen-go: Makefile go.mod 88 | @mkdir -p $(@D) 89 | @# The version of protoc-gen-go is determined by the version in go.mod 90 | go install google.golang.org/protobuf/cmd/protoc-gen-go 91 | 92 | .PHONY: $(BIN)/protoc-gen-pluginrpc-go 93 | $(BIN)/protoc-gen-pluginrpc-go: 94 | @mkdir -p $(@D) 95 | go build -o $(@) ./cmd/protoc-gen-pluginrpc-go 96 | 97 | .PHONY: $(BIN)/echo-plugin 98 | $(BIN)/echo-plugin: 99 | @mkdir -p $(@D) 100 | go build -o $(@) ./internal/example/cmd/echo-plugin 101 | -------------------------------------------------------------------------------- /wire.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | pluginrpcv1 "buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go/pluginrpc/v1" 19 | "google.golang.org/protobuf/proto" 20 | "google.golang.org/protobuf/types/known/anypb" 21 | ) 22 | 23 | func marshalRequest(format Format, requestValue any) ([]byte, error) { 24 | if requestValue == nil { 25 | return nil, nil 26 | } 27 | protoRequestValue, err := toProtoMessage(requestValue) 28 | if err != nil { 29 | return nil, err 30 | } 31 | anyRequestValue, err := anypb.New(protoRequestValue) 32 | if err != nil { 33 | return nil, err 34 | } 35 | protoRequest := &pluginrpcv1.Request{ 36 | Value: anyRequestValue, 37 | } 38 | codec, err := codecForFormat(format) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return codec.Marshal(protoRequest) 43 | } 44 | 45 | func unmarshalRequest(format Format, data []byte, requestValue any) error { 46 | if len(data) == 0 { 47 | return nil 48 | } 49 | codec, err := codecForFormat(format) 50 | if err != nil { 51 | return err 52 | } 53 | protoRequest := &pluginrpcv1.Request{} 54 | if err := codec.Unmarshal(data, protoRequest); err != nil { 55 | return err 56 | } 57 | anyRequestValue := protoRequest.GetValue() 58 | if anyRequestValue == nil { 59 | return nil 60 | } 61 | protoRequestValue, err := toProtoMessage(requestValue) 62 | if err != nil { 63 | return err 64 | } 65 | return anypb.UnmarshalTo(anyRequestValue, protoRequestValue, proto.UnmarshalOptions{}) 66 | } 67 | 68 | func marshalResponse(format Format, responseValue any, err error) ([]byte, error) { 69 | var anyResponseValue *anypb.Any 70 | if responseValue != nil { 71 | protoResponseValue, err := toProtoMessage(responseValue) 72 | if err != nil { 73 | return nil, err 74 | } 75 | anyResponseValue, err = anypb.New(protoResponseValue) 76 | if err != nil { 77 | return nil, err 78 | } 79 | } 80 | protoResponse := &pluginrpcv1.Response{ 81 | Value: anyResponseValue, 82 | Error: WrapError(err).ToProto(), 83 | } 84 | codec, err := codecForFormat(format) 85 | if err != nil { 86 | return nil, err 87 | } 88 | return codec.Marshal(protoResponse) 89 | } 90 | 91 | func unmarshalResponse(format Format, data []byte, responseValue any) error { 92 | if len(data) == 0 { 93 | return nil 94 | } 95 | codec, err := codecForFormat(format) 96 | if err != nil { 97 | return err 98 | } 99 | protoResponse := &pluginrpcv1.Response{} 100 | if err := codec.Unmarshal(data, protoResponse); err != nil { 101 | return err 102 | } 103 | if anyResponseValue := protoResponse.GetValue(); anyResponseValue != nil { 104 | protoResponseValue, err := toProtoMessage(responseValue) 105 | if err != nil { 106 | return err 107 | } 108 | if err := anypb.UnmarshalTo(anyResponseValue, protoResponseValue, proto.UnmarshalOptions{}); err != nil { 109 | return err 110 | } 111 | } 112 | if protoError := protoResponse.GetError(); protoError != nil { 113 | return NewErrorForProto(protoError) 114 | } 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "errors" 19 | "slices" 20 | 21 | pluginrpcv1 "buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go/pluginrpc/v1" 22 | ) 23 | 24 | // Spec specifies a set of Procedures that a plugin implements. This describes 25 | // the shape of the plugin to clients. 26 | // 27 | // Specs are returned on stdout when `--spec` is called. 28 | // 29 | // A given Spec will have no duplicate Procedures either by path or args. 30 | type Spec interface { 31 | // ProcedureForPath returns the Procedure for the given path. 32 | // 33 | // If no such procedure exists, this returns nil. 34 | ProcedureForPath(path string) Procedure 35 | // Procedures returns all Procedures. 36 | // 37 | // Never empty. 38 | Procedures() []Procedure 39 | 40 | isSpec() 41 | } 42 | 43 | // NewSpec returns a new validated Spec for the given Procedures. 44 | func NewSpec(procedures ...Procedure) (Spec, error) { 45 | return newSpec(procedures) 46 | } 47 | 48 | // NewSpecForProto returns a new validated Spec for the given pluginrpcv1.Spec. 49 | func NewSpecForProto(protoSpec *pluginrpcv1.Spec) (Spec, error) { 50 | procedures := make([]Procedure, len(protoSpec.GetProcedures())) 51 | for i, protoProcedure := range protoSpec.GetProcedures() { 52 | procedure, err := NewProcedureForProto(protoProcedure) 53 | if err != nil { 54 | return nil, err 55 | } 56 | procedures[i] = procedure 57 | } 58 | return NewSpec(procedures...) 59 | } 60 | 61 | // NewProtoSpec returns a new pluginrpcv1.Spec for the given Spec. 62 | func NewProtoSpec(spec Spec) *pluginrpcv1.Spec { 63 | procedures := spec.Procedures() 64 | protoProcedures := make([]*pluginrpcv1.Procedure, len(procedures)) 65 | for i, procedure := range procedures { 66 | protoProcedures[i] = NewProtoProcedure(procedure) 67 | } 68 | return &pluginrpcv1.Spec{ 69 | Procedures: protoProcedures, 70 | } 71 | } 72 | 73 | // MergeSpecs merges the given Specs. 74 | // 75 | // Input Specs can be nil. If all input Specs are nil, an error is returned 76 | // as Specs must have at least one Procedure.. 77 | // 78 | // Returns error if any Procedures overlap by Path or Args. 79 | func MergeSpecs(specs ...Spec) (Spec, error) { 80 | var procedures []Procedure 81 | for _, spec := range specs { 82 | if spec == nil { 83 | continue 84 | } 85 | procedures = append(procedures, spec.Procedures()...) 86 | } 87 | return NewSpec(procedures...) 88 | } 89 | 90 | // *** PRIVATE *** 91 | 92 | type spec struct { 93 | procedures []Procedure 94 | pathToProcedure map[string]Procedure 95 | } 96 | 97 | func newSpec(procedures []Procedure) (*spec, error) { 98 | if len(procedures) == 0 { 99 | return nil, errors.New("no procedures specified") 100 | } 101 | if err := validateProcedures(procedures); err != nil { 102 | return nil, err 103 | } 104 | pathToProcedure := make(map[string]Procedure) 105 | for _, procedure := range procedures { 106 | pathToProcedure[procedure.Path()] = procedure 107 | } 108 | return &spec{ 109 | procedures: procedures, 110 | pathToProcedure: pathToProcedure, 111 | }, nil 112 | } 113 | 114 | func (s *spec) ProcedureForPath(path string) Procedure { 115 | return s.pathToProcedure[path] 116 | } 117 | 118 | func (s *spec) Procedures() []Procedure { 119 | return slices.Clone(s.procedures) 120 | } 121 | 122 | func (*spec) isSpec() {} 123 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "slices" 22 | 23 | "github.com/spf13/pflag" 24 | ) 25 | 26 | // Server is the server for plugin implementations. 27 | // 28 | // The easiest way to run a server for a plugin is to call ServerMain. 29 | type Server interface { 30 | // Serve serves the plugin. 31 | Serve(ctx context.Context, env Env) error 32 | 33 | isServer() 34 | } 35 | 36 | // NewServer returns a new Server for a given Spec and ServerRegistrar. 37 | // 38 | // The Spec will be validated against the ServerRegistar to make sure there is a 39 | // 1-1 mapping between Procedures and registered paths. 40 | // 41 | // Once passed to this constructor, the ServerRegistrar can no longer have new 42 | // paths registered to it. 43 | func NewServer(spec Spec, serverRegistrar ServerRegistrar, options ...ServerOption) (Server, error) { 44 | return newServer(spec, serverRegistrar, options...) 45 | } 46 | 47 | // ServerOption is an option for a new Server. 48 | type ServerOption func(*serverOptions) 49 | 50 | // ServerWithDoc will attach the given documentation to the server. 51 | // 52 | // This will add ths given docs as a prefix when the flag -h/--help is used. 53 | func ServerWithDoc(doc string) ServerOption { 54 | return func(serverOptions *serverOptions) { 55 | serverOptions.doc = doc 56 | } 57 | } 58 | 59 | // *** PRIVATE *** 60 | 61 | type server struct { 62 | spec Spec 63 | pathToHandleFunc map[string]func(context.Context, HandleEnv, ...HandleOption) error 64 | doc string 65 | } 66 | 67 | func newServer(spec Spec, serverRegistrar ServerRegistrar, options ...ServerOption) (*server, error) { 68 | serverOptions := newServerOptions() 69 | for _, option := range options { 70 | option(serverOptions) 71 | } 72 | pathToHandleFunc, err := serverRegistrar.pathToHandleFunc() 73 | if err != nil { 74 | return nil, err 75 | } 76 | for path := range pathToHandleFunc { 77 | if spec.ProcedureForPath(path) == nil { 78 | return nil, fmt.Errorf("path %q not contained within spec", path) 79 | } 80 | } 81 | for _, procedure := range spec.Procedures() { 82 | if _, ok := pathToHandleFunc[procedure.Path()]; !ok { 83 | return nil, fmt.Errorf("path %q not registered", procedure.Path()) 84 | } 85 | } 86 | return &server{ 87 | spec: spec, 88 | pathToHandleFunc: pathToHandleFunc, 89 | doc: serverOptions.doc, 90 | }, nil 91 | } 92 | 93 | func (s *server) Serve(ctx context.Context, env Env) error { 94 | flags, args, err := parseFlags(env.Stderr, env.Args, s.spec, s.doc) 95 | if err != nil { 96 | if errors.Is(err, pflag.ErrHelp) { 97 | return nil 98 | } 99 | return err 100 | } 101 | if flags.printProtocol { 102 | _, err := env.Stdout.Write(marshalProtocol(protocolVersion)) 103 | return err 104 | } 105 | if flags.printSpec { 106 | data, err := marshalSpec(flags.format, NewProtoSpec(s.spec)) 107 | if err != nil { 108 | return err 109 | } 110 | _, err = env.Stdout.Write(data) 111 | return err 112 | } 113 | for _, procedure := range s.spec.Procedures() { 114 | if slices.Equal(args, []string{procedure.Path()}) { 115 | handleFunc := s.pathToHandleFunc[procedure.Path()] 116 | return handleFunc(ctx, handleEnvForEnv(env), HandleWithFormat(flags.format)) 117 | } 118 | // TODO: Make sure args do not overlap in procedures 119 | if slices.Equal(args, procedure.Args()) { 120 | handleFunc := s.pathToHandleFunc[procedure.Path()] 121 | return handleFunc(ctx, handleEnvForEnv(env), HandleWithFormat(flags.format)) 122 | } 123 | } 124 | return fmt.Errorf("args not recognized: %v", args) 125 | } 126 | 127 | func (*server) isServer() {} 128 | 129 | type serverOptions struct { 130 | doc string 131 | } 132 | 133 | func newServerOptions() *serverOptions { 134 | return &serverOptions{} 135 | } 136 | -------------------------------------------------------------------------------- /flags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "sort" 21 | "strconv" 22 | "strings" 23 | 24 | "github.com/spf13/pflag" 25 | ) 26 | 27 | const ( 28 | // ProtocolFlagName is the name of the protocol bool flag. 29 | ProtocolFlagName = "protocol" 30 | // SpecFlagName is the name of the spec bool flag. 31 | SpecFlagName = "spec" 32 | // FormatFlagName is the name of the format string flag. 33 | FormatFlagName = "format" 34 | 35 | protocolVersion = 1 36 | flagWrapping = 140 37 | ) 38 | 39 | type flags struct { 40 | printProtocol bool 41 | printSpec bool 42 | format Format 43 | } 44 | 45 | func parseFlags(output io.Writer, args []string, spec Spec, doc string) (*flags, []string, error) { 46 | flags := &flags{} 47 | var formatString string 48 | flagSet := pflag.NewFlagSet("plugin", pflag.ContinueOnError) 49 | flagSet.Usage = func() { 50 | _, _ = fmt.Fprint(output, getFlagUsage(flagSet, spec, doc)) 51 | } 52 | flagSet.SetOutput(output) 53 | flagSet.BoolVar(&flags.printProtocol, ProtocolFlagName, false, "Print the protocol to stdout and exit.") 54 | flagSet.BoolVar(&flags.printSpec, SpecFlagName, false, "Print the spec to stdout in the specified format and exit.") 55 | flagSet.StringVar(&formatString, FormatFlagName, formatBinaryString, fmt.Sprintf("The format to use for requests, responses, and specs. Must be one of [%q, %q].", formatBinaryString, formatJSONString)) 56 | if err := flagSet.Parse(args); err != nil { 57 | return nil, nil, err 58 | } 59 | if flags.printProtocol && flags.printSpec { 60 | return nil, nil, fmt.Errorf("cannot specify both --%s and --%s", ProtocolFlagName, SpecFlagName) 61 | } 62 | format := FormatBinary 63 | if formatString != "" { 64 | format = FormatForString(formatString) 65 | if format == 0 { 66 | return nil, nil, fmt.Errorf("invalid value for --%s: %q", FormatFlagName, formatString) 67 | } 68 | } 69 | if err := validateFormat(format); err != nil { 70 | return nil, nil, err 71 | } 72 | flags.format = format 73 | return flags, flagSet.Args(), nil 74 | } 75 | 76 | func getFlagUsage(flagSet *pflag.FlagSet, spec Spec, doc string) string { 77 | var sb strings.Builder 78 | if doc != "" { 79 | _, _ = sb.WriteString(doc) 80 | _, _ = sb.WriteString("\n\n") 81 | } 82 | _, _ = sb.WriteString("Commands:\n\n") 83 | var argBasedProcedureStrings []string 84 | var pathBasedProcedureStrings []string 85 | for _, procedure := range spec.Procedures() { 86 | if args := procedure.Args(); len(args) > 0 { 87 | argBasedProcedureStrings = append(argBasedProcedureStrings, strings.Join(args, " ")) 88 | } else { 89 | pathBasedProcedureStrings = append(pathBasedProcedureStrings, procedure.Path()) 90 | } 91 | } 92 | sort.Strings(argBasedProcedureStrings) 93 | sort.Strings(pathBasedProcedureStrings) 94 | for _, procedureString := range append(argBasedProcedureStrings, pathBasedProcedureStrings...) { 95 | _, _ = sb.WriteString(" ") 96 | _, _ = sb.WriteString(procedureString) 97 | _, _ = sb.WriteString("\n") 98 | } 99 | _, _ = sb.WriteString("\nFlags:\n\n") 100 | _, _ = sb.WriteString(flagSet.FlagUsagesWrapped(flagWrapping)) 101 | _, _ = sb.WriteString(" -h, --help Show this help.\n") 102 | return sb.String() 103 | } 104 | 105 | func marshalProtocol(value int) []byte { 106 | return []byte(strconv.Itoa(value) + "\n") 107 | } 108 | 109 | func unmarshalProtocol(data []byte) (int, error) { 110 | dataString := strings.TrimSpace(string(data)) 111 | value, err := strconv.Atoi(dataString) 112 | if err != nil { 113 | return 0, fmt.Errorf("invalid protocol: %q", dataString) 114 | } 115 | return value, err 116 | } 117 | 118 | func marshalSpec(format Format, value any) ([]byte, error) { 119 | protoValue, err := toProtoMessage(value) 120 | if err != nil { 121 | return nil, err 122 | } 123 | codec, err := codecForFormat(format) 124 | if err != nil { 125 | return nil, err 126 | } 127 | return codec.Marshal(protoValue) 128 | } 129 | 130 | func unmarshalSpec(format Format, data []byte, value any) error { 131 | if len(data) == 0 { 132 | return nil 133 | } 134 | codec, err := codecForFormat(format) 135 | if err != nil { 136 | return err 137 | } 138 | protoValue, err := toProtoMessage(value) 139 | if err != nil { 140 | return err 141 | } 142 | return codec.Unmarshal(data, protoValue) 143 | } 144 | -------------------------------------------------------------------------------- /runner.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "io" 21 | "os/exec" 22 | "slices" 23 | ) 24 | 25 | var emptyEnv = []string{"__EMPTY_ENV=1"} 26 | 27 | // Runner runs external commands. 28 | // 29 | // Runners should not proxy any environment variables to the commands they run. 30 | type Runner interface { 31 | // Run runs the external command with the given environment. 32 | // 33 | // The environment variables are always cleared before running the command. 34 | // If no stdin, stdout, or stderr are provided, the equivalent of /dev/null are given to the command. 35 | // The command is run in the context of the current working directory. 36 | // 37 | // If there is an exit error, it is returned as a *ExitError. 38 | Run(ctx context.Context, env Env) error 39 | } 40 | 41 | // NewExecRunner returns a new Runner that uses os/exec to call the given 42 | // external command given by the program name. 43 | func NewExecRunner(programName string, options ...ExecRunnerOption) Runner { 44 | return newExecRunner(programName, options...) 45 | } 46 | 47 | // ExecRunnerOption is an option for a new os/exec Runner. 48 | type ExecRunnerOption func(*execRunnerOptions) 49 | 50 | // ExecRunnerWithArgs returns a new ExecRunnerOption that specifies a sub-command to invoke 51 | // on the program. 52 | // 53 | // For example, if the plugin is implemented under the sub-command `foo bar` 54 | // on the program `plug`, specifying ExecRunnerWithArgs("foo", "bar") will result in the 55 | // command `plug foo bar` being invoked as the plugin. In this scenario, all procedures 56 | // and flag will be implemented under this sub-command. In this example, 57 | // `plug foo bar --plugin-spec` should produce the spec. 58 | func ExecRunnerWithArgs(args ...string) ExecRunnerOption { 59 | return func(execRunnerOptions *execRunnerOptions) { 60 | execRunnerOptions.args = args 61 | } 62 | } 63 | 64 | // NewServerRunner returns a new Runner that directly calls the server. 65 | // 66 | // This is primarily used for testing. 67 | func NewServerRunner(server Server, _ ...ServerRunnerOption) Runner { 68 | return newServerRunner(server) 69 | } 70 | 71 | // ServerRunnerOption is an option for a new ServerRunner. 72 | type ServerRunnerOption func(*serverRunnerOptions) 73 | 74 | // *** PRIVATE *** 75 | 76 | type execRunner struct { 77 | programName string 78 | programBaseArgs []string 79 | } 80 | 81 | func newExecRunner(programName string, options ...ExecRunnerOption) *execRunner { 82 | execRunnerOptions := newExecRunnerOptions() 83 | for _, option := range options { 84 | option(execRunnerOptions) 85 | } 86 | return &execRunner{ 87 | programName: programName, 88 | programBaseArgs: execRunnerOptions.args, 89 | } 90 | } 91 | 92 | func (e *execRunner) Run(ctx context.Context, env Env) error { 93 | cmd := exec.CommandContext(ctx, e.programName, append(slices.Clone(e.programBaseArgs), env.Args...)...) 94 | // We want to make sure the command has access to no env vars, as the default is the current env. 95 | cmd.Env = emptyEnv 96 | // If the user did not specify various stdio, we want to make sure 97 | // the command has access to no stdio. 98 | if env.Stdin == nil { 99 | cmd.Stdin = discardReader{} 100 | } else { 101 | cmd.Stdin = env.Stdin 102 | } 103 | if env.Stdout == nil { 104 | cmd.Stdout = io.Discard 105 | } else { 106 | cmd.Stdout = env.Stdout 107 | } 108 | if env.Stderr == nil { 109 | cmd.Stderr = io.Discard 110 | } else { 111 | cmd.Stderr = env.Stderr 112 | } 113 | // The default behavior for dir is what we want already, i.e. the current 114 | // working directory. 115 | 116 | if err := cmd.Run(); err != nil { 117 | exitError := &exec.ExitError{} 118 | if errors.As(err, &exitError) { 119 | return NewExitError(exitError.ExitCode(), exitError) 120 | } 121 | return err 122 | } 123 | return nil 124 | } 125 | 126 | type serverRunner struct { 127 | server Server 128 | errs []error 129 | } 130 | 131 | func newServerRunner(server Server) *serverRunner { 132 | return &serverRunner{ 133 | server: server, 134 | } 135 | } 136 | 137 | func (s *serverRunner) Run(ctx context.Context, env Env) error { 138 | if len(s.errs) > 0 { 139 | return errors.Join(s.errs...) 140 | } 141 | // Servers directly return ExitErrors, so this fulfills the contract. 142 | return s.server.Serve(ctx, env) 143 | } 144 | 145 | type discardReader struct{} 146 | 147 | func (discardReader) Read([]byte) (int, error) { 148 | return 0, io.EOF 149 | } 150 | 151 | type execRunnerOptions struct { 152 | args []string 153 | } 154 | 155 | func newExecRunnerOptions() *execRunnerOptions { 156 | return &execRunnerOptions{} 157 | } 158 | 159 | type serverRunnerOptions struct{} 160 | -------------------------------------------------------------------------------- /procedure.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "net/url" 21 | "regexp" 22 | "slices" 23 | "strings" 24 | 25 | pluginrpcv1 "buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go/pluginrpc/v1" 26 | ) 27 | 28 | const minProcedureArgLength = 2 29 | 30 | var argRegexp = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]$`) 31 | 32 | // Procedure defines a single procedure that a plugin exposes. 33 | type Procedure interface { 34 | // Path returns the path of the Procedure. 35 | // 36 | // Paths are always valid URIs. 37 | Path() string 38 | // Args returns optional custom args which can be used to invoke the Procedure. 39 | // 40 | // If there are no args, the Procedure can be invoked with the single arg equal to the path. 41 | // Arg values may only use the characters [a-zA-Z0-9-_], and never start or end with a dash 42 | // or underscore. 43 | Args() []string 44 | 45 | isProcedure() 46 | } 47 | 48 | // NewProcedure returns a new validated Procedure for the given path. 49 | func NewProcedure(path string, options ...ProcedureOption) (Procedure, error) { 50 | return newProcedure(path, options...) 51 | } 52 | 53 | // NewProcedureForProto returns a new validated Procedure for the given pluginrpcv1.Procedure. 54 | func NewProcedureForProto(protoProcedure *pluginrpcv1.Procedure) (Procedure, error) { 55 | return newProcedure(protoProcedure.GetPath(), ProcedureWithArgs(protoProcedure.GetArgs()...)) 56 | } 57 | 58 | // NewProtoProcedure returns a new pluginrpcv1.Procedure for the given Procedure. 59 | func NewProtoProcedure(procedure Procedure) *pluginrpcv1.Procedure { 60 | return &pluginrpcv1.Procedure{ 61 | Path: procedure.Path(), 62 | Args: procedure.Args(), 63 | } 64 | } 65 | 66 | // ProcedureOption is an option for a new Procedure. 67 | type ProcedureOption func(*procedureOptions) 68 | 69 | // ProcedureWithArgs specifies optional custom args which can be used to invoke the Procedure. 70 | // 71 | // If there are no args, the Procedure can be invoked with the single arg equal to the path. 72 | // Arg values may only use the characters [a-zA-Z0-9-_], and never start with a dash or underscore. 73 | func ProcedureWithArgs(args ...string) ProcedureOption { 74 | return func(procedureOptions *procedureOptions) { 75 | procedureOptions.args = args 76 | } 77 | } 78 | 79 | // *** PRIVATE *** 80 | 81 | type procedure struct { 82 | path string 83 | args []string 84 | } 85 | 86 | func newProcedure(path string, options ...ProcedureOption) (*procedure, error) { 87 | procedureOptions := newProcedureOptions() 88 | for _, option := range options { 89 | option(procedureOptions) 90 | } 91 | procedure := &procedure{ 92 | path: path, 93 | args: procedureOptions.args, 94 | } 95 | if err := validateProcedure(procedure); err != nil { 96 | return nil, err 97 | } 98 | return procedure, nil 99 | } 100 | 101 | func (p *procedure) Path() string { 102 | return p.path 103 | } 104 | 105 | func (p *procedure) Args() []string { 106 | return slices.Clone(p.args) 107 | } 108 | 109 | func (*procedure) isProcedure() {} 110 | 111 | type procedureOptions struct { 112 | args []string 113 | } 114 | 115 | func newProcedureOptions() *procedureOptions { 116 | return &procedureOptions{} 117 | } 118 | 119 | func validateProcedures(procedures []Procedure) error { 120 | usedPathMap := make(map[string]struct{}) 121 | usedArgsMap := make(map[string]struct{}) 122 | for _, procedure := range procedures { 123 | path := procedure.Path() 124 | if _, ok := usedPathMap[path]; ok { 125 | return fmt.Errorf("duplicate procedure path: %q", path) 126 | } 127 | usedPathMap[path] = struct{}{} 128 | args := procedure.Args() 129 | if len(args) > 0 { 130 | // We can do this given that we have a valid Spec where 131 | // args do not contain spaces. 132 | joinedArgs := strings.Join(args, " ") 133 | if _, ok := usedArgsMap[joinedArgs]; ok { 134 | return fmt.Errorf("duplicate procedure args: %q", joinedArgs) 135 | } 136 | usedArgsMap[joinedArgs] = struct{}{} 137 | } 138 | } 139 | return nil 140 | } 141 | 142 | func validateProcedure(procedure *procedure) error { 143 | if procedure.path == "" { 144 | return errors.New("procedure path is empty") 145 | } 146 | if _, err := url.ParseRequestURI(procedure.path); err != nil { 147 | return fmt.Errorf("invalid procedure path: %w", err) 148 | } 149 | for _, arg := range procedure.args { 150 | if len(arg) < minProcedureArgLength { 151 | return fmt.Errorf("arg %q for procedure %q must be at least length %d", arg, procedure.path, minProcedureArgLength) 152 | } 153 | if !argRegexp.MatchString(arg) { 154 | return fmt.Errorf("arg %q for procedure %q must only consist of characters [a-zA-Z0-9-_] and cannot start or end with a dash or underscore", arg, procedure.path) 155 | } 156 | } 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "io" 21 | "os" 22 | 23 | "github.com/mattn/go-isatty" 24 | ) 25 | 26 | // Handler handles requests on the server side. 27 | // 28 | // This is used within generated code when registering an implementation of a service. 29 | // 30 | // Currently, Handlers do not have any customization, however this type is exposes 31 | // so that customization can be provided in the future. 32 | type Handler interface { 33 | Handle( 34 | ctx context.Context, 35 | handleEnv HandleEnv, 36 | request any, 37 | handle func(context.Context, any) (any, error), 38 | options ...HandleOption, 39 | ) error 40 | 41 | isHandler() 42 | } 43 | 44 | // NewHandler returns a new Handler. 45 | func NewHandler(spec Spec, _ ...HandlerOption) Handler { 46 | return newHandler(spec) 47 | } 48 | 49 | // HandlerOption is an option for a new Handler. 50 | type HandlerOption func(*handlerOptions) 51 | 52 | // HandleOption is an option for handler.Handle. 53 | type HandleOption func(*handleOptions) 54 | 55 | // HandleWithFormat returns a new HandleOption that says to marshal and unmarshal requests, 56 | // responses, and errors in the given format. 57 | // 58 | // The default is FormatBinary. 59 | func HandleWithFormat(format Format) HandleOption { 60 | return func(handleOptions *handleOptions) { 61 | handleOptions.format = format 62 | } 63 | } 64 | 65 | // HandleEnv is the part of the environment that Handlers can have access to. 66 | type HandleEnv struct { 67 | Stdin io.Reader 68 | Stdout io.Writer 69 | Stderr io.Writer 70 | } 71 | 72 | // *** PRIVATE *** 73 | 74 | type handler struct { 75 | spec Spec 76 | } 77 | 78 | func newHandler(spec Spec) *handler { 79 | return &handler{ 80 | spec: spec, 81 | } 82 | } 83 | 84 | func (h *handler) Handle( 85 | ctx context.Context, 86 | handleEnv HandleEnv, 87 | request any, 88 | handle func(context.Context, any) (any, error), 89 | options ...HandleOption, 90 | ) (retErr error) { 91 | handleOptions := newHandleOptions() 92 | for _, option := range options { 93 | option(handleOptions) 94 | } 95 | if err := validateFormat(handleOptions.format); err != nil { 96 | return err 97 | } 98 | 99 | defer func() { 100 | if retErr != nil { 101 | retErr = h.writeError(handleOptions.format, handleEnv, retErr) 102 | } 103 | }() 104 | 105 | data, err := readStdin(handleEnv.Stdin) 106 | if err != nil { 107 | return err 108 | } 109 | if err := unmarshalRequest(handleOptions.format, data, request); err != nil { 110 | return err 111 | } 112 | response, err := handle(ctx, request) 113 | if err != nil { 114 | // TODO: This results in writeError being called, but ignores marshaling 115 | // the response, so we will never have a non-nil response and non-nil 116 | // error together, which the protocol says we can have. 117 | // 118 | // This just needs some refactoring. 119 | return err 120 | } 121 | data, err = marshalResponse(handleOptions.format, response, nil) 122 | if err != nil { 123 | return err 124 | } 125 | if _, err = handleEnv.Stdout.Write(data); err != nil { 126 | return fmt.Errorf("failed to write response to stdout: %w", err) 127 | } 128 | return err 129 | } 130 | 131 | func (h *handler) writeError(format Format, handleEnv HandleEnv, inputErr error) error { 132 | if inputErr == nil { 133 | return nil 134 | } 135 | // TODO: Format doesn't matter here, as we don't marshal any response. 136 | // However, if we fix the above and do marshal responses with errors, it will matter. 137 | data, err := marshalResponse(format, nil, inputErr) 138 | if err != nil { 139 | return err 140 | } 141 | if _, err := handleEnv.Stdout.Write(data); err != nil { 142 | return fmt.Errorf("failed to write error to stdout: %w", err) 143 | } 144 | return nil 145 | } 146 | 147 | func (*handler) isHandler() {} 148 | 149 | // readStdin handles stdin specially to determine if stdin is a *os.File (likely os.Stdin) 150 | // and is itself a terminal. If so, we don't block on io.ReadAll, as we know that there 151 | // is no data in stdin and we can return. 152 | // 153 | // This allows server-side implementations of services to not require i.e.: 154 | // 155 | // echo '{}' | plugin-server /pkg.Service/Method 156 | // 157 | // Instead allowing to just invoke the following if there is no request data: 158 | // 159 | // plugin-server /pkg.Service/Method 160 | func readStdin(stdin io.Reader) ([]byte, error) { 161 | file, ok := stdin.(*os.File) 162 | if ok { 163 | if isatty.IsTerminal(file.Fd()) || isatty.IsCygwinTerminal(file.Fd()) { 164 | // Nothing on stdin 165 | return nil, nil 166 | } 167 | } 168 | return io.ReadAll(stdin) 169 | } 170 | 171 | func handleEnvForEnv(env Env) HandleEnv { 172 | return HandleEnv{ 173 | Stdin: env.Stdin, 174 | Stdout: env.Stdout, 175 | Stderr: env.Stderr, 176 | } 177 | } 178 | 179 | type handlerOptions struct{} 180 | 181 | type handleOptions struct { 182 | format Format 183 | } 184 | 185 | func newHandleOptions() *handleOptions { 186 | return &handleOptions{ 187 | format: FormatBinary, 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "strings" 21 | 22 | pluginrpcv1 "buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go/pluginrpc/v1" 23 | ) 24 | 25 | // TODO: Figure out when and where to wrap errors created by this package with Errors. 26 | 27 | // Error is an error with a Code. 28 | type Error struct { 29 | code Code 30 | underlying error 31 | } 32 | 33 | // NewError returns a new Error. 34 | // 35 | // Code and underlying with a non-empty message are required. 36 | // 37 | // An Error will never have an invalid Code or nil underlying error 38 | // when returned from this function. 39 | func NewError(code Code, underlying error) *Error { 40 | return validateError( 41 | &Error{ 42 | code: code, 43 | underlying: underlying, 44 | }, 45 | ) 46 | } 47 | 48 | // NewErrorf returns a new Error. 49 | 50 | // Code and a non-empty message are required. 51 | // 52 | // An Error will never have an invalid Code or nil underlying error 53 | // when returned from this function. 54 | func NewErrorf(code Code, format string, args ...any) *Error { 55 | return NewError(code, fmt.Errorf(format, args...)) 56 | } 57 | 58 | // NewErrorForProto returns a new Error for the given pluginrpcv1.Error. 59 | // 60 | // If protoError is nil, this returns nil. 61 | func NewErrorForProto(protoError *pluginrpcv1.Error) *Error { 62 | if protoError == nil { 63 | return nil 64 | } 65 | code, err := CodeForProto(protoError.GetCode()) 66 | if err != nil { 67 | return NewError( 68 | CodeInternal, 69 | fmt.Errorf("Error created with invalid code: %s: %w", protoError.GetMessage(), err), 70 | ) 71 | } 72 | return NewError( 73 | code, 74 | errors.New(protoError.GetMessage()), 75 | ) 76 | } 77 | 78 | // WrapError wraps the given error as a Error. 79 | // 80 | // If the given error is nil, this returns nil. 81 | // If the given error is already a Error, this is returned. 82 | // Otherwise, an error with code CodeUnknown is returned. 83 | // 84 | // An Error will never have an invalid Code when returned from this function. 85 | func WrapError(err error) *Error { 86 | if err == nil { 87 | return nil 88 | } 89 | pluginrpcError := &Error{} 90 | if errors.As(err, &pluginrpcError) { 91 | return validateError(pluginrpcError) 92 | } 93 | return NewError(CodeUnknown, err) 94 | } 95 | 96 | // Code returns the error code. 97 | // 98 | // If e is nil, this returns 0. 99 | func (e *Error) Code() Code { 100 | if e == nil { 101 | return Code(0) 102 | } 103 | return e.code 104 | } 105 | 106 | // ToProto converts the Error to a pluginrpcv1.Error. 107 | // 108 | // If e is nil, this returns nil. 109 | func (e *Error) ToProto() *pluginrpcv1.Error { 110 | if e == nil { 111 | return nil 112 | } 113 | pluginrpcError := validateError(e) 114 | protoCode, err := pluginrpcError.Code().ToProto() 115 | if err != nil { 116 | return &pluginrpcv1.Error{ 117 | Code: pluginrpcv1.Code_CODE_INTERNAL, 118 | Message: fmt.Sprintf("Error created with invalid code: %s: %s", e.underlying.Error(), err.Error()), 119 | } 120 | } 121 | return &pluginrpcv1.Error{ 122 | Code: protoCode, 123 | Message: pluginrpcError.Unwrap().Error(), 124 | } 125 | } 126 | 127 | // Error implements error. 128 | // 129 | // If e is nil, this returns the empty string. 130 | func (e *Error) Error() string { 131 | if e == nil { 132 | return "" 133 | } 134 | var sb strings.Builder 135 | _, _ = sb.WriteString(`Failed with code `) 136 | _, _ = sb.WriteString(e.code.String()) 137 | if e.underlying != nil { 138 | _, _ = sb.WriteString(`: `) 139 | _, _ = sb.WriteString(e.underlying.Error()) 140 | } 141 | return sb.String() 142 | } 143 | 144 | // Unwrap implements error. 145 | // 146 | // If e is nil, this returns nil. 147 | func (e *Error) Unwrap() error { 148 | if e == nil { 149 | return nil 150 | } 151 | return e.underlying 152 | } 153 | 154 | // *** PRIVATE *** 155 | 156 | func validateError(pluginrpcError *Error) *Error { 157 | code := pluginrpcError.Code() 158 | underlying := pluginrpcError.Unwrap() 159 | if !isValidCode(code) { 160 | return newInvalidCodeError(pluginrpcError) 161 | } 162 | if underlying == nil { 163 | return newNilUnderlyingError(pluginrpcError) 164 | } 165 | if underlyingString := underlying.Error(); underlyingString == "" { 166 | return newEmptyUnderlyingError(pluginrpcError) 167 | } 168 | return pluginrpcError 169 | } 170 | 171 | func newInvalidCodeError(pluginrpcError *Error) *Error { 172 | return &Error{ 173 | code: CodeInternal, 174 | underlying: fmt.Errorf("Error created with code %v: %w", pluginrpcError.Code(), pluginrpcError.Unwrap()), 175 | } 176 | } 177 | 178 | func newNilUnderlyingError(pluginrpcError *Error) *Error { 179 | return &Error{ 180 | code: CodeInternal, 181 | underlying: fmt.Errorf("Error created with code %v and nil underlying error", pluginrpcError.Code()), 182 | } 183 | } 184 | 185 | func newEmptyUnderlyingError(pluginrpcError *Error) *Error { 186 | return &Error{ 187 | code: CodeInternal, 188 | underlying: fmt.Errorf("Error created with code %v and empty underlying error", pluginrpcError.Code()), 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /code.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "fmt" 19 | 20 | pluginrpcv1 "buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go/pluginrpc/v1" 21 | ) 22 | 23 | // Code is an error code. There are no user-defined codes, so only the codes 24 | // enumerated below are valid. In both name and semantics, these codes match the gRPC status codes. 25 | type Code uint32 26 | 27 | const ( 28 | // The zero code in gRPC is OK, which indicates that the operation was a 29 | // success. We don't define a constant for it because it overlaps awkwardly 30 | // with Go's error semantics: what does it mean to have a non-nil error with 31 | // an OK status? 32 | 33 | // CodeCanceled indicates that the operation was canceled, typically by the 34 | // caller. 35 | CodeCanceled Code = 1 36 | 37 | // CodeUnknown indicates that the operation failed for an unknown reason. 38 | CodeUnknown Code = 2 39 | 40 | // CodeInvalidArgument indicates that client supplied an invalid argument. 41 | CodeInvalidArgument Code = 3 42 | 43 | // CodeDeadlineExceeded indicates that deadline expired before the operation 44 | // could complete. 45 | CodeDeadlineExceeded Code = 4 46 | 47 | // CodeNotFound indicates that some requested entity (for example, a file or 48 | // directory) was not found. 49 | CodeNotFound Code = 5 50 | 51 | // CodeAlreadyExists indicates that client attempted to create an entity (for 52 | // example, a file or directory) that already exists. 53 | CodeAlreadyExists Code = 6 54 | 55 | // CodePermissionDenied indicates that the caller doesn't have permission to 56 | // execute the specified operation. 57 | CodePermissionDenied Code = 7 58 | 59 | // CodeResourceExhausted indicates that some resource has been exhausted. For 60 | // example, a per-user quota may be exhausted or the entire file system may 61 | // be full. 62 | CodeResourceExhausted Code = 8 63 | 64 | // CodeFailedPrecondition indicates that the system is not in a state 65 | // required for the operation's execution. 66 | CodeFailedPrecondition Code = 9 67 | 68 | // CodeAborted indicates that operation was aborted by the system, usually 69 | // because of a concurrency issue such as a sequencer check failure or 70 | // transaction abort. 71 | CodeAborted Code = 10 72 | 73 | // CodeOutOfRange indicates that the operation was attempted past the valid 74 | // range (for example, seeking past end-of-file). 75 | CodeOutOfRange Code = 11 76 | 77 | // CodeUnimplemented indicates that the operation isn't implemented, 78 | // supported, or enabled in this service. 79 | CodeUnimplemented Code = 12 80 | 81 | // CodeInternal indicates that some invariants expected by the underlying 82 | // system have been broken. This code is reserved for serious errors. 83 | CodeInternal Code = 13 84 | 85 | // CodeUnavailable indicates that the service is currently unavailable. This 86 | // is usually temporary, so clients can back off and retry idempotent 87 | // operations. 88 | CodeUnavailable Code = 14 89 | 90 | // CodeDataLoss indicates that the operation has resulted in unrecoverable 91 | // data loss or corruption. 92 | CodeDataLoss Code = 15 93 | 94 | // CodeUnauthenticated indicates that the request does not have valid 95 | // authentication credentials for the operation. 96 | CodeUnauthenticated Code = 16 97 | 98 | minCode = CodeCanceled 99 | maxCode = CodeUnauthenticated 100 | ) 101 | 102 | // String implements fmt.Stringer. 103 | func (c Code) String() string { 104 | switch c { 105 | case CodeCanceled: 106 | return "canceled" 107 | case CodeUnknown: 108 | return "unknown" 109 | case CodeInvalidArgument: 110 | return "invalid_argument" 111 | case CodeDeadlineExceeded: 112 | return "deadline_exceeded" 113 | case CodeNotFound: 114 | return "not_found" 115 | case CodeAlreadyExists: 116 | return "already_exists" 117 | case CodePermissionDenied: 118 | return "permission_denied" 119 | case CodeResourceExhausted: 120 | return "resource_exhausted" 121 | case CodeFailedPrecondition: 122 | return "failed_precondition" 123 | case CodeAborted: 124 | return "aborted" 125 | case CodeOutOfRange: 126 | return "out_of_range" 127 | case CodeUnimplemented: 128 | return "unimplemented" 129 | case CodeInternal: 130 | return "internal" 131 | case CodeUnavailable: 132 | return "unavailable" 133 | case CodeDataLoss: 134 | return "data_loss" 135 | case CodeUnauthenticated: 136 | return "unauthenticated" 137 | } 138 | return fmt.Sprintf("code_%d", c) 139 | } 140 | 141 | // ToProto returns the pluginrpcv1.Code for the given Code. 142 | // 143 | // Returns error if the Code is not valid. 144 | func (c Code) ToProto() (pluginrpcv1.Code, error) { 145 | if isValidCode(c) { 146 | return pluginrpcv1.Code(c), nil 147 | } 148 | return 0, fmt.Errorf("unknown Code: %v", c) 149 | } 150 | 151 | // CodeForProto returns the Code for the pluginrpcv1.Code. 152 | // 153 | // Returns error the pluginrpcv1.Code is not valid. 154 | func CodeForProto(protoCode pluginrpcv1.Code) (Code, error) { 155 | if code := Code(protoCode); isValidCode(code) { 156 | return code, nil 157 | } 158 | return 0, fmt.Errorf("unknown pluginrpcv1.Code: %v", protoCode) 159 | } 160 | 161 | // *** PRIVATE *** 162 | 163 | func isValidCode(code Code) bool { 164 | return code >= minCode && code <= maxCode 165 | } 166 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | conduct@buf.build. All complaints will be reviewed and investigated promptly 64 | and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 126 | at [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pluginrpc-go 2 | 3 | [![Build](https://github.com/pluginrpc/pluginrpc-go/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/pluginrpc/pluginrpc-go/actions/workflows/ci.yaml) 4 | [![Report Card](https://goreportcard.com/badge/pluginrpc.com/pluginrpc)](https://goreportcard.com/report/pluginrpc.com/pluginrpc) 5 | [![GoDoc](https://pkg.go.dev/badge/pluginrpc.com/pluginrpc.svg)](https://pkg.go.dev/pluginrpc.com/pluginrpc) 6 | [![Slack](https://img.shields.io/badge/slack-buf-%23e01563)](https://buf.build/links/slack) 7 | 8 | The Golang library for [PluginRPC](https://github.com/pluginrpc/pluginrpc). 9 | 10 | The [pluginrpc.com/pluginrpc](https://pkg.go.dev/pluginrpc.com/pluginrpc) library provides all the 11 | primitives necessary to operate with the PluginRPC ecosystem. The `protoc-gen-pluginrpc-go` plugin 12 | generates stubs for Protobuf services to work with PluginRPC. It makes authoring and consuming 13 | plugins based on Protobuf services incredibly simple. 14 | 15 | For more on the motivation behind PluginRPC, see the 16 | [github.com/pluginrpc/pluginrpc](https://github.com/pluginrpc/pluginrpc) documentation. 17 | 18 | For a full example, see the [internal/example](internal/example) directory. This contains: 19 | 20 | - [proto/pluginrpc/example/v1](internal/example/proto/pluginrpc/example/v1): An Protobuf package 21 | that contains an example Protobuf service `EchoService`. 22 | - [gen/pluginrpc/example/v1](internal/example/gen/pluginrpc/example/v1): The generated code from 23 | `protoc-gen-go` and `protoc-gen-pluginrpc-go` for the example Protobuf Package. 24 | - [echo-plugin](internal/example/cmd/echo-plugin): An implementation of a PluginRPC plugin for 25 | `EchoService`. 26 | - [echo-request-client](internal/example/cmd/echo-request-client): A simple client that calls the 27 | `EchoRequest` RPC via invoking `echo-plugin`. 28 | - [echo-list-client](internal/example/cmd/echo-request-client): A simple client that calls the 29 | `EchoList` RPC via invoking `echo-plugin`. 30 | - [echo-error-client](internal/example/cmd/echo-error-client): A simple client that calls the 31 | `EchoError` RPC via invoking `echo-plugin`. 32 | 33 | ## Usage 34 | 35 | Install the `protoc-gen-go` and `protoc-gen-pluginrpc-go` plugins: 36 | 37 | ```bash 38 | $ go install \ 39 | google.golang.org/protobuf/cmd/protoc-gen-go@latest \ 40 | pluginrpc.com/pluginrpc/cmd/protoc-gen-pluginrpc-go@latest 41 | ``` 42 | 43 | Generate stubs. The easiest way to do so is by using [buf](github.com/bufbuild/buf). See Buf's 44 | [generation tutorial] for more details on setting up generation. You'll likely need a `buf.gen.yaml` 45 | file that looks approximately like the following: 46 | 47 | ```yaml 48 | version: v2 49 | inputs: 50 | # Or wherever your .proto files live 51 | - directory: proto 52 | managed: 53 | enabled: true 54 | override: 55 | - file_option: go_package_prefix 56 | # Replace github.com/acme/foo with the name of your Golang module 57 | value: github.com/acme/foo/gen 58 | plugins: 59 | - local: protoc-gen-go 60 | out: gen 61 | opt: paths=source_relative 62 | - local: protoc-gen-pluginrpc-go 63 | out: gen 64 | opt: paths=source_relative 65 | ``` 66 | 67 | Build your plugin. See [echo-plugin](internal/example/cmd/echo-plugin) for a full example. Assuming 68 | you intend to expose the `EchoService` as a plugin, your code will look something like this: 69 | 70 | ```go 71 | func main() { 72 | pluginrpc.Main(newServer) 73 | } 74 | 75 | func newServer() (pluginrpc.Server, error) { 76 | spec, err := examplev1pluginrpc.EchoServiceSpecBuilder{ 77 | // Note that EchoList does not have optional args and will default to path being the only arg. 78 | // 79 | // This means that the following commands will invoke their respective procedures: 80 | // 81 | // echo-plugin echo request 82 | // echo-plugin /pluginrpc.example.v1.EchoService/EchoList 83 | // echo-plugin echo error 84 | EchoRequest: []pluginrpc.ProcedureOption{pluginrpc.ProcedureWithArgs("echo", "request")}, 85 | EchoError: []pluginrpc.ProcedureOption{pluginrpc.ProcedureWithArgs("echo", "error")}, 86 | }.Build() 87 | if err != nil { 88 | return nil, err 89 | } 90 | serverRegistrar := pluginrpc.NewServerRegistrar() 91 | echoServiceServer := examplev1pluginrpc.NewEchoServiceServer(pluginrpc.NewHandler(spec), echoServiceHandler{}) 92 | examplev1pluginrpc.RegisterEchoServiceServer(serverRegistrar, echoServiceServer) 93 | return pluginrpc.NewServer(spec, serverRegistrar) 94 | } 95 | 96 | type echoServiceHandler struct{} 97 | 98 | func (echoServiceHandler) EchoRequest(_ context.Context, request *examplev1.EchoRequestRequest) (*examplev1.EchoRequestResponse, error) { 99 | ... 100 | } 101 | 102 | func (echoServiceHandler) EchoList(context.Context, *examplev1.EchoListRequest) (*examplev1.EchoListResponse, error) { 103 | ... 104 | } 105 | 106 | func (echoServiceHandler) EchoError(_ context.Context, request *examplev1.EchoErrorRequest) (*examplev1.EchoErrorResponse, error) { 107 | ... 108 | } 109 | ``` 110 | 111 | Invoke your plugin. You'll create a client that points to your plugin. See 112 | [echo-request-client](internal/example/cmd/echo-request-client) for a full example. Invocation will 113 | look something like this: 114 | 115 | ```go 116 | client := pluginrpc.NewClient(pluginrpc.NewExecRunner("echo-plugin")) 117 | echoServiceClient, err := examplev1pluginrpc.NewEchoServiceClient(client) 118 | if err != nil { 119 | return err 120 | } 121 | response, err := echoServiceClient.EchoRequest( 122 | context.Background(), 123 | &examplev1.EchoRequestRequest{ 124 | ... 125 | }, 126 | ) 127 | ``` 128 | 129 | See [pluginrpc_test.go](pluginrpc_test.go) for an example of how to test plugins. 130 | 131 | ## Plugin Options 132 | 133 | The `protoc-gen-pluginrpc-go` has an option `streaming` that specifies how to handle streaming RPCs. 134 | PluginRPC does not support streaming methods. There are three valid values for `streaming`: `error`, 135 | `warn`, `ignore`. The default is `warn`: 136 | 137 | - `streaming=error`: The plugin will error if a streaming method is encountered. 138 | - `streaming=warn`: The plugin will produce a warning to stderr if a streaming method is 139 | encountered. 140 | - `streaming=ignore`: The plugin will ignore streaming methods and not produce a warning. 141 | 142 | In the case of `warn` or `ignore`, streaming RPCs will be skipped and no functions will be generated 143 | for them. If a service only has streaming RPCs, no interfaces will be generated for this service. If 144 | a file only has services with only streaming RPCs, no file will be generated. 145 | 146 | Additionally, `protoc-gen-pluginrpc-go has all the 147 | [standard Go plugin options](https://pkg.go.dev/google.golang.org/protobuf@v1.34.2/compiler/protogen): 148 | 149 | - `module=` 150 | - `paths={import,source_relative}` 151 | - `annotate_code={true,false}` 152 | - `M=` 153 | 154 | 155 | ## Status: Beta 156 | 157 | This framework is in active development, and should not be considered stable. 158 | 159 | ## Legal 160 | 161 | Offered under the [Apache 2 license](https://github.com/pluginrpc/pluginrpc-go/blob/main/LICENSE). 162 | -------------------------------------------------------------------------------- /pluginrpc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc_test 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "slices" 21 | "strconv" 22 | "testing" 23 | 24 | pluginrpcv1 "buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go/pluginrpc/v1" 25 | "github.com/stretchr/testify/require" 26 | "pluginrpc.com/pluginrpc" 27 | examplev1 "pluginrpc.com/pluginrpc/internal/example/gen/pluginrpc/example/v1" 28 | "pluginrpc.com/pluginrpc/internal/example/gen/pluginrpc/example/v1/examplev1pluginrpc" 29 | ) 30 | 31 | const echoPluginProgramName = "echo-plugin" 32 | 33 | // We want to append 0 so that we call pluginrpc.ClientWithFormat with the default Format. 34 | var allTestFormats = append(slices.Clone(pluginrpc.AllFormats), 0) 35 | 36 | func TestEchoRequest(t *testing.T) { 37 | t.Parallel() 38 | forEachDimension( 39 | t, 40 | func(t *testing.T, client pluginrpc.Client) { 41 | echoServiceClient, err := examplev1pluginrpc.NewEchoServiceClient(client) 42 | require.NoError(t, err) 43 | response, err := echoServiceClient.EchoRequest( 44 | context.Background(), 45 | &examplev1.EchoRequestRequest{ 46 | Message: "hello", 47 | }, 48 | ) 49 | require.NoError(t, err) 50 | require.NotNil(t, response) 51 | require.Equal(t, "hello", response.GetMessage()) 52 | }, 53 | ) 54 | } 55 | 56 | func TestEchoRequestNil(t *testing.T) { 57 | t.Parallel() 58 | forEachDimension( 59 | t, 60 | func(t *testing.T, client pluginrpc.Client) { 61 | echoServiceClient, err := examplev1pluginrpc.NewEchoServiceClient(client) 62 | require.NoError(t, err) 63 | response, err := echoServiceClient.EchoRequest(context.Background(), nil) 64 | require.NoError(t, err) 65 | require.NotNil(t, response) 66 | require.Equal(t, "", response.GetMessage()) 67 | }, 68 | ) 69 | } 70 | 71 | func TestEchoList(t *testing.T) { 72 | t.Parallel() 73 | forEachDimension( 74 | t, 75 | func(t *testing.T, client pluginrpc.Client) { 76 | echoServiceClient, err := examplev1pluginrpc.NewEchoServiceClient(client) 77 | require.NoError(t, err) 78 | response, err := echoServiceClient.EchoList(context.Background(), nil) 79 | require.NoError(t, err) 80 | require.NotNil(t, response) 81 | require.Equal(t, []string{"foo", "bar"}, response.GetList()) 82 | }, 83 | ) 84 | } 85 | 86 | func TestEchoError(t *testing.T) { 87 | t.Parallel() 88 | forEachDimension( 89 | t, 90 | func(t *testing.T, client pluginrpc.Client) { 91 | echoServiceClient, err := examplev1pluginrpc.NewEchoServiceClient(client) 92 | require.NoError(t, err) 93 | _, err = echoServiceClient.EchoError( 94 | context.Background(), 95 | &examplev1.EchoErrorRequest{ 96 | Code: pluginrpcv1.Code_CODE_DEADLINE_EXCEEDED, 97 | Message: "hello", 98 | }, 99 | ) 100 | pluginrpcError := &pluginrpc.Error{} 101 | require.Error(t, err) 102 | require.ErrorAs(t, err, &pluginrpcError) 103 | require.Equal(t, pluginrpc.CodeDeadlineExceeded, pluginrpcError.Code()) 104 | unwrappedErr := pluginrpcError.Unwrap() 105 | require.Error(t, unwrappedErr) 106 | require.Equal(t, "hello", unwrappedErr.Error()) 107 | }, 108 | ) 109 | } 110 | 111 | func TestUnimplemented(t *testing.T) { 112 | t.Parallel() 113 | forEachDimension( 114 | t, 115 | func(t *testing.T, client pluginrpc.Client) { 116 | err := client.Call( 117 | context.Background(), 118 | "/foo/bar", 119 | nil, 120 | nil, 121 | ) 122 | pluginrpcError := &pluginrpc.Error{} 123 | require.Error(t, err) 124 | require.ErrorAs(t, err, &pluginrpcError) 125 | require.Equal(t, pluginrpc.CodeUnimplemented, pluginrpcError.Code()) 126 | }, 127 | ) 128 | } 129 | 130 | func forEachDimension(t *testing.T, f func(*testing.T, pluginrpc.Client)) { 131 | for _, format := range allTestFormats { 132 | for j, newClient := range []func(...pluginrpc.ClientOption) (pluginrpc.Client, error){newExecRunnerClient, newServerRunnerClient} { 133 | j := j 134 | format := format 135 | newClient := newClient 136 | t.Run( 137 | format.String()+strconv.Itoa(j), 138 | func(t *testing.T) { 139 | t.Parallel() 140 | client, err := newClient(pluginrpc.ClientWithFormat(format)) 141 | require.NoError(t, err) 142 | f(t, client) 143 | }, 144 | ) 145 | } 146 | } 147 | } 148 | 149 | func newExecRunnerClient(clientOptions ...pluginrpc.ClientOption) (pluginrpc.Client, error) { 150 | return pluginrpc.NewClient(pluginrpc.NewExecRunner(echoPluginProgramName), clientOptions...), nil 151 | } 152 | 153 | func newServerRunnerClient(clientOptions ...pluginrpc.ClientOption) (pluginrpc.Client, error) { 154 | server, err := newServer() 155 | if err != nil { 156 | return nil, err 157 | } 158 | return pluginrpc.NewClient(pluginrpc.NewServerRunner(server), clientOptions...), nil 159 | } 160 | 161 | func newServer() (pluginrpc.Server, error) { 162 | spec, err := examplev1pluginrpc.EchoServiceSpecBuilder{ 163 | // Note that EchoList does not have a ProcedureBuilder and will default to path being the only arg. 164 | EchoRequest: []pluginrpc.ProcedureOption{pluginrpc.ProcedureWithArgs("echo", "request")}, 165 | EchoError: []pluginrpc.ProcedureOption{pluginrpc.ProcedureWithArgs("echo", "error")}, 166 | }.Build() 167 | if err != nil { 168 | return nil, err 169 | } 170 | serverRegistrar := pluginrpc.NewServerRegistrar() 171 | handler := pluginrpc.NewHandler(spec) 172 | echoServiceHandler := newEchoServiceHandler() 173 | echoServiceServer := examplev1pluginrpc.NewEchoServiceServer(handler, echoServiceHandler) 174 | examplev1pluginrpc.RegisterEchoServiceServer(serverRegistrar, echoServiceServer) 175 | return pluginrpc.NewServer(spec, serverRegistrar) 176 | } 177 | 178 | type echoServiceHandler struct{} 179 | 180 | func newEchoServiceHandler() *echoServiceHandler { 181 | return &echoServiceHandler{} 182 | } 183 | 184 | func (*echoServiceHandler) EchoRequest( 185 | _ context.Context, 186 | request *examplev1.EchoRequestRequest, 187 | ) (*examplev1.EchoRequestResponse, error) { 188 | return &examplev1.EchoRequestResponse{ 189 | Message: request.GetMessage(), 190 | }, nil 191 | } 192 | 193 | func (*echoServiceHandler) EchoList( 194 | context.Context, 195 | *examplev1.EchoListRequest, 196 | ) (*examplev1.EchoListResponse, error) { 197 | return &examplev1.EchoListResponse{ 198 | List: []string{ 199 | "foo", 200 | "bar", 201 | }, 202 | }, nil 203 | } 204 | 205 | func (*echoServiceHandler) EchoError( 206 | _ context.Context, 207 | request *examplev1.EchoErrorRequest, 208 | ) (*examplev1.EchoErrorResponse, error) { 209 | return nil, pluginrpc.NewError(pluginrpc.Code(request.GetCode()), errors.New(request.GetMessage())) 210 | } 211 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pluginrpc 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | "io" 22 | "sync" 23 | 24 | pluginrpcv1 "buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go/pluginrpc/v1" 25 | ) 26 | 27 | var ( 28 | defaultStderr = io.Discard 29 | ) 30 | 31 | // Client is a client that calls plugins. 32 | // 33 | // Typically, Clients are not directly invoked. Instead, the generated code for a given 34 | // service will use a Client to call the Procedures that the service specifies. 35 | type Client interface { 36 | // Spec returns the Spec that the client receives. 37 | // 38 | // Clients will cache retrieved protocols and Specs. If it is possible that a plugin will 39 | // change during the lifetime of a Client, it is the responsibility of the caller to 40 | // create a new Client. We may change this requirement in the future. 41 | Spec(ctx context.Context) (Spec, error) 42 | // Call calls the given Procedure. 43 | // 44 | // The request will be sent over stdin, with a response being sent on stdout. 45 | // The response given will then be populated. 46 | Call( 47 | ctx context.Context, 48 | procedurePath string, 49 | request any, 50 | response any, 51 | options ...CallOption, 52 | ) error 53 | 54 | isClient() 55 | } 56 | 57 | // NewClient returns a new Client for the given Runner. 58 | func NewClient(runner Runner, options ...ClientOption) Client { 59 | return newClient(runner, options...) 60 | } 61 | 62 | // ClientOption is an option for a new Client. 63 | type ClientOption func(*clientOptions) 64 | 65 | // ClientWithStderr will result in the stderr of the plugin being propagated to the given writer. 66 | // 67 | // The default is to drop stderr. 68 | func ClientWithStderr(stderr io.Writer) ClientOption { 69 | return func(clientOptions *clientOptions) { 70 | clientOptions.stderr = stderr 71 | } 72 | } 73 | 74 | // ClientWithFormat will result in the given Format being used for requests 75 | // and responses. 76 | // 77 | // The default is FormatBinary. 78 | func ClientWithFormat(format Format) ClientOption { 79 | return func(clientOptions *clientOptions) { 80 | clientOptions.format = format 81 | } 82 | } 83 | 84 | // CallOption is an option for an individual client call. 85 | type CallOption func(*callOptions) 86 | 87 | // *** PRIVATE *** 88 | 89 | type client struct { 90 | runner Runner 91 | stderr io.Writer 92 | format Format 93 | 94 | spec Spec 95 | specErr error 96 | lock sync.RWMutex 97 | } 98 | 99 | func newClient( 100 | runner Runner, 101 | options ...ClientOption, 102 | ) *client { 103 | clientOptions := newClientOptions() 104 | for _, option := range options { 105 | option(clientOptions) 106 | } 107 | if clientOptions.stderr == nil { 108 | clientOptions.stderr = defaultStderr 109 | } 110 | if clientOptions.format == 0 { 111 | clientOptions.format = FormatBinary 112 | } 113 | return &client{ 114 | runner: runner, 115 | stderr: clientOptions.stderr, 116 | format: clientOptions.format, 117 | } 118 | } 119 | 120 | // TODO: Provide ability for Spec to be invalidated via cache invalidate. 121 | // 122 | // One way this could look: A request sends over a "spec ID", which is an ID that is returned when 123 | // getting a spec from a plugin. If the plugin does not currently match this spec ID, an error 124 | // is returned on the response, and the client invalidates the Spec cache, and retries. This will 125 | // be desirable for situations where clients are long-lived, for example in services. 126 | func (c *client) Spec(ctx context.Context) (Spec, error) { 127 | // Difficult to use sync.OnceValues since we want to use the context for cancellation 128 | // when passing to the runner. It's awkward if the client constructor took a conteext. 129 | c.lock.RLock() 130 | if c.spec != nil || c.specErr != nil { 131 | c.lock.RUnlock() 132 | return c.spec, c.specErr 133 | } 134 | c.lock.RUnlock() 135 | 136 | c.lock.Lock() 137 | defer c.lock.Unlock() 138 | 139 | if c.spec != nil || c.specErr != nil { 140 | return c.spec, c.specErr 141 | } 142 | c.spec, c.specErr = c.getSpecUncached(ctx) 143 | return c.spec, c.specErr 144 | } 145 | 146 | func (c *client) Call( 147 | ctx context.Context, 148 | procedurePath string, 149 | request any, 150 | response any, 151 | _ ...CallOption, 152 | ) error { 153 | // Could make the constructor return an error and validate this at construction 154 | // but it seems like a bad ROI for such a simple check. 155 | if err := validateFormat(c.format); err != nil { 156 | return err 157 | } 158 | spec, err := c.Spec(ctx) 159 | if err != nil { 160 | return err 161 | } 162 | procedure := spec.ProcedureForPath(procedurePath) 163 | if procedure == nil { 164 | return NewErrorf(CodeUnimplemented, "procedure unimplemented: %q", procedurePath) 165 | } 166 | data, err := marshalRequest(c.format, request) 167 | if err != nil { 168 | return err 169 | } 170 | stdin := bytes.NewReader(data) 171 | stdout := bytes.NewBuffer(nil) 172 | args := procedure.Args() 173 | if len(args) == 0 { 174 | args = []string{procedure.Path()} 175 | } 176 | args = append(args, "--"+FormatFlagName, c.format.String()) 177 | if err := c.runner.Run( 178 | ctx, 179 | Env{ 180 | Args: args, 181 | Stdin: stdin, 182 | Stdout: stdout, 183 | Stderr: c.stderr, 184 | }, 185 | ); err != nil { 186 | return WrapExitError(err) 187 | } 188 | return unmarshalResponse(c.format, stdout.Bytes(), response) 189 | } 190 | 191 | func (*client) isClient() {} 192 | 193 | func (c *client) getSpecUncached(ctx context.Context) (Spec, error) { 194 | if err := c.checkProtocolVersion(ctx); err != nil { 195 | return nil, err 196 | } 197 | stdout := bytes.NewBuffer(nil) 198 | if err := c.runner.Run( 199 | ctx, 200 | Env{ 201 | Args: []string{"--" + SpecFlagName, "--" + FormatFlagName, c.format.String()}, 202 | Stdout: stdout, 203 | Stderr: c.stderr, 204 | }, 205 | ); err != nil { 206 | return nil, err 207 | } 208 | data := stdout.Bytes() 209 | if len(data) == 0 { 210 | return nil, fmt.Errorf("--%s did not return a spec", SpecFlagName) 211 | } 212 | protoSpec := &pluginrpcv1.Spec{} 213 | if err := unmarshalSpec(c.format, data, protoSpec); err != nil { 214 | return nil, fmt.Errorf("--%s did not return a properly-formed spec: %w", SpecFlagName, err) 215 | } 216 | return NewSpecForProto(protoSpec) 217 | } 218 | 219 | func (c *client) checkProtocolVersion(ctx context.Context) error { 220 | version, err := c.getProtocolVersionUncached(ctx) 221 | if err != nil { 222 | return err 223 | } 224 | if version != protocolVersion { 225 | return fmt.Errorf("--%s returned unknown protocol version %d", ProtocolFlagName, version) 226 | } 227 | return nil 228 | } 229 | 230 | func (c *client) getProtocolVersionUncached(ctx context.Context) (int, error) { 231 | stdout := bytes.NewBuffer(nil) 232 | if err := c.runner.Run( 233 | ctx, 234 | Env{ 235 | Args: []string{"--" + ProtocolFlagName}, 236 | Stdout: stdout, 237 | Stderr: c.stderr, 238 | }, 239 | ); err != nil { 240 | return 0, err 241 | } 242 | data := stdout.Bytes() 243 | if len(data) == 0 { 244 | return 0, fmt.Errorf("--%s did not return a protocol version", ProtocolFlagName) 245 | } 246 | version, err := unmarshalProtocol(data) 247 | if err != nil { 248 | return 0, fmt.Errorf("--%s did not return a properly-formed protocol version: %w", ProtocolFlagName, err) 249 | } 250 | return version, nil 251 | } 252 | 253 | type clientOptions struct { 254 | stderr io.Writer 255 | format Format 256 | } 257 | 258 | func newClientOptions() *clientOptions { 259 | return &clientOptions{} 260 | } 261 | 262 | type callOptions struct{} 263 | -------------------------------------------------------------------------------- /internal/example/gen/pluginrpc/example/v1/examplev1pluginrpc/example.pluginrpc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by protoc-gen-pluginrpc-go. DO NOT EDIT. 16 | // 17 | // Source: pluginrpc/example/v1/example.proto 18 | 19 | package examplev1pluginrpc 20 | 21 | import ( 22 | context "context" 23 | fmt "fmt" 24 | pluginrpc "pluginrpc.com/pluginrpc" 25 | v1 "pluginrpc.com/pluginrpc/internal/example/gen/pluginrpc/example/v1" 26 | ) 27 | 28 | // This is a compile-time assertion to ensure that this generated file and the pluginrpc package are 29 | // compatible. If you get a compiler error that this constant is not defined, this code was 30 | // generated with a version of pluginrpc newer than the one compiled into your binary. You can fix 31 | // the problem by either regenerating this code with an older version of pluginrpc or updating the 32 | // pluginrpc version compiled into your binary. 33 | const _ = pluginrpc.IsAtLeastVersion0_1_0 34 | 35 | const ( 36 | // EchoServiceEchoRequestPath is the path of the EchoService's EchoRequest RPC. 37 | EchoServiceEchoRequestPath = "/pluginrpc.example.v1.EchoService/EchoRequest" 38 | // EchoServiceEchoErrorPath is the path of the EchoService's EchoError RPC. 39 | EchoServiceEchoErrorPath = "/pluginrpc.example.v1.EchoService/EchoError" 40 | // EchoServiceEchoListPath is the path of the EchoService's EchoList RPC. 41 | EchoServiceEchoListPath = "/pluginrpc.example.v1.EchoService/EchoList" 42 | ) 43 | 44 | // EchoServiceSpecBuilder builds a Spec for the pluginrpc.example.v1.EchoService service. 45 | type EchoServiceSpecBuilder struct { 46 | EchoRequest []pluginrpc.ProcedureOption 47 | EchoError []pluginrpc.ProcedureOption 48 | EchoList []pluginrpc.ProcedureOption 49 | } 50 | 51 | // Build builds a Spec for the pluginrpc.example.v1.EchoService service. 52 | func (s EchoServiceSpecBuilder) Build() (pluginrpc.Spec, error) { 53 | procedures := make([]pluginrpc.Procedure, 0, 3) 54 | procedure, err := pluginrpc.NewProcedure(EchoServiceEchoRequestPath, s.EchoRequest...) 55 | if err != nil { 56 | return nil, err 57 | } 58 | procedures = append(procedures, procedure) 59 | procedure, err = pluginrpc.NewProcedure(EchoServiceEchoErrorPath, s.EchoError...) 60 | if err != nil { 61 | return nil, err 62 | } 63 | procedures = append(procedures, procedure) 64 | procedure, err = pluginrpc.NewProcedure(EchoServiceEchoListPath, s.EchoList...) 65 | if err != nil { 66 | return nil, err 67 | } 68 | procedures = append(procedures, procedure) 69 | return pluginrpc.NewSpec(procedures...) 70 | } 71 | 72 | // EchoServiceClient is a client for the pluginrpc.example.v1.EchoService service. 73 | type EchoServiceClient interface { 74 | // Echo the request back. 75 | EchoRequest(context.Context, *v1.EchoRequestRequest, ...pluginrpc.CallOption) (*v1.EchoRequestResponse, error) 76 | // Echo the error specified back as an error. 77 | EchoError(context.Context, *v1.EchoErrorRequest, ...pluginrpc.CallOption) (*v1.EchoErrorResponse, error) 78 | // Echo a static list ["foo", "bar"] back given an empty request. 79 | EchoList(context.Context, *v1.EchoListRequest, ...pluginrpc.CallOption) (*v1.EchoListResponse, error) 80 | } 81 | 82 | // NewEchoServiceClient constructs a client for the pluginrpc.example.v1.EchoService service. 83 | func NewEchoServiceClient(client pluginrpc.Client) (EchoServiceClient, error) { 84 | return &echoServiceClient{ 85 | client: client, 86 | }, nil 87 | } 88 | 89 | // EchoServiceHandler is an implementation of the pluginrpc.example.v1.EchoService service. 90 | type EchoServiceHandler interface { 91 | // Echo the request back. 92 | EchoRequest(context.Context, *v1.EchoRequestRequest) (*v1.EchoRequestResponse, error) 93 | // Echo the error specified back as an error. 94 | EchoError(context.Context, *v1.EchoErrorRequest) (*v1.EchoErrorResponse, error) 95 | // Echo a static list ["foo", "bar"] back given an empty request. 96 | EchoList(context.Context, *v1.EchoListRequest) (*v1.EchoListResponse, error) 97 | } 98 | 99 | // EchoServiceServer serves the pluginrpc.example.v1.EchoService service. 100 | type EchoServiceServer interface { 101 | // Echo the request back. 102 | EchoRequest(context.Context, pluginrpc.HandleEnv, ...pluginrpc.HandleOption) error 103 | // Echo the error specified back as an error. 104 | EchoError(context.Context, pluginrpc.HandleEnv, ...pluginrpc.HandleOption) error 105 | // Echo a static list ["foo", "bar"] back given an empty request. 106 | EchoList(context.Context, pluginrpc.HandleEnv, ...pluginrpc.HandleOption) error 107 | } 108 | 109 | // NewEchoServiceServer constructs a server for the pluginrpc.example.v1.EchoService service. 110 | func NewEchoServiceServer(handler pluginrpc.Handler, echoServiceHandler EchoServiceHandler) EchoServiceServer { 111 | return &echoServiceServer{ 112 | handler: handler, 113 | echoServiceHandler: echoServiceHandler, 114 | } 115 | } 116 | 117 | // RegisterEchoServiceServer registers the server for the pluginrpc.example.v1.EchoService service. 118 | func RegisterEchoServiceServer(serverRegistrar pluginrpc.ServerRegistrar, echoServiceServer EchoServiceServer) { 119 | serverRegistrar.Register(EchoServiceEchoRequestPath, echoServiceServer.EchoRequest) 120 | serverRegistrar.Register(EchoServiceEchoErrorPath, echoServiceServer.EchoError) 121 | serverRegistrar.Register(EchoServiceEchoListPath, echoServiceServer.EchoList) 122 | } 123 | 124 | // *** PRIVATE *** 125 | 126 | // echoServiceClient implements EchoServiceClient. 127 | type echoServiceClient struct { 128 | client pluginrpc.Client 129 | } 130 | 131 | // EchoRequest calls pluginrpc.example.v1.EchoService.EchoRequest. 132 | func (c *echoServiceClient) EchoRequest(ctx context.Context, req *v1.EchoRequestRequest, opts ...pluginrpc.CallOption) (*v1.EchoRequestResponse, error) { 133 | res := &v1.EchoRequestResponse{} 134 | if err := c.client.Call(ctx, EchoServiceEchoRequestPath, req, res, opts...); err != nil { 135 | return nil, err 136 | } 137 | return res, nil 138 | } 139 | 140 | // EchoError calls pluginrpc.example.v1.EchoService.EchoError. 141 | func (c *echoServiceClient) EchoError(ctx context.Context, req *v1.EchoErrorRequest, opts ...pluginrpc.CallOption) (*v1.EchoErrorResponse, error) { 142 | res := &v1.EchoErrorResponse{} 143 | if err := c.client.Call(ctx, EchoServiceEchoErrorPath, req, res, opts...); err != nil { 144 | return nil, err 145 | } 146 | return res, nil 147 | } 148 | 149 | // EchoList calls pluginrpc.example.v1.EchoService.EchoList. 150 | func (c *echoServiceClient) EchoList(ctx context.Context, req *v1.EchoListRequest, opts ...pluginrpc.CallOption) (*v1.EchoListResponse, error) { 151 | res := &v1.EchoListResponse{} 152 | if err := c.client.Call(ctx, EchoServiceEchoListPath, req, res, opts...); err != nil { 153 | return nil, err 154 | } 155 | return res, nil 156 | } 157 | 158 | // echoServiceServer implements EchoServiceServer. 159 | type echoServiceServer struct { 160 | handler pluginrpc.Handler 161 | echoServiceHandler EchoServiceHandler 162 | } 163 | 164 | // EchoRequest calls pluginrpc.example.v1.EchoService.EchoRequest. 165 | func (c *echoServiceServer) EchoRequest(ctx context.Context, handleEnv pluginrpc.HandleEnv, options ...pluginrpc.HandleOption) error { 166 | return c.handler.Handle( 167 | ctx, 168 | handleEnv, 169 | &v1.EchoRequestRequest{}, 170 | func(ctx context.Context, anyReq any) (any, error) { 171 | req, ok := anyReq.(*v1.EchoRequestRequest) 172 | if !ok { 173 | return nil, fmt.Errorf("could not cast %T to a *v1.EchoRequestRequest", anyReq) 174 | } 175 | return c.echoServiceHandler.EchoRequest(ctx, req) 176 | }, 177 | options..., 178 | ) 179 | } 180 | 181 | // EchoError calls pluginrpc.example.v1.EchoService.EchoError. 182 | func (c *echoServiceServer) EchoError(ctx context.Context, handleEnv pluginrpc.HandleEnv, options ...pluginrpc.HandleOption) error { 183 | return c.handler.Handle( 184 | ctx, 185 | handleEnv, 186 | &v1.EchoErrorRequest{}, 187 | func(ctx context.Context, anyReq any) (any, error) { 188 | req, ok := anyReq.(*v1.EchoErrorRequest) 189 | if !ok { 190 | return nil, fmt.Errorf("could not cast %T to a *v1.EchoErrorRequest", anyReq) 191 | } 192 | return c.echoServiceHandler.EchoError(ctx, req) 193 | }, 194 | options..., 195 | ) 196 | } 197 | 198 | // EchoList calls pluginrpc.example.v1.EchoService.EchoList. 199 | func (c *echoServiceServer) EchoList(ctx context.Context, handleEnv pluginrpc.HandleEnv, options ...pluginrpc.HandleOption) error { 200 | return c.handler.Handle( 201 | ctx, 202 | handleEnv, 203 | &v1.EchoListRequest{}, 204 | func(ctx context.Context, anyReq any) (any, error) { 205 | req, ok := anyReq.(*v1.EchoListRequest) 206 | if !ok { 207 | return nil, fmt.Errorf("could not cast %T to a *v1.EchoListRequest", anyReq) 208 | } 209 | return c.echoServiceHandler.EchoList(ctx, req) 210 | }, 211 | options..., 212 | ) 213 | } 214 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Buf Technologies, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /internal/example/gen/pluginrpc/example/v1/example.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by protoc-gen-go. DO NOT EDIT. 16 | // versions: 17 | // protoc-gen-go v1.34.2 18 | // protoc (unknown) 19 | // source: pluginrpc/example/v1/example.proto 20 | 21 | package examplev1 22 | 23 | import ( 24 | v1 "buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go/pluginrpc/v1" 25 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 26 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 27 | reflect "reflect" 28 | sync "sync" 29 | ) 30 | 31 | const ( 32 | // Verify that this generated code is sufficiently up-to-date. 33 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 34 | // Verify that runtime/protoimpl is sufficiently up-to-date. 35 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 36 | ) 37 | 38 | // A request to echo the given message. 39 | type EchoRequestRequest struct { 40 | state protoimpl.MessageState 41 | sizeCache protoimpl.SizeCache 42 | unknownFields protoimpl.UnknownFields 43 | 44 | // The message to echo back. 45 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` 46 | } 47 | 48 | func (x *EchoRequestRequest) Reset() { 49 | *x = EchoRequestRequest{} 50 | if protoimpl.UnsafeEnabled { 51 | mi := &file_pluginrpc_example_v1_example_proto_msgTypes[0] 52 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 53 | ms.StoreMessageInfo(mi) 54 | } 55 | } 56 | 57 | func (x *EchoRequestRequest) String() string { 58 | return protoimpl.X.MessageStringOf(x) 59 | } 60 | 61 | func (*EchoRequestRequest) ProtoMessage() {} 62 | 63 | func (x *EchoRequestRequest) ProtoReflect() protoreflect.Message { 64 | mi := &file_pluginrpc_example_v1_example_proto_msgTypes[0] 65 | if protoimpl.UnsafeEnabled && x != nil { 66 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 67 | if ms.LoadMessageInfo() == nil { 68 | ms.StoreMessageInfo(mi) 69 | } 70 | return ms 71 | } 72 | return mi.MessageOf(x) 73 | } 74 | 75 | // Deprecated: Use EchoRequestRequest.ProtoReflect.Descriptor instead. 76 | func (*EchoRequestRequest) Descriptor() ([]byte, []int) { 77 | return file_pluginrpc_example_v1_example_proto_rawDescGZIP(), []int{0} 78 | } 79 | 80 | func (x *EchoRequestRequest) GetMessage() string { 81 | if x != nil { 82 | return x.Message 83 | } 84 | return "" 85 | } 86 | 87 | // A response to echo. 88 | type EchoRequestResponse struct { 89 | state protoimpl.MessageState 90 | sizeCache protoimpl.SizeCache 91 | unknownFields protoimpl.UnknownFields 92 | 93 | // The echoed message. 94 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` 95 | } 96 | 97 | func (x *EchoRequestResponse) Reset() { 98 | *x = EchoRequestResponse{} 99 | if protoimpl.UnsafeEnabled { 100 | mi := &file_pluginrpc_example_v1_example_proto_msgTypes[1] 101 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 102 | ms.StoreMessageInfo(mi) 103 | } 104 | } 105 | 106 | func (x *EchoRequestResponse) String() string { 107 | return protoimpl.X.MessageStringOf(x) 108 | } 109 | 110 | func (*EchoRequestResponse) ProtoMessage() {} 111 | 112 | func (x *EchoRequestResponse) ProtoReflect() protoreflect.Message { 113 | mi := &file_pluginrpc_example_v1_example_proto_msgTypes[1] 114 | if protoimpl.UnsafeEnabled && x != nil { 115 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 116 | if ms.LoadMessageInfo() == nil { 117 | ms.StoreMessageInfo(mi) 118 | } 119 | return ms 120 | } 121 | return mi.MessageOf(x) 122 | } 123 | 124 | // Deprecated: Use EchoRequestResponse.ProtoReflect.Descriptor instead. 125 | func (*EchoRequestResponse) Descriptor() ([]byte, []int) { 126 | return file_pluginrpc_example_v1_example_proto_rawDescGZIP(), []int{1} 127 | } 128 | 129 | func (x *EchoRequestResponse) GetMessage() string { 130 | if x != nil { 131 | return x.Message 132 | } 133 | return "" 134 | } 135 | 136 | // An error to echo back. 137 | type EchoErrorRequest struct { 138 | state protoimpl.MessageState 139 | sizeCache protoimpl.SizeCache 140 | unknownFields protoimpl.UnknownFields 141 | 142 | // The error code to return as part of the error. 143 | Code v1.Code `protobuf:"varint,1,opt,name=code,proto3,enum=pluginrpc.v1.Code" json:"code,omitempty"` 144 | // The error message to return as part of the error. 145 | Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` 146 | } 147 | 148 | func (x *EchoErrorRequest) Reset() { 149 | *x = EchoErrorRequest{} 150 | if protoimpl.UnsafeEnabled { 151 | mi := &file_pluginrpc_example_v1_example_proto_msgTypes[2] 152 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 153 | ms.StoreMessageInfo(mi) 154 | } 155 | } 156 | 157 | func (x *EchoErrorRequest) String() string { 158 | return protoimpl.X.MessageStringOf(x) 159 | } 160 | 161 | func (*EchoErrorRequest) ProtoMessage() {} 162 | 163 | func (x *EchoErrorRequest) ProtoReflect() protoreflect.Message { 164 | mi := &file_pluginrpc_example_v1_example_proto_msgTypes[2] 165 | if protoimpl.UnsafeEnabled && x != nil { 166 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 167 | if ms.LoadMessageInfo() == nil { 168 | ms.StoreMessageInfo(mi) 169 | } 170 | return ms 171 | } 172 | return mi.MessageOf(x) 173 | } 174 | 175 | // Deprecated: Use EchoErrorRequest.ProtoReflect.Descriptor instead. 176 | func (*EchoErrorRequest) Descriptor() ([]byte, []int) { 177 | return file_pluginrpc_example_v1_example_proto_rawDescGZIP(), []int{2} 178 | } 179 | 180 | func (x *EchoErrorRequest) GetCode() v1.Code { 181 | if x != nil { 182 | return x.Code 183 | } 184 | return v1.Code(0) 185 | } 186 | 187 | func (x *EchoErrorRequest) GetMessage() string { 188 | if x != nil { 189 | return x.Message 190 | } 191 | return "" 192 | } 193 | 194 | // A blank response. 195 | type EchoErrorResponse struct { 196 | state protoimpl.MessageState 197 | sizeCache protoimpl.SizeCache 198 | unknownFields protoimpl.UnknownFields 199 | } 200 | 201 | func (x *EchoErrorResponse) Reset() { 202 | *x = EchoErrorResponse{} 203 | if protoimpl.UnsafeEnabled { 204 | mi := &file_pluginrpc_example_v1_example_proto_msgTypes[3] 205 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 206 | ms.StoreMessageInfo(mi) 207 | } 208 | } 209 | 210 | func (x *EchoErrorResponse) String() string { 211 | return protoimpl.X.MessageStringOf(x) 212 | } 213 | 214 | func (*EchoErrorResponse) ProtoMessage() {} 215 | 216 | func (x *EchoErrorResponse) ProtoReflect() protoreflect.Message { 217 | mi := &file_pluginrpc_example_v1_example_proto_msgTypes[3] 218 | if protoimpl.UnsafeEnabled && x != nil { 219 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 220 | if ms.LoadMessageInfo() == nil { 221 | ms.StoreMessageInfo(mi) 222 | } 223 | return ms 224 | } 225 | return mi.MessageOf(x) 226 | } 227 | 228 | // Deprecated: Use EchoErrorResponse.ProtoReflect.Descriptor instead. 229 | func (*EchoErrorResponse) Descriptor() ([]byte, []int) { 230 | return file_pluginrpc_example_v1_example_proto_rawDescGZIP(), []int{3} 231 | } 232 | 233 | // A request to echo a static list back The request is purposefully 234 | // empty to demonstrate how pluginrpc works with empty requests. 235 | type EchoListRequest struct { 236 | state protoimpl.MessageState 237 | sizeCache protoimpl.SizeCache 238 | unknownFields protoimpl.UnknownFields 239 | } 240 | 241 | func (x *EchoListRequest) Reset() { 242 | *x = EchoListRequest{} 243 | if protoimpl.UnsafeEnabled { 244 | mi := &file_pluginrpc_example_v1_example_proto_msgTypes[4] 245 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 246 | ms.StoreMessageInfo(mi) 247 | } 248 | } 249 | 250 | func (x *EchoListRequest) String() string { 251 | return protoimpl.X.MessageStringOf(x) 252 | } 253 | 254 | func (*EchoListRequest) ProtoMessage() {} 255 | 256 | func (x *EchoListRequest) ProtoReflect() protoreflect.Message { 257 | mi := &file_pluginrpc_example_v1_example_proto_msgTypes[4] 258 | if protoimpl.UnsafeEnabled && x != nil { 259 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 260 | if ms.LoadMessageInfo() == nil { 261 | ms.StoreMessageInfo(mi) 262 | } 263 | return ms 264 | } 265 | return mi.MessageOf(x) 266 | } 267 | 268 | // Deprecated: Use EchoListRequest.ProtoReflect.Descriptor instead. 269 | func (*EchoListRequest) Descriptor() ([]byte, []int) { 270 | return file_pluginrpc_example_v1_example_proto_rawDescGZIP(), []int{4} 271 | } 272 | 273 | // A response that will always contain the list ["foo", "bar"]. 274 | type EchoListResponse struct { 275 | state protoimpl.MessageState 276 | sizeCache protoimpl.SizeCache 277 | unknownFields protoimpl.UnknownFields 278 | 279 | // The list that will always be ["foo", "bar"]. 280 | List []string `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` 281 | } 282 | 283 | func (x *EchoListResponse) Reset() { 284 | *x = EchoListResponse{} 285 | if protoimpl.UnsafeEnabled { 286 | mi := &file_pluginrpc_example_v1_example_proto_msgTypes[5] 287 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 288 | ms.StoreMessageInfo(mi) 289 | } 290 | } 291 | 292 | func (x *EchoListResponse) String() string { 293 | return protoimpl.X.MessageStringOf(x) 294 | } 295 | 296 | func (*EchoListResponse) ProtoMessage() {} 297 | 298 | func (x *EchoListResponse) ProtoReflect() protoreflect.Message { 299 | mi := &file_pluginrpc_example_v1_example_proto_msgTypes[5] 300 | if protoimpl.UnsafeEnabled && x != nil { 301 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 302 | if ms.LoadMessageInfo() == nil { 303 | ms.StoreMessageInfo(mi) 304 | } 305 | return ms 306 | } 307 | return mi.MessageOf(x) 308 | } 309 | 310 | // Deprecated: Use EchoListResponse.ProtoReflect.Descriptor instead. 311 | func (*EchoListResponse) Descriptor() ([]byte, []int) { 312 | return file_pluginrpc_example_v1_example_proto_rawDescGZIP(), []int{5} 313 | } 314 | 315 | func (x *EchoListResponse) GetList() []string { 316 | if x != nil { 317 | return x.List 318 | } 319 | return nil 320 | } 321 | 322 | var File_pluginrpc_example_v1_example_proto protoreflect.FileDescriptor 323 | 324 | var file_pluginrpc_example_v1_example_proto_rawDesc = []byte{ 325 | 0x0a, 0x22, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x2f, 0x65, 0x78, 0x61, 0x6d, 326 | 0x70, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x70, 327 | 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x14, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x2e, 328 | 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x70, 0x6c, 0x75, 0x67, 329 | 0x69, 0x6e, 0x72, 0x70, 0x63, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 330 | 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2e, 0x0a, 0x12, 0x45, 0x63, 0x68, 0x6f, 331 | 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 332 | 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 333 | 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x2f, 0x0a, 0x13, 0x45, 0x63, 0x68, 0x6f, 334 | 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 335 | 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 336 | 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x54, 0x0a, 0x10, 0x45, 0x63, 0x68, 337 | 0x6f, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 338 | 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x70, 0x6c, 339 | 0x75, 0x67, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x64, 0x65, 0x52, 340 | 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 341 | 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 342 | 0x13, 0x0a, 0x11, 0x45, 0x63, 0x68, 0x6f, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 343 | 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x11, 0x0a, 0x0f, 0x45, 0x63, 0x68, 0x6f, 0x4c, 0x69, 0x73, 0x74, 344 | 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x26, 0x0a, 0x10, 0x45, 0x63, 0x68, 0x6f, 0x4c, 345 | 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6c, 346 | 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x32, 347 | 0xaa, 0x02, 0x0a, 0x0b, 0x45, 0x63, 0x68, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 348 | 0x62, 0x0a, 0x0b, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 349 | 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 350 | 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 351 | 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 352 | 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 353 | 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 354 | 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x09, 0x45, 0x63, 0x68, 0x6f, 0x45, 0x72, 0x72, 0x6f, 0x72, 355 | 0x12, 0x26, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x65, 0x78, 0x61, 356 | 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x45, 0x72, 0x72, 0x6f, 357 | 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 358 | 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 359 | 0x45, 0x63, 0x68, 0x6f, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 360 | 0x65, 0x12, 0x59, 0x0a, 0x08, 0x45, 0x63, 0x68, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x25, 0x2e, 361 | 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 362 | 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 363 | 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 0x70, 0x63, 364 | 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x63, 0x68, 0x6f, 365 | 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xe7, 0x01, 0x0a, 366 | 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x65, 367 | 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x45, 0x78, 0x61, 0x6d, 0x70, 368 | 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4b, 0x70, 0x6c, 0x75, 0x67, 0x69, 369 | 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 370 | 0x70, 0x63, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x65, 0x78, 0x61, 0x6d, 371 | 0x70, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 0x70, 372 | 0x63, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x65, 0x78, 0x61, 373 | 0x6d, 0x70, 0x6c, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x50, 0x45, 0x58, 0xaa, 0x02, 0x14, 0x50, 374 | 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 375 | 0x2e, 0x56, 0x31, 0xca, 0x02, 0x14, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x5c, 376 | 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x20, 0x50, 0x6c, 0x75, 377 | 0x67, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x5c, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x5c, 0x56, 378 | 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x16, 379 | 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x3a, 0x3a, 0x45, 0x78, 0x61, 0x6d, 0x70, 380 | 0x6c, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 381 | } 382 | 383 | var ( 384 | file_pluginrpc_example_v1_example_proto_rawDescOnce sync.Once 385 | file_pluginrpc_example_v1_example_proto_rawDescData = file_pluginrpc_example_v1_example_proto_rawDesc 386 | ) 387 | 388 | func file_pluginrpc_example_v1_example_proto_rawDescGZIP() []byte { 389 | file_pluginrpc_example_v1_example_proto_rawDescOnce.Do(func() { 390 | file_pluginrpc_example_v1_example_proto_rawDescData = protoimpl.X.CompressGZIP(file_pluginrpc_example_v1_example_proto_rawDescData) 391 | }) 392 | return file_pluginrpc_example_v1_example_proto_rawDescData 393 | } 394 | 395 | var file_pluginrpc_example_v1_example_proto_msgTypes = make([]protoimpl.MessageInfo, 6) 396 | var file_pluginrpc_example_v1_example_proto_goTypes = []any{ 397 | (*EchoRequestRequest)(nil), // 0: pluginrpc.example.v1.EchoRequestRequest 398 | (*EchoRequestResponse)(nil), // 1: pluginrpc.example.v1.EchoRequestResponse 399 | (*EchoErrorRequest)(nil), // 2: pluginrpc.example.v1.EchoErrorRequest 400 | (*EchoErrorResponse)(nil), // 3: pluginrpc.example.v1.EchoErrorResponse 401 | (*EchoListRequest)(nil), // 4: pluginrpc.example.v1.EchoListRequest 402 | (*EchoListResponse)(nil), // 5: pluginrpc.example.v1.EchoListResponse 403 | (v1.Code)(0), // 6: pluginrpc.v1.Code 404 | } 405 | var file_pluginrpc_example_v1_example_proto_depIdxs = []int32{ 406 | 6, // 0: pluginrpc.example.v1.EchoErrorRequest.code:type_name -> pluginrpc.v1.Code 407 | 0, // 1: pluginrpc.example.v1.EchoService.EchoRequest:input_type -> pluginrpc.example.v1.EchoRequestRequest 408 | 2, // 2: pluginrpc.example.v1.EchoService.EchoError:input_type -> pluginrpc.example.v1.EchoErrorRequest 409 | 4, // 3: pluginrpc.example.v1.EchoService.EchoList:input_type -> pluginrpc.example.v1.EchoListRequest 410 | 1, // 4: pluginrpc.example.v1.EchoService.EchoRequest:output_type -> pluginrpc.example.v1.EchoRequestResponse 411 | 3, // 5: pluginrpc.example.v1.EchoService.EchoError:output_type -> pluginrpc.example.v1.EchoErrorResponse 412 | 5, // 6: pluginrpc.example.v1.EchoService.EchoList:output_type -> pluginrpc.example.v1.EchoListResponse 413 | 4, // [4:7] is the sub-list for method output_type 414 | 1, // [1:4] is the sub-list for method input_type 415 | 1, // [1:1] is the sub-list for extension type_name 416 | 1, // [1:1] is the sub-list for extension extendee 417 | 0, // [0:1] is the sub-list for field type_name 418 | } 419 | 420 | func init() { file_pluginrpc_example_v1_example_proto_init() } 421 | func file_pluginrpc_example_v1_example_proto_init() { 422 | if File_pluginrpc_example_v1_example_proto != nil { 423 | return 424 | } 425 | if !protoimpl.UnsafeEnabled { 426 | file_pluginrpc_example_v1_example_proto_msgTypes[0].Exporter = func(v any, i int) any { 427 | switch v := v.(*EchoRequestRequest); i { 428 | case 0: 429 | return &v.state 430 | case 1: 431 | return &v.sizeCache 432 | case 2: 433 | return &v.unknownFields 434 | default: 435 | return nil 436 | } 437 | } 438 | file_pluginrpc_example_v1_example_proto_msgTypes[1].Exporter = func(v any, i int) any { 439 | switch v := v.(*EchoRequestResponse); i { 440 | case 0: 441 | return &v.state 442 | case 1: 443 | return &v.sizeCache 444 | case 2: 445 | return &v.unknownFields 446 | default: 447 | return nil 448 | } 449 | } 450 | file_pluginrpc_example_v1_example_proto_msgTypes[2].Exporter = func(v any, i int) any { 451 | switch v := v.(*EchoErrorRequest); i { 452 | case 0: 453 | return &v.state 454 | case 1: 455 | return &v.sizeCache 456 | case 2: 457 | return &v.unknownFields 458 | default: 459 | return nil 460 | } 461 | } 462 | file_pluginrpc_example_v1_example_proto_msgTypes[3].Exporter = func(v any, i int) any { 463 | switch v := v.(*EchoErrorResponse); i { 464 | case 0: 465 | return &v.state 466 | case 1: 467 | return &v.sizeCache 468 | case 2: 469 | return &v.unknownFields 470 | default: 471 | return nil 472 | } 473 | } 474 | file_pluginrpc_example_v1_example_proto_msgTypes[4].Exporter = func(v any, i int) any { 475 | switch v := v.(*EchoListRequest); i { 476 | case 0: 477 | return &v.state 478 | case 1: 479 | return &v.sizeCache 480 | case 2: 481 | return &v.unknownFields 482 | default: 483 | return nil 484 | } 485 | } 486 | file_pluginrpc_example_v1_example_proto_msgTypes[5].Exporter = func(v any, i int) any { 487 | switch v := v.(*EchoListResponse); i { 488 | case 0: 489 | return &v.state 490 | case 1: 491 | return &v.sizeCache 492 | case 2: 493 | return &v.unknownFields 494 | default: 495 | return nil 496 | } 497 | } 498 | } 499 | type x struct{} 500 | out := protoimpl.TypeBuilder{ 501 | File: protoimpl.DescBuilder{ 502 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 503 | RawDescriptor: file_pluginrpc_example_v1_example_proto_rawDesc, 504 | NumEnums: 0, 505 | NumMessages: 6, 506 | NumExtensions: 0, 507 | NumServices: 1, 508 | }, 509 | GoTypes: file_pluginrpc_example_v1_example_proto_goTypes, 510 | DependencyIndexes: file_pluginrpc_example_v1_example_proto_depIdxs, 511 | MessageInfos: file_pluginrpc_example_v1_example_proto_msgTypes, 512 | }.Build() 513 | File_pluginrpc_example_v1_example_proto = out.File 514 | file_pluginrpc_example_v1_example_proto_rawDesc = nil 515 | file_pluginrpc_example_v1_example_proto_goTypes = nil 516 | file_pluginrpc_example_v1_example_proto_depIdxs = nil 517 | } 518 | -------------------------------------------------------------------------------- /cmd/protoc-gen-pluginrpc-go/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "os" 21 | "path" 22 | "path/filepath" 23 | "strings" 24 | "unicode/utf8" 25 | 26 | "google.golang.org/protobuf/compiler/protogen" 27 | "google.golang.org/protobuf/reflect/protoreflect" 28 | "google.golang.org/protobuf/types/descriptorpb" 29 | "google.golang.org/protobuf/types/pluginpb" 30 | "pluginrpc.com/pluginrpc" 31 | ) 32 | 33 | const ( 34 | contextPackage = protogen.GoImportPath("context") 35 | fmtPackage = protogen.GoImportPath("fmt") 36 | pluginrpcPackage = protogen.GoImportPath("pluginrpc.com/pluginrpc") 37 | 38 | generatedFilenameExtension = ".pluginrpc.go" 39 | generatedPackageSuffix = "pluginrpc" 40 | 41 | usage = "Flags:\n -h, --help\tPrint this help and exit.\n --version\tPrint the version and exit." 42 | 43 | optionStreamingKey = "streaming" 44 | optionStreamingValueError = "error" 45 | optionStreamingValueWarn = "warn" 46 | optionStreamingValueIgnore = "ignore" 47 | 48 | commentWidth = 97 // leave room for "// " 49 | 50 | // To propagate top-level comments, we need the field number of the syntax 51 | // declaration and the package name in the file descriptor. 52 | protoSyntaxFieldNum = 12 53 | protoPackageFieldNum = 2 54 | ) 55 | 56 | func main() { 57 | if len(os.Args) == 2 && os.Args[1] == "--version" { 58 | fmt.Fprintln(os.Stdout, pluginrpc.Version) 59 | os.Exit(0) 60 | } 61 | if len(os.Args) == 2 && (os.Args[1] == "-h" || os.Args[1] == "--help") { 62 | fmt.Fprintln(os.Stdout, usage) 63 | os.Exit(0) 64 | } 65 | if len(os.Args) != 1 { 66 | fmt.Fprintln(os.Stderr, usage) 67 | os.Exit(1) 68 | } 69 | 70 | flags := newFlags() 71 | protogen.Options{ 72 | ParamFunc: flags.Set, 73 | }.Run( 74 | func(plugin *protogen.Plugin) error { 75 | plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) 76 | if err := validate(plugin, flags); err != nil { 77 | return err 78 | } 79 | return generate(plugin) 80 | }, 81 | ) 82 | } 83 | 84 | type flags struct { 85 | streaming string 86 | } 87 | 88 | func newFlags() *flags { 89 | return &flags{} 90 | } 91 | 92 | func (f *flags) Set(name string, value string) error { 93 | switch name { 94 | case optionStreamingKey: 95 | switch value { 96 | case optionStreamingValueError, optionStreamingValueWarn, optionStreamingValueIgnore: 97 | f.streaming = value 98 | return nil 99 | default: 100 | return fmt.Errorf("unknown value for parameter %q: %q", name, value) 101 | } 102 | default: 103 | return fmt.Errorf("unknown parameter: %q", name) 104 | } 105 | } 106 | 107 | func validate(plugin *protogen.Plugin, flags *flags) error { 108 | var streamingError bool 109 | switch flags.streaming { 110 | case optionStreamingValueError: 111 | streamingError = true 112 | case "", optionStreamingValueWarn: 113 | case optionStreamingValueIgnore: 114 | // Ignore, no validation to do at this time since we only validate streaming. 115 | return nil 116 | default: 117 | // This should never happen. 118 | return fmt.Errorf("unknown value for parameter %q after parsing: %q", optionStreamingKey, flags.streaming) 119 | } 120 | 121 | var streamingMethods []*protogen.Method 122 | for _, file := range plugin.Files { 123 | if file.Generate { 124 | streamingMethods = append(streamingMethods, getStreamingMethodsForFile(file)...) 125 | } 126 | } 127 | if len(streamingMethods) == 0 { 128 | return nil 129 | } 130 | streamingMethodStrings := make([]string, len(streamingMethods)) 131 | for i, streamingMethod := range streamingMethods { 132 | streamingMethodStrings[i] = string(streamingMethod.Desc.FullName()) 133 | } 134 | if streamingError { 135 | // optionStreamingValueError 136 | return fmt.Errorf("streaming methods are not supported: %s", strings.Join(streamingMethodStrings, ", ")) 137 | } 138 | 139 | // We're now in optionStreamingValueWarn territory. 140 | for i, streamingMethodString := range streamingMethodStrings { 141 | streamingMethodStrings[i] = " - " + streamingMethodString 142 | } 143 | _, err := fmt.Fprintf( 144 | os.Stderr, 145 | `Warning: streaming methods are not supported, these methods will be skipped and not part of generated interfaces: 146 | 147 | %s 148 | 149 | To error on streaming methods, set the parameter "%s=%s". 150 | `, 151 | strings.Join(streamingMethodStrings, "\n"), 152 | optionStreamingKey, 153 | optionStreamingValueError, 154 | ) 155 | return err 156 | } 157 | 158 | func generate(plugin *protogen.Plugin) error { 159 | for _, file := range plugin.Files { 160 | if file.Generate { 161 | if err := generateFile(plugin, file); err != nil { 162 | return err 163 | } 164 | } 165 | } 166 | return nil 167 | } 168 | 169 | func generateFile(plugin *protogen.Plugin, file *protogen.File) error { 170 | if len(getUnaryMethodsForFile(file)) == 0 { 171 | return nil 172 | } 173 | 174 | file.GoPackageName += generatedPackageSuffix 175 | 176 | generatedFilenamePrefixToSlash := filepath.ToSlash(file.GeneratedFilenamePrefix) 177 | file.GeneratedFilenamePrefix = path.Join( 178 | path.Dir(generatedFilenamePrefixToSlash), 179 | string(file.GoPackageName), 180 | path.Base(generatedFilenamePrefixToSlash), 181 | ) 182 | generatedFile := plugin.NewGeneratedFile( 183 | file.GeneratedFilenamePrefix+generatedFilenameExtension, 184 | protogen.GoImportPath(path.Join( 185 | string(file.GoImportPath), 186 | string(file.GoPackageName), 187 | )), 188 | ) 189 | generatedFile.Import(file.GoImportPath) 190 | 191 | generatePreamble(generatedFile, file) 192 | generatePathConstants(generatedFile, file) 193 | for _, service := range file.Services { 194 | names := newNames(service) 195 | generateSpecBuilder(generatedFile, service, names) 196 | generateClientInterface(generatedFile, service, names) 197 | generateClientConstructor(generatedFile, service, names) 198 | generateHandlerInterface(generatedFile, service, names) 199 | generateServerInterface(generatedFile, service, names) 200 | generateServerConstructor(generatedFile, service, names) 201 | generateServerRegister(generatedFile, service, names) 202 | } 203 | generatedFile.P("// *** PRIVATE ***") 204 | generatedFile.P() 205 | for _, service := range file.Services { 206 | names := newNames(service) 207 | generateClientImplementation(generatedFile, service, names) 208 | generateServerImplementation(generatedFile, service, names) 209 | } 210 | return nil 211 | } 212 | 213 | func generatePreamble(g *protogen.GeneratedFile, file *protogen.File) { 214 | syntaxPath := protoreflect.SourcePath{protoSyntaxFieldNum} 215 | syntaxLocation := file.Desc.SourceLocations().ByPath(syntaxPath) 216 | for _, comment := range syntaxLocation.LeadingDetachedComments { 217 | leadingComments(g, protogen.Comments(comment), false /* deprecated */) 218 | } 219 | g.P() 220 | leadingComments(g, protogen.Comments(syntaxLocation.LeadingComments), false /* deprecated */) 221 | g.P() 222 | 223 | programName := filepath.Base(os.Args[0]) 224 | // Remove .exe suffix on Windows so that generated code is stable, regardless 225 | // of whether it was generated on a Windows machine or not. 226 | if ext := filepath.Ext(programName); strings.ToLower(ext) == ".exe" { 227 | programName = strings.TrimSuffix(programName, ext) 228 | } 229 | g.P("// Code generated by ", programName, ". DO NOT EDIT.") 230 | g.P("//") 231 | if file.Proto.GetOptions().GetDeprecated() { 232 | wrapComments(g, file.Desc.Path(), " is a deprecated file.") 233 | } else { 234 | g.P("// Source: ", file.Desc.Path()) 235 | } 236 | g.P() 237 | 238 | pkgPath := protoreflect.SourcePath{protoPackageFieldNum} 239 | pkgLocation := file.Desc.SourceLocations().ByPath(pkgPath) 240 | for _, comment := range pkgLocation.LeadingDetachedComments { 241 | leadingComments(g, protogen.Comments(comment), false /* deprecated */) 242 | } 243 | g.P() 244 | leadingComments(g, protogen.Comments(pkgLocation.LeadingComments), false /* deprecated */) 245 | 246 | g.P("package ", file.GoPackageName) 247 | g.P() 248 | wrapComments(g, "This is a compile-time assertion to ensure that this generated file ", 249 | "and the pluginrpc package are compatible. If you get a compiler error that this constant ", 250 | "is not defined, this code was generated with a version of pluginrpc newer than the one ", 251 | "compiled into your binary. You can fix the problem by either regenerating this code ", 252 | "with an older version of pluginrpc or updating the pluginrpc version compiled into your binary.") 253 | g.P("const _ = ", pluginrpcPackage.Ident("IsAtLeastVersion0_1_0")) 254 | g.P() 255 | } 256 | 257 | func generatePathConstants(g *protogen.GeneratedFile, file *protogen.File) { 258 | unaryMethods := getUnaryMethodsForFile(file) 259 | if len(unaryMethods) == 0 { 260 | return 261 | } 262 | g.P("const (") 263 | for _, method := range unaryMethods { 264 | wrapComments(g, pathConstName(method), " is the path of the ", 265 | method.Parent.Desc.Name(), "'s ", method.Desc.Name(), " RPC.") 266 | g.P(pathConstName(method), ` = "`, fmt.Sprintf("/%s/%s", method.Parent.Desc.FullName(), method.Desc.Name()), `"`) 267 | } 268 | g.P(")") 269 | g.P() 270 | } 271 | 272 | func generateSpecBuilder(g *protogen.GeneratedFile, service *protogen.Service, names names) { 273 | unaryMethods := getUnaryMethodsForService(service) 274 | if len(unaryMethods) == 0 { 275 | return 276 | } 277 | wrapComments(g, names.SpecBuilder, " builds a Spec for the ", service.Desc.FullName(), " service.") 278 | if isDeprecatedService(service) { 279 | g.P("//") 280 | deprecated(g) 281 | } 282 | g.AnnotateSymbol(names.SpecBuilder, protogen.Annotation{Location: service.Location}) 283 | g.P("type ", names.SpecBuilder, " struct {") 284 | for _, method := range unaryMethods { 285 | g.P(method.GoName, " []", pluginrpcPackage.Ident("ProcedureOption")) 286 | } 287 | g.P("}") 288 | g.P() 289 | wrapComments(g, "Build builds a Spec for the ", service.Desc.FullName(), " service.") 290 | g.P("func (s ", names.SpecBuilder, ") Build() (", pluginrpcPackage.Ident("Spec"), ", error) {") 291 | g.P("procedures := make([]", pluginrpcPackage.Ident("Procedure"), ", 0, ", len(unaryMethods), ")") 292 | for i, method := range unaryMethods { 293 | equals := "=" 294 | if i == 0 { 295 | equals = ":=" 296 | } 297 | g.P("procedure, err ", equals, " ", pluginrpcPackage.Ident("NewProcedure"), "(", pathConstName(method), ", s.", method.GoName, "...)") 298 | g.P("if err != nil {") 299 | g.P("return nil, err") 300 | g.P("}") 301 | g.P("procedures = append(procedures, procedure)") 302 | } 303 | g.P("return ", pluginrpcPackage.Ident("NewSpec"), "(procedures...)") 304 | g.P("}") 305 | g.P() 306 | } 307 | func generateClientInterface(g *protogen.GeneratedFile, service *protogen.Service, names names) { 308 | unaryMethods := getUnaryMethodsForService(service) 309 | if len(unaryMethods) == 0 { 310 | return 311 | } 312 | wrapComments(g, names.Client, " is a client for the ", service.Desc.FullName(), " service.") 313 | if isDeprecatedService(service) { 314 | g.P("//") 315 | deprecated(g) 316 | } 317 | g.AnnotateSymbol(names.Client, protogen.Annotation{Location: service.Location}) 318 | g.P("type ", names.Client, " interface {") 319 | for _, method := range unaryMethods { 320 | g.AnnotateSymbol(names.Client+"."+method.GoName, protogen.Annotation{Location: method.Location}) 321 | leadingComments( 322 | g, 323 | method.Comments.Leading, 324 | isDeprecatedMethod(method), 325 | ) 326 | g.P(clientSignature(g, method, false /* named */)) 327 | } 328 | g.P("}") 329 | g.P() 330 | } 331 | 332 | func generateClientConstructor(g *protogen.GeneratedFile, service *protogen.Service, names names) { 333 | unaryMethods := getUnaryMethodsForService(service) 334 | if len(unaryMethods) == 0 { 335 | return 336 | } 337 | // Client constructor. 338 | wrapComments(g, names.ClientConstructor, " constructs a client for the ", service.Desc.FullName(), " service.") 339 | g.P("//") 340 | if isDeprecatedService(service) { 341 | g.P("//") 342 | deprecated(g) 343 | } 344 | g.P("func ", names.ClientConstructor, " (client ", pluginrpcPackage.Ident("Client"), 345 | ") (", names.Client, ", error) {") 346 | g.P("return &", names.ClientImpl, "{") 347 | g.P("client: client,") 348 | g.P("}, nil") 349 | g.P("}") 350 | g.P() 351 | } 352 | 353 | func generateClientImplementation(g *protogen.GeneratedFile, service *protogen.Service, names names) { 354 | unaryMethods := getUnaryMethodsForService(service) 355 | if len(unaryMethods) == 0 { 356 | return 357 | } 358 | // Client struct. 359 | wrapComments(g, names.ClientImpl, " implements ", names.Client, ".") 360 | g.P("type ", names.ClientImpl, " struct {") 361 | g.P("client ", pluginrpcPackage.Ident("Client")) 362 | g.P("}") 363 | g.P() 364 | for _, method := range unaryMethods { 365 | generateClientMethod(g, method, names) 366 | } 367 | } 368 | 369 | func generateClientMethod(g *protogen.GeneratedFile, method *protogen.Method, names names) { 370 | receiver := names.ClientImpl 371 | wrapComments(g, method.GoName, " calls ", method.Desc.FullName(), ".") 372 | if isDeprecatedMethod(method) { 373 | g.P("//") 374 | deprecated(g) 375 | } 376 | g.P("func (c *", receiver, ") ", clientSignature(g, method, true /* named */), " {") 377 | g.P("res := &", g.QualifiedGoIdent(method.Output.GoIdent), "{}") 378 | g.P("if err := c.client.Call(ctx, ", pathConstName(method), ", req, res, opts...); err != nil {") 379 | g.P("return nil, err") 380 | g.P("}") 381 | g.P("return res, nil") 382 | g.P("}") 383 | g.P() 384 | } 385 | 386 | func generateHandlerInterface(g *protogen.GeneratedFile, service *protogen.Service, names names) { 387 | unaryMethods := getUnaryMethodsForService(service) 388 | if len(unaryMethods) == 0 { 389 | return 390 | } 391 | wrapComments(g, names.Handler, " is an implementation of the ", service.Desc.FullName(), " service.") 392 | if isDeprecatedService(service) { 393 | g.P("//") 394 | deprecated(g) 395 | } 396 | g.AnnotateSymbol(names.Handler, protogen.Annotation{Location: service.Location}) 397 | g.P("type ", names.Handler, " interface {") 398 | for _, method := range unaryMethods { 399 | leadingComments( 400 | g, 401 | method.Comments.Leading, 402 | isDeprecatedMethod(method), 403 | ) 404 | g.AnnotateSymbol(names.Handler+"."+method.GoName, protogen.Annotation{Location: method.Location}) 405 | g.P(handlerSignature(g, method)) 406 | } 407 | g.P("}") 408 | g.P() 409 | } 410 | 411 | func generateServerInterface(g *protogen.GeneratedFile, service *protogen.Service, names names) { 412 | unaryMethods := getUnaryMethodsForService(service) 413 | if len(unaryMethods) == 0 { 414 | return 415 | } 416 | wrapComments(g, names.Server, " serves the ", service.Desc.FullName(), " service.") 417 | if isDeprecatedService(service) { 418 | g.P("//") 419 | deprecated(g) 420 | } 421 | g.AnnotateSymbol(names.Server, protogen.Annotation{Location: service.Location}) 422 | g.P("type ", names.Server, " interface {") 423 | for _, method := range unaryMethods { 424 | leadingComments( 425 | g, 426 | method.Comments.Leading, 427 | isDeprecatedMethod(method), 428 | ) 429 | g.AnnotateSymbol(names.Handler+"."+method.GoName, protogen.Annotation{Location: method.Location}) 430 | g.P(serverSignature(g, method, false)) 431 | } 432 | g.P("}") 433 | g.P() 434 | } 435 | 436 | func generateServerConstructor(g *protogen.GeneratedFile, service *protogen.Service, names names) { 437 | unaryMethods := getUnaryMethodsForService(service) 438 | if len(unaryMethods) == 0 { 439 | return 440 | } 441 | wrapComments(g, names.ServerConstructor, " constructs a server for the ", service.Desc.FullName(), " service.") 442 | g.P("//") 443 | if isDeprecatedService(service) { 444 | g.P("//") 445 | deprecated(g) 446 | } 447 | g.P("func ", names.ServerConstructor, " (handler ", pluginrpcPackage.Ident("Handler"), 448 | ", ", unexport(names.Handler), " ", names.Handler, ") ", names.Server, " {") 449 | g.P("return &", names.ServerImpl, "{") 450 | g.P("handler: handler,") 451 | g.P(unexport(names.Handler), ": ", unexport(names.Handler), ",") 452 | g.P("}") 453 | g.P("}") 454 | g.P() 455 | } 456 | 457 | func generateServerRegister(g *protogen.GeneratedFile, service *protogen.Service, names names) { 458 | unaryMethods := getUnaryMethodsForService(service) 459 | if len(unaryMethods) == 0 { 460 | return 461 | } 462 | wrapComments(g, names.ServerRegister, " registers the server for the ", service.Desc.FullName(), " service.") 463 | g.P("//") 464 | if isDeprecatedService(service) { 465 | g.P("//") 466 | deprecated(g) 467 | } 468 | g.P("func ", names.ServerRegister, " (serverRegistrar ", pluginrpcPackage.Ident("ServerRegistrar"), 469 | ", ", unexport(names.Server), " ", names.Server, ") {") 470 | for _, method := range unaryMethods { 471 | g.P("serverRegistrar.Register(", pathConstName(method), ", ", unexport(names.Server), ".", method.GoName, ")") 472 | } 473 | g.P("}") 474 | g.P() 475 | } 476 | 477 | func generateServerImplementation(g *protogen.GeneratedFile, service *protogen.Service, names names) { 478 | unaryMethods := getUnaryMethodsForService(service) 479 | if len(unaryMethods) == 0 { 480 | return 481 | } 482 | wrapComments(g, names.ServerImpl, " implements ", names.Server, ".") 483 | g.P("type ", names.ServerImpl, " struct {") 484 | g.P("handler ", pluginrpcPackage.Ident("Handler")) 485 | g.P(unexport(names.Handler), " ", names.Handler) 486 | g.P("}") 487 | g.P() 488 | for _, method := range unaryMethods { 489 | generateServerMethod(g, method, names) 490 | } 491 | } 492 | 493 | func generateServerMethod(g *protogen.GeneratedFile, method *protogen.Method, names names) { 494 | receiver := names.ServerImpl 495 | wrapComments(g, method.GoName, " calls ", method.Desc.FullName(), ".") 496 | if isDeprecatedMethod(method) { 497 | g.P("//") 498 | deprecated(g) 499 | } 500 | g.P("func (c *", receiver, ") ", serverSignature(g, method, true /* named */), " {") 501 | g.P("return c.handler.Handle(") 502 | g.P("ctx,") 503 | g.P("handleEnv,") 504 | g.P("&", g.QualifiedGoIdent(method.Input.GoIdent), "{},") 505 | g.P("func(ctx ", contextPackage.Ident("Context"), ", anyReq any) (any, error) {") 506 | g.P("req, ok := anyReq.(*", g.QualifiedGoIdent(method.Input.GoIdent), ")") 507 | g.P("if !ok {") 508 | g.P("return nil, ", fmtPackage.Ident("Errorf"), `("could not cast %T to a *`, g.QualifiedGoIdent(method.Input.GoIdent), `", anyReq)`) 509 | g.P("}") 510 | g.P("return c.", unexport(names.Handler), ".", method.GoName, "(ctx, req)") 511 | g.P("},") 512 | g.P("options...,") 513 | g.P(")") 514 | g.P("}") 515 | g.P() 516 | } 517 | 518 | func clientSignature(g *protogen.GeneratedFile, method *protogen.Method, named bool) string { 519 | // unary; symmetric so we can re-use server templating 520 | return method.GoName + clientSignatureParams(g, method, named) 521 | } 522 | 523 | func clientSignatureParams(g *protogen.GeneratedFile, method *protogen.Method, named bool) string { 524 | ctxName := "ctx " 525 | reqName := "req " 526 | optsName := "opts " 527 | if !named { 528 | ctxName, reqName, optsName = "", "", "" 529 | } 530 | // unary 531 | return "(" + ctxName + g.QualifiedGoIdent(contextPackage.Ident("Context")) + 532 | ", " + reqName + "*" + g.QualifiedGoIdent(method.Input.GoIdent) + 533 | ", " + optsName + "..." + g.QualifiedGoIdent(pluginrpcPackage.Ident("CallOption")) + ") " + 534 | "(*" + g.QualifiedGoIdent(method.Output.GoIdent) + ", error)" 535 | } 536 | 537 | func handlerSignature(g *protogen.GeneratedFile, method *protogen.Method) string { 538 | return method.GoName + handlerSignatureParams(g, method, false) 539 | } 540 | 541 | func handlerSignatureParams(g *protogen.GeneratedFile, method *protogen.Method, named bool) string { 542 | ctxName := "ctx " 543 | reqName := "req " 544 | if !named { 545 | ctxName, reqName = "", "" 546 | } 547 | // unary 548 | return "(" + ctxName + g.QualifiedGoIdent(contextPackage.Ident("Context")) + 549 | ", " + reqName + "*" + g.QualifiedGoIdent(method.Input.GoIdent) + ") " + 550 | "(*" + g.QualifiedGoIdent(method.Output.GoIdent) + ", error)" 551 | } 552 | 553 | func serverSignature(g *protogen.GeneratedFile, method *protogen.Method, named bool) string { 554 | return method.GoName + serverSignatureParams(g, method, named) 555 | } 556 | 557 | func serverSignatureParams(g *protogen.GeneratedFile, _ *protogen.Method, named bool) string { 558 | ctxName := "ctx " 559 | handleEnvName := "handleEnv " 560 | optionsName := "options" 561 | if !named { 562 | ctxName, handleEnvName, optionsName = "", "", "" 563 | } 564 | // unary 565 | return "(" + ctxName + g.QualifiedGoIdent(contextPackage.Ident("Context")) + 566 | ", " + handleEnvName + g.QualifiedGoIdent(pluginrpcPackage.Ident("HandleEnv")) + 567 | ", " + optionsName + " ..." + g.QualifiedGoIdent(pluginrpcPackage.Ident("HandleOption")) + 568 | ") error" 569 | } 570 | 571 | func pathConstName(m *protogen.Method) string { 572 | return fmt.Sprintf("%s%sPath", m.Parent.GoName, m.GoName) 573 | } 574 | 575 | func isDeprecatedService(service *protogen.Service) bool { 576 | serviceOptions, ok := service.Desc.Options().(*descriptorpb.ServiceOptions) 577 | return ok && serviceOptions.GetDeprecated() 578 | } 579 | 580 | func isDeprecatedMethod(method *protogen.Method) bool { 581 | methodOptions, ok := method.Desc.Options().(*descriptorpb.MethodOptions) 582 | return ok && methodOptions.GetDeprecated() 583 | } 584 | 585 | func getUnaryMethodsForFile(file *protogen.File) []*protogen.Method { 586 | var methods []*protogen.Method 587 | for _, service := range file.Services { 588 | methods = append(methods, getUnaryMethodsForService(service)...) 589 | } 590 | return methods 591 | } 592 | 593 | func getUnaryMethodsForService(service *protogen.Service) []*protogen.Method { 594 | var methods []*protogen.Method 595 | for _, method := range service.Methods { 596 | if isUnaryMethod(method) { 597 | methods = append(methods, method) 598 | } 599 | } 600 | return methods 601 | } 602 | 603 | func getStreamingMethodsForFile(file *protogen.File) []*protogen.Method { 604 | var methods []*protogen.Method 605 | for _, service := range file.Services { 606 | methods = append(methods, getStreamingMethodsForService(service)...) 607 | } 608 | return methods 609 | } 610 | 611 | func getStreamingMethodsForService(service *protogen.Service) []*protogen.Method { 612 | var methods []*protogen.Method 613 | for _, method := range service.Methods { 614 | if !isUnaryMethod(method) { 615 | methods = append(methods, method) 616 | } 617 | } 618 | return methods 619 | } 620 | 621 | func isUnaryMethod(method *protogen.Method) bool { 622 | return !(method.Desc.IsStreamingClient() || method.Desc.IsStreamingServer()) 623 | } 624 | 625 | // Raggedy comments in the generated code are driving me insane. This 626 | // word-wrapping function is ruinously inefficient, but it gets the job done. 627 | func wrapComments(g *protogen.GeneratedFile, elems ...any) { 628 | text := &bytes.Buffer{} 629 | for _, el := range elems { 630 | switch el := el.(type) { 631 | case protogen.GoIdent: 632 | fmt.Fprint(text, g.QualifiedGoIdent(el)) 633 | default: 634 | fmt.Fprint(text, el) 635 | } 636 | } 637 | words := strings.Fields(text.String()) 638 | text.Reset() 639 | var pos int 640 | for _, word := range words { 641 | numRunes := utf8.RuneCountInString(word) 642 | if pos > 0 && pos+numRunes+1 > commentWidth { 643 | g.P("// ", text.String()) 644 | text.Reset() 645 | pos = 0 646 | } 647 | if pos > 0 { 648 | text.WriteRune(' ') 649 | pos++ 650 | } 651 | text.WriteString(word) 652 | pos += numRunes 653 | } 654 | if text.Len() > 0 { 655 | g.P("// ", text.String()) 656 | } 657 | } 658 | 659 | func leadingComments(g *protogen.GeneratedFile, comments protogen.Comments, isDeprecated bool) { 660 | if comments.String() != "" { 661 | g.P(strings.TrimSpace(comments.String())) 662 | } 663 | if isDeprecated { 664 | if comments.String() != "" { 665 | g.P("//") 666 | } 667 | deprecated(g) 668 | } 669 | } 670 | 671 | func deprecated(g *protogen.GeneratedFile) { 672 | g.P("// Deprecated: do not use.") 673 | } 674 | 675 | func unexport(s string) string { 676 | lowercased := strings.ToLower(s[:1]) + s[1:] 677 | switch lowercased { 678 | // https://go.dev/ref/spec#Keywords 679 | case "break", "default", "func", "interface", "select", 680 | "case", "defer", "go", "map", "struct", 681 | "chan", "else", "goto", "package", "switch", 682 | "const", "fallthrough", "if", "range", "type", 683 | "continue", "for", "import", "return", "var": 684 | return "_" + lowercased 685 | default: 686 | return lowercased 687 | } 688 | } 689 | 690 | type names struct { 691 | Base string 692 | SpecBuilder string 693 | Client string 694 | ClientConstructor string 695 | ClientImpl string 696 | Handler string 697 | Server string 698 | ServerConstructor string 699 | ServerRegister string 700 | ServerImpl string 701 | } 702 | 703 | func newNames(service *protogen.Service) names { 704 | base := service.GoName 705 | return names{ 706 | Base: base, 707 | SpecBuilder: base + "SpecBuilder", 708 | Client: base + "Client", 709 | ClientConstructor: "New" + base + "Client", 710 | ClientImpl: unexport(base) + "Client", 711 | Handler: base + "Handler", 712 | Server: base + "Server", 713 | ServerConstructor: "New" + base + "Server", 714 | ServerRegister: "Register" + base + "Server", 715 | ServerImpl: unexport(base) + "Server", 716 | } 717 | } 718 | --------------------------------------------------------------------------------