diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index 2b9df1606b..078439706c 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -52,3 +52,47 @@ message = "Add codegen version to generated package metadata" references = ["smithy-rs#1612"] meta = { "breaking" = false, "tada" = false, "bug" = false } author = "unexge" + +[[smithy-rs]] +message = """ +Support granular control of specifying runtime crate versions. + +For code generation, the field `runtimeConfig.version` in smithy-build.json has been removed. +The new field `runtimeConfig.versions` is an object whose keys are runtime crate names (e.g. `aws-smithy-http`), +and values are user-specified versions. + +If you previously set `version = "DEFAULT"`, the migration path is simple. +By setting `versions` with an empty object or just not setting it at all, +the version number of the code generator will be used as the version for all runtime crates. + +If you specified a certain version such as `version = "0.47.0", you can migrate to a special reserved key `DEFAULT`. +The equivalent JSON config would look like: + +```json +{ + "runtimeConfig": { + "versions": { + "DEFAULT": "0.47.0" + } + } +} +``` + +Then all runtime crates are set with version 0.47.0 by default unless overridden by specific crates. For example, + +```json +{ + "runtimeConfig": { + "versions": { + "DEFAULT": "0.47.0", + "aws-smithy-http": "0.47.1" + } + } +} +``` + +implies that we're using `aws-smithy-http` 0.47.1 specifically. For the rest of the crates, it will default to 0.47.0. +""" +references = ["smithy-rs#1635", "smithy-rs#1416"] +meta = { "breaking" = true, "tada" = true, "bug" = false } +author = "weihanglo" diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeDependency.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeDependency.kt index 1b0fb39c4c..70377d90bc 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeDependency.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeDependency.kt @@ -37,7 +37,7 @@ fun RuntimeConfig.awsRoot(): RuntimeCrateLocation { path } return runtimeCrateLocation.copy( - path = updatedPath, version = runtimeCrateLocation.version?.let { defaultSdkVersion() }, + path = updatedPath, versions = runtimeCrateLocation.versions, ) } @@ -61,7 +61,7 @@ object AwsRuntimeType { } fun RuntimeConfig.awsRuntimeDependency(name: String, features: Set = setOf()): CargoDependency = - CargoDependency(name, awsRoot().crateLocation(), features = features) + CargoDependency(name, awsRoot().crateLocation(null), features = features) fun RuntimeConfig.awsHttp(): CargoDependency = awsRuntimeDependency("aws-http") fun RuntimeConfig.awsTypes(): CargoDependency = awsRuntimeDependency("aws-types") diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt index da7078da05..acee524ba0 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.rust.codegen.smithy import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.node.Node import software.amazon.smithy.model.node.ObjectNode import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.rust.codegen.rustlang.CargoDependency @@ -23,26 +24,27 @@ import software.amazon.smithy.rust.codegen.rustlang.asType import software.amazon.smithy.rust.codegen.util.orNull import java.util.Optional +private const val DEFAULT_KEY = "DEFAULT" + /** * Location of the runtime crates (aws-smithy-http, aws-smithy-types etc.) * - * This can be configured via the `runtimeConfig.version` field in smithy-build.json + * This can be configured via the `runtimeConfig.versions` field in smithy-build.json */ -data class RuntimeCrateLocation(val path: String?, val version: String?) { - init { - check(path != null || version != null) { - "path ($path) or version ($version) must not be null" - } - } - +data class RuntimeCrateLocation(val path: String?, val versions: CrateVersionMap) { companion object { - fun Path(path: String) = RuntimeCrateLocation(path, null) + fun Path(path: String) = RuntimeCrateLocation(path, CrateVersionMap(emptyMap())) } } -fun RuntimeCrateLocation.crateLocation(): DependencyLocation = when (this.path) { - null -> CratesIo(this.version!!) - else -> Local(this.path, this.version) +fun RuntimeCrateLocation.crateLocation(crateName: String?): DependencyLocation { + val version = crateName.let { versions.map[crateName] } ?: versions.map[DEFAULT_KEY] + return when (this.path) { + // CratesIo needs an exact version. However, for local runtime crates we do not + // provide a detected version unless the user explicitly sets one via the `versions` map. + null -> CratesIo(version ?: defaultRuntimeCrateVersion()) + else -> Local(this.path, version) + } } fun defaultRuntimeCrateVersion(): String { @@ -53,6 +55,14 @@ fun defaultRuntimeCrateVersion(): String { } } +/** + * A mapping from crate name to a user-specified version. + */ +@JvmInline +value class CrateVersionMap( + val map: Map, +) + /** * Prefix & crate location for the runtime crates. */ @@ -67,13 +77,12 @@ data class RuntimeConfig( */ fun fromNode(node: Optional): RuntimeConfig { return if (node.isPresent) { - val resolvedVersion = when (val configuredVersion = node.get().getStringMember("version").orNull()?.value) { - "DEFAULT" -> defaultRuntimeCrateVersion() - null -> null - else -> configuredVersion + val crateVersionMap = node.get().getObjectMember("versions").orElse(Node.objectNode()).members.entries.let { members -> + val map = members.associate { it.key.toString() to it.value.expectStringNode().value } + CrateVersionMap(map) } val path = node.get().getStringMember("relativePath").orNull()?.value - val runtimeCrateLocation = RuntimeCrateLocation(path = path, version = resolvedVersion) + val runtimeCrateLocation = RuntimeCrateLocation(path = path, versions = crateVersionMap) RuntimeConfig( node.get().getStringMemberOrDefault("cratePrefix", "aws-smithy"), runtimeCrateLocation = runtimeCrateLocation, @@ -86,8 +95,15 @@ data class RuntimeConfig( val crateSrcPrefix: String = cratePrefix.replace("-", "_") - fun runtimeCrate(runtimeCrateName: String, optional: Boolean = false, scope: DependencyScope = DependencyScope.Compile): CargoDependency = - CargoDependency("$cratePrefix-$runtimeCrateName", runtimeCrateLocation.crateLocation(), optional = optional, scope = scope) + fun runtimeCrate(runtimeCrateName: String, optional: Boolean = false, scope: DependencyScope = DependencyScope.Compile): CargoDependency { + val crateName = "$cratePrefix-$runtimeCrateName" + return CargoDependency( + crateName, + runtimeCrateLocation.crateLocation(crateName), + optional = optional, + scope = scope, + ) + } } /** diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypesTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypesTest.kt new file mode 100644 index 0000000000..61e850993e --- /dev/null +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypesTest.kt @@ -0,0 +1,177 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.smithy + +import io.kotest.matchers.shouldBe +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.node.Node +import software.amazon.smithy.rust.codegen.rustlang.CratesIo +import software.amazon.smithy.rust.codegen.rustlang.DependencyLocation +import software.amazon.smithy.rust.codegen.rustlang.Local + +class RuntimeTypesTest { + @ParameterizedTest + @MethodSource("runtimeConfigProvider") + fun `succeeded to parse runtime config`( + runtimeConfig: String, + expectedCrateLocation: RuntimeCrateLocation, + ) { + val node = Node.parse(runtimeConfig) + val cfg = RuntimeConfig.fromNode(node.asObjectNode()) + cfg.runtimeCrateLocation shouldBe expectedCrateLocation + } + + @ParameterizedTest + @MethodSource("runtimeCrateLocationProvider") + fun `runtimeCrateLocation provides dependency location`( + path: String?, + versions: CrateVersionMap, + crateName: String?, + expectedDependencyLocation: DependencyLocation, + ) { + val crateLoc = RuntimeCrateLocation(path, versions) + val depLoc = crateLoc.crateLocation(crateName) + depLoc shouldBe expectedDependencyLocation + } + + companion object { + @JvmStatic + private val defaultVersion = defaultRuntimeCrateVersion() + + @JvmStatic + fun runtimeConfigProvider() = listOf( + Arguments.of( + "{}", + RuntimeCrateLocation(null, CrateVersionMap(mapOf())), + ), + Arguments.of( + """ + { + "relativePath": "/path" + } + """, + RuntimeCrateLocation("/path", CrateVersionMap(mapOf())), + ), + Arguments.of( + """ + { + "versions": { + "a": "1.0", + "b": "2.0" + } + } + """, + RuntimeCrateLocation(null, CrateVersionMap(mapOf("a" to "1.0", "b" to "2.0"))), + ), + Arguments.of( + """ + { + "relativePath": "/path", + "versions": { + "a": "1.0", + "b": "2.0" + } + } + """, + RuntimeCrateLocation("/path", CrateVersionMap(mapOf("a" to "1.0", "b" to "2.0"))), + ), + ) + + @JvmStatic + fun runtimeCrateLocationProvider() = listOf( + // If user specifies `relativePath` in `runtimeConfig`, then that always takes precedence over versions. + Arguments.of( + "/path", + mapOf(), + null, + Local("/path"), + ), + Arguments.of( + "/path", + mapOf("a" to "1.0", "b" to "2.0"), + null, + Local("/path"), + ), + Arguments.of( + "/path", + mapOf("DEFAULT" to "0.1", "a" to "1.0", "b" to "2.0"), + null, + Local("/path", "0.1"), + ), + + // User does not specify the versions object. + // The version number of the code-generator should be used as the version for all runtime crates. + Arguments.of( + null, + mapOf(), + null, + CratesIo(defaultVersion), + ), + Arguments.of( + null, + mapOf(), + "a", + CratesIo(defaultVersion), + ), + + // User specifies versions object, setting explicit version numbers for some runtime crates. + // Then the rest of the runtime crates use the code-generator's version as their version. + Arguments.of( + null, + mapOf("a" to "1.0", "b" to "2.0"), + null, + CratesIo(defaultVersion), + ), + Arguments.of( + null, + mapOf("a" to "1.0", "b" to "2.0"), + "a", + CratesIo("1.0"), + ), + Arguments.of( + null, + mapOf("a" to "1.0", "b" to "2.0"), + "b", + CratesIo("2.0"), + ), + Arguments.of( + null, + mapOf("a" to "1.0", "b" to "2.0"), + "c", + CratesIo(defaultVersion), + ), + + // User specifies versions object, setting DEFAULT and setting version numbers for some runtime crates. + // Then the specified version in DEFAULT is used for all runtime crates, except for those where the user specified a value for in the map. + Arguments.of( + null, + mapOf("DEFAULT" to "0.1", "a" to "1.0", "b" to "2.0"), + null, + CratesIo("0.1"), + ), + Arguments.of( + null, + mapOf("DEFAULT" to "0.1", "a" to "1.0", "b" to "2.0"), + "a", + CratesIo("1.0"), + ), + Arguments.of( + null, + mapOf("DEFAULT" to "0.1", "a" to "1.0", "b" to "2.0"), + "b", + CratesIo("2.0"), + ), + Arguments.of( + null, + mapOf("DEFAULT" to "0.1", "a" to "1.0", "b" to "2.0"), + "c", + CratesIo("0.1"), + ), + ) + } +}