Skip to content

Commit fc8538e

Browse files
authored
chore: add scaffolding task (#1202)
1 parent 7beac10 commit fc8538e

File tree

10 files changed

+2699
-18
lines changed

10 files changed

+2699
-18
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ jobs:
6767
shell: bash
6868
run: |
6969
pwd
70+
./gradlew :build-support:test
7071
./gradlew publishToMavenLocal
7172
./gradlew apiCheck
7273
./gradlew test jvmTest

build-support/src/main/kotlin/aws/sdk/kotlin/gradle/sdk/AwsService.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,18 @@ data class AwsService(
4949
*/
5050
val version: String,
5151

52+
/**
53+
* Get the artifact name to use for the service derived from the sdkId. This will be the `A` in the GAV coordinates
54+
* and the directory name under `services/`.
55+
*/
56+
val artifactName: String,
57+
5258
/**
5359
* A description of the service (taken from the title trait)
5460
*/
5561
val description: String? = null,
56-
5762
)
5863

59-
/**
60-
* Get the artifact name to use for the service derived from the sdkId. This will be the `A` in the GAV coordinates
61-
* and the directory name under `services/`.
62-
*/
63-
val AwsService.artifactName: String
64-
get() = sdkIdToArtifactName(sdkId)
65-
6664
/**
6765
* Returns a lambda for a service model file that respects the given bootstrap config
6866
*
@@ -72,6 +70,7 @@ val AwsService.artifactName: String
7270
fun fileToService(
7371
project: Project,
7472
bootstrap: BootstrapConfig,
73+
pkgManifest: PackageManifest,
7574
): (File) -> AwsService? = { file: File ->
7675
val sdkVersion = project.findProperty("sdkVersion") as? String ?: error("expected sdkVersion to be set on project ${project.name}")
7776
val filename = file.nameWithoutExtension
@@ -111,14 +110,18 @@ fun fileToService(
111110

112111
else -> {
113112
project.logger.info("discovered service: ${serviceTrait.sdkId}")
113+
// FIXME - re-enable making this an error after migration is finished
114+
// val pkgMetadata = pkgManifest.bySdkId[sdkId] ?: error("unable to find package metadata for sdkId: $sdkId")
115+
val pkgMetadata = pkgManifest.bySdkId[sdkId] ?: PackageMetadata.from(sdkId)
114116
AwsService(
115117
serviceShapeId = service.id.toString(),
116-
packageName = packageNamespaceForService(sdkId),
118+
packageName = pkgMetadata.namespace,
117119
packageVersion = sdkVersion,
118120
modelFile = file,
119121
projectionName = filename,
120122
sdkId = sdkId,
121123
version = service.version,
124+
artifactName = pkgMetadata.artifactName,
122125
description = packageDescription,
123126
)
124127
}

build-support/src/main/kotlin/aws/sdk/kotlin/gradle/sdk/Naming.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,16 @@ internal fun sdkIdToArtifactName(sdkId: String): String = sdkId.replace(" ", "")
3737
* catapult! See AwsSdkCatapultWorkspaceTools:lib/source/merge/smithy-model-handler.ts
3838
*/
3939
fun sdkIdToModelFilename(sdkId: String): String = sdkId.trim().replace("""[\s]+""".toRegex(), "-").lowercase()
40+
41+
// FIXME - replace with case utils from smithy-kotlin once we verify we can change the implementation
42+
private fun String.lowercaseAndCapitalize() = lowercase().replaceFirstChar(Char::uppercaseChar)
43+
private val wordBoundary = "[^a-zA-Z0-9]+".toRegex()
44+
private fun String.pascalCase(): String = split(wordBoundary).pascalCase()
45+
fun List<String>.pascalCase() = joinToString(separator = "") { it.lowercaseAndCapitalize() }
46+
47+
private const val BRAZIL_GROUP_NAME = "AwsSdkKotlin"
48+
49+
/**
50+
* Maps an sdkId from a model to the brazil package name to use
51+
*/
52+
fun sdkIdToBrazilName(sdkId: String): String = "${BRAZIL_GROUP_NAME}${sdkId.pascalCase()}"
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.gradle.sdk
6+
7+
import kotlinx.serialization.ExperimentalSerializationApi
8+
import kotlinx.serialization.Serializable
9+
import kotlinx.serialization.json.Json
10+
import kotlinx.serialization.json.decodeFromStream
11+
import java.io.File
12+
13+
/**
14+
* Manifest containing additional metadata about services.
15+
*/
16+
@OptIn(ExperimentalSerializationApi::class)
17+
@Serializable
18+
data class PackageManifest(
19+
val packages: List<PackageMetadata>,
20+
) {
21+
22+
val bySdkId: Map<String, PackageMetadata> = packages.associateBy(PackageMetadata::sdkId)
23+
companion object {
24+
fun fromFile(file: File): PackageManifest =
25+
file.inputStream().use {
26+
Json.decodeFromStream<PackageManifest>(it)
27+
}
28+
}
29+
}
30+
31+
/**
32+
* Validate the package manifest for errors throwing an exception if any exist.
33+
*/
34+
fun PackageManifest.validate() {
35+
val distinct = mutableMapOf<String, PackageMetadata>()
36+
val errors = mutableListOf<String>()
37+
packages.forEach {
38+
val existing = distinct[it.sdkId]
39+
if (existing != null) {
40+
errors.add("multiple packages with same sdkId `${it.sdkId}`: first: $existing; second: $it")
41+
}
42+
distinct[it.sdkId] = it
43+
}
44+
45+
check(errors.isEmpty()) { errors.joinToString(separator = "\n") }
46+
}
47+
48+
/**
49+
* Per/package metadata stored with the repository.
50+
*
51+
* @param sdkId the unique SDK ID from the model this metadata applies to
52+
* @param namespace the package namespace to use as the root namespace when generating code for this package
53+
* @param artifactName the Maven artifact name (i.e. the 'A' in 'GAV' coordinates)
54+
* @param brazilName the internal Brazil package name for this package
55+
*/
56+
@Serializable
57+
data class PackageMetadata(
58+
public val sdkId: String,
59+
public val namespace: String,
60+
public val artifactName: String,
61+
public val brazilName: String,
62+
) {
63+
companion object {
64+
65+
/**
66+
* Create a new [PackageMetadata] from inferring values using the given sdkId
67+
*/
68+
fun from(sdkId: String): PackageMetadata =
69+
PackageMetadata(
70+
sdkId,
71+
packageNamespaceForService(sdkId),
72+
sdkIdToArtifactName(sdkId),
73+
sdkIdToBrazilName(sdkId),
74+
)
75+
}
76+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.gradle.sdk.tasks
6+
7+
import aws.sdk.kotlin.gradle.sdk.PackageManifest
8+
import aws.sdk.kotlin.gradle.sdk.PackageMetadata
9+
import aws.sdk.kotlin.gradle.sdk.orNull
10+
import aws.sdk.kotlin.gradle.sdk.validate
11+
import kotlinx.serialization.ExperimentalSerializationApi
12+
import kotlinx.serialization.encodeToString
13+
import kotlinx.serialization.json.Json
14+
import org.gradle.api.DefaultTask
15+
import org.gradle.api.file.DirectoryProperty
16+
import org.gradle.api.file.RegularFileProperty
17+
import org.gradle.api.provider.Property
18+
import org.gradle.api.tasks.*
19+
import org.gradle.api.tasks.options.Option
20+
import software.amazon.smithy.aws.traits.ServiceTrait
21+
import software.amazon.smithy.model.Model
22+
import software.amazon.smithy.model.shapes.ServiceShape
23+
import kotlin.streams.toList
24+
25+
/**
26+
* Task to update the package manifest which is used by the bootstrap process to generate service clients.
27+
* New services are required to be scaffolded
28+
*/
29+
abstract class UpdatePackageManifest : DefaultTask() {
30+
31+
@get:Option(option = "model", description = "the path to a single model file to scaffold")
32+
@get:Optional
33+
@get:InputFile
34+
public abstract val modelFile: RegularFileProperty
35+
36+
@get:Optional
37+
@get:Option(option = "model-dir", description = "the path to a directory of model files to scaffold")
38+
@get:InputDirectory
39+
public abstract val modelDir: DirectoryProperty
40+
41+
@get:Optional
42+
@get:Option(
43+
option = "discover",
44+
description = "Flag to discover and process only new packages not currently in the manifest. Only applicable when used in conjunction with `model-dir`",
45+
)
46+
@get:Input
47+
public abstract val discover: Property<Boolean>
48+
49+
@OptIn(ExperimentalSerializationApi::class)
50+
@TaskAction
51+
fun updatePackageManifest() {
52+
check(modelFile.isPresent != modelDir.isPresent) { "Exactly one of `model` or `model-dir` must be set" }
53+
54+
val manifestFile = project.file("packages.json")
55+
56+
val manifest = if (manifestFile.exists()) {
57+
val manifest = PackageManifest.fromFile(manifestFile)
58+
manifest.validate()
59+
manifest
60+
} else {
61+
PackageManifest(emptyList())
62+
}
63+
64+
val model = Model.assembler()
65+
.discoverModels()
66+
.apply {
67+
val import = if (modelFile.isPresent) modelFile else modelDir
68+
addImport(import.get().asFile.absolutePath)
69+
}
70+
.assemble()
71+
.result
72+
.get()
73+
74+
val discoveredPackages = model
75+
.shapes(ServiceShape::class.java)
76+
.toList()
77+
.mapNotNull { it.getTrait(ServiceTrait::class.java).orNull()?.sdkId }
78+
.map { PackageMetadata.from(it) }
79+
80+
val newPackages = validatedPackages(manifest, discoveredPackages)
81+
82+
if (newPackages.isEmpty()) {
83+
logger.lifecycle("no new packages to scaffold")
84+
return
85+
}
86+
87+
logger.lifecycle("scaffolding ${newPackages.size} new service packages")
88+
89+
val updatedPackages = manifest.packages + newPackages
90+
val updatedManifest = manifest.copy(packages = updatedPackages.sortedBy { it.sdkId })
91+
92+
val json = Json { prettyPrint = true }
93+
val contents = json.encodeToString(updatedManifest)
94+
manifestFile.writeText(contents)
95+
}
96+
97+
private fun validatedPackages(manifest: PackageManifest, discovered: List<PackageMetadata>): List<PackageMetadata> =
98+
if (modelDir.isPresent && discover.orNull == true) {
99+
val bySdkId = manifest.packages.associateBy(PackageMetadata::sdkId)
100+
discovered.filter { it.sdkId !in bySdkId }
101+
} else {
102+
discovered.forEach { pkg ->
103+
val existing = manifest.packages.find { it.sdkId == pkg.sdkId }
104+
check(existing == null) { "found existing package in manifest for sdkId `${pkg.sdkId}`: $existing" }
105+
}
106+
discovered
107+
}
108+
}

build-support/src/test/kotlin/aws/sdk/kotlin/gradle/sdk/AwsServiceTest.kt

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,20 @@ import org.gradle.kotlin.dsl.extra
88
import org.gradle.testfixtures.ProjectBuilder
99
import org.junit.jupiter.api.io.TempDir
1010
import java.io.File
11-
import kotlin.test.Test
12-
import kotlin.test.assertEquals
13-
import kotlin.test.assertNull
11+
import kotlin.test.*
1412

1513
class AwsServiceTest {
1614

1715
val modelContents = """
18-
${"$"}version: "2.0"
16+
${"$"}version: "2"
1917
namespace gradle.test
2018
2119
use aws.api#service
2220
use aws.protocols#awsJson1_0
2321
2422
@service(sdkId: "Test Gradle")
2523
@awsJson1_0
26-
service TestService{
24+
service TestService {
2725
operations: [],
2826
version: "1-alpha"
2927
}
@@ -34,9 +32,23 @@ class AwsServiceTest {
3432
val actual: AwsService?,
3533
)
3634

35+
private val defaultPackageManifest = PackageManifest(
36+
listOf(
37+
PackageMetadata(
38+
"Test Gradle",
39+
// namespace and artifact name intentionally don't match the sdkId derivations to verify we pull from
40+
// the metadata rather than inferring again
41+
"aws.sdk.kotlin.services.testgradle2",
42+
"test-gradle",
43+
"AwsSdkKotlinTestGradle",
44+
),
45+
),
46+
)
47+
3748
private fun testWith(
3849
tempDir: File,
3950
bootstrap: BootstrapConfig,
51+
manifest: PackageManifest = defaultPackageManifest,
4052
): TestResult {
4153
val project = ProjectBuilder.builder()
4254
.build()
@@ -46,7 +58,7 @@ class AwsServiceTest {
4658
val model = tempDir.resolve("test-gradle.smithy")
4759
model.writeText(modelContents)
4860

49-
val lambda = fileToService(project, bootstrap)
61+
val lambda = fileToService(project, bootstrap, manifest)
5062
val actual = lambda(model)
5163
return TestResult(model, actual)
5264
}
@@ -69,12 +81,13 @@ class AwsServiceTest {
6981
val result = testWith(tempDir, bootstrap)
7082
val expected = AwsService(
7183
"gradle.test#TestService",
72-
"aws.sdk.kotlin.services.testgradle",
84+
"aws.sdk.kotlin.services.testgradle2",
7385
"1.2.3",
7486
result.model,
7587
"test-gradle",
7688
"Test Gradle",
7789
"1-alpha",
90+
"test-gradle",
7891
"The AWS SDK for Kotlin client for Test Gradle",
7992
)
8093
assertEquals(expected, result.actual)
@@ -98,4 +111,13 @@ class AwsServiceTest {
98111
assertNull(result.actual, "expected null for bootstrap with $bootstrap")
99112
}
100113
}
114+
115+
// FIXME - re-enable after migration
116+
// @Test
117+
// fun testFileToServiceMissingPackageMetadata(@TempDir tempDir: File) {
118+
// val ex = assertFailsWith<IllegalStateException> {
119+
// testWith(tempDir, BootstrapConfig.ALL, PackageManifest(emptyList()))
120+
// }
121+
// assertContains(ex.message!!, "unable to find package metadata for sdkId: Test Gradle")
122+
// }
101123
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.gradle.sdk
6+
7+
import kotlin.test.Test
8+
import kotlin.test.assertContains
9+
import kotlin.test.assertFailsWith
10+
11+
class PackageManifestTest {
12+
@Test
13+
fun testValidate() {
14+
val manifest = PackageManifest(
15+
listOf(
16+
PackageMetadata("Package 1", "aws.sdk.kotlin.services.package1", "package1", "AwsSdkKotlinPackage1"),
17+
PackageMetadata("Package 2", "aws.sdk.kotlin.services.package2", "package2", "AwsSdkKotlinPackage2"),
18+
),
19+
)
20+
21+
manifest.validate()
22+
23+
val badManifest = manifest.copy(
24+
manifest.packages + listOf(
25+
PackageMetadata("Package 2", "aws.sdk.kotlin.services.package2", "package2", "AwsSdkKotlinPackage2"),
26+
),
27+
)
28+
29+
val ex = assertFailsWith<IllegalStateException> { badManifest.validate() }
30+
31+
assertContains(ex.message!!, "multiple packages with same sdkId `Package 2`")
32+
}
33+
}

0 commit comments

Comments
 (0)