diff --git a/protoc-gen-connect-kotlin/proto/buf/deprecation/v1/file_deprecated.proto b/protoc-gen-connect-kotlin/proto/buf/deprecation/v1/file_deprecated.proto new file mode 100644 index 00000000..1928113e --- /dev/null +++ b/protoc-gen-connect-kotlin/proto/buf/deprecation/v1/file_deprecated.proto @@ -0,0 +1,27 @@ +// Copyright 2022-2023 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package buf.deprecation.v1; + +option deprecated = true; + +message DeprecatedByFileRequest {} + +message DeprecatedByFileResponse {} + +service FileDeprecatedService { + rpc DeprecatedByFile(DeprecatedByFileRequest) returns (DeprecatedByFileResponse) {} +} diff --git a/protoc-gen-connect-kotlin/proto/buf/deprecation/v1/method_deprecated.proto b/protoc-gen-connect-kotlin/proto/buf/deprecation/v1/method_deprecated.proto new file mode 100644 index 00000000..2ed11748 --- /dev/null +++ b/protoc-gen-connect-kotlin/proto/buf/deprecation/v1/method_deprecated.proto @@ -0,0 +1,27 @@ +// Copyright 2022-2023 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package buf.deprecation.v1; + +message DeprecatedMethodRequest {} + +message DeprecatedMethodResponse {} + +service MethodDeprecatedService { + rpc DeprecatedMethod(DeprecatedMethodRequest) returns (DeprecatedMethodResponse) { + option deprecated = true; + } +} diff --git a/protoc-gen-connect-kotlin/proto/buf/deprecation/v1/service_deprecated.proto b/protoc-gen-connect-kotlin/proto/buf/deprecation/v1/service_deprecated.proto new file mode 100644 index 00000000..83f5abb3 --- /dev/null +++ b/protoc-gen-connect-kotlin/proto/buf/deprecation/v1/service_deprecated.proto @@ -0,0 +1,26 @@ +// Copyright 2022-2023 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package buf.deprecation.v1; + +message DeprecatedByServiceRequest {} + +message DeprecatedByServiceResponse {} + +service ServiceDeprecatedService { + option deprecated = true; + rpc DeprecatedByService(DeprecatedByServiceRequest) returns (DeprecatedByServiceResponse) {} +} diff --git a/protoc-gen-connect-kotlin/src/main/kotlin/com/connectrpc/protocgen/connect/Generator.kt b/protoc-gen-connect-kotlin/src/main/kotlin/com/connectrpc/protocgen/connect/Generator.kt index f7ede2f6..13b29a25 100644 --- a/protoc-gen-connect-kotlin/src/main/kotlin/com/connectrpc/protocgen/connect/Generator.kt +++ b/protoc-gen-connect-kotlin/src/main/kotlin/com/connectrpc/protocgen/connect/Generator.kt @@ -36,6 +36,7 @@ import com.google.protobuf.DescriptorProtos.FileDescriptorProto import com.google.protobuf.DescriptorProtos.MethodOptions.IdempotencyLevel import com.google.protobuf.Descriptors import com.google.protobuf.compiler.PluginProtos +import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FileSpec @@ -122,7 +123,8 @@ class Generator : CodeGenerator { .addFileComment("Code generated by connect-kotlin. DO NOT EDIT.\n") .addFileComment("\n") .addFileComment("Source: ${file.name}\n") - .addType(serviceClientInterface(packageName, service, sourceInfo)) + .suppressDeprecationWarnings(file) + .addType(serviceClientInterface(packageName, service, file, sourceInfo)) .build() fileSpecs[serviceClientInterfaceClassName(packageName, service)] = interfaceFileSpec @@ -132,8 +134,9 @@ class Generator : CodeGenerator { .addFileComment("Code generated by connect-kotlin. DO NOT EDIT.\n") .addFileComment("\n") .addFileComment("Source: ${file.name}\n") + .suppressDeprecationWarnings(file) // Set the file package for the generated methods. - .addType(serviceClientImplementation(packageName, service, sourceInfo)) + .addType(serviceClientImplementation(packageName, service, file, sourceInfo)) for (method in service.methods) { if (method.options.hasIdempotencyLevel()) { implementationFileSpecBuilder.addImport(Idempotency::class.java.`package`.name, "Idempotency") @@ -149,11 +152,13 @@ class Generator : CodeGenerator { private fun serviceClientInterface( packageName: String, service: Descriptors.ServiceDescriptor, + file: Descriptors.FileDescriptor, sourceInfo: SourceInfo, ): TypeSpec { val interfaceBuilder = TypeSpec.interfaceBuilder(serviceClientInterfaceClassName(packageName, service)) val functionSpecs = interfaceMethods(service.methods, sourceInfo) return interfaceBuilder + .addServiceDeprecation(service, file) .addKdoc(sourceInfo.comment().sanitizeKdoc()) .addFunctions(functionSpecs) .build() @@ -176,6 +181,7 @@ class Generator : CodeGenerator { if (method.isClientStreaming && method.isServerStreaming) { val streamingBuilder = FunSpec.builder(method.name.lowerCamelCase()) .addKdoc(sourceInfo.comment().sanitizeKdoc()) + .addMethodDeprecation(method) .addModifiers(KModifier.ABSTRACT) .addModifiers(KModifier.SUSPEND) .addParameter(headerParameterSpec) @@ -187,6 +193,7 @@ class Generator : CodeGenerator { } else if (method.isServerStreaming) { val serverStreamingFunction = FunSpec.builder(method.name.lowerCamelCase()) .addKdoc(sourceInfo.comment().sanitizeKdoc()) + .addMethodDeprecation(method) .addModifiers(KModifier.ABSTRACT) .addModifiers(KModifier.SUSPEND) .addParameter(headerParameterSpec) @@ -198,6 +205,7 @@ class Generator : CodeGenerator { } else if (method.isClientStreaming) { val clientStreamingFunction = FunSpec.builder(method.name.lowerCamelCase()) .addKdoc(sourceInfo.comment().sanitizeKdoc()) + .addMethodDeprecation(method) .addModifiers(KModifier.ABSTRACT) .addModifiers(KModifier.SUSPEND) .addParameter(headerParameterSpec) @@ -210,6 +218,7 @@ class Generator : CodeGenerator { if (configuration.generateCoroutineMethods) { val unarySuspendFunction = FunSpec.builder(method.name.lowerCamelCase()) .addKdoc(sourceInfo.comment().sanitizeKdoc()) + .addMethodDeprecation(method) .addModifiers(KModifier.ABSTRACT) .addModifiers(KModifier.SUSPEND) .addParameter("request", inputClassName) @@ -230,6 +239,7 @@ class Generator : CodeGenerator { ) val unaryCallbackFunction = FunSpec.builder(method.name.lowerCamelCase()) .addKdoc(sourceInfo.comment().sanitizeKdoc()) + .addMethodDeprecation(method) .addModifiers(KModifier.ABSTRACT) .addParameter("request", inputClassName) .addParameter(headerParameterSpec) @@ -256,6 +266,7 @@ class Generator : CodeGenerator { private fun serviceClientImplementation( javaPackageName: String, service: Descriptors.ServiceDescriptor, + file: Descriptors.FileDescriptor, sourceInfo: SourceInfo, ): TypeSpec { // The javaPackageName is used instead of the package name for imports and code references. @@ -274,6 +285,7 @@ class Generator : CodeGenerator { val functionSpecs = implementationMethods(service.methods, sourceInfo) return classBuilder .addKdoc(sourceInfo.comment().sanitizeKdoc()) + .addServiceDeprecation(service, file) .addFunctions(functionSpecs) .build() } @@ -318,6 +330,7 @@ class Generator : CodeGenerator { if (method.isClientStreaming && method.isServerStreaming) { val streamingFunction = FunSpec.builder(method.name.lowerCamelCase()) .addKdoc(sourceInfo.comment().sanitizeKdoc()) + .addMethodDeprecation(method) .addModifiers(KModifier.OVERRIDE) .addModifiers(KModifier.SUSPEND) .addParameter("headers", HEADERS_CLASS_NAME) @@ -344,6 +357,7 @@ class Generator : CodeGenerator { } else if (method.isServerStreaming) { val serverStreamingFunction = FunSpec.builder(method.name.lowerCamelCase()) .addKdoc(sourceInfo.comment().sanitizeKdoc()) + .addMethodDeprecation(method) .addModifiers(KModifier.OVERRIDE) .addModifiers(KModifier.SUSPEND) .addParameter("headers", HEADERS_CLASS_NAME) @@ -366,6 +380,7 @@ class Generator : CodeGenerator { } else if (method.isClientStreaming) { val clientStreamingFunction = FunSpec.builder(method.name.lowerCamelCase()) .addKdoc(sourceInfo.comment().sanitizeKdoc()) + .addMethodDeprecation(method) .addModifiers(KModifier.OVERRIDE) .addModifiers(KModifier.SUSPEND) .addParameter("headers", HEADERS_CLASS_NAME) @@ -389,6 +404,7 @@ class Generator : CodeGenerator { if (configuration.generateCoroutineMethods) { val unarySuspendFunction = FunSpec.builder(method.name.lowerCamelCase()) .addKdoc(sourceInfo.comment().sanitizeKdoc()) + .addMethodDeprecation(method) .addModifiers(KModifier.SUSPEND) .addModifiers(KModifier.OVERRIDE) .addParameter("request", inputClassName) @@ -421,6 +437,7 @@ class Generator : CodeGenerator { ) val unaryCallbackFunction = FunSpec.builder(method.name.lowerCamelCase()) .addKdoc(sourceInfo.comment().sanitizeKdoc()) + .addMethodDeprecation(method) .addModifiers(KModifier.OVERRIDE) .addParameter("request", inputClassName) .addParameter("headers", HEADERS_CLASS_NAME) @@ -524,3 +541,50 @@ private fun String.packageToDirectory(): String { } return dir } + +private fun TypeSpec.Builder.addServiceDeprecation( + service: Descriptors.ServiceDescriptor, + file: Descriptors.FileDescriptor, +): TypeSpec.Builder { + if (service.options.deprecated) { + this.addAnnotation( + AnnotationSpec.builder(Deprecated::class) + .addMember("%S", "The service is deprecated in the Protobuf source file.") + .build(), + ) + } else if (file.options.deprecated) { + this.addAnnotation( + AnnotationSpec.builder(Deprecated::class) + .addMember("%S", "The Protobuf source file that defines this service is deprecated.") + .build(), + ) + } + return this +} + +private fun FunSpec.Builder.addMethodDeprecation( + method: Descriptors.MethodDescriptor, +): FunSpec.Builder { + if (method.options.deprecated) { + this.addAnnotation( + AnnotationSpec.builder(Deprecated::class) + .addMember("%S", "The method is deprecated in the Protobuf source file.") + .build(), + ) + } + return this +} + +private fun FileSpec.Builder.suppressDeprecationWarnings( + file: Descriptors.FileDescriptor, +): FileSpec.Builder { + val hasDeprecated = file.options.deprecated || file.services.find { s -> s.options.deprecated || s.methods.find { m -> m.options.deprecated } != null } != null + if (hasDeprecated) { + this.addAnnotation( + AnnotationSpec.builder(Suppress::class) + .addMember("%S", "DEPRECATION") + .build(), + ) + } + return this +}