diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index d1d98ba8ae5..7f57d3f2623 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -198,3 +198,9 @@ message = "Fix inconsistent casing in services re-export." references = ["smithy-rs#2349"] meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "server" } author = "hlbarber" + +[[smithy-rs]] +message = "Ensure the server side code generator creates valid code for services with no operations." +references = ["smithy-rs#2351"] +meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "server"} +author = "hlbarber" diff --git a/codegen-client-test/build.gradle.kts b/codegen-client-test/build.gradle.kts index f3796a6911f..ad4f5affd09 100644 --- a/codegen-client-test/build.gradle.kts +++ b/codegen-client-test/build.gradle.kts @@ -92,6 +92,11 @@ val allCodegenTests = "../codegen-core/common-test-models".let { commonModels -> """.trimIndent(), imports = listOf("$commonModels/naming-obstacle-course-structs.smithy"), ), + CodegenTest( + "emptyservice#EmptyService", + "empty_service", + imports = listOf("$commonModels/empty-service.smithy"), + ), CodegenTest("aws.protocoltests.json#TestService", "endpoint-rules"), CodegenTest("com.aws.example.rust#PokemonService", "pokemon-service-client", imports = listOf("$commonModels/pokemon.smithy", "$commonModels/pokemon-common.smithy")), CodegenTest("com.aws.example.rust#PokemonService", "pokemon-service-awsjson-client", imports = listOf("$commonModels/pokemon-awsjson.smithy", "$commonModels/pokemon-common.smithy")), diff --git a/codegen-core/common-test-models/empty-service.smithy b/codegen-core/common-test-models/empty-service.smithy new file mode 100644 index 00000000000..70e0bbec464 --- /dev/null +++ b/codegen-core/common-test-models/empty-service.smithy @@ -0,0 +1,8 @@ +namespace emptyservice + +use aws.protocols#restJson1 + +@restJson1 +service EmptyService { + operations: [] +} diff --git a/codegen-core/common-test-models/naming-obstacle-course-casing.smithy b/codegen-core/common-test-models/naming-obstacle-course-casing.smithy index fb80a46d48b..b509d5ff7e6 100644 --- a/codegen-core/common-test-models/naming-obstacle-course-casing.smithy +++ b/codegen-core/common-test-models/naming-obstacle-course-casing.smithy @@ -10,14 +10,11 @@ use aws.protocols#awsJson1_1 @awsJson1_1 service ACRONYMInside_Service { operations: [ - DoNothing, // ACRONYMInside_Op // ACRONYM_InsideOp ] } -operation DoNothing {} - // operation ACRONYMInside_Op { // input: Input, // output: Output, diff --git a/codegen-server-test/build.gradle.kts b/codegen-server-test/build.gradle.kts index d8ec164a406..7c1703ae491 100644 --- a/codegen-server-test/build.gradle.kts +++ b/codegen-server-test/build.gradle.kts @@ -102,6 +102,11 @@ val allCodegenTests = "../codegen-core/common-test-models".let { commonModels -> extraConfig = """, "codegen": { "ignoreUnsupportedConstraints": true } """, ), CodegenTest("com.amazonaws.s3#AmazonS3", "s3"), + CodegenTest( + "emptyservice#EmptyService", + "empty_service", + imports = listOf("$commonModels/empty-service.smithy"), + ), CodegenTest( "com.aws.example.rust#PokemonService", "pokemon-service-server-sdk", diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorV2.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorV2.kt index 3b7d09c3cea..ebae8efee6e 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorV2.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorV2.kt @@ -10,6 +10,7 @@ import software.amazon.smithy.model.neighbor.Walker import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.traits.PatternTrait +import software.amazon.smithy.rust.codegen.core.rustlang.Attribute import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.Writable @@ -184,12 +185,14 @@ class ServerServiceGeneratorV2( for (operationShape in operations) { val fieldName = builderFieldNames[operationShape]!! val operationZstTypeName = operationStructNames[operationShape]!! - rust( + rustTemplate( """ if self.$fieldName.is_none() { + use #{SmithyHttpServer}::operation::OperationShape; $missingOperationsVariableName.insert(crate::operation_shape::$operationZstTypeName::NAME, ".$fieldName()"); } """, + "SmithyHttpServer" to smithyHttpServer, ) } } @@ -205,6 +208,13 @@ class ServerServiceGeneratorV2( } } + var missingOperationsUnusedMut = writable("") + var unusedExpected = writable("") + if (operations.isEmpty()) { + missingOperationsUnusedMut = writable { Attribute.AllowUnusedMut.render(this) } + unusedExpected = writable { Attribute.AllowUnusedVariables.render(this) } + } + rustTemplate( """ /// Constructs a [`$serviceName`] from the arguments provided to the builder. @@ -216,7 +226,7 @@ class ServerServiceGeneratorV2( pub fn build(self) -> Result<$serviceName<#{SmithyHttpServer}::routing::Route<$builderBodyGenericTypeName>>, MissingOperationsError> { let router = { - use #{SmithyHttpServer}::operation::OperationShape; + #{MissingOperationsUnusedMut:W} let mut $missingOperationsVariableName = std::collections::HashMap::new(); #{NullabilityChecks:W} if !$missingOperationsVariableName.is_empty() { @@ -224,6 +234,7 @@ class ServerServiceGeneratorV2( operation_names2setter_methods: $missingOperationsVariableName, }); } + #{UnusedExpected:W} let $expectMessageVariableName = "this should never panic since we are supposed to check beforehand that a handler has been registered for this operation; please file a bug report under https://github.com/awslabs/smithy-rs/issues"; #{PatternInitializations:W} @@ -240,6 +251,8 @@ class ServerServiceGeneratorV2( "RoutesArrayElements" to routesArrayElements, "SmithyHttpServer" to smithyHttpServer, "PatternInitializations" to patternInitializations(), + "MissingOperationsUnusedMut" to missingOperationsUnusedMut, + "UnusedExpected" to unusedExpected, ) } @@ -319,14 +332,23 @@ class ServerServiceGeneratorV2( /** Returns a `Writable` containing the builder struct definition and its implementations. */ private fun builder(): Writable = writable { val builderGenerics = listOf(builderBodyGenericTypeName, builderPluginGenericTypeName).joinToString(", ") + var allBuilderFields = builderFields + "plugin: $builderPluginGenericTypeName" + var allowDeadCodePlugin = writable("") + + // With no operations there is no use of the `Body` type variable and `plugin: Plugin` becomes dead code + if (operations.isEmpty()) { + allBuilderFields += "_body: std::marker::PhantomData<$builderBodyGenericTypeName>" + allowDeadCodePlugin = writable { Attribute.AllowDeadCode.render(this) } + } + rustTemplate( """ /// The service builder for [`$serviceName`]. /// /// Constructed via [`$serviceName::builder_with_plugins`] or [`$serviceName::builder_without_plugins`]. + #{AllowDeadCodePlugin:W} pub struct $builderName<$builderGenerics> { - ${builderFields.joinToString(", ")}, - plugin: $builderPluginGenericTypeName, + ${allBuilderFields.joinToString(",")} } impl<$builderGenerics> $builderName<$builderGenerics> { @@ -342,6 +364,7 @@ class ServerServiceGeneratorV2( "Setters" to builderSetters(), "BuildMethod" to buildMethod(), "BuildUncheckedMethod" to buildUncheckedMethod(), + "AllowDeadCodePlugin" to allowDeadCodePlugin, *codegenScope, ) } @@ -381,6 +404,13 @@ class ServerServiceGeneratorV2( private fun serviceStruct(): Writable = writable { documentShape(service, model) + var builderFields = notSetFields + writable("plugin") + // With no operations there is no use of the `Body` type variable + if (operations.isEmpty()) { + builderFields += writable("_body: std::marker::PhantomData") + } + val joinedBuilderFields = builderFields.join(",") + rustTemplate( """ /// @@ -400,8 +430,7 @@ class ServerServiceGeneratorV2( /// multiple plugins. pub fn builder_with_plugins(plugin: Plugin) -> $builderName { $builderName { - #{NotSetFields:W}, - plugin + #{BuilderFields:W} } } @@ -470,7 +499,7 @@ class ServerServiceGeneratorV2( } } """, - "NotSetFields" to notSetFields.join(", "), + "BuilderFields" to joinedBuilderFields, "Router" to protocol.routerType(), "Protocol" to protocol.markerStruct(), *codegenScope,