diff --git a/README.md b/README.md index bfe94a94d89..260112c9875 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ To see list of all projects run `./gradlew projects` See the local.properties definition above to specify this in a config file. ```sh -./gradlew -Paws.services=lambda :codegen:sdk:bootstrap +./gradlew -Paws.services=+lambda :codegen:sdk:bootstrap ``` ##### Testing Locally @@ -65,20 +65,51 @@ This will output HTML formatted documentation to `build/dokka/htmlMultiModule` NOTE: You currently need an HTTP server to view the documentation in browser locally. You can either use the builtin server in Intellij or use your favorite local server (e.g. `python3 -m http.server`). See [Kotlin/dokka#1795](https://github.com/Kotlin/dokka/issues/1795) -### Build properties +### Build Properties You can define a `local.properties` config file at the root of the project to modify build behavior. -An example config with the various properties is below: +|Property|Description| +|---|---| +|`compositeProjects`|Specify paths to repos the SDK depends upon such as `smithy-kotlin`| +|`aws.services`|Specify inclusions (+ prefix) and exclusions (- prefix) of service names to generate| +|`aws.protocols`|Specify inclusions (+ prefix) and exclusions (- prefix) of AWS protocols to generate| -``` +#### Composite Projects + +Dependencies of the SDK can be added as composite build such that multiple repos may appear as one +holistic source project in the IDE. + +```ini # comma separated list of paths to `includeBuild()` # This is useful for local development of smithy-kotlin in particular compositeProjects=../smithy-kotlin +``` + +#### Generating Specific Services Based on Name or Protocol -# comma separated list of services to generate from codegen/sdk/aws-models. When not specified all services are generated -# service names match the filenames in the models directory `service.VERSION.json` -aws.services=lambda +A comma separated list of services to include or exclude for generation from codegen/sdk/aws-models may +be specified with the `aws.services` property. A list of protocols of services to generate may be specified +with the `aws.protocols` property. + +Included services require a '+' character prefix and excluded services require a '-' character. +If any items are specified for inclusion, only specified included members will be generated. If no items +are specified for inclusion, all members not excluded will be generated. +When unspecified all services found in the directory specified by the `modelsDir` property are generated. +Service names match the filenames in the models directory `service.VERSION.json`. + +Some example entries for `local.properties`: +```ini +# Generate only AWS Lambda: +aws.services=+lambda ``` +```ini +# Generate all services but AWS location and AWS DynamoDB: +aws.services=-location,-dynamodb +``` +```ini +# Generate all services except those using the restJson1 protocol: +aws.protocols=-restJson1 +``` diff --git a/codegen/sdk/build.gradle.kts b/codegen/sdk/build.gradle.kts index de1a2a5a0f4..ec7d21328c9 100644 --- a/codegen/sdk/build.gradle.kts +++ b/codegen/sdk/build.gradle.kts @@ -110,21 +110,36 @@ val sdkPackageNamePrefix = "aws.sdk.kotlin.services." // Returns an AwsService model for every JSON file found in in directory defined by property `modelsDirProp` fun discoverServices(): List { val modelsDir: String by project - val serviceIncludeList = getProperty("aws.services")?.split(",")?.map { it.trim() } + val serviceMembership = parseMembership(getProperty("aws.services")) + val protocolMembership = parseMembership(getProperty("aws.protocols")) return fileTree(project.file(modelsDir)) - .filter { - val includeModel = serviceIncludeList?.contains(it.name.split(".").first()) ?: true - if (!includeModel) { - logger.info("skipping ${it.absolutePath}, name not included in aws.services") + .filter { file -> + val svcName = file.name.split(".").first() + val include = serviceMembership.isMember(svcName) + + if (!include) { + logger.info("skipping ${file.absolutePath}, $svcName not a member of $serviceMembership") } - includeModel + include } .map { file -> val model = Model.assembler().addImport(file.absolutePath).assemble().result.get() val services: List = model.shapes(ServiceShape::class.java).sorted().toList() require(services.size == 1) { "Expected one service per aws model, but found ${services.size} in ${file.absolutePath}: ${services.map { it.id }}" } val service = services.first() + file to service + } + .filter { (file, service) -> + val protocol = service.protocol() + val include = protocolMembership.isMember(protocol) + + if (!include) { + logger.info("skipping ${file.absolutePath}, $protocol not a member of $protocolMembership") + } + include + } + .map { (file, service) -> val serviceApi = service.getTrait(software.amazon.smithy.aws.traits.ServiceTrait::class.java).orNull() ?: error { "Expected aws.api#service trait attached to model ${file.absolutePath}" } val (name, version, _) = file.name.split(".") @@ -147,6 +162,46 @@ fun discoverServices(): List { } } +// Returns the trait name of the protocol of the service +fun ServiceShape.protocol(): String = + listOf( + "aws.protocols#awsJson1_0", + "aws.protocols#awsJson1_1", + "aws.protocols#awsQuery", + "aws.protocols#ec2Query", + "aws.protocols#ec2QueryName", + "aws.protocols#restJson1", + "aws.protocols#restXml" + ).first { protocol -> findTrait(protocol).isPresent }.split("#")[1] + +// Class and functions for service and protocol membership for SDK generation +data class Membership(val inclusions: Set = emptySet(), val exclusions: Set = emptySet()) +fun Membership.isMember(member: String): Boolean = when { + exclusions.contains(member) -> false + inclusions.contains(member) -> true + inclusions.isEmpty() -> true + else -> false +} +fun parseMembership(rawList: String?): Membership { + if (rawList == null) return Membership() + + val inclusions = mutableSetOf() + val exclusions = mutableSetOf() + + rawList.split(",").map { it.trim() }.forEach { item -> + when { + item.startsWith('-') -> exclusions.add(item.substring(1)) + item.startsWith('+') -> inclusions.add(item.substring(1)) + else -> error("Must specify inclusion (+) or exclusion (-) prefix character to $item.") + } + } + + val conflictingMembers = inclusions.intersect(exclusions) + require(conflictingMembers.isEmpty()) { "$conflictingMembers specified both for inclusion and exclusion in $rawList" } + + return Membership(inclusions, exclusions) +} + fun java.util.Optional.orNull(): T? = this.orElse(null) /**