diff --git a/.changelog/1762113734.md b/.changelog/1762113734.md new file mode 100644 index 00000000000..130f44e14d0 --- /dev/null +++ b/.changelog/1762113734.md @@ -0,0 +1,29 @@ +--- +applies_to: +- server +authors: +- drganjoo +references: +- smithy-rs#3362 +breaking: false +new_feature: true +bug_fix: false +--- +Server SDK generation supports http/hyper@1. By default, the generated SDK will target http/hyper@0 based crates. + + To generate the SDK for http/hyper@1, add the `http-1x` flag to the codegen section of smithy-build-template.json + + ```json + "plugins": { + "rust-server-codegen": { + "service": "", + "module": "", + "moduleVersion": "", + "moduleDescription": "", + "codegen": { + "http-1x": true + } + } + } + ``` + diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt index be60ef3da25..4a19e8b309b 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt @@ -8,6 +8,7 @@ package software.amazon.smithy.rust.codegen.core.rustlang import software.amazon.smithy.codegen.core.SymbolDependency import software.amazon.smithy.codegen.core.SymbolDependencyContainer import software.amazon.smithy.rust.codegen.core.smithy.ConstrainedModule +import software.amazon.smithy.rust.codegen.core.smithy.HttpVersion import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.util.PANIC @@ -369,6 +370,13 @@ data class CargoDependency( CargoDependency("http-body-1x", CratesIo("1"), `package` = "http-body") val HttpBodyUtil01x: CargoDependency = CargoDependency("http-body-util", CratesIo("0.1.3")) + private val Hyper1x: CargoDependency = CargoDependency("hyper-1x", CratesIo("1"), `package` = "hyper") + + fun hyper(runtimeConfig: RuntimeConfig) = + when (runtimeConfig.httpVersion) { + HttpVersion.Http1x -> CargoDependency("hyper", CratesIo("1")) + HttpVersion.Http0x -> CargoDependency("hyper", CratesIo("0.14.26")) + } fun smithyAsync(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-async") @@ -380,7 +388,17 @@ data class CargoDependency( fun smithyEventStream(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-eventstream") - fun smithyHttp(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-http") + /** + * Returns the appropriate smithy-http dependency based on HTTP version. + * + * For HTTP 1.x: returns `aws-smithy-http` (latest version) + * For HTTP 0.x: returns `aws-smithy-legacy-http` (forked version supporting http@0.2) + */ + fun smithyHttp(runtimeConfig: RuntimeConfig): CargoDependency = + when (runtimeConfig.httpVersion) { + HttpVersion.Http1x -> runtimeConfig.smithyRuntimeCrate("smithy-http") + HttpVersion.Http0x -> runtimeConfig.smithyRuntimeCrate("smithy-legacy-http") + } fun smithyHttpClient(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-http-client") @@ -408,8 +426,17 @@ data class CargoDependency( fun smithyRuntimeApiTestUtil(runtimeConfig: RuntimeConfig) = smithyRuntimeApi(runtimeConfig).toDevDependency().withFeature("test-util") - fun smithyTypes(runtimeConfig: RuntimeConfig) = - runtimeConfig.smithyRuntimeCrate("smithy-types").withFeature("http-body-1-x") + /** + * Returns smithy-types with the appropriate http-body feature. + * + * For HTTP 1.x: adds "http-body-1-x" feature + * For HTTP 0.x: adds "http-body-0-4-x" feature + */ + fun smithyTypes(runtimeConfig: RuntimeConfig): CargoDependency = + when (runtimeConfig.httpVersion) { + HttpVersion.Http1x -> runtimeConfig.smithyRuntimeCrate("smithy-types").withFeature("http-body-1-x") + HttpVersion.Http0x -> runtimeConfig.smithyRuntimeCrate("smithy-types").withFeature("http-body-0-4-x") + } fun smithyXml(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-xml") diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt index f15be607427..ca43dd24d66 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt @@ -55,12 +55,30 @@ value class CrateVersionMap( val map: Map, ) +/** + * HTTP version to use for code generation. + * + * This determines which versions of http/http-body/hyper crates to use, + * as well as which smithy-http-server crate to use (legacy vs current). + */ +enum class HttpVersion { + /** HTTP 0.x: http@0.2, http-body@0.4, hyper@0.14, aws-smithy-legacy-http-server */ + Http0x, + + /** HTTP 1.x: http@1, http-body@1, hyper@1, aws-smithy-http-server@1 */ + Http1x, +} + /** * Prefix & crate location for the runtime crates. */ data class RuntimeConfig( val cratePrefix: String = "aws", val runtimeCrateLocation: RuntimeCrateLocation = RuntimeCrateLocation.path("../"), + /** + * HTTP version to use for code generation. Defaults to Http1x as that is the default for Clients.. + */ + val httpVersion: HttpVersion = HttpVersion.Http1x, ) { companion object { /** @@ -289,10 +307,51 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null) // Http1x types val Http1x = CargoDependency.Http1x.toType() val HttpBody1x = CargoDependency.HttpBody1x.toType() - val HttpRequest1x = Http1x.resolve("Request") val HttpRequestBuilder1x = Http1x.resolve("request::Builder") - val HttpResponse1x = Http1x.resolve("Response") - val HttpResponseBuilder1x = Http1x.resolve("response::Builder") + + /** + * Returns the appropriate http crate based on HTTP version. + * + * For HTTP 1.x: returns http@1.x crate + * For HTTP 0.x: returns http@0.2.x crate + */ + fun httpForConfig(runtimeConfig: RuntimeConfig): RuntimeType = + when (runtimeConfig.httpVersion) { + HttpVersion.Http1x -> Http1x + HttpVersion.Http0x -> Http + } + + /** + * Returns the appropriate http::request::Builder based on HTTP version. + * + * For HTTP 1.x: returns http@1.x request::Builder + * For HTTP 0.x: returns http@0.2.x request::Builder + */ + fun httpRequestBuilderForConfig(runtimeConfig: RuntimeConfig): RuntimeType = + httpForConfig(runtimeConfig).resolve("request::Builder") + + /** + * Returns the appropriate http::response::Builder based on HTTP version. + * + * For HTTP 1.x: returns http@1.x response::Builder + * For HTTP 0.x: returns http@0.2.x response::Builder + */ + fun httpResponseBuilderForConfig(runtimeConfig: RuntimeConfig): RuntimeType = + httpForConfig(runtimeConfig).resolve("response::Builder") + + /** + * Returns the appropriate http-body crate module based on HTTP version. + * + * For HTTP 1.x: returns http-body@1.x crate + * For HTTP 0.x: returns http-body@0.4.x crate + */ + fun httpBodyForConfig(runtimeConfig: RuntimeConfig): RuntimeType = + when (runtimeConfig.httpVersion) { + HttpVersion.Http1x -> HttpBody1x + HttpVersion.Http0x -> HttpBody + } + + fun hyperForConfig(runtimeConfig: RuntimeConfig) = CargoDependency.hyper(runtimeConfig).toType() // external cargo dependency types val Bytes = CargoDependency.Bytes.toType().resolve("Bytes") @@ -337,6 +396,21 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null) fun smithyRuntimeApi(runtimeConfig: RuntimeConfig) = CargoDependency.smithyRuntimeApi(runtimeConfig).toType() + /** + * Returns smithy-runtime-api with the appropriate HTTP version feature enabled. + * + * This is needed when accessing HTTP types from smithy-runtime-api like http::Request. + * For HTTP 1.x: adds "http-1x" feature + * For HTTP 0.x: adds "http-02x" feature + * + * Use this instead of smithyRuntimeApi() when you need HTTP type re-exports. + */ + fun smithyRuntimeApiWithHttpFeature(runtimeConfig: RuntimeConfig): RuntimeType = + when (runtimeConfig.httpVersion) { + HttpVersion.Http1x -> CargoDependency.smithyRuntimeApi(runtimeConfig).withFeature("http-1x").toType() + HttpVersion.Http0x -> CargoDependency.smithyRuntimeApi(runtimeConfig).withFeature("http-02x").toType() + } + fun smithyRuntimeApiClient(runtimeConfig: RuntimeConfig) = CargoDependency.smithyRuntimeApiClient(runtimeConfig).toType() diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/http/HttpBindingGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/http/HttpBindingGenerator.kt index 7b4a9aa5976..75a3730b64f 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/http/HttpBindingGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/http/HttpBindingGenerator.kt @@ -535,8 +535,8 @@ class HttpBindingGenerator( val codegenScope = arrayOf( "BuildError" to runtimeConfig.operationBuildError(), - HttpMessageType.REQUEST.name to RuntimeType.HttpRequestBuilder1x, - HttpMessageType.RESPONSE.name to RuntimeType.HttpResponseBuilder1x, + HttpMessageType.REQUEST.name to RuntimeType.httpRequestBuilderForConfig(runtimeConfig), + HttpMessageType.RESPONSE.name to RuntimeType.httpResponseBuilderForConfig(runtimeConfig), "Shape" to shapeSymbol, ) rustBlockTemplate( @@ -712,7 +712,7 @@ class HttpBindingGenerator( builder = builder.header("$headerName", header_value); """, - "HeaderValue" to RuntimeType.Http1x.resolve("HeaderValue"), + "HeaderValue" to RuntimeType.httpForConfig(runtimeConfig).resolve("HeaderValue"), "invalid_field_error" to renderErrorMessage("header_value"), ) } @@ -767,8 +767,8 @@ class HttpBindingGenerator( } """, - "HeaderValue" to RuntimeType.Http1x.resolve("HeaderValue"), - "HeaderName" to RuntimeType.Http1x.resolve("HeaderName"), + "HeaderValue" to RuntimeType.httpForConfig(runtimeConfig).resolve("HeaderValue"), + "HeaderName" to RuntimeType.httpForConfig(runtimeConfig).resolve("HeaderName"), "invalid_header_name" to OperationBuildError(runtimeConfig).invalidField(memberName) { rust("""format!("`{k}` cannot be used as a header name: {err}")""") diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/HttpBoundProtocolPayloadGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/HttpBoundProtocolPayloadGenerator.kt index 0479a8696f9..6ee4284452e 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/HttpBoundProtocolPayloadGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/HttpBoundProtocolPayloadGenerator.kt @@ -14,7 +14,6 @@ import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.EnumTrait -import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate @@ -66,7 +65,6 @@ class HttpBoundProtocolPayloadGenerator( private val smithyEventStream = RuntimeType.smithyEventStream(runtimeConfig) private val codegenScope = arrayOf( - "hyper" to CargoDependency.HyperWithStream.toType(), "SdkBody" to RuntimeType.sdkBody(runtimeConfig), "BuildError" to runtimeConfig.operationBuildError(), "SmithyHttp" to RuntimeType.smithyHttp(runtimeConfig), diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/CodegenIntegrationTest.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/CodegenIntegrationTest.kt index fbb3742a4a8..20900b303ea 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/CodegenIntegrationTest.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/CodegenIntegrationTest.kt @@ -115,6 +115,11 @@ class ServerAdditionalSettings private constructor(settings: List runtimeConfig.smithyRuntimeCrate("smithy-http-server") + HttpVersion.Http0x -> runtimeConfig.smithyRuntimeCrate("smithy-legacy-http-server") + } + + fun hyperDev(runtimeConfig: RuntimeConfig): CargoDependency = + CargoDependency.hyper(runtimeConfig).copy(scope = DependencyScope.Dev) fun smithyTypes(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-types") } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRustSettings.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRustSettings.kt index 0ab70f13619..1d646d91ad7 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRustSettings.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRustSettings.kt @@ -11,6 +11,7 @@ import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.rust.codegen.core.smithy.CODEGEN_SETTINGS import software.amazon.smithy.rust.codegen.core.smithy.CoreCodegenConfig import software.amazon.smithy.rust.codegen.core.smithy.CoreRustSettings +import software.amazon.smithy.rust.codegen.core.smithy.HttpVersion import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import java.util.Optional @@ -62,6 +63,16 @@ data class ServerRustSettings( val coreRustSettings = CoreRustSettings.from(model, config) val codegenSettingsNode = config.getObjectMember(CODEGEN_SETTINGS) val coreCodegenConfig = CoreCodegenConfig.fromNode(codegenSettingsNode) + + // Create ServerCodegenConfig first to read the http-1x flag + val serverCodegenConfig = ServerCodegenConfig.fromCodegenConfigAndNode(coreCodegenConfig, codegenSettingsNode) + + // Use the http1x field from ServerCodegenConfig to set RuntimeConfig httpVersion + // This must be done because RuntimeConfig is created in CoreRustSettings.from() + // before we have access to the http-1x flag + val httpVersion = if (serverCodegenConfig.http1x) HttpVersion.Http1x else HttpVersion.Http0x + val runtimeConfig = coreRustSettings.runtimeConfig.copy(httpVersion = httpVersion) + return ServerRustSettings( service = coreRustSettings.service, moduleName = coreRustSettings.moduleName, @@ -69,8 +80,8 @@ data class ServerRustSettings( moduleAuthors = coreRustSettings.moduleAuthors, moduleDescription = coreRustSettings.moduleDescription, moduleRepository = coreRustSettings.moduleRepository, - runtimeConfig = coreRustSettings.runtimeConfig, - codegenConfig = ServerCodegenConfig.fromCodegenConfigAndNode(coreCodegenConfig, codegenSettingsNode), + runtimeConfig = runtimeConfig, + codegenConfig = serverCodegenConfig, license = coreRustSettings.license, examplesUri = coreRustSettings.examplesUri, minimumSupportedRustVersion = coreRustSettings.minimumSupportedRustVersion, @@ -83,6 +94,7 @@ data class ServerRustSettings( /** * [publicConstrainedTypes]: Generate constrained wrapper newtypes for constrained shapes * [ignoreUnsupportedConstraints]: Generate model even though unsupported constraints are present + * [http1x]: Enable HTTP 1.x support (hyper 1.x and http 1.x types) */ data class ServerCodegenConfig( override val formatTimeoutSeconds: Int = DEFAULT_FORMAT_TIMEOUT_SECONDS, @@ -98,6 +110,7 @@ data class ServerCodegenConfig( val experimentalCustomValidationExceptionWithReasonPleaseDoNotUse: String? = defaultExperimentalCustomValidationExceptionWithReasonPleaseDoNotUse, val addValidationExceptionToConstrainedOperations: Boolean = DEFAULT_ADD_VALIDATION_EXCEPTION_TO_CONSTRAINED_OPERATIONS, val alwaysSendEventStreamInitialResponse: Boolean = DEFAULT_SEND_EVENT_STREAM_INITIAL_RESPONSE, + val http1x: Boolean = DEFAULT_HTTP_1X, ) : CoreCodegenConfig( formatTimeoutSeconds, debugMode, ) { @@ -107,11 +120,51 @@ data class ServerCodegenConfig( private val defaultExperimentalCustomValidationExceptionWithReasonPleaseDoNotUse = null private const val DEFAULT_ADD_VALIDATION_EXCEPTION_TO_CONSTRAINED_OPERATIONS = false private const val DEFAULT_SEND_EVENT_STREAM_INITIAL_RESPONSE = false + const val DEFAULT_HTTP_1X = false + + /** + * Configuration key for the HTTP 1.x flag. + * + * When set to true in codegen configuration, generates code that uses http@1.x/hyper@1.x + * instead of http@0.2.x/hyper@0.14.x. + * + * **Usage:** + * - Use this constant when reading/writing the codegen configuration + * - Use this constant in test utilities that set configuration (e.g., ServerCodegenIntegrationTest) + * + * **Do NOT use this constant for:** + * - External crate feature names (e.g., `smithyRuntimeApi.withFeature("http-1x")`) + * Those feature names are defined by the external crates and may change independently + * - Cargo.toml feature names unless they are explicitly defined by us to match this value + */ + const val HTTP_1X_CONFIG_KEY = "http-1x" + + private val KNOWN_CONFIG_KEYS = + setOf( + "formatTimeoutSeconds", + "debugMode", + "publicConstrainedTypes", + "ignoreUnsupportedConstraints", + "experimentalCustomValidationExceptionWithReasonPleaseDoNotUse", + "addValidationExceptionToConstrainedOperations", + "alwaysSendEventStreamInitialResponse", + HTTP_1X_CONFIG_KEY, + ) fun fromCodegenConfigAndNode( coreCodegenConfig: CoreCodegenConfig, node: Optional, ) = if (node.isPresent) { + // Validate that all config keys are known + val configNode = node.get() + val unknownKeys = configNode.members.keys.map { it.toString() }.filter { it !in KNOWN_CONFIG_KEYS } + if (unknownKeys.isNotEmpty()) { + throw IllegalArgumentException( + "Unknown codegen configuration key(s): ${unknownKeys.joinToString(", ")}. " + + "Known keys are: ${KNOWN_CONFIG_KEYS.joinToString(", ")}. ", + ) + } + ServerCodegenConfig( formatTimeoutSeconds = coreCodegenConfig.formatTimeoutSeconds, debugMode = coreCodegenConfig.debugMode, @@ -139,6 +192,11 @@ data class ServerCodegenConfig( "alwaysSendEventStreamInitialResponse", DEFAULT_SEND_EVENT_STREAM_INITIAL_RESPONSE, ), + http1x = + node.get().getBooleanMemberOrDefault( + HTTP_1X_CONFIG_KEY, + DEFAULT_HTTP_1X, + ), ) } else { ServerCodegenConfig( diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/ServerRequiredCustomizations.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/ServerRequiredCustomizations.kt index a366b39c2ea..0bb69c31497 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/ServerRequiredCustomizations.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/ServerRequiredCustomizations.kt @@ -13,6 +13,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.customizations.AllowLints import software.amazon.smithy.rust.codegen.core.smithy.customizations.CrateVersionCustomization import software.amazon.smithy.rust.codegen.core.smithy.customizations.pubUseSmithyPrimitives import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsCustomization +import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator @@ -48,11 +49,15 @@ class ServerRequiredCustomizations : ServerCodegenDecorator { ), ) + // Use version-aware smithy-http-server dependency name for features + val smithyHttpServerDep = ServerCargoDependency.smithyHttpServer(rc) + val smithyHttpServerName = smithyHttpServerDep.name + rustCrate.mergeFeature( Feature( "aws-lambda", false, - listOf("aws-smithy-http-server/aws-lambda"), + listOf("$smithyHttpServerName/aws-lambda"), ), ) @@ -60,7 +65,7 @@ class ServerRequiredCustomizations : ServerCodegenDecorator { Feature( "request-id", true, - listOf("aws-smithy-http-server/request-id"), + listOf("$smithyHttpServerName/request-id"), ), ) diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerHttpSensitivityGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerHttpSensitivityGenerator.kt index 468c59547b0..6ea4b4756be 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerHttpSensitivityGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerHttpSensitivityGenerator.kt @@ -17,13 +17,13 @@ import software.amazon.smithy.model.traits.HttpQueryParamsTrait import software.amazon.smithy.model.traits.HttpQueryTrait import software.amazon.smithy.model.traits.HttpTrait import software.amazon.smithy.model.traits.SensitiveTrait -import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.plus import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.util.dq import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rust.codegen.core.util.hasTrait @@ -135,7 +135,7 @@ sealed class HeaderSensitivity( private val codegenScope = arrayOf( "SmithyHttpServer" to ServerCargoDependency.smithyHttpServer(runtimeConfig).toType(), - "Http" to CargoDependency.Http.toType(), + "Http" to RuntimeType.httpForConfig(runtimeConfig), ) /** The case where `prefixHeaders` value is not sensitive. */ @@ -348,7 +348,7 @@ class ServerHttpSensitivityGenerator( private val codegenScope = arrayOf( "SmithyHttpServer" to ServerCargoDependency.smithyHttpServer(runtimeConfig).toType(), - "Http" to CargoDependency.Http.toType(), + "Http" to RuntimeType.httpForConfig(runtimeConfig), ) /** Constructs `StatusCodeSensitivity` of a `Shape` */ diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRootGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRootGenerator.kt index d23801549e1..8f144d583a7 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRootGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRootGenerator.kt @@ -12,6 +12,9 @@ import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.join import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.HttpVersion +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.util.toPascalCase import software.amazon.smithy.rust.codegen.core.util.toSnakeCase import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency @@ -42,6 +45,145 @@ open class ServerRootGenerator( ).toList() private val serviceName = codegenContext.serviceShape.id.name.toPascalCase() + /** + * Returns a Writable containing the appropriate Hyper usage example based on HTTP version. + * For HTTP 1.x: Uses tokio::net::TcpListener and serve() function + * For HTTP 0.x: Uses hyper::Server::bind() API + */ + private fun hyperServeExample( + crateName: String, + serviceName: String, + unwrapConfigBuilder: String, + ): Writable = + writable { + when (codegenContext.settings.runtimeConfig.httpVersion) { + HttpVersion.Http1x -> + rustTemplate( + """ + //! ```rust,no_run + //! ## use std::net::SocketAddr; + //! ## async fn dummy() { + //! use $crateName::{$serviceName, ${serviceName}Config}; + //! use $crateName::server::serve; + //! use #{Tokio}::net::TcpListener; + //! + //! ## let app = $serviceName::builder( + //! ## ${serviceName}Config::builder() + //! ## .build()$unwrapConfigBuilder + //! ## ).build_unchecked(); + //! let bind: SocketAddr = "127.0.0.1:6969".parse() + //! .expect("unable to parse the server bind address and port"); + //! let listener = TcpListener::bind(bind).await + //! .expect("failed to bind TCP listener"); + //! serve(listener, app.into_make_service()).await.unwrap(); + //! ## } + //! ``` + """, + "Tokio" to ServerCargoDependency.TokioDev.toType(), + ) + + HttpVersion.Http0x -> + rustTemplate( + """ + //! ```rust,no_run + //! ## use std::net::SocketAddr; + //! ## async fn dummy() { + //! use $crateName::{$serviceName, ${serviceName}Config}; + //! + //! ## let app = $serviceName::builder( + //! ## ${serviceName}Config::builder() + //! ## .build()$unwrapConfigBuilder + //! ## ).build_unchecked(); + //! let server = app.into_make_service(); + //! let bind: SocketAddr = "127.0.0.1:6969".parse() + //! .expect("unable to parse the server bind address and port"); + //! #{Hyper0}::Server::bind(&bind).serve(server).await.unwrap(); + //! ## } + //! + //! ``` + """, + "Hyper0" to ServerCargoDependency.hyperDev(codegenContext.runtimeConfig).toType(), + ) + } + } + + /** + * Returns a Writable containing the appropriate full example code based on HTTP version. + * For HTTP 1.x: Uses tokio::net::TcpListener and serve() function + * For HTTP 0.x: Uses hyper::Server::bind() API + */ + private fun fullExample( + crateName: String, + serviceName: String, + unwrapConfigBuilder: String, + builderFieldNames: Map<*, String>, + ): Writable = + writable { + when (codegenContext.settings.runtimeConfig.httpVersion) { + HttpVersion.Http1x -> + rustTemplate( + """ + //! ```rust,no_run + //! ## use std::net::SocketAddr; + //! use $crateName::{$serviceName, ${serviceName}Config}; + //! use $crateName::server::serve; + //! use #{Tokio}::net::TcpListener; + //! + //! ##[#{Tokio}::main] + //! pub async fn main() { + //! let config = ${serviceName}Config::builder().build()$unwrapConfigBuilder; + //! let app = $serviceName::builder(config) + ${builderFieldNames.values.joinToString("\n") { "//! .$it($it)" }} + //! .build() + //! .expect("failed to build an instance of $serviceName"); + //! + //! let bind: SocketAddr = "127.0.0.1:6969".parse() + //! .expect("unable to parse the server bind address and port"); + //! let listener = TcpListener::bind(bind).await + //! .expect("failed to bind TCP listener"); + //! ## let server = async { Ok::<_, ()>(()) }; + //! + //! // Run your service! + //! if let Err(err) = serve(listener, app.into_make_service()).await { + //! eprintln!("server error: {:?}", err); + //! } + //! } + """, + "Tokio" to ServerCargoDependency.TokioDev.toType(), + ) + + HttpVersion.Http0x -> + rustTemplate( + """ + //! ```rust,no_run + //! ## use std::net::SocketAddr; + //! use $crateName::{$serviceName, ${serviceName}Config}; + //! + //! ##[#{Tokio}::main] + //! pub async fn main() { + //! let config = ${serviceName}Config::builder().build()$unwrapConfigBuilder; + //! let app = $serviceName::builder(config) + ${builderFieldNames.values.joinToString("\n") { "//! .$it($it)" }} + //! .build() + //! .expect("failed to build an instance of $serviceName"); + //! + //! let bind: SocketAddr = "127.0.0.1:6969".parse() + //! .expect("unable to parse the server bind address and port"); + //! let server = #{Hyper}::Server::bind(&bind).serve(app.into_make_service()); + //! ## let server = async { Ok::<_, ()>(()) }; + //! + //! // Run your service! + //! if let Err(err) = server.await { + //! eprintln!("server error: {:?}", err); + //! } + //! } + """, + "Hyper" to ServerCargoDependency.hyperDev(codegenContext.runtimeConfig).toType(), + "Tokio" to ServerCargoDependency.TokioDev.toType(), + ) + } + } + fun documentation(writer: RustWriter) { val builderFieldNames = operations.associateWith { @@ -82,21 +224,7 @@ open class ServerRootGenerator( //! //! ###### Running on Hyper //! - //! ```rust,no_run - //! ## use std::net::SocketAddr; - //! ## async fn dummy() { - //! use $crateName::{$serviceName, ${serviceName}Config}; - //! - //! ## let app = $serviceName::builder( - //! ## ${serviceName}Config::builder() - //! ## .build()$unwrapConfigBuilder - //! ## ).build_unchecked(); - //! let server = app.into_make_service(); - //! let bind: SocketAddr = "127.0.0.1:6969".parse() - //! .expect("unable to parse the server bind address and port"); - //! #{Hyper}::Server::bind(&bind).serve(server).await.unwrap(); - //! ## } - //! ``` + #{HyperServeExample:W} //! //! ###### Running on Lambda //! @@ -127,7 +255,7 @@ open class ServerRootGenerator( //! ```rust,no_run //! ## use $crateName::server::plugin::IdentityPlugin as LoggingPlugin; //! ## use $crateName::server::plugin::IdentityPlugin as MetricsPlugin; - //! ## use #{Hyper}::Body; + //! ## use #{Body}; //! use $crateName::server::plugin::HttpPlugins; //! use $crateName::{$serviceName, ${serviceName}Config, $builderName}; //! @@ -135,7 +263,7 @@ open class ServerRootGenerator( //! .push(LoggingPlugin) //! .push(MetricsPlugin); //! let config = ${serviceName}Config::builder().build()$unwrapConfigBuilder; - //! let builder: $builderName = $serviceName::builder(config); + //! let builder: $builderName<#{Body}, _, _, _> = $serviceName::builder(config); //! ``` //! //! Check out [`crate::server::plugin`] to learn more about plugins. @@ -199,28 +327,7 @@ open class ServerRootGenerator( //! //! ## Example //! - //! ```rust,no_run - //! ## use std::net::SocketAddr; - //! use $crateName::{$serviceName, ${serviceName}Config}; - //! - //! ##[#{Tokio}::main] - //! pub async fn main() { - //! let config = ${serviceName}Config::builder().build()$unwrapConfigBuilder; - //! let app = $serviceName::builder(config) - ${builderFieldNames.values.joinToString("\n") { "//! .$it($it)" }} - //! .build() - //! .expect("failed to build an instance of $serviceName"); - //! - //! let bind: SocketAddr = "127.0.0.1:6969".parse() - //! .expect("unable to parse the server bind address and port"); - //! let server = #{Hyper}::Server::bind(&bind).serve(app.into_make_service()); - //! ## let server = async { Ok::<_, ()>(()) }; - //! - //! // Run your service! - //! if let Err(err) = server.await { - //! eprintln!("server error: {:?}", err); - //! } - //! } + #{FullExample:W} //! #{HandlerImports:W} //! @@ -228,22 +335,57 @@ open class ServerRootGenerator( //! //! ``` //! - //! [`serve`]: https://docs.rs/hyper/0.14.16/hyper/server/struct.Builder.html##method.serve + #{ServeLink} //! [`tower::make::MakeService`]: https://docs.rs/tower/latest/tower/make/trait.MakeService.html //! [HTTP binding traits]: https://smithy.io/2.0/spec/http-bindings.html //! [operations]: https://smithy.io/2.0/spec/service-types.html##operation - //! [hyper server]: https://docs.rs/hyper/latest/hyper/server/index.html //! [Service]: https://docs.rs/tower-service/latest/tower_service/trait.Service.html """, "HandlerImports" to handlerImports(crateName, operations, commentToken = "//!"), "Handlers" to handlers, "ExampleHandler" to operations.take(1).map { operation -> DocHandlerGenerator(codegenContext, operation, builderFieldNames[operation]!!, "//!").docSignature() }, - "Hyper" to ServerCargoDependency.HyperDev.toType(), + "HyperServeExample" to hyperServeExample(crateName, serviceName, unwrapConfigBuilder), + "FullExample" to fullExample(crateName, serviceName, unwrapConfigBuilder, builderFieldNames), + "Hyper" to ServerCargoDependency.hyperDev(codegenContext.runtimeConfig).toType(), "Tokio" to ServerCargoDependency.TokioDev.toType(), "Tower" to ServerCargoDependency.Tower.toType(), + "Body" to + when (codegenContext.runtimeConfig.httpVersion) { + HttpVersion.Http0x -> + ServerCargoDependency.hyperDev(codegenContext.runtimeConfig).toType().resolve("Body") + HttpVersion.Http1x -> + ServerCargoDependency.hyperDev(codegenContext.runtimeConfig).toType().resolve("body::Incoming") + }, + "ServeLink" to serveLink(codegenContext.runtimeConfig, crateName), ) } + private fun serveLink( + runtimeConfig: RuntimeConfig, + crateName: String, + ) = writable { + when (runtimeConfig.httpVersion) { + HttpVersion.Http0x -> { + rustTemplate( + """ + //! [`serve`]: https://docs.rs/hyper/0.14.16/hyper/server/struct.Builder.html##method.serve + //! [hyper server]: https://docs.rs/hyper/#{Hyper0Version}/hyper/server/index.html + """, + "Hyper0Version" to writable { rust(ServerCargoDependency.hyperDev(codegenContext.runtimeConfig).version()) }, + ) + } + + HttpVersion.Http1x -> { + rustTemplate( + """ + //! [`serve`]: $crateName::server::serve + //! [hyper server]: https://docs.rs/hyper/latest/hyper/server/index.html + """, + ) + } + } + } + /** * Render Service Specific code. Code will end up in different files via [useShapeWriter]. See `SymbolVisitor.kt` * which assigns a symbol location to each shape. diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGenerator.kt index 76cdc86fe0f..efbf4be77ad 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGenerator.kt @@ -40,9 +40,9 @@ class ServerServiceGenerator( private val codegenScope = arrayOf( "Bytes" to RuntimeType.Bytes, - "Http" to RuntimeType.Http, + "Http" to RuntimeType.httpForConfig(runtimeConfig), "SmithyHttp" to RuntimeType.smithyHttp(runtimeConfig), - "HttpBody" to RuntimeType.HttpBody, + "HttpBody" to RuntimeType.httpBodyForConfig(runtimeConfig), "SmithyHttpServer" to smithyHttpServer, "Tower" to RuntimeType.Tower, *RuntimeType.preludeScope, diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/http/RestRequestSpecGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/http/RestRequestSpecGenerator.kt index a02a0634a23..4f72d66eccc 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/http/RestRequestSpecGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/http/RestRequestSpecGenerator.kt @@ -10,6 +10,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.withBlock import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.protocols.HttpBindingResolver @@ -19,6 +20,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.protocols.HttpBindingReso class RestRequestSpecGenerator( private val httpBindingResolver: HttpBindingResolver, private val requestSpecModule: RuntimeType, + private val runtimeConfig: RuntimeConfig, ) { fun generate(operationShape: OperationShape): Writable { val httpTrait = httpBindingResolver.httpTrait(operationShape) @@ -85,7 +87,7 @@ class RestRequestSpecGenerator( *extraCodegenScope, "PathSegmentsVec" to pathSegmentsVec, "QuerySegmentsVec" to querySegmentsVec, - "Method" to RuntimeType.Http.resolve("Method"), + "Method" to RuntimeType.httpForConfig(runtimeConfig).resolve("Method"), ) } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocol.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocol.kt index 43982b9b3e0..0a7362dc906 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocol.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocol.kt @@ -259,7 +259,8 @@ class ServerRestJsonProtocol( operationName: String, serviceName: String, requestSpecModule: RuntimeType, - ): Writable = RestRequestSpecGenerator(httpBindingResolver, requestSpecModule).generate(operationShape) + ): Writable = + RestRequestSpecGenerator(httpBindingResolver, requestSpecModule, runtimeConfig).generate(operationShape) override fun serverRouterRequestSpecType(requestSpecModule: RuntimeType): RuntimeType = requestSpecModule.resolve("RequestSpec") @@ -292,7 +293,8 @@ class ServerRestXmlProtocol( operationName: String, serviceName: String, requestSpecModule: RuntimeType, - ): Writable = RestRequestSpecGenerator(httpBindingResolver, requestSpecModule).generate(operationShape) + ): Writable = + RestRequestSpecGenerator(httpBindingResolver, requestSpecModule, runtimeConfig).generate(operationShape) override fun serverRouterRequestSpecType(requestSpecModule: RuntimeType): RuntimeType = requestSpecModule.resolve("RequestSpec") diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt index f2781f576a1..a81e0775f08 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt @@ -19,6 +19,7 @@ import software.amazon.smithy.protocoltests.traits.HttpMalformedResponseBodyDefi import software.amazon.smithy.protocoltests.traits.HttpMalformedResponseDefinition import software.amazon.smithy.protocoltests.traits.HttpRequestTestCase import software.amazon.smithy.protocoltests.traits.HttpResponseTestCase +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency 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 @@ -29,6 +30,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.withBlock import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext +import software.amazon.smithy.rust.codegen.core.smithy.HttpVersion import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.BrokenTest import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.FailingTest @@ -281,17 +283,34 @@ class ServerProtocolTestGenerator( private val instantiator = ServerInstantiator(codegenContext, withinTest = true) + /** + * Returns the protocol test dependency with the correct HTTP version feature. + * For HTTP 0.x: uses http-02x feature (default). + * For HTTP 1.x: uses http-1x feature. + */ + private val protocolTestRuntimeType = + when (codegenContext.runtimeConfig.httpVersion) { + HttpVersion.Http1x -> + CargoDependency.smithyProtocolTestHelpers(codegenContext.runtimeConfig) + .withFeature("http-1x") + .toType() + + HttpVersion.Http0x -> + CargoDependency.smithyProtocolTestHelpers(codegenContext.runtimeConfig).toType() + } + private val codegenScope = arrayOf( "AssertEq" to RuntimeType.PrettyAssertions.resolve("assert_eq!"), "Base64SimdDev" to ServerCargoDependency.Base64SimdDev.toType(), "Bytes" to RuntimeType.Bytes, - "Hyper" to RuntimeType.Hyper, - "MediaType" to RuntimeType.protocolTest(codegenContext.runtimeConfig, "MediaType"), + "Http" to RuntimeType.httpForConfig(codegenContext.runtimeConfig), + "Hyper" to RuntimeType.hyperForConfig(codegenContext.runtimeConfig), + "MediaType" to protocolTestRuntimeType.resolve("MediaType"), "Tokio" to ServerCargoDependency.TokioDev.toType(), "Tower" to RuntimeType.Tower, "SmithyHttpServer" to ServerCargoDependency.smithyHttpServer(codegenContext.runtimeConfig).toType(), - "decode_body_data" to RuntimeType.protocolTest(codegenContext.runtimeConfig, "decode_body_data"), + "decode_body_data" to protocolTestRuntimeType.resolve("decode_body_data"), ) override fun RustWriter.renderAllTestCases(allTests: List) { @@ -432,7 +451,7 @@ class ServerProtocolTestGenerator( rustTemplate( """ ##[allow(unused_mut)] - let mut http_request = http::Request::builder() + let mut http_request = #{Http}::Request::builder() .uri("$uri") .method("$method") """, @@ -441,34 +460,40 @@ class ServerProtocolTestGenerator( for (header in headers) { rust(".header(${header.key.dq()}, ${header.value.dq()})") } + + // Determine if we need Sync bounds for the body (streaming operations) + val needsSync = operationShape.inputShape(model).hasStreamingMember(model) + + val bodyCode = + if (body != null) { + // The `replace` is necessary to fix the malformed request test `RestJsonInvalidJsonBody`. + // https://github.com/awslabs/smithy/blob/887ae4f6d118e55937105583a07deb90d8fabe1c/smithy-aws-protocol-tests/model/restJson1/malformedRequests/malformed-request-body.smithy#L47 + // + // Smithy is written in Java, which parses `\u000c` within a `String` as a single char given by the + // corresponding Unicode code point. That is the "form feed" 0x0c character. When printing it, + // it gets written as "\f", which is an invalid Rust escape sequence: https://static.rust-lang.org/doc/master/reference.html#literals + // So we need to write the corresponding Rust Unicode escape sequence to make the program compile. + // + // We also escape to avoid interactions with templating in the case where the body contains `#`. + val sanitizedBody = escape(body.replace("\u000c", "\\u{000c}")).dq() + + val encodedBodyTemplate = """ + #{Bytes}::copy_from_slice( + &#{decode_body_data}($sanitizedBody.as_bytes(), #{MediaType}::from(${(bodyMediaType ?: "unknown").dq()})) + ) + """ + + requestBodyConstructor(encodedBodyTemplate, needsSync) + } else { + requestBodyConstructor(null, needsSync) + } + rustTemplate( """ - .body(${ - if (body != null) { - // The `replace` is necessary to fix the malformed request test `RestJsonInvalidJsonBody`. - // https://github.com/awslabs/smithy/blob/887ae4f6d118e55937105583a07deb90d8fabe1c/smithy-aws-protocol-tests/model/restJson1/malformedRequests/malformed-request-body.smithy#L47 - // - // Smithy is written in Java, which parses `\u000c` within a `String` as a single char given by the - // corresponding Unicode code point. That is the "form feed" 0x0c character. When printing it, - // it gets written as "\f", which is an invalid Rust escape sequence: https://static.rust-lang.org/doc/master/reference.html#literals - // So we need to write the corresponding Rust Unicode escape sequence to make the program compile. - // - // We also escape to avoid interactions with templating in the case where the body contains `#`. - val sanitizedBody = escape(body.replace("\u000c", "\\u{000c}")).dq() - - val encodedBody = """ - #{Bytes}::copy_from_slice( - &#{decode_body_data}($sanitizedBody.as_bytes(), #{MediaType}::from(${(bodyMediaType ?: "unknown").dq()})) - ) - """ - - "#{SmithyHttpServer}::body::Body::from($encodedBody)" - } else { - "#{SmithyHttpServer}::body::Body::empty()" - } - }).unwrap(); - """, + .body(#{BodyCode}).unwrap(); + """, *codegenScope, + "BodyCode" to bodyCode, ) if (queryParams.isNotEmpty()) { val queryParamsString = queryParams.joinToString(separator = "&") @@ -479,6 +504,58 @@ class ServerProtocolTestGenerator( } } + /** + * Returns code for creating an HTTP request body in protocol tests. + * For HTTP 0.x: uses Body::from(bytes) or Body::empty() + * For HTTP 1.x: boxes the body since there's no concrete Body type + * + * @param bytesExpr Expression for the bytes to put in the body, or null for empty body + * @param needsSync If true, uses boxed_sync for operations that require Sync bounds + */ + private fun requestBodyConstructor( + bytesExpr: String?, + needsSync: Boolean = false, + ) = writable { + val runtimeConfig = codegenContext.runtimeConfig + val serverCrate = ServerCargoDependency.smithyHttpServer(runtimeConfig).toType() + + if (runtimeConfig.httpVersion == software.amazon.smithy.rust.codegen.core.smithy.HttpVersion.Http1x) { + val bodyUtil = software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency.HttpBodyUtil01x.toType() + + val boxFn = if (needsSync) "boxed_sync" else "boxed" + if (bytesExpr != null) { + rustTemplate( + "#{BoxFn}(#{Full}::new($bytesExpr))", + "BoxFn" to serverCrate.resolve("body::$boxFn"), + "Full" to bodyUtil.resolve("Full"), + *codegenScope, + ) + } else { + rustTemplate( + "#{BoxFn}(#{Empty}::new())", + "BoxFn" to serverCrate.resolve("body::$boxFn"), + "Empty" to bodyUtil.resolve("Empty"), + *codegenScope, + ) + } + } else { + // HTTP 0.x: use Body::from or Body::empty + if (bytesExpr != null) { + rustTemplate( + "#{BodyFrom}($bytesExpr)", + "BodyFrom" to serverCrate.resolve("body::Body::from"), + *codegenScope, + ) + } else { + rustTemplate( + "#{Empty}()", + "Empty" to serverCrate.resolve("body::Body::empty"), + *codegenScope, + ) + } + } + } + /** Returns the body of the operation handler in a request test. */ private fun checkRequestHandler( operationShape: OperationShape, @@ -515,12 +592,16 @@ class ServerProtocolTestGenerator( ) { val (inputT, _) = operationInputOutputTypes[operationShape]!! val operationName = RustReservedWords.escapeIfNeeded(operationSymbol.name.toSnakeCase()) + + val inputShape = operationShape.inputShape(model) + val needsSync = inputShape.hasStreamingMember(model) + rustWriter.rustTemplate( """ ##[allow(unused_mut)] let (sender, mut receiver) = #{Tokio}::sync::mpsc::channel(1); let config = crate::service::${serviceName}Config::builder().build(); - let service = crate::service::$serviceName::builder::<#{Hyper}::body::Body, _, _, _>(config) + let service = crate::service::$serviceName::builder::<#{BodyType}, _, _, _>(config) .$operationName(move |input: $inputT| { let sender = sender.clone(); async move { @@ -535,6 +616,7 @@ class ServerProtocolTestGenerator( .expect("unable to make an HTTP request"); """, "Body" to body, + "BodyType" to serviceBuilderBodyType(needsSync), *codegenScope, ) } @@ -589,7 +671,7 @@ class ServerProtocolTestGenerator( when (codegenContext.model.expectShape(member.target)) { is DoubleShape, is FloatShape -> { rustWriter.addUseImports( - RuntimeType.protocolTest(codegenContext.runtimeConfig, "FloatEquals") + protocolTestRuntimeType.resolve("FloatEquals") .toSymbol(), ) rustWriter.rust( @@ -672,17 +754,61 @@ class ServerProtocolTestGenerator( } } + /** + * Returns the body type to use for service builder in protocol tests. + * For HTTP 0.x: uses hyper::body::Body (concrete type). + * For HTTP 1.x: uses smithy BoxBody (no Sync requirement for protocol tests). + */ + private fun serviceBuilderBodyType(needsSync: Boolean): RuntimeType = + when (codegenContext.runtimeConfig.httpVersion) { + HttpVersion.Http1x -> + // HTTP 1.x: use BoxBodySync for streaming operations that need Sync + if (needsSync) { + ServerCargoDependency.smithyHttpServer(codegenContext.runtimeConfig).toType().resolve("body::BoxBodySync") + } else { + ServerCargoDependency.smithyHttpServer(codegenContext.runtimeConfig).toType().resolve("body::BoxBody") + } + + HttpVersion.Http0x -> + // HTTP 0.x: use hyper::body::Body (concrete type) + RuntimeType.Hyper.resolve("body::Body") + } + + /** + * Returns a Writable that generates version-appropriate code for reading HTTP response body to bytes. + * For HTTP 1.x: Uses http_body_util::BodyExt::collect() + * For HTTP 0.x: Uses hyper::body::to_bytes() + * + * @param responseVarName The name of the HTTP response variable (e.g., "http_response") + */ + private fun httpBodyToBytes(): Writable = + writable { + when (codegenContext.runtimeConfig.httpVersion) { + HttpVersion.Http1x -> + rustTemplate( + """ + use #{HttpBodyUtil}::BodyExt; + let body = http_response.into_body().collect().await.expect("unable to collect body").to_bytes(); + """, + "HttpBodyUtil" to CargoDependency.HttpBodyUtil01x.toType(), + ) + + HttpVersion.Http0x -> + rustTemplate( + """ + let body = #{Hyper}::body::to_bytes(http_response.into_body()).await.expect("unable to extract body to bytes"); + """, + "Hyper" to RuntimeType.Hyper, + ) + } + } + private fun checkBody( rustWriter: RustWriter, body: String, mediaType: String?, ) { - rustWriter.rustTemplate( - """ - let body = #{Hyper}::body::to_bytes(http_response.into_body()).await.expect("unable to extract body to bytes"); - """, - *codegenScope, - ) + httpBodyToBytes().invoke(rustWriter) if (body == "") { rustWriter.rustTemplate( """ @@ -697,8 +823,8 @@ class ServerProtocolTestGenerator( "#T(&body, ${ rustWriter.escape(body).dq() }, #T::from(${(mediaType ?: "unknown").dq()}))", - RuntimeType.protocolTest(codegenContext.runtimeConfig, "validate_body"), - RuntimeType.protocolTest(codegenContext.runtimeConfig, "MediaType"), + protocolTestRuntimeType.resolve("validate_body"), + protocolTestRuntimeType.resolve("MediaType"), ) } } @@ -711,7 +837,7 @@ class ServerProtocolTestGenerator( rustWriter.rustTemplate( """ #{AssertEq}( - http::StatusCode::from_u16($statusCode).expect("invalid expected HTTP status code"), + #{Http}::StatusCode::from_u16($statusCode).expect("invalid expected HTTP status code"), http_response.status() ); """, diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerHttpBoundProtocolGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerHttpBoundProtocolGenerator.kt index a0532b79fe6..420a3f881e5 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerHttpBoundProtocolGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerHttpBoundProtocolGenerator.kt @@ -182,13 +182,14 @@ class ServerHttpBoundProtocolTraitImplGenerator( private val codegenScope = arrayOf( "AsyncTrait" to ServerCargoDependency.AsyncTrait.toType(), + "Bytes" to RuntimeType.Bytes, "Cow" to RuntimeType.Cow, "DateTime" to RuntimeType.dateTime(runtimeConfig), "FormUrlEncoded" to ServerCargoDependency.FormUrlEncoded.toType(), "FuturesUtil" to ServerCargoDependency.FuturesUtil.toType(), - "HttpBody" to RuntimeType.HttpBody, + "HttpBody" to RuntimeType.httpBodyForConfig(runtimeConfig), "header_util" to RuntimeType.smithyHttp(runtimeConfig).resolve("header"), - "Hyper" to RuntimeType.Hyper, + "Hyper" to RuntimeType.hyperForConfig(runtimeConfig), "LazyStatic" to RuntimeType.LazyStatic, "Mime" to ServerCargoDependency.Mime.toType(), "Nom" to ServerCargoDependency.Nom.toType(), @@ -201,8 +202,9 @@ class ServerHttpBoundProtocolTraitImplGenerator( "RequestRejection" to protocol.requestRejection(runtimeConfig), "ResponseRejection" to protocol.responseRejection(runtimeConfig), "PinProjectLite" to ServerCargoDependency.PinProjectLite.toType(), - "http" to RuntimeType.Http, + "http" to RuntimeType.httpForConfig(runtimeConfig), "Tracing" to RuntimeType.Tracing, + *preludeScope, ) fun generateTraitImpls( @@ -554,8 +556,18 @@ class ServerHttpBoundProtocolTraitImplGenerator( operationShape.outputShape(model).findStreamingMember(model)?.let { val payloadGenerator = ServerHttpBoundProtocolPayloadGenerator(codegenContext, protocol) + + // For HTTP 1.x, use the wrap_stream function instead of Body::wrap_stream method + // since Body is just a type alias for hyper::body::Incoming + val wrapStreamCall = + if (runtimeConfig.httpVersion == software.amazon.smithy.rust.codegen.core.smithy.HttpVersion.Http1x) { + "let body = #{SmithyHttpServer}::body::boxed(#{SmithyHttpServer}::body::wrap_stream(" + } else { + "let body = #{SmithyHttpServer}::body::boxed(#{SmithyHttpServer}::body::Body::wrap_stream(" + } + withBlockTemplate( - "let body = #{SmithyHttpServer}::body::boxed(#{SmithyHttpServer}::body::Body::wrap_stream(", + wrapStreamCall, "));", *codegenScope, ) { @@ -749,9 +761,9 @@ class ServerHttpBoundProtocolTraitImplGenerator( """, *preludeScope, "ParseError" to RuntimeType.smithyHttp(runtimeConfig).resolve("header::ParseError"), - "Request" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("http::Request"), + "Request" to RuntimeType.smithyRuntimeApiWithHttpFeature(runtimeConfig).resolve("http::Request"), "RequestParts" to - RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("http::RequestParts"), + RuntimeType.smithyRuntimeApiWithHttpFeature(runtimeConfig).resolve("http::RequestParts"), ) val parser = structuredDataParser.serverInputParser(operationShape) @@ -764,7 +776,24 @@ class ServerHttpBoundProtocolTraitImplGenerator( // there's something to parse (i.e. `parser != null`), so `!!` is safe here. val expectedRequestContentType = httpBindingResolver.requestContentType(operationShape)!! - rustTemplate("let bytes = #{Hyper}::body::to_bytes(body).await?;", *codegenScope) + + // Generate different body collection code based on HTTP version + if (runtimeConfig.httpVersion == software.amazon.smithy.rust.codegen.core.smithy.HttpVersion.Http1x) { + // For HTTP 1.x: use http-body-util's BodyExt trait + rustTemplate( + """ + let bytes = { + use #{HttpBodyUtil}::BodyExt; + body.collect().await?.to_bytes() + }; + """, + *codegenScope, + "HttpBodyUtil" to software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency.HttpBodyUtil01x.toType(), + ) + } else { + // For HTTP 0.x: use hyper's to_bytes + rustTemplate("let bytes = #{Hyper}::body::to_bytes(body).await?;", *codegenScope) + } // Note that the server is being very lenient here. We're accepting an empty body for when there is modeled // operation input; we simply parse it as empty operation input. // This behavior applies to all protocols. This might seem like a bug, but it isn't. There's protocol tests @@ -910,7 +939,7 @@ class ServerHttpBoundProtocolTraitImplGenerator( rustTemplate( """ { - let mut receiver = #{Deserializer}(&mut body.into().into_inner())?; + let mut receiver = #{Deserializer}(&mut #{eventStreamBodyInto:W})?; if let Some(_initial_event) = receiver .try_recv_initial(#{InitialMessageType}::Request) .await @@ -931,16 +960,18 @@ class ServerHttpBoundProtocolTraitImplGenerator( .resolve("event_stream::InitialMessageType"), "parseInitialRequest" to parseInitialRequest, "AllowUselessConversion" to Attribute.AllowClippyUselessConversion.writable(), + "eventStreamBodyInto" to eventStreamBodyInto(), *codegenScope, ) } else { rustTemplate( """ { - Some(#{Deserializer}(&mut body.into().into_inner())?) + Some(#{Deserializer}(&mut #{eventStreamBodyInto:W})?) } """, "Deserializer" to deserializer, + "eventStreamBodyInto" to eventStreamBodyInto(), *codegenScope, ) } @@ -979,18 +1010,40 @@ class ServerHttpBoundProtocolTraitImplGenerator( } } } - rustTemplate( - """ - { - let bytes = #{Hyper}::body::to_bytes(body).await?; - #{VerifyRequestContentTypeHeader:W} - #{Deserializer}(&bytes)? - } - """, - "Deserializer" to deserializer, - "VerifyRequestContentTypeHeader" to verifyRequestContentTypeHeader, - *codegenScope, - ) + // Generate different body collection code based on HTTP version + if (runtimeConfig.httpVersion == software.amazon.smithy.rust.codegen.core.smithy.HttpVersion.Http1x) { + // For HTTP 1.x: use http-body-util's BodyExt trait + rustTemplate( + """ + { + let bytes = { + use #{HttpBodyUtil}::BodyExt; + body.collect().await?.to_bytes() + }; + #{VerifyRequestContentTypeHeader:W} + #{Deserializer}(&bytes)? + } + """, + "Deserializer" to deserializer, + "VerifyRequestContentTypeHeader" to verifyRequestContentTypeHeader, + *codegenScope, + "HttpBodyUtil" to software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency.HttpBodyUtil01x.toType(), + ) + } else { + // For HTTP 0.x: use hyper's to_bytes + rustTemplate( + """ + { + let bytes = #{Hyper}::body::to_bytes(body).await?; + #{VerifyRequestContentTypeHeader:W} + #{Deserializer}(&bytes)? + } + """, + "Deserializer" to deserializer, + "VerifyRequestContentTypeHeader" to verifyRequestContentTypeHeader, + *codegenScope, + ) + } } } } @@ -1470,11 +1523,29 @@ class ServerHttpBoundProtocolTraitImplGenerator( private fun streamingBodyTraitBounds(operationShape: OperationShape) = if (operationShape.inputShape(model).hasStreamingMember(model)) { - "\n B: Into<#{SmithyTypes}::byte_stream::ByteStream>," + if (runtimeConfig.httpVersion == software.amazon.smithy.rust.codegen.core.smithy.HttpVersion.Http1x) { + // HTTP/1: No Into available, must use ByteStream::from_body_1_x() which requires Send + Sync. + // HTTP/0.4: Uses Into conversion without these bounds. + """ + B: #{HttpBody}::Body + #{Send} + #{Sync} + 'static, + B::Error: Into<::aws_smithy_types::body::Error> + 'static, + """ + } else { + "\n B: Into<#{SmithyTypes}::byte_stream::ByteStream>," + } } else { "" } + private fun eventStreamBodyInto() = + writable { + if (runtimeConfig.httpVersion == software.amazon.smithy.rust.codegen.core.smithy.HttpVersion.Http1x) { + rustTemplate("#{SmithyTypes}::body::SdkBody::from_body_1_x(body)", *codegenScope) + } else { + rust("body.into().into_inner()") + } + } + private fun httpBindingGenerator(operationShape: OperationShape) = ServerRequestBindingGenerator(protocol, codegenContext, operationShape, additionalHttpBindingCustomizations) } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/MultiVersionTestFailure.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/MultiVersionTestFailure.kt new file mode 100644 index 00000000000..cf666ae65d0 --- /dev/null +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/MultiVersionTestFailure.kt @@ -0,0 +1,72 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy.testutil + +import kotlin.reflect.KClass + +/** + * Exception thrown when tests fail for one or more HTTP versions in dual-version testing. + * + * This exception provides structured access to failures across multiple HTTP versions, + * allowing tests to programmatically inspect which versions failed and with what exceptions. + */ +class MultiVersionTestFailure( + val failures: List, +) : AssertionError(buildMessage(failures), failures.firstOrNull()?.exception) { + init { + // Add remaining exceptions as suppressed for compatibility with existing error reporting + failures.drop(1).forEach { addSuppressed(it.exception) } + } + + /** + * Represents a test failure for a specific HTTP version. + */ + data class HttpVersionFailure( + val version: HttpVersion, + val exception: Throwable, + ) + + /** + * HTTP version identifier for dual-version testing. + */ + enum class HttpVersion(val displayName: String) { + HTTP_0_X("HTTP 0.x"), + HTTP_1_X("HTTP 1.x"), + } + + /** + * Returns true if all failures are of the specified exception type. + */ + fun allFailuresAreOfType(type: KClass): Boolean = failures.all { type.isInstance(it.exception) } + + /** + * Returns failures that are of the specified exception type. + */ + fun getFailuresOfType(type: KClass): List = + failures.filter { type.isInstance(it.exception) } + + /** + * Returns true if there is a failure for the specified HTTP version. + */ + fun hasFailureFor(version: HttpVersion): Boolean = failures.any { it.version == version } + + /** + * Returns the failure for the specified HTTP version, or null if none exists. + */ + fun getFailureFor(version: HttpVersion): HttpVersionFailure? = failures.find { it.version == version } + + companion object { + private fun buildMessage(failures: List): String = + buildString { + appendLine("Test failed for ${failures.size} HTTP version(s):") + failures.forEach { (version, exception) -> + appendLine() + appendLine("=== Failure in ${version.displayName} ===") + appendLine(exception.message ?: exception.toString()) + } + } + } +} diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerCodegenIntegrationTest.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerCodegenIntegrationTest.kt index 8c0254904e8..3f4341d7a8f 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerCodegenIntegrationTest.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerCodegenIntegrationTest.kt @@ -8,13 +8,46 @@ package software.amazon.smithy.rust.codegen.server.smithy.testutil import software.amazon.smithy.build.PluginContext import software.amazon.smithy.build.SmithyBuildPlugin import software.amazon.smithy.model.Model +import software.amazon.smithy.model.node.Node import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams import software.amazon.smithy.rust.codegen.core.testutil.codegenIntegrationTest import software.amazon.smithy.rust.codegen.server.smithy.RustServerCodegenPlugin +import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenConfig import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator import java.nio.file.Path +import java.util.concurrent.CompletableFuture + +/** + * Specifies which HTTP version(s) to test in [serverIntegrationTest]. + */ +enum class HttpTestType { + /** + * Run the test twice: once with http-1x=false (HTTP 0.x) and once with http-1x=true (HTTP 1.x). + * This is the default to ensure comprehensive coverage across both HTTP versions. + */ + BOTH, + + /** + * Run the test once with http-1x=false (HTTP 0.x only). + * Use this for tests that are specific to HTTP 0.x behavior. + */ + HTTP_0_ONLY, + + /** + * Run the test once with http-1x=true (HTTP 1.x only). + * Use this for tests that are specific to HTTP 1.x behavior. + */ + HTTP_1_ONLY, + + /** + * Run the test once with whatever http-1x value is provided in the params. + * The framework will not override the http-1x flag. + * Use this when you want to explicitly control the http-1x setting or test default behavior. + */ + AS_CONFIGURED, +} /** * This file is entirely analogous to [software.amazon.smithy.rust.codegen.client.testutil.ClientCodegenIntegrationTest.kt]. @@ -24,8 +57,9 @@ fun serverIntegrationTest( model: Model, params: IntegrationTestParams = IntegrationTestParams(), additionalDecorators: List = listOf(), + testCoverage: HttpTestType = HttpTestType.BOTH, test: (ServerCodegenContext, RustCrate) -> Unit = { _, _ -> }, -): Path { +): List { fun invokeRustCodegenPlugin(ctx: PluginContext) { val codegenDecorator = object : ServerCodegenDecorator { @@ -43,7 +77,99 @@ fun serverIntegrationTest( } RustServerCodegenPlugin().executeWithDecorator(ctx, codegenDecorator, *additionalDecorators.toTypedArray()) } - return codegenIntegrationTest(model, params, invokePlugin = ::invokeRustCodegenPlugin) + + // Handle AS_CONFIGURED case separately - run once without modifying params + if (testCoverage == HttpTestType.AS_CONFIGURED) { + return listOf(codegenIntegrationTest(model, params, invokePlugin = ::invokeRustCodegenPlugin)) + } + + // Determine which HTTP versions to test + val shouldTestHttp0 = testCoverage == HttpTestType.BOTH || testCoverage == HttpTestType.HTTP_0_ONLY + val shouldTestHttp1 = testCoverage == HttpTestType.BOTH || testCoverage == HttpTestType.HTTP_1_ONLY + + // Check if parallel execution is enabled via environment variable + val runInParallel = System.getenv("PARALLEL_HTTP_TESTS")?.toBoolean() ?: false + + val paths = mutableListOf() + val errors = mutableListOf() + + // Helper function to run test for a specific HTTP version + val runTestForVersion: (MultiVersionTestFailure.HttpVersion, Boolean) -> Result = { version, http1xEnabled -> + try { + // Deep merge the codegen settings to preserve existing keys + val existingCodegenSettings = + params.additionalSettings + .expectObjectNode() + .getMember("codegen") + .orElse(Node.objectNode()) + .expectObjectNode() + + val mergedCodegenSettings = + existingCodegenSettings.toBuilder() + .withMember(ServerCodegenConfig.HTTP_1X_CONFIG_KEY, http1xEnabled) + .build() + + val versionParams = + params.copy( + additionalSettings = + params.additionalSettings.merge( + Node.objectNodeBuilder() + .withMember("codegen", mergedCodegenSettings) + .build(), + ), + ) + Result.success(codegenIntegrationTest(model, versionParams, invokePlugin = ::invokeRustCodegenPlugin)) + } catch (e: Throwable) { + Result.failure(e) + } + } + + // Helper function to handle test results + val handleResult: (MultiVersionTestFailure.HttpVersion, Result) -> Unit = { version, result -> + result.onSuccess { paths.add(it) } + .onFailure { errors.add(MultiVersionTestFailure.HttpVersionFailure(version, it)) } + } + + if (runInParallel) { + // Run tests in parallel + val http0Future = + if (shouldTestHttp0) { + CompletableFuture.supplyAsync { + runTestForVersion(MultiVersionTestFailure.HttpVersion.HTTP_0_X, false) + } + } else { + null + } + + val http1Future = + if (shouldTestHttp1) { + CompletableFuture.supplyAsync { + runTestForVersion(MultiVersionTestFailure.HttpVersion.HTTP_1_X, true) + } + } else { + null + } + + // Wait for all futures to complete and collect results + http0Future?.get()?.let { handleResult(MultiVersionTestFailure.HttpVersion.HTTP_0_X, it) } + http1Future?.get()?.let { handleResult(MultiVersionTestFailure.HttpVersion.HTTP_1_X, it) } + } else { + // Run tests sequentially (original behavior) + if (shouldTestHttp0) { + handleResult(MultiVersionTestFailure.HttpVersion.HTTP_0_X, runTestForVersion(MultiVersionTestFailure.HttpVersion.HTTP_0_X, false)) + } + + if (shouldTestHttp1) { + handleResult(MultiVersionTestFailure.HttpVersion.HTTP_1_X, runTestForVersion(MultiVersionTestFailure.HttpVersion.HTTP_1_X, true)) + } + } + + // If there were any errors, throw a combined exception with clear version information + if (errors.isNotEmpty()) { + throw MultiVersionTestFailure(errors) + } + + return paths.ifEmpty { throw IllegalStateException("No tests were run") } } abstract class ServerDecoratableBuildPlugin : SmithyBuildPlugin { diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerTestHelpers.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerTestHelpers.kt index a03dfbdd4e3..8220c47e901 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerTestHelpers.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerTestHelpers.kt @@ -13,6 +13,7 @@ import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.implBlock +import software.amazon.smithy.rust.codegen.core.smithy.HttpVersion import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider @@ -34,10 +35,26 @@ import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerBuilde import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol import software.amazon.smithy.rust.codegen.server.smithy.protocols.ServerProtocolLoader +/** + * Server-specific test RuntimeConfig that uses the same HTTP version default as production server code. + * + * This is used for server unit tests to ensure they test the default HTTP version behavior. + * Integration tests using serverIntegrationTest() can override this via additionalSettings. + * + * The HTTP version is derived from ServerCodegenConfig.DEFAULT_HTTP_1X to maintain a single source of truth. + * + * Note: Client tests use TestRuntimeConfig which defaults to HTTP 1.x. + */ +val ServerTestRuntimeConfig = + RuntimeConfig( + runtimeCrateLocation = TestRuntimeConfig.runtimeCrateLocation, + httpVersion = if (ServerCodegenConfig.DEFAULT_HTTP_1X) HttpVersion.Http1x else HttpVersion.Http0x, + ) + // These are the settings we default to if the user does not override them in their `smithy-build.json`. val ServerTestRustSymbolProviderConfig = RustSymbolProviderConfig( - runtimeConfig = TestRuntimeConfig, + runtimeConfig = ServerTestRuntimeConfig, renameExceptions = false, nullabilityCheckMode = NullableIndex.CheckMode.SERVER, moduleProvider = ServerModuleProvider, diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstraintsMemberShapeTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstraintsMemberShapeTest.kt index 0e58d60a62d..463803bc085 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstraintsMemberShapeTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstraintsMemberShapeTest.kt @@ -25,6 +25,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.testModule import software.amazon.smithy.rust.codegen.core.testutil.unitTest import software.amazon.smithy.rust.codegen.core.util.toPascalCase import software.amazon.smithy.rust.codegen.core.util.toSnakeCase +import software.amazon.smithy.rust.codegen.server.smithy.testutil.HttpTestType import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest import software.amazon.smithy.rust.codegen.server.smithy.transformers.ConstrainedMemberTransform import kotlin.streams.toList @@ -304,6 +305,7 @@ class ConstraintsMemberShapeTest { IntegrationTestParams( service = "constrainedMemberShape#ConstrainedService", ), + testCoverage = HttpTestType.AS_CONFIGURED, ) { _, rustCrate -> fun RustWriter.testTypeExistsInBuilderModule(typeName: String) { unitTest( diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/Http1xDependencyTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/Http1xDependencyTest.kt new file mode 100644 index 00000000000..be21464ada9 --- /dev/null +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/Http1xDependencyTest.kt @@ -0,0 +1,454 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy + +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.CratesIo +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope +import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams +import software.amazon.smithy.rust.codegen.core.testutil.ServerAdditionalSettings +import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.core.testutil.unitTest +import software.amazon.smithy.rust.codegen.server.smithy.testutil.HttpTestType +import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest +import java.io.File + +/** + * Tests for HTTP dependency selection based on http-1x flag. + * Verifies that the correct dependencies are included in Cargo.toml and that code compiles. + * Uses cargo_metadata to parse and validate dependencies instead of string matching. + */ +internal class Http1xDependencyTest { + private val cargoMetadata = CargoDependency("cargo_metadata", CratesIo("0.18"), features = setOf("builder")).toType() + private val semver = CargoDependency("semver", CratesIo("1.0")).toType() + + private fun buildAdditionalSettings(publicConstrainedTypes: Boolean): ObjectNode { + val builder = ServerAdditionalSettings.builder() + return builder + .publicConstrainedTypes(publicConstrainedTypes) + .generateCodegenComments(true) + .toObjectNode() + } + + private fun verify_http0x_dependencies( + testName: String, + additionalDeps: List?>> = emptyList(), + ) = writable { + rustTemplate( + """ + let metadata = #{MetadataCommand}::new() + .exec() + .expect("Failed to run cargo metadata"); + + let root_package = metadata.root_package() + .expect("Failed to get root package"); + + // Check all HTTP 0.x dependencies have minimum versions + let http0_crates = parse_crate_min_versions(&[ + ("http", "0.2.0", None), + ("aws-smithy-legacy-http-server", "0.65.7", None), + ("aws-smithy-legacy-http", "0.62.5", None), + ("aws-smithy-runtime-api", "1.9.1", Some(&["http-02x"])), + ]); + + verify_dependencies(&metadata, root_package, &http0_crates); + + // Verify http crate does NOT accept version 1.x + let http_dep = root_package.dependencies.iter() + .find(|dep| dep.name == "http") + .expect("Should have http dependency"); + let http_req = #{VersionReq}::parse(&http_dep.req.to_string()) + .expect("Failed to parse http version requirement"); + let v1 = #{Version}::parse("1.0.0").unwrap(); + assert!( + !http_req.matches(&v1), + "http dependency should NOT accept version 1.x (must be < 1.0), but requirement is: {}", http_dep.req + ); + + // Should NOT have HTTP 1.x specific dependencies + let http1_only_crates = ["http-body-util", "aws-smithy-http", "aws-smithy-http-server"]; + for dep_name in http1_only_crates { + assert!( + !root_package.dependencies.iter().any(|d| d.name == dep_name), + "Should NOT have `{}` dependency for HTTP 0.x", dep_name + ); + } + """, + "MetadataCommand" to cargoMetadata.resolve("MetadataCommand"), + "VersionReq" to semver.resolve("VersionReq"), + "Version" to semver.resolve("Version"), + *preludeScope, + ) + } + + private fun define_util_functions() = + writable { + rustTemplate( + """ + ##[cfg(test)] + fn extract_min_version(req: &#{VersionReq}) -> #{Version} { + // Handle wildcard (*) requirements - empty comparators means "any version" + if req.comparators.is_empty() { + return #{Version}::new(0, 0, 0); + } + + req.comparators.iter() + .filter_map(|c| { + match c.op { + #{Op}::GreaterEq | #{Op}::Exact | #{Op}::Caret | #{Op}::Tilde => { + Some(#{Version} { + major: c.major, + minor: c.minor.unwrap_or(0), + patch: c.patch.unwrap_or(0), + pre: #{Prerelease}::EMPTY, + build: #{BuildMetadata}::EMPTY, + }) + } + _ => None, + } + }) + .min() + .expect("Could not determine minimum version from requirement") + } + + ##[cfg(test)] + fn get_package_version(metadata: &#{Metadata}, package_name: &str) -> #{Version} { + // Find the package in the metadata (works for both path and registry dependencies) + metadata.packages.iter() + .find(|pkg| pkg.name == package_name) + .map(|pkg| #{Version}::parse(&pkg.version.to_string()).expect("Failed to parse package version")) + .expect(&format!("Could not find package {} in metadata", package_name)) + } + + ##[cfg(test)] + fn parse_crate_min_versions<'a>( + crates: &[(&'a str, &str, #{Option}<&[&str]>)] + ) -> #{Vec}<(&'a str, #{Version}, #{Option}<#{Vec}<#{String}>>)> { + crates.iter() + .map(|(name, ver_str, features_opt)| { + let version = #{Version}::parse(ver_str) + .expect(&format!("Invalid version string '{}' for {}", ver_str, name)); + let features = features_opt.map(|f| { + f.iter().map(|s| s.to_string()).collect::<#{Vec}<#{String}>>() + }); + (*name, version, features) + }) + .collect() + } + + ##[cfg(test)] + fn satisfies_minimum_version(actual: &#{Dependency}, expected_min: &#{Version}, metadata: &#{Metadata}) -> bool { + let actual_version = if actual.path.is_some() || actual.req.to_string() == "*" { + // For path dependencies, get actual version from package metadata + get_package_version(metadata, &actual.name) + } else { + // For registry dependencies, extract minimum version from requirement + extract_min_version(&actual.req) + }; + + actual_version >= *expected_min + } + + ##[allow(dead_code)] + ##[cfg(test)] + fn has_required_features(actual: &#{Dependency}, required: &[#{String}]) -> bool { + required.iter().all(|f| actual.features.contains(f)) + } + + ##[cfg(test)] + fn verify_dependencies( + metadata: &#{Metadata}, + root_package: &#{Package}, + expected: &[(&str, #{Version}, #{Option}<#{Vec}<#{String}>>)] + ) { + for (dep_name, expected_min, expected_features) in expected { + let dep = root_package.dependencies.iter() + .find(|d| d.name == *dep_name) + .expect(&format!("Must have `{}` dependency", dep_name)); + + // Check version + assert!( + satisfies_minimum_version(dep, expected_min, metadata), + "{} does not satisfy minimum version >= {}. Actual requirement: {} (path: {:?})", + dep_name, expected_min, dep.req, dep.path + ); + + // Check features if specified + if let #{Some}(required_features) = expected_features { + assert!( + has_required_features(dep, required_features), + "{} does not have required features: {:?}. Actual features: {:?}", + dep_name, required_features, dep.features + ); + } + } + } + """, + "VersionReq" to semver.resolve("VersionReq"), + "Version" to semver.resolve("Version"), + "Op" to semver.resolve("Op"), + "Prerelease" to semver.resolve("Prerelease"), + "BuildMetadata" to semver.resolve("BuildMetadata"), + "Metadata" to cargoMetadata.resolve("Metadata"), + "Package" to cargoMetadata.resolve("Package"), + "Dependency" to cargoMetadata.resolve("Dependency"), + *preludeScope, + ) + } + + @ParameterizedTest + @MethodSource("protocolAndConstrainedTypesProvider") + fun `SDK with http-1x enabled compiles and has correct dependencies`( + protocol: ModelProtocol, + publicConstrainedTypes: Boolean, + ) { + val (model) = loadSmithyConstraintsModelForProtocol(protocol) + serverIntegrationTest( + model, + IntegrationTestParams( + additionalSettings = buildAdditionalSettings(publicConstrainedTypes), + cargoCommand = "cargo test --all-features", + ), + testCoverage = HttpTestType.HTTP_1_ONLY, + ) { _, rustCrate -> + rustCrate.lib { + define_util_functions().invoke(this) + + unitTest("http_1x_dependencies") { + rustTemplate( + """ + let metadata = #{MetadataCommand}::new() + .exec() + .expect("Failed to run cargo metadata"); + + let root_package = metadata.root_package() + .expect("Failed to get root package"); + + // Check all HTTP 1.x dependencies have minimum versions and features + let http1_crates = parse_crate_min_versions(&[ + ("http", "1.0.0", None), + ("aws-smithy-http", "0.63.0", None), + ("aws-smithy-http-server", "0.66.0", None), + ("http-body-util", "0.1.3", None), + ("aws-smithy-types", "1.3.3", Some(&["http-body-1-x"])), + ("aws-smithy-runtime-api", "1.9.1", Some(&["http-1x"])), + ]); + + verify_dependencies(&metadata, root_package, &http1_crates); + + // Should NOT have legacy dependencies + let legacy = ["aws-smithy-http-legacy-server", "aws-smithy-legacy-http"]; + for legacy_crate in legacy { + assert!( + !root_package.dependencies.iter().any(|dep| dep.name == legacy_crate), + "Should NOT have {legacy_crate} dependency" + ); + } + """, + "MetadataCommand" to cargoMetadata.resolve("MetadataCommand"), + *preludeScope, + ) + } + } + } + } + + @ParameterizedTest + @MethodSource("protocolAndConstrainedTypesProvider") + fun `SDK defaults to http-1x disabled, and dependencies are correct`(protocol: ModelProtocol) { + val (model) = loadSmithyConstraintsModelForProtocol(protocol) + serverIntegrationTest( + model, + IntegrationTestParams( + additionalSettings = buildAdditionalSettings(publicConstrainedTypes = false), + cargoCommand = "cargo test --all-features", + ), + testCoverage = HttpTestType.AS_CONFIGURED, + ) { _, rustCrate -> + rustCrate.lib { + define_util_functions().invoke(this) + + unitTest("http_0x_dependencies") { + verify_http0x_dependencies("http_0x_dependencies").invoke(this) + } + } + } + } + + @Test + fun `SDK with http-1x enabled for rpcv2Cbor extras model has correct dependencies`() { + val model = loadRpcv2CborExtrasModel() + serverIntegrationTest( + model, + IntegrationTestParams( + additionalSettings = buildAdditionalSettings(publicConstrainedTypes = false), + cargoCommand = "cargo test --all-features", + ), + testCoverage = HttpTestType.HTTP_1_ONLY, + ) { _, rustCrate -> + rustCrate.lib { + define_util_functions().invoke(this) + + unitTest("http_1x_dependencies_rpcv2cbor_extras") { + rustTemplate( + """ + let metadata = #{MetadataCommand}::new() + .exec() + .expect("Failed to run cargo metadata"); + + let root_package = metadata.root_package() + .expect("Failed to get root package"); + + // Check all HTTP 1.x dependencies have minimum versions and features + let http1_crates = parse_crate_min_versions(&[ + ("http", "1.0.0", None), + ("aws-smithy-http", "0.63.0", None), + ("aws-smithy-http-server", "0.66.0", None), + ("http-body-util", "0.1.3", None), + ("aws-smithy-types", "1.3.3", Some(&["http-body-1-x"])), + ("aws-smithy-runtime-api", "1.9.1", Some(&["http-1x"])), + ]); + + verify_dependencies(&metadata, root_package, &http1_crates); + + // Should NOT have legacy dependencies + let legacy = ["aws-smithy-http-legacy-server", "aws-smithy-legacy-http"]; + for legacy_crate in legacy { + assert!( + !root_package.dependencies.iter().any(|dep| dep.name == legacy_crate), + "Should NOT have {legacy_crate} dependency" + ); + } + """, + "MetadataCommand" to cargoMetadata.resolve("MetadataCommand"), + *preludeScope, + ) + } + } + } + } + + @Test + fun `SDK defaults to http-0x for rpcv2Cbor extras model and dependencies are correct`() { + val model = loadRpcv2CborExtrasModel() + serverIntegrationTest( + model, + IntegrationTestParams( + additionalSettings = buildAdditionalSettings(publicConstrainedTypes = false), + cargoCommand = "cargo test --all-features", + ), + testCoverage = HttpTestType.AS_CONFIGURED, + ) { _, rustCrate -> + rustCrate.lib { + define_util_functions().invoke(this) + + unitTest("http_0x_dependencies_rpcv2cbor_extras") { + rustTemplate( + """ + let metadata = #{MetadataCommand}::new() + .exec() + .expect("Failed to run cargo metadata"); + + let root_package = metadata.root_package() + .expect("Failed to get root package"); + + // Check all HTTP 0.x dependencies have minimum versions + let http0_crates = parse_crate_min_versions(&[ + ("http", "0.2.0", None), + ("aws-smithy-legacy-http-server", "0.65.7", None), + ("aws-smithy-legacy-http", "0.62.5", Some(&["event-stream"])), + ("aws-smithy-runtime-api", "1.9.1", Some(&["http-02x"])), + ]); + + verify_dependencies(&metadata, root_package, &http0_crates); + + // Verify http crate does NOT accept version 1.x + let http_dep = root_package.dependencies.iter() + .find(|dep| dep.name == "http") + .expect("Should have http dependency"); + let http_req = #{VersionReq}::parse(&http_dep.req.to_string()) + .expect("Failed to parse http version requirement"); + let v1 = #{Version}::parse("1.0.0").unwrap(); + assert!( + !http_req.matches(&v1), + "http dependency should NOT accept version 1.x (must be < 1.0), but requirement is: {}", http_dep.req + ); + + // Should NOT have HTTP 1.x specific dependencies + let http1_only_crates = ["http-body-util", "aws-smithy-http", "aws-smithy-http-server"]; + for dep_name in http1_only_crates { + assert!( + !root_package.dependencies.iter().any(|d| d.name == dep_name), + "Should NOT have `{}` dependency for HTTP 0.x", dep_name + ); + } + """, + "MetadataCommand" to cargoMetadata.resolve("MetadataCommand"), + "VersionReq" to semver.resolve("VersionReq"), + "Version" to semver.resolve("Version"), + *preludeScope, + ) + } + } + } + } + + @Test + fun `SDK with explicit http-1x disabled has correct dependencies`() { + val (model) = loadSmithyConstraintsModelForProtocol(ModelProtocol.RestJson) + serverIntegrationTest( + model, + IntegrationTestParams( + additionalSettings = buildAdditionalSettings(publicConstrainedTypes = false), + cargoCommand = "cargo test --all-features", + ), + testCoverage = HttpTestType.HTTP_0_ONLY, + ) { _, rustCrate -> + rustCrate.lib { + define_util_functions().invoke(this) + + unitTest("explicit_http_0x_disabled") { + verify_http0x_dependencies("explicit_http_0x_disabled").invoke(this) + } + } + } + } + + companion object { + @JvmStatic + fun protocolAndConstrainedTypesProvider(): List { + // Constraints are not implemented for RestXML protocol. + val protocols = + ModelProtocol.entries.filter { + !it.name.matches(Regex("RestXml.*")) + } + val constrainedSettings = listOf(true, false) + + return protocols.flatMap { protocol -> + constrainedSettings.map { publicConstrained -> + Arguments.of(protocol, publicConstrained) + } + } + } + } +} + +/** + * Loads the rpcv2Cbor-extras model defined in the common repository and returns the model. + */ +fun loadRpcv2CborExtrasModel(): Model { + val filePath = "../codegen-core/common-test-models/rpcv2Cbor-extras.smithy" + return File(filePath).readText().asSmithyModel() +} diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/UnionWithUnitTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/UnionWithUnitTest.kt index 3f48d86e1fa..b2dfc637794 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/UnionWithUnitTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/UnionWithUnitTest.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.rust.codegen.server.smithy import org.junit.jupiter.api.Test import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.server.smithy.testutil.HttpTestType import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest class UnionWithUnitTest { @@ -72,6 +73,6 @@ class UnionWithUnitTest { """.asSmithyModel() // Ensure the generated SDK compiles. - serverIntegrationTest(model) { _, _ -> } + serverIntegrationTest(model, testCoverage = HttpTestType.AS_CONFIGURED) { _, _ -> } } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/AddValidationExceptionToConstrainedOperationsTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/AddValidationExceptionToConstrainedOperationsTest.kt index d56dd5c6f29..699f45b424c 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/AddValidationExceptionToConstrainedOperationsTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/AddValidationExceptionToConstrainedOperationsTest.kt @@ -11,6 +11,7 @@ import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams import software.amazon.smithy.rust.codegen.core.testutil.ServerAdditionalSettings import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.server.smithy.testutil.HttpTestType import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest /** @@ -66,6 +67,7 @@ internal class AddValidationExceptionToConstrainedOperationsTest { serverIntegrationTest( testModelWithValidationExceptionImported, IntegrationTestParams(), + testCoverage = HttpTestType.AS_CONFIGURED, ) } } @@ -80,6 +82,7 @@ internal class AddValidationExceptionToConstrainedOperationsTest { .addValidationExceptionToConstrainedOperations() .toObjectNode(), ), + testCoverage = HttpTestType.AS_CONFIGURED, ) } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecoratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecoratorTest.kt index 9176a7bf086..6876a09089a 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecoratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecoratorTest.kt @@ -13,6 +13,7 @@ import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.transform.ModelTransformer import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.server.smithy.testutil.HttpTestType import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest import java.io.File import kotlin.streams.toList @@ -107,6 +108,7 @@ internal class CustomValidationExceptionWithReasonDecoratorTest { .build(), ).build(), ), + testCoverage = HttpTestType.AS_CONFIGURED, ) } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/PostprocessValidationExceptionNotAttachedErrorMessageDecoratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/PostprocessValidationExceptionNotAttachedErrorMessageDecoratorTest.kt index 11a2f882aba..9c649b1225b 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/PostprocessValidationExceptionNotAttachedErrorMessageDecoratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/PostprocessValidationExceptionNotAttachedErrorMessageDecoratorTest.kt @@ -14,6 +14,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.server.smithy.LogMessage import software.amazon.smithy.rust.codegen.server.smithy.ValidationResult import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator +import software.amazon.smithy.rust.codegen.server.smithy.testutil.HttpTestType import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest internal class PostprocessValidationExceptionNotAttachedErrorMessageDecoratorTest { @@ -68,6 +69,7 @@ internal class PostprocessValidationExceptionNotAttachedErrorMessageDecoratorTes serverIntegrationTest( model, additionalDecorators = listOf(validationExceptionNotAttachedErrorMessageDummyPostprocessorDecorator), + testCoverage = HttpTestType.AS_CONFIGURED, ) } val exceptionCause = (exception.cause!! as ValidationResult) diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/UserProvidedValidationExceptionDecoratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/UserProvidedValidationExceptionDecoratorTest.kt index 22a4b37203e..dc9aa19faa9 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/UserProvidedValidationExceptionDecoratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/UserProvidedValidationExceptionDecoratorTest.kt @@ -16,6 +16,7 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.server.smithy.testutil.HttpTestType import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext @@ -309,7 +310,7 @@ internal class UserProvidedValidationExceptionDecoratorTest { @Test fun `code compiles with custom validation exception`() { - serverIntegrationTest(completeTestModel) + serverIntegrationTest(completeTestModel, testCoverage = HttpTestType.AS_CONFIGURED) } private val completeTestModelWithOptionals = @@ -382,6 +383,6 @@ internal class UserProvidedValidationExceptionDecoratorTest { @Test fun `code compiles with custom validation exception using optionals`() { - serverIntegrationTest(completeTestModelWithOptionals) + serverIntegrationTest(completeTestModelWithOptionals, testCoverage = HttpTestType.AS_CONFIGURED) } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/EventStreamAcceptHeaderTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/EventStreamAcceptHeaderTest.kt index dd8909671ba..542d67ed7d4 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/EventStreamAcceptHeaderTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/EventStreamAcceptHeaderTest.kt @@ -9,13 +9,15 @@ import org.junit.jupiter.api.Test import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate -import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.testModule import software.amazon.smithy.rust.codegen.core.testutil.tokioTest import software.amazon.smithy.rust.codegen.core.util.dq import software.amazon.smithy.rust.codegen.core.util.toSnakeCase +import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency +import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.testutil.ServerHttpTestHelpers import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest internal class EventStreamAcceptHeaderTest { @@ -88,29 +90,32 @@ internal class EventStreamAcceptHeaderTest { private fun RustWriter.generateAcceptHeaderTest( acceptHeader: String, shouldFail: Boolean, - codegenContext: CodegenContext, + codegenContext: ServerCodegenContext, testName: String = acceptHeader.toSnakeCase(), ) { + val smithyHttpServer = ServerCargoDependency.smithyHttpServer(codegenContext.runtimeConfig).toType() + val httpModule = RuntimeType.httpForConfig(codegenContext.runtimeConfig) tokioTest("test_header_$testName") { rustTemplate( """ - use aws_smithy_http_server::body::Body; - use aws_smithy_http_server::request::FromRequest; + use #{SmithyHttpServer}::request::FromRequest; let cbor_empty_bytes = #{Bytes}::copy_from_slice(&#{decode_body_data}( "oA==".as_bytes(), #{MediaType}::from("application/cbor"), )); - let http_request = ::http::Request::builder() + let http_request = #{Http}::Request::builder() .uri("/service/TestService/operation/StreamingOutputOperation") .method("POST") .header("Accept", ${acceptHeader.dq()}) .header("Content-Type", "application/cbor") .header("smithy-protocol", "rpc-v2-cbor") - .body(Body::from(cbor_empty_bytes)) + .body(#{BodyFromCborBytes}) .unwrap(); let parsed = crate::input::StreamingOutputOperationInput::from_request(http_request).await; """, + "SmithyHttpServer" to smithyHttpServer, + "Http" to httpModule, "Bytes" to RuntimeType.Bytes, "MediaType" to RuntimeType.protocolTest(codegenContext.runtimeConfig, "MediaType"), "decode_body_data" to @@ -118,6 +123,7 @@ internal class EventStreamAcceptHeaderTest { codegenContext.runtimeConfig, "decode_body_data", ), + "BodyFromCborBytes" to ServerHttpTestHelpers.createBodyFromBytes(codegenContext, "cbor_empty_bytes"), ) if (shouldFail) { diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerHttpSensitivityGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerHttpSensitivityGeneratorTest.kt index 0cddbd246a3..ad943291ef8 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerHttpSensitivityGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerHttpSensitivityGeneratorTest.kt @@ -10,7 +10,6 @@ import org.junit.jupiter.api.Test import software.amazon.smithy.model.traits.HttpTrait import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate -import software.amazon.smithy.rust.codegen.core.testutil.TestRuntimeConfig import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest @@ -19,12 +18,13 @@ import software.amazon.smithy.rust.codegen.core.testutil.unitTest import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rust.codegen.core.util.inputShape import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency +import software.amazon.smithy.rust.codegen.server.smithy.testutil.ServerTestRuntimeConfig import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestSymbolProvider class ServerHttpSensitivityGeneratorTest { private val codegenScope = arrayOf( - "SmithyHttpServer" to ServerCargoDependency.smithyHttpServer(TestRuntimeConfig).toType(), + "SmithyHttpServer" to ServerCargoDependency.smithyHttpServer(ServerTestRuntimeConfig).toType(), "Http" to CargoDependency.Http.toType(), ) @@ -52,7 +52,7 @@ class ServerHttpSensitivityGeneratorTest { } """.asSmithyModel() val operation = model.operationShapes.toList()[0] - val generator = ServerHttpSensitivityGenerator(model, operation, TestRuntimeConfig) + val generator = ServerHttpSensitivityGenerator(model, operation, ServerTestRuntimeConfig) val input = generator.input()!! val querySensitivity = generator.findQuerySensitivity(input) @@ -100,7 +100,7 @@ class ServerHttpSensitivityGeneratorTest { } """.asSmithyModel() val operation = model.operationShapes.toList()[0] - val generator = ServerHttpSensitivityGenerator(model, operation, TestRuntimeConfig) + val generator = ServerHttpSensitivityGenerator(model, operation, ServerTestRuntimeConfig) val input = generator.input()!! val querySensitivity = generator.findQuerySensitivity(input) @@ -150,7 +150,7 @@ class ServerHttpSensitivityGeneratorTest { """.asSmithyModel() val operation = model.operationShapes.toList()[0] - val generator = ServerHttpSensitivityGenerator(model, operation, TestRuntimeConfig) + val generator = ServerHttpSensitivityGenerator(model, operation, ServerTestRuntimeConfig) val input = generator.input()!! val querySensitivity = generator.findQuerySensitivity(input) @@ -199,7 +199,7 @@ class ServerHttpSensitivityGeneratorTest { """.asSmithyModel() val operation = model.operationShapes.toList()[0] - val generator = ServerHttpSensitivityGenerator(model, operation, TestRuntimeConfig) + val generator = ServerHttpSensitivityGenerator(model, operation, ServerTestRuntimeConfig) val input = generator.input()!! val querySensitivity = generator.findQuerySensitivity(input) @@ -245,7 +245,7 @@ class ServerHttpSensitivityGeneratorTest { """.asSmithyModel() val operation = model.operationShapes.toList()[0] - val generator = ServerHttpSensitivityGenerator(model, operation, TestRuntimeConfig) + val generator = ServerHttpSensitivityGenerator(model, operation, ServerTestRuntimeConfig) val input = generator.input()!! val querySensitivity = generator.findQuerySensitivity(input) @@ -278,7 +278,7 @@ class ServerHttpSensitivityGeneratorTest { } """.asSmithyModel() val operation = model.operationShapes.toList()[0] - val generator = ServerHttpSensitivityGenerator(model, operation, TestRuntimeConfig) + val generator = ServerHttpSensitivityGenerator(model, operation, ServerTestRuntimeConfig) val inputShape = operation.inputShape(model) val headerData = generator.findHeaderSensitivity(inputShape) @@ -328,7 +328,7 @@ class ServerHttpSensitivityGeneratorTest { """.asSmithyModel() val operation = model.operationShapes.toList()[0] - val generator = ServerHttpSensitivityGenerator(model, operation, TestRuntimeConfig) + val generator = ServerHttpSensitivityGenerator(model, operation, ServerTestRuntimeConfig) val inputShape = operation.inputShape(model) val headerData = generator.findHeaderSensitivity(inputShape) @@ -378,7 +378,7 @@ class ServerHttpSensitivityGeneratorTest { """.asSmithyModel() val operation = model.operationShapes.toList()[0] - val generator = ServerHttpSensitivityGenerator(model, operation, TestRuntimeConfig) + val generator = ServerHttpSensitivityGenerator(model, operation, ServerTestRuntimeConfig) val inputShape = operation.inputShape(model) val headerData = generator.findHeaderSensitivity(inputShape) @@ -411,7 +411,7 @@ class ServerHttpSensitivityGeneratorTest { """.asSmithyModel() val operation = model.operationShapes.toList()[0] - val generator = ServerHttpSensitivityGenerator(model, operation, TestRuntimeConfig) + val generator = ServerHttpSensitivityGenerator(model, operation, ServerTestRuntimeConfig) val inputShape = operation.inputShape(model) val headerData = generator.findHeaderSensitivity(inputShape) @@ -465,7 +465,7 @@ class ServerHttpSensitivityGeneratorTest { } """.asSmithyModel() val operation = model.operationShapes.toList()[0] - val generator = ServerHttpSensitivityGenerator(model, operation, TestRuntimeConfig) + val generator = ServerHttpSensitivityGenerator(model, operation, ServerTestRuntimeConfig) val inputShape = operation.inputShape(model) val headerData = generator.findHeaderSensitivity(inputShape) @@ -519,7 +519,7 @@ class ServerHttpSensitivityGeneratorTest { } """.asSmithyModel() val operation = model.operationShapes.toList()[0] - val generator = ServerHttpSensitivityGenerator(model, operation, TestRuntimeConfig) + val generator = ServerHttpSensitivityGenerator(model, operation, ServerTestRuntimeConfig) val input = generator.input()!! val uri = operation.getTrait()!!.uri @@ -569,7 +569,7 @@ class ServerHttpSensitivityGeneratorTest { } """.asSmithyModel() val operation = model.operationShapes.toList()[0] - val generator = ServerHttpSensitivityGenerator(model, operation, TestRuntimeConfig) + val generator = ServerHttpSensitivityGenerator(model, operation, ServerTestRuntimeConfig) val input = generator.input()!! val uri = operation.getTrait()!!.uri diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorTest.kt index 8316c705e66..1038570590c 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorTest.kt @@ -21,7 +21,7 @@ internal class ServerServiceGeneratorTest { fun `one should be able to return a built service from a function`() { val model = File("../codegen-core/common-test-models/simple.smithy").readText().asSmithyModel() - var testDir = + val testDirs = serverIntegrationTest(model) { _, rustCrate -> rustCrate.testModule { // No actual tests: we just want to check that this compiles. @@ -38,9 +38,11 @@ internal class ServerServiceGeneratorTest { } } - // test the generated metadata - val cargoToml = testDir.resolve("Cargo.toml").readText() - assert(cargoToml.contains("codegen-version =")) { cargoToml } - assert(cargoToml.contains("protocol = \"aws.protocols#restJson1\"")) { cargoToml } + // test the generated metadata for all generated projects (both HTTP 0.x and HTTP 1.x) + testDirs.forEach { testDir -> + val cargoToml = testDir.resolve("Cargo.toml").readText() + assert(cargoToml.contains("codegen-version =")) { cargoToml } + assert(cargoToml.contains("protocol = \"aws.protocols#restJson1\"")) { cargoToml } + } } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServiceConfigGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServiceConfigGeneratorTest.kt index fcb61ae53a2..3c384cf9aac 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServiceConfigGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServiceConfigGeneratorTest.kt @@ -6,6 +6,7 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import org.junit.jupiter.api.Test import software.amazon.smithy.codegen.core.CodegenException @@ -19,6 +20,8 @@ import software.amazon.smithy.rust.codegen.core.testutil.unitTest import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator +import software.amazon.smithy.rust.codegen.server.smithy.testutil.HttpTestType +import software.amazon.smithy.rust.codegen.server.smithy.testutil.MultiVersionTestFailure import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest import java.io.File @@ -92,14 +95,20 @@ internal class ServiceConfigGeneratorTest { } } - serverIntegrationTest(model, additionalDecorators = listOf(decorator)) { _, rustCrate -> + serverIntegrationTest( + model, + additionalDecorators = listOf(decorator), + testCoverage = HttpTestType.BOTH, + ) { context, rustCrate -> + val smithyServer = ServerCargoDependency.smithyHttpServer(context.runtimeConfig).toType() rustCrate.testModule { - rust( + rustTemplate( """ use crate::{SimpleServiceConfig, SimpleServiceConfigError}; - use aws_smithy_http_server::plugin::IdentityPlugin; + use #{SmithyHttpServer}::plugin::IdentityPlugin; use crate::server::plugin::PluginStack; """, + "SmithyHttpServer" to smithyServer, ) unitTest("successful_config_initialization") { @@ -208,10 +217,15 @@ internal class ServiceConfigGeneratorTest { } } - serverIntegrationTest(model, additionalDecorators = listOf(decorator)) { _, rustCrate -> + serverIntegrationTest( + model, + additionalDecorators = listOf(decorator), + testCoverage = HttpTestType.BOTH, + ) { context, rustCrate -> + val smithyServer = ServerCargoDependency.smithyHttpServer(context.runtimeConfig).toType() rustCrate.testModule { unitTest("successful_config_initialization_applying_the_three_layers") { - rust( + rustTemplate( """ let _: crate::SimpleServiceConfig< // Three Tower layers have been applied. @@ -225,12 +239,13 @@ internal class ServiceConfigGeneratorTest { >, >, >, - aws_smithy_http_server::plugin::IdentityPlugin, - aws_smithy_http_server::plugin::IdentityPlugin, + #{SmithyHttpServer}::plugin::IdentityPlugin, + #{SmithyHttpServer}::plugin::IdentityPlugin, > = crate::SimpleServiceConfig::builder() .three_non_required_layers() .build(); """, + "SmithyHttpServer" to smithyServer, ) } @@ -283,11 +298,30 @@ internal class ServiceConfigGeneratorTest { } } - val codegenException = - shouldThrow { - serverIntegrationTest(model, additionalDecorators = listOf(decorator)) { _, _ -> } + // When running for both HTTP versions, the framework throws MultiVersionTestFailure + val failure = + shouldThrow { + serverIntegrationTest( + model, + additionalDecorators = listOf(decorator), + testCoverage = HttpTestType.BOTH, + ) { _, _ -> } } - codegenException.message.shouldContain("Injected config method `invalid_generic_bindings` has generic bindings that use `L`, `H`, or `M` to refer to the generic types. This is not allowed. Invalid generic bindings:") + // Verify both HTTP versions failed + failure.failures.size shouldBe 2 + failure.hasFailureFor(MultiVersionTestFailure.HttpVersion.HTTP_0_X) shouldBe true + failure.hasFailureFor(MultiVersionTestFailure.HttpVersion.HTTP_1_X) shouldBe true + + // Verify all failures are CodegenExceptions + failure.allFailuresAreOfType(CodegenException::class) shouldBe true + + // Verify each failure has the expected error message + failure.failures.forEach { (version, exception) -> + val codegenException = exception as CodegenException + codegenException.message.shouldContain( + "Injected config method `invalid_generic_bindings` has generic bindings that use `L`, `H`, or `M` to refer to the generic types. This is not allowed. Invalid generic bindings:", + ) + } } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGeneratorTest.kt index feceb422099..54866dd6042 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGeneratorTest.kt @@ -10,6 +10,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.testModule import software.amazon.smithy.rust.codegen.core.testutil.unitTest +import software.amazon.smithy.rust.codegen.server.smithy.testutil.HttpTestType import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest class UnconstrainedCollectionGeneratorTest { @@ -55,7 +56,7 @@ class UnconstrainedCollectionGeneratorTest { } """.asSmithyModel() - serverIntegrationTest(model) { _, rustCrate -> + serverIntegrationTest(model, testCoverage = HttpTestType.AS_CONFIGURED) { _, rustCrate -> rustCrate.testModule { unitTest("list_a_unconstrained_fail_to_constrain_with_first_error") { rust( diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGeneratorTest.kt index ce6c693d4cd..2e93a033c03 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGeneratorTest.kt @@ -15,6 +15,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest import software.amazon.smithy.rust.codegen.core.testutil.testModule import software.amazon.smithy.rust.codegen.core.testutil.unitTest import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.server.smithy.testutil.HttpTestType import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext @@ -70,7 +71,7 @@ class UnconstrainedMapGeneratorTest { val project = TestWorkspace.testProject(symbolProvider, CoreCodegenConfig(debugMode = true)) - serverIntegrationTest(model) { _, rustCrate -> + serverIntegrationTest(model, testCoverage = HttpTestType.AS_CONFIGURED) { _, rustCrate -> rustCrate.testModule { TestUtility.generateIsDisplay().invoke(this) TestUtility.generateIsError().invoke(this) diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/serialize/CborServiceShapePreservesCasing.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/serialize/CborServiceShapePreservesCasing.kt index 3fea733d93f..9f746bbded5 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/serialize/CborServiceShapePreservesCasing.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/serialize/CborServiceShapePreservesCasing.kt @@ -10,10 +10,10 @@ import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams -import software.amazon.smithy.rust.codegen.core.testutil.ServerAdditionalSettings import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.testModule import software.amazon.smithy.rust.codegen.core.testutil.tokioTest +import software.amazon.smithy.rust.codegen.server.smithy.testutil.ServerHttpTestHelpers import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest class CborServiceShapePreservesCasing { @@ -35,24 +35,24 @@ class CborServiceShapePreservesCasing { } """.asSmithyModel(smithyVersion = "2") - val codegenScope = - arrayOf( - "SerdeJson" to CargoDependency.SerdeJson.toDevDependency().toType(), - "Ciborium" to CargoDependency.Ciborium.toDevDependency().toType(), - "Hyper" to RuntimeType.Hyper, - "Http" to RuntimeType.Http, - "Tower" to RuntimeType.Tower, - "HashMap" to RuntimeType.HashMap, - *RuntimeType.preludeScope, - ) - @Test fun `service shape ID is preserved`() { val serviceShape = model.expectShape(ShapeId.from("test#SampleServiceWITHDifferentCASE")) serverIntegrationTest( model, - params = IntegrationTestParams(service = serviceShape.id.toString(), additionalSettings = ServerAdditionalSettings.builder().generateCodegenComments().toObjectNode()), - ) { _codegenContext, rustCrate -> + params = IntegrationTestParams(service = serviceShape.id.toString()), + ) { codegenContext, rustCrate -> + val codegenScope = + arrayOf( + "SerdeJson" to CargoDependency.SerdeJson.toDevDependency().toType(), + "Ciborium" to CargoDependency.Ciborium.toDevDependency().toType(), + "Hyper" to RuntimeType.hyperForConfig(codegenContext.runtimeConfig), + "Http" to RuntimeType.httpForConfig(codegenContext.runtimeConfig), + "Tower" to RuntimeType.Tower, + "HashMap" to RuntimeType.HashMap, + *RuntimeType.preludeScope, + ) + rustCrate.testModule { rustTemplate( """ @@ -95,7 +95,7 @@ class CborServiceShapePreservesCasing { .method("POST") .header("content-type", "application/cbor") .header("Smithy-Protocol", "rpc-v2-cbor") - .body(#{Hyper}::Body::from(cbor_data)) + .body(#{CreateBody:W}) .expect("Failed to build request"); let response = #{Tower}::ServiceExt::oneshot(service, request) @@ -103,11 +103,9 @@ class CborServiceShapePreservesCasing { .expect("Failed to call service"); assert!(response.status().is_success()); - let body_bytes = #{Hyper}::body::to_bytes(response.into_body()) - .await - .expect("could not get bytes from the body"); + #{ReadBodyBytes:W} let data: #{HashMap} = - #{Ciborium}::de::from_reader(body_bytes.as_ref()).expect("could not convert into BTreeMap"); + #{Ciborium}::de::from_reader(body.as_ref()).expect("could not convert into BTreeMap"); let value = data.get("y") .and_then(|y| y.as_str()) @@ -115,6 +113,8 @@ class CborServiceShapePreservesCasing { assert_eq!(value, "test response", "response doesn't contain expected value"); """, *codegenScope, + "CreateBody" to ServerHttpTestHelpers.createBodyFromBytes(codegenContext, "cbor_data"), + "ReadBodyBytes" to ServerHttpTestHelpers.httpBodyToBytes(codegenContext.runtimeConfig, "body", "response"), ) } @@ -134,7 +134,7 @@ class CborServiceShapePreservesCasing { .method("POST") .header("content-type", "application/cbor") .header("Smithy-Protocol", "rpc-v2-cbor") - .body(#{Hyper}::Body::from(cbor_data.clone())) + .body(#{CreateBody1:W}) .expect("failed to build request"); let response = #{Tower}::ServiceExt::oneshot(service.clone(), request) @@ -150,7 +150,7 @@ class CborServiceShapePreservesCasing { .method("POST") .header("content-type", "application/cbor") .header("Smithy-Protocol", "rpc-v2-cbor") - .body(#{Hyper}::Body::from(cbor_data)) + .body(#{CreateBody2:W}) .expect("failed to build request"); let response = #{Tower}::ServiceExt::oneshot(service, request) @@ -161,6 +161,8 @@ class CborServiceShapePreservesCasing { assert_eq!(response.status(), #{Http}::StatusCode::NOT_FOUND); """, *codegenScope, + "CreateBody1" to ServerHttpTestHelpers.createBodyFromBytes(codegenContext, "cbor_data.clone()"), + "CreateBody2" to ServerHttpTestHelpers.createBodyFromBytes(codegenContext, "cbor_data"), ) } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerHttpTestHelpers.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerHttpTestHelpers.kt new file mode 100644 index 00000000000..973a043568a --- /dev/null +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerHttpTestHelpers.kt @@ -0,0 +1,151 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy.testutil + +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.HttpVersion +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency +import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext + +/** + * Central location for HTTP version-agnostic test helpers. + * These helpers work with both HTTP 0.x and HTTP 1.x depending on the codegenContext. + */ +object ServerHttpTestHelpers { + /** + * Returns a RuntimeType scope that includes the correct HTTP types for the given context. + * This should be used instead of hardcoding RuntimeType.Http, RuntimeType.Hyper, etc. + */ + fun getHttpRuntimeTypeScope(codegenContext: ServerCodegenContext): Array> { + val httpModule = + if (codegenContext.runtimeConfig.httpVersion == HttpVersion.Http0x) { + CargoDependency.Http + } else { + CargoDependency.Http1x + } + return arrayOf( + "Http" to httpModule.toType(), + "Hyper" to RuntimeType.hyperForConfig(codegenContext.runtimeConfig), + "Tower" to RuntimeType.Tower, + *RuntimeType.preludeScope, + ) + } + + /** + * Creates a writable that generates code to create an HTTP body from bytes. + * + * For HTTP 0.x: `smithy_http_server::body::Body::from(bytes)` (legacy) + * For HTTP 1.x: `http_body_util::Full::new(Bytes::from(bytes))` + * + * Note: The `bytesVariable` should be a variable containing bytes data (Vec, Bytes, etc.) + */ + fun createBodyFromBytes( + codegenContext: ServerCodegenContext, + bytesVariable: String, + ): Writable = + writable { + if (codegenContext.runtimeConfig.httpVersion == HttpVersion.Http1x) { + rustTemplate( + """#{HttpBodyUtilFull}::new(#{Bytes}::from($bytesVariable))""", + "HttpBodyUtilFull" to CargoDependency.HttpBodyUtil01x.toType().resolve("Full"), + "Bytes" to RuntimeType.Bytes, + ) + } else { + rustTemplate( + """#{SmithyHttpServerBody}::from($bytesVariable)""", + "SmithyHttpServerBody" to + ServerCargoDependency.smithyHttpServer(codegenContext.runtimeConfig) + .toType().resolve("body").resolve("Body"), + ) + } + } + + /** + * Returns a Writable that generates version-appropriate code for reading HTTP response body to bytes. + * For HTTP 1.x: Uses http_body_util::BodyExt::collect() + * For HTTP 0.x: Uses hyper::body::to_bytes() + * + * @param responseVarName The name of the HTTP response variable (e.g., "http_response") + */ + fun httpBodyToBytes( + runtimeConfig: RuntimeConfig, + bodyVarName: String, + responseVarName: String, + ): Writable = + writable { + when (runtimeConfig.httpVersion) { + HttpVersion.Http1x -> + rustTemplate( + """ + use #{HttpBodyUtil}::BodyExt; + let $bodyVarName = $responseVarName.into_body().collect().await.expect("unable to collect body").to_bytes(); + """, + "HttpBodyUtil" to CargoDependency.HttpBodyUtil01x.toType(), + ) + + HttpVersion.Http0x -> + rustTemplate( + """ + let $bodyVarName = #{Hyper}::body::to_bytes($responseVarName.into_body()).await.expect("unable to extract body to bytes"); + """, + "Hyper" to RuntimeType.Hyper, + ) + } + } + + /** + * Creates a writable that generates the proper HTTP request builder code. + * Returns a string expression that creates a request with the given body. + */ + fun createHttpRequest( + codegenContext: ServerCodegenContext, + uri: String, + method: String, + headers: Map, + bodyVariable: String, + ): Writable = + writable { + val httpModule = + if (codegenContext.runtimeConfig.httpVersion == HttpVersion.Http1x) { + CargoDependency.Http1x.toType() + } else { + CargoDependency.Http.toType() + } + rustTemplate( + """ + #{Http}::Request::builder() + .uri($uri) + .method($method) + #{Headers:W} + .body(#{Body:W}) + .expect("failed to build request") + """, + "Http" to httpModule, + "Headers" to + writable { + headers.forEach { (name, value) -> + rust(".header(${name.dq()}, ${value.dq()})") + } + }, + "Body" to createBodyFromBytes(codegenContext, bodyVariable), + ) + } +} + +/** + * Extension function to get HTTP dependencies scope with correct types. + */ +fun ServerCodegenContext.getHttpTestScope(): Array> { + return ServerHttpTestHelpers.getHttpRuntimeTypeScope(this) +} + +private fun String.dq() = "\"$this\""