diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index f0a3bf3c707..469b82d3803 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -242,12 +242,13 @@ message = """ * The `length` trait on `string` shapes. * The `length` trait on `map` shapes. +* The `pattern` trait on `string` shapes. Upon receiving a request that violates the modeled constraints, the server SDK will reject it with a message indicating why. Unsupported (constraint trait, target shape) combinations will now fail at code generation time, whereas previously they were just ignored. This is a breaking change to raise awareness in service owners of their server SDKs behaving differently than what was modeled. To continue generating a server SDK with unsupported constraint traits, set `codegenConfig.ignoreUnsupportedConstraints` to `true` in your `smithy-build.json`. """ -references = ["smithy-rs#1199", "smithy-rs#1342", "smithy-rs#1401"] +references = ["smithy-rs#1199", "smithy-rs#1342", "smithy-rs#1401", "smithy-rs#1998"] meta = { "breaking" = true, "tada" = true, "bug" = false, "target" = "server" } author = "david-perez" diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt index 3a9662d8691..2ec1b5e290e 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt @@ -107,7 +107,7 @@ class AwsFluentClientDecorator : RustCodegenDecorator` for the nested item diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt index 262afbd616b..fa4e7ae29ac 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt @@ -13,10 +13,8 @@ import software.amazon.smithy.model.traits.IdempotencyTokenTrait import software.amazon.smithy.model.traits.PaginatedTrait import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientGenerics import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency -import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustType -import software.amazon.smithy.rust.codegen.core.rustlang.Visibility import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.render import software.amazon.smithy.rust.codegen.core.rustlang.rust @@ -78,11 +76,7 @@ class PaginatorGenerator private constructor( private val idx = PaginatedIndex.of(model) private val paginationInfo = idx.getPaginationInfo(service, operation).orNull() ?: PANIC("failed to load pagination info") - private val module = RustModule( - "paginator", - RustMetadata(visibility = Visibility.PUBLIC), - documentation = "Paginators for the service", - ) + private val module = RustModule.public("paginator", "Paginators for the service") private val inputType = symbolProvider.toSymbol(operation.inputShape(model)) private val outputShape = operation.outputShape(model) @@ -99,7 +93,12 @@ class PaginatorGenerator private constructor( "generics" to generics.decl, "bounds" to generics.bounds, "page_size_setter" to pageSizeSetter(), - "send_bounds" to generics.sendBounds(symbolProvider.toSymbol(operation), outputType, errorType, retryClassifier), + "send_bounds" to generics.sendBounds( + symbolProvider.toSymbol(operation), + outputType, + errorType, + retryClassifier, + ), // Operation Types "operation" to symbolProvider.toSymbol(operation), @@ -288,7 +287,8 @@ class PaginatorGenerator private constructor( private fun pageSizeSetter() = writable { paginationInfo.pageSizeMember.orNull()?.also { val memberName = symbolProvider.toMemberName(it) - val pageSizeT = symbolProvider.toSymbol(it).rustType().stripOuter().render(true) + val pageSizeT = + symbolProvider.toSymbol(it).rustType().stripOuter().render(true) rust( """ /// Set the page size diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/CustomizableOperationGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/CustomizableOperationGenerator.kt index 1b1a9872549..74dcb5a3dc7 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/CustomizableOperationGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/CustomizableOperationGenerator.kt @@ -11,8 +11,6 @@ import software.amazon.smithy.rust.codegen.core.rustlang.RustGenerics import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.Visibility -import software.amazon.smithy.rust.codegen.core.rustlang.docs -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.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RustCrate @@ -26,20 +24,16 @@ class CustomizableOperationGenerator( private val generics: FluentClientGenerics, private val includeFluentClient: Boolean, ) { + companion object { - const val CUSTOMIZE_MODULE = "crate::operation::customize" + val CustomizeModule = RustModule.public("customize", "Operation customization and supporting types", parent = RustModule.operation(Visibility.PUBLIC)) } private val smithyHttp = CargoDependency.smithyHttp(runtimeConfig).toType() private val smithyTypes = CargoDependency.smithyTypes(runtimeConfig).toType() fun render(crate: RustCrate) { - crate.withModule(RustModule.operation(Visibility.PUBLIC)) { - docs("Operation customization and supporting types") - rust("pub mod customize;") - } - - crate.withNonRootModule(CUSTOMIZE_MODULE) { + crate.withModule(CustomizeModule) { rustTemplate( """ pub use #{Operation}; diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt index b79537dd010..03ce4859ea3 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt @@ -21,6 +21,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords import software.amazon.smithy.rust.codegen.core.rustlang.RustType import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.core.rustlang.Visibility import software.amazon.smithy.rust.codegen.core.rustlang.asArgumentType import software.amazon.smithy.rust.codegen.core.rustlang.asOptional import software.amazon.smithy.rust.codegen.core.rustlang.deprecatedShape @@ -220,7 +221,7 @@ class FluentClientGenerator( ) } } - writer.withModule(RustModule.public("fluent_builders")) { + writer.withInlineModule(RustModule.new("fluent_builders", visibility = Visibility.PUBLIC, inline = true)) { docs( """ Utilities to ergonomically construct a request to the service. diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ProtocolTestGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ProtocolTestGenerator.kt index 830308060f4..82ecfe6967c 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ProtocolTestGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ProtocolTestGenerator.kt @@ -100,7 +100,7 @@ class ProtocolTestGenerator( Attribute.Custom("allow(unreachable_code, unused_variables)"), ), ) - writer.withModule(RustModule(testModuleName, moduleMeta)) { + writer.withInlineModule(RustModule.LeafModule(testModuleName, moduleMeta, inline = true)) { renderAllTestCases(allTests) } } diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/SmithyTypesPubUseGeneratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/SmithyTypesPubUseGeneratorTest.kt index d402fb8acfd..6a7ca5be25f 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/SmithyTypesPubUseGeneratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/SmithyTypesPubUseGeneratorTest.kt @@ -13,7 +13,6 @@ import software.amazon.smithy.rust.codegen.core.testutil.TestRuntimeConfig import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel class SmithyTypesPubUseGeneratorTest { - private fun emptyModel() = modelWithMember() private fun modelWithMember( inputMember: String = "", outputMember: String = "", diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/CodegenVisitorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/CodegenVisitorTest.kt index eccb9058d5b..b86e3b35c77 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/CodegenVisitorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/CodegenVisitorTest.kt @@ -15,8 +15,6 @@ import software.amazon.smithy.rust.codegen.client.smithy.customize.RequiredCusto import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientDecorator import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.generatePluginContext -import kotlin.io.path.createDirectory -import kotlin.io.path.writeText class CodegenVisitorTest { @Test @@ -48,8 +46,6 @@ class CodegenVisitorTest { } """.asSmithyModel(smithyVersion = "2.0") val (ctx, testDir) = generatePluginContext(model) - testDir.resolve("src").createDirectory() - testDir.resolve("src/main.rs").writeText("fn main() {}") val codegenDecorator = CombinedCodegenDecorator.fromClasspath( ctx, diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/SymbolVisitorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/SymbolVisitorTest.kt index e7e87a89a68..bc26847bef5 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/SymbolVisitorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/SymbolVisitorTest.kt @@ -29,9 +29,9 @@ import software.amazon.smithy.model.traits.SparseTrait import software.amazon.smithy.rust.codegen.client.testutil.testSymbolProvider import software.amazon.smithy.rust.codegen.core.rustlang.RustType import software.amazon.smithy.rust.codegen.core.rustlang.render -import software.amazon.smithy.rust.codegen.core.smithy.Errors -import software.amazon.smithy.rust.codegen.core.smithy.Models -import software.amazon.smithy.rust.codegen.core.smithy.Operations +import software.amazon.smithy.rust.codegen.core.smithy.ErrorsModule +import software.amazon.smithy.rust.codegen.core.smithy.ModelsModule +import software.amazon.smithy.rust.codegen.core.smithy.OperationsModule import software.amazon.smithy.rust.codegen.core.smithy.isOptional import software.amazon.smithy.rust.codegen.core.smithy.rustType import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel @@ -57,7 +57,7 @@ class SymbolVisitorTest { val provider: SymbolProvider = testSymbolProvider(model) val sym = provider.toSymbol(struct) sym.rustType().render(false) shouldBe "MyStruct" - sym.definitionFile shouldContain Models.filename + sym.definitionFile shouldContain ModelsModule.definitionFile() sym.namespace shouldBe "crate::model" } @@ -77,7 +77,7 @@ class SymbolVisitorTest { val provider: SymbolProvider = testSymbolProvider(model) val sym = provider.toSymbol(struct) sym.rustType().render(false) shouldBe "TerribleError" - sym.definitionFile shouldContain Errors.filename + sym.definitionFile shouldContain ErrorsModule.definitionFile() } @Test @@ -101,7 +101,7 @@ class SymbolVisitorTest { val provider: SymbolProvider = testSymbolProvider(model) val sym = provider.toSymbol(shape) sym.rustType().render(false) shouldBe "StandardUnit" - sym.definitionFile shouldContain Models.filename + sym.definitionFile shouldContain ModelsModule.definitionFile() sym.namespace shouldBe "crate::model" } @@ -260,7 +260,7 @@ class SymbolVisitorTest { } """.asSmithyModel() val symbol = testSymbolProvider(model).toSymbol(model.expectShape(ShapeId.from("smithy.example#PutObject"))) - symbol.definitionFile shouldBe("src/${Operations.filename}") + symbol.definitionFile shouldBe(OperationsModule.definitionFile()) symbol.name shouldBe "PutObject" } } diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/EndpointTraitBindingsTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/EndpointTraitBindingsTest.kt index 41aa7eacb96..5448636ac11 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/EndpointTraitBindingsTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/EndpointTraitBindingsTest.kt @@ -12,7 +12,6 @@ import software.amazon.smithy.model.traits.EndpointTrait import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest import software.amazon.smithy.rust.codegen.client.testutil.testSymbolProvider import software.amazon.smithy.rust.codegen.core.rustlang.RustModule -import software.amazon.smithy.rust.codegen.core.rustlang.Visibility import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock import software.amazon.smithy.rust.codegen.core.smithy.generators.implBlock @@ -59,7 +58,7 @@ internal class EndpointTraitBindingsTest { operationShape.expectTrait(EndpointTrait::class.java), ) val project = TestWorkspace.testProject() - project.withModule(RustModule.default("test", visibility = Visibility.PRIVATE)) { + project.withModule(RustModule.private("test")) { rust( """ struct GetStatusInput { diff --git a/codegen-core/common-test-models/constraints.smithy b/codegen-core/common-test-models/constraints.smithy index ddfe920cdde..0bdfe6d2a10 100644 --- a/codegen-core/common-test-models/constraints.smithy +++ b/codegen-core/common-test-models/constraints.smithy @@ -23,6 +23,12 @@ service ConstraintsService { QueryParamsTargetingMapOfListOfLengthStringOperation, QueryParamsTargetingMapOfSetOfLengthStringOperation, QueryParamsTargetingMapOfListOfEnumStringOperation, + + QueryParamsTargetingMapOfPatternStringOperation, + QueryParamsTargetingMapOfListOfPatternStringOperation, + QueryParamsTargetingMapOfLengthPatternStringOperation, + QueryParamsTargetingMapOfListOfLengthPatternStringOperation, + HttpPrefixHeadersTargetingLengthMapOperation, // TODO(https://github.com/awslabs/smithy-rs/issues/1431) // HttpPrefixHeadersTargetingMapOfEnumStringOperation, @@ -97,6 +103,34 @@ operation QueryParamsTargetingMapOfListOfEnumStringOperation { errors: [ValidationException] } +@http(uri: "/query-params-targeting-map-of-pattern-string-operation", method: "POST") +operation QueryParamsTargetingMapOfPatternStringOperation { + input: QueryParamsTargetingMapOfPatternStringOperationInputOutput, + output: QueryParamsTargetingMapOfPatternStringOperationInputOutput, + errors: [ValidationException] +} + +@http(uri: "/query-params-targeting-map-of-list-of-pattern-string-operation", method: "POST") +operation QueryParamsTargetingMapOfListOfPatternStringOperation { + input: QueryParamsTargetingMapOfListOfPatternStringOperationInputOutput, + output: QueryParamsTargetingMapOfListOfPatternStringOperationInputOutput, + errors: [ValidationException] +} + +@http(uri: "/query-params-targeting-map-of-length-pattern-string", method: "POST") +operation QueryParamsTargetingMapOfLengthPatternStringOperation { + input: QueryParamsTargetingMapOfLengthPatternStringOperationInputOutput, + output: QueryParamsTargetingMapOfLengthPatternStringOperationInputOutput, + errors: [ValidationException], +} + +@http(uri: "/query-params-targeting-map-of-list-of-length-pattern-string-operation", method: "POST") +operation QueryParamsTargetingMapOfListOfLengthPatternStringOperation { + input: QueryParamsTargetingMapOfListOfLengthPatternStringOperationInputOutput, + output: QueryParamsTargetingMapOfListOfLengthPatternStringOperationInputOutput, + errors: [ValidationException] +} + @http(uri: "/http-prefix-headers-targeting-length-map-operation", method: "POST") operation HttpPrefixHeadersTargetingLengthMapOperation { input: HttpPrefixHeadersTargetingLengthMapOperationInputOutput, @@ -187,6 +221,26 @@ structure ConstrainedHttpBoundShapesOperationInputOutput { enumStringListQuery: ListOfEnumString, } +structure QueryParamsTargetingMapOfPatternStringOperationInputOutput { + @httpQueryParams + mapOfPatternString: MapOfPatternString +} + +structure QueryParamsTargetingMapOfListOfPatternStringOperationInputOutput { + @httpQueryParams + mapOfListOfPatternString: MapOfListOfPatternString +} + +structure QueryParamsTargetingMapOfLengthPatternStringOperationInputOutput { + @httpQueryParams + mapOfLengthPatternString: MapOfLengthPatternString, +} + +structure QueryParamsTargetingMapOfListOfLengthPatternStringOperationInputOutput { + @httpQueryParams + mapOfLengthPatternString: MapOfListOfLengthPatternString, +} + structure HttpPrefixHeadersTargetingLengthMapOperationInputOutput { @httpPrefixHeaders("X-Prefix-Headers-LengthMap-") lengthMap: ConBMap, @@ -298,7 +352,21 @@ structure ConA { // setOfLengthString: SetOfLengthString, mapOfLengthString: MapOfLengthString, - nonStreamingBlob: NonStreamingBlob + nonStreamingBlob: NonStreamingBlob, + + patternString: PatternString, + mapOfPatternString: MapOfPatternString, + listOfPatternString: ListOfPatternString, + // TODO(https://github.com/awslabs/smithy-rs/issues/1401): a `set` shape is + // just a `list` shape with `uniqueItems`, which hasn't been implemented yet. + // setOfPatternString: SetOfPatternString, + + lengthLengthPatternString: LengthPatternString, + mapOfLengthPatternString: MapOfLengthPatternString, + listOfLengthPatternString: ListOfLengthPatternString + // TODO(https://github.com/awslabs/smithy-rs/issues/1401): a `set` shape is + // just a `list` shape with `uniqueItems`, which hasn't been implemented yet. + // setOfLengthPatternString: SetOfLengthPatternString, } map MapOfLengthString { @@ -321,6 +389,16 @@ map MapOfListOfEnumString { value: ListOfEnumString, } +map MapOfListOfPatternString { + key: PatternString, + value: ListOfPatternString +} + +map MapOfListOfLengthPatternString { + key: LengthPatternString, + value: ListOfLengthPatternString +} + map MapOfSetOfLengthString { key: LengthString, // TODO(https://github.com/awslabs/smithy-rs/issues/1401): a `set` shape is @@ -346,6 +424,13 @@ string MaxLengthString @length(min: 69, max: 69) string FixedLengthString +@pattern("[a-d]{5}") +string PatternString + +@pattern("[a-f0-5]*") +@length(min: 5, max: 10) +string LengthPatternString + @mediaType("video/quicktime") @length(min: 1, max: 69) string MediaTypeLengthString @@ -383,6 +468,14 @@ set SetOfLengthString { member: LengthString } +set SetOfPatternString { + member: PatternString +} + +set SetOfLengthPatternString { + member: LengthPatternString +} + list ListOfLengthString { member: LengthString } @@ -391,6 +484,14 @@ list ListOfEnumString { member: EnumString } +list ListOfPatternString { + member: PatternString +} + +list ListOfLengthPatternString { + member: LengthPatternString +} + structure ConB { @required nice: String, @@ -443,6 +544,16 @@ list NestedList { // member: String // } +map MapOfPatternString { + key: PatternString, + value: PatternString, +} + +map MapOfLengthPatternString { + key: LengthPatternString, + value: LengthPatternString, +} + @length(min: 1, max: 69) map ConBMap { key: String, 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 4150d56e81b..3b17bb2855d 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 @@ -7,6 +7,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.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.util.dq @@ -67,57 +68,52 @@ class InlineDependency( return extraDependencies } - fun key() = "${module.name}::$name" + fun key() = "${module.fullyQualifiedPath()}::$name" companion object { fun forRustFile( - name: String, - baseDir: String, - vararg additionalDependencies: RustDependency, - ): InlineDependency = forRustFile(name, baseDir, visibility = Visibility.PRIVATE, *additionalDependencies) - - fun forRustFile( - name: String, - baseDir: String, - visibility: Visibility, + module: RustModule.LeafModule, + resourcePath: String, vararg additionalDependencies: RustDependency, ): InlineDependency { - val module = RustModule.default(name, visibility) - val filename = if (name.endsWith(".rs")) { name } else { "$name.rs" } // The inline crate is loaded as a dependency on the runtime classpath - val rustFile = this::class.java.getResource("/$baseDir/src/$filename") - check(rustFile != null) { "Rust file /$baseDir/src/$filename was missing from the resource bundle!" } - return InlineDependency(name, module, additionalDependencies.toList()) { + val rustFile = this::class.java.getResource(resourcePath) + check(rustFile != null) { "Rust file $resourcePath was missing from the resource bundle!" } + return InlineDependency(module.name, module, additionalDependencies.toList()) { raw(rustFile.readText()) } } - fun forRustFile(name: String, vararg additionalDependencies: RustDependency) = - forRustFile(name, "inlineable", *additionalDependencies) - - fun eventStream(runtimeConfig: RuntimeConfig) = - forRustFile("event_stream", CargoDependency.smithyEventStream(runtimeConfig)) + private fun forInlineableRustFile(name: String, vararg additionalDependencies: RustDependency) = + forRustFile(RustModule.private(name), "/inlineable/src/$name.rs", *additionalDependencies) fun jsonErrors(runtimeConfig: RuntimeConfig) = - forRustFile("json_errors", CargoDependency.Http, CargoDependency.smithyTypes(runtimeConfig)) + forInlineableRustFile( + "json_errors", + CargoDependency.smithyJson(runtimeConfig), + CargoDependency.Bytes, + CargoDependency.Http, + ) fun idempotencyToken() = - forRustFile("idempotency_token", CargoDependency.FastRand) + forInlineableRustFile("idempotency_token", CargoDependency.FastRand) fun ec2QueryErrors(runtimeConfig: RuntimeConfig): InlineDependency = - forRustFile("ec2_query_errors", CargoDependency.smithyXml(runtimeConfig)) + forInlineableRustFile("ec2_query_errors", CargoDependency.smithyXml(runtimeConfig)) fun wrappedXmlErrors(runtimeConfig: RuntimeConfig): InlineDependency = - forRustFile("rest_xml_wrapped_errors", CargoDependency.smithyXml(runtimeConfig)) + forInlineableRustFile("rest_xml_wrapped_errors", CargoDependency.smithyXml(runtimeConfig)) fun unwrappedXmlErrors(runtimeConfig: RuntimeConfig): InlineDependency = - forRustFile("rest_xml_unwrapped_errors", CargoDependency.smithyXml(runtimeConfig)) + forInlineableRustFile("rest_xml_unwrapped_errors", CargoDependency.smithyXml(runtimeConfig)) fun constrained(): InlineDependency = - forRustFile("constrained") + InlineDependency.forRustFile(ConstrainedModule, "/inlineable/src/constrained.rs") } } +fun InlineDependency.asType() = RuntimeType(name = null, dependency = this, namespace = module.fullyQualifiedPath()) + data class Feature(val name: String, val default: Boolean, val deps: List) /** diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustModule.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustModule.kt index aa7a0fb8d33..b9ecd1b5104 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustModule.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustModule.kt @@ -5,23 +5,72 @@ package software.amazon.smithy.rust.codegen.core.rustlang -data class RustModule(val name: String, val rustMetadata: RustMetadata, val documentation: String? = null) { - fun render(writer: RustWriter) { - documentation?.let { docs -> writer.docs(docs) } - rustMetadata.render(writer) - writer.write("mod $name;") +/** + * RustModule system. + * + * RustModules are idempotent, BUT, you must take care to always use the same module (matching docs, visibility, etc.): + * - There is no guarantee _which_ module will be rendered. + */ +sealed class RustModule { + + /** lib.rs */ + object LibRs : RustModule() + + /** + * LeafModule + * + * A LeafModule is _all_ modules that are not `lib.rs`. To create a nested leaf module, set `parent` to a module + * _other_ than `LibRs`. + * + * To avoid infinite loops, avoid setting parent to itself ;-) + */ + data class LeafModule( + val name: String, + val rustMetadata: RustMetadata, + val documentation: String? = null, + val parent: RustModule = LibRs, + val inline: Boolean = false, + ) : RustModule() { + init { + check(!name.contains("::")) { + "Module names CANNOT contain `::`—modules must be nested with parent (name was: `$name`)" + } + check(name != "") { + "Module name cannot be empty" + } + + check(!RustReservedWords.isReserved(name)) { + "Module `$name` cannot be a module name—it is a reserved word." + } + } } companion object { - fun default(name: String, visibility: Visibility, documentation: String? = null): RustModule { - return RustModule(name, RustMetadata(visibility = visibility), documentation) + + /** Creates a new module with the specified visibility */ + fun new( + name: String, + visibility: Visibility, + documentation: String? = null, + inline: Boolean = false, + parent: RustModule = LibRs, + ): LeafModule { + return LeafModule( + RustReservedWords.escapeIfNeeded(name), + RustMetadata(visibility = visibility), + documentation, + inline = inline, + parent = parent, + ) } - fun public(name: String, documentation: String? = null): RustModule = - default(name, visibility = Visibility.PUBLIC, documentation = documentation) + /** Creates a new public module */ + fun public(name: String, documentation: String? = null, parent: RustModule = LibRs): LeafModule = + new(name, visibility = Visibility.PUBLIC, documentation = documentation, inline = false, parent = parent) - fun private(name: String, documentation: String? = null): RustModule = - default(name, visibility = Visibility.PRIVATE, documentation = documentation) + /** Creates a new private module */ + fun private(name: String, documentation: String? = null, parent: RustModule = LibRs): LeafModule = + new(name, visibility = Visibility.PRIVATE, documentation = documentation, inline = false, parent = parent) /* Common modules used across client, server and tests */ val Config = public("config", documentation = "Configuration for the service.") @@ -36,6 +85,53 @@ data class RustModule(val name: String, val rustMetadata: RustMetadata, val docu * Its visibility depends on the generation context (client or server). */ fun operation(visibility: Visibility): RustModule = - default("operation", visibility = visibility, documentation = "All operations that this crate can perform.") + new( + "operation", + visibility = visibility, + documentation = "All operations that this crate can perform.", + ) + } + + fun isInline(): Boolean = when (this) { + is LibRs -> false + is LeafModule -> this.inline + } + + /** + * Fully qualified path to this module, e.g. `crate::grandparent::parent::child` + */ + fun fullyQualifiedPath(): String = when (this) { + is LibRs -> "crate" + is LeafModule -> parent.fullyQualifiedPath() + "::" + name + } + + /** + * The file this module is homed in, e.g. `src/grandparent/parent/child.rs` + */ + fun definitionFile(): String = when (this) { + is LibRs -> "src/lib.rs" + is LeafModule -> { + val path = fullyQualifiedPath().split("::").drop(1).joinToString("/") + "src/$path.rs" + } + } + + /** + * Renders the usage statement, approximately: + * ```rust + * /// My docs + * pub mod my_module_name + * ``` + */ + fun renderModStatement(writer: RustWriter) { + when (this) { + is LeafModule -> { + documentation?.let { docs -> writer.docs(docs) } + rustMetadata.render(writer) + writer.write("mod $name;") + } + + else -> {} + } } } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustReservedWords.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustReservedWords.kt index 33f15aa89e5..efe9ae7cc88 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustReservedWords.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustReservedWords.kt @@ -39,8 +39,10 @@ class RustReservedWordSymbolProvider(private val base: RustSymbolProvider, priva // To avoid conflicts with the `make_operation` and `presigned` functions on generated inputs "make_operation" -> "make_operation_value" "presigned" -> "presigned_value" + "customize" -> "customize_value" else -> baseName } + is UnionShape -> when (baseName) { // Unions contain an `Unknown` variant. This exists to support parsing data returned from the server // that represent union variants that have been added since this SDK was generated. @@ -53,6 +55,7 @@ class RustReservedWordSymbolProvider(private val base: RustSymbolProvider, priva "SelfValue" -> "SelfValue_" else -> baseName } + else -> error("unexpected container: $container") } } @@ -78,6 +81,7 @@ class RustReservedWordSymbolProvider(private val base: RustSymbolProvider, priva it.toBuilder().renamedFrom(previousName).build() } } + else -> base.toSymbol(shape) } } @@ -150,7 +154,6 @@ object RustReservedWords : ReservedWords { "abstract", "become", "box", - "customize", "do", "final", "macro", diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt index 8ef035f042b..ebc4acd6042 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt @@ -429,7 +429,8 @@ class RustWriter private constructor( } /** - * Create an inline module. + * Create an inline module. Instead of being in a new file, inline modules are written as a `mod { ... }` block + * directly into the parent. * * Callers must take care to use [this] when writing to ensure code is written to the right place: * ```kotlin @@ -442,15 +443,19 @@ class RustWriter private constructor( * * The returned writer will inject any local imports into the module as needed. */ - fun withModule( - module: RustModule, + fun withInlineModule( + module: RustModule.LeafModule, moduleWriter: Writable, ): RustWriter { + check(module.isInline()) { + "Only inline modules may be used with `withInlineModule`: $module" + } // In Rust, modules must specify their own imports—they don't have access to the parent scope. // To easily handle this, create a new inner writer to collect imports, then dump it // into an inline module. val innerWriter = RustWriter(this.filename, "${this.namespace}::${module.name}", printWarning = false) moduleWriter(innerWriter) + module.documentation?.let { docs -> docs(docs) } module.rustMetadata.render(this) rustBlock("mod ${module.name}") { writeWithNoFormatting(innerWriter.toString()) diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt index f1739324a7c..3a3434b8bc3 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CodegenDelegator.kt @@ -41,23 +41,24 @@ import software.amazon.smithy.rust.codegen.core.smithy.generators.ManifestCustom */ open class RustCrate( fileManifest: FileManifest, - symbolProvider: SymbolProvider, - /** - * For core modules like `input`, `output`, and `error`, we need to specify whether these modules should be public or - * private as well as any other metadata. [baseModules] enables configuring this. See [DefaultPublicModules]. - */ - baseModules: Map, + private val symbolProvider: SymbolProvider, coreCodegenConfig: CoreCodegenConfig, ) { private val inner = WriterDelegator(fileManifest, symbolProvider, RustWriter.factory(coreCodegenConfig.debugMode)) - private val modules: MutableMap = baseModules.toMutableMap() private val features: MutableSet = mutableSetOf() + // used to ensure we never create accidentally discard docs / incorrectly create modules with incorrect visibility + private var duplicateModuleWarningSystem: MutableMap = mutableMapOf() + /** * Write into the module that this shape is [locatedIn] */ fun useShapeWriter(shape: Shape, f: Writable) { - inner.useShapeWriter(shape, f) + val module = symbolProvider.toSymbol(shape).module() + check(!module.isInline()) { + "Cannot use useShapeWriter with inline modules—use [RustWriter.withInlineModule] instead" + } + withModule(symbolProvider.toSymbol(shape).module(), f) } /** @@ -94,14 +95,11 @@ open class RustCrate( requireDocs: Boolean = true, ) { injectInlineDependencies() - val modules = inner.writers.values.mapNotNull { it.module() }.filter { it != "lib" } - .mapNotNull { modules[it] } inner.finalize( settings, model, manifestCustomizations, libRsCustomizations, - modules, this.features.toList(), requireDocs, ) @@ -126,6 +124,17 @@ open class RustCrate( } } + private fun checkDups(module: RustModule.LeafModule) { + duplicateModuleWarningSystem[module.fullyQualifiedPath()]?.also { preexistingModule -> + check(module == preexistingModule) { + "Duplicate modules with differing properties were created! This will lead to non-deterministic behavior." + + "\n Previous module: $preexistingModule." + + "\n New module: $module" + } + } + duplicateModuleWarningSystem[module.fullyQualifiedPath()] = module + } + /** * Create a new module directly. The resulting module will be placed in `src/.rs` */ @@ -133,31 +142,22 @@ open class RustCrate( module: RustModule, moduleWriter: Writable, ): RustCrate { - val moduleName = module.name - modules[moduleName] = module - inner.useFileWriter("src/$moduleName.rs", "crate::$moduleName", moduleWriter) - return this - } - - /** - * Create a new non-root module directly. For example, if given the namespace `crate::foo::bar`, - * this will create `src/foo/bar.rs` with the contents from the given `moduleWriter`. - * Multiple calls to this with the same namespace are additive, so new code can be added - * by various customizations. - * - * Caution: this does not automatically add the required Rust `mod` statements to make this - * file an official part of the generated crate. This step needs to be done manually. - */ - fun withNonRootModule( - namespace: String, - moduleWriter: Writable, - ): RustCrate { - val parts = namespace.split("::") - require(parts.size > 2) { "Cannot create root modules using withNonRootModule" } - require(parts[0] == "crate") { "Namespace must start with crate::" } - - val fileName = "src/" + parts.filterIndexed { index, _ -> index > 0 }.joinToString("/") + ".rs" - inner.useFileWriter(fileName, namespace, moduleWriter) + when (module) { + is RustModule.LibRs -> lib { moduleWriter(this) } + is RustModule.LeafModule -> { + checkDups(module) + // Create a dependency which adds the mod statement for this module. This will be added to the writer + // so that _usage_ of this module will generate _exactly one_ `mod ` with the correct modifiers. + val modStatement = RuntimeType.forInlineFun("mod_" + module.fullyQualifiedPath(), module.parent) { + module.renderModStatement(this) + } + val path = module.fullyQualifiedPath().split("::").drop(1).joinToString("/") + inner.useFileWriter("src/$path.rs", module.fullyQualifiedPath()) { writer -> + moduleWriter(writer) + writer.addDependency(modStatement.dependency) + } + } + } return this } @@ -176,19 +176,11 @@ val OperationsModule = RustModule.public("operation", documentation = "All opera val ModelsModule = RustModule.public("model", documentation = "Data structures used by operation inputs/outputs.") val InputsModule = RustModule.public("input", documentation = "Input structures for operations.") val OutputsModule = RustModule.public("output", documentation = "Output structures for operations.") -val ConfigModule = RustModule.public("config", documentation = "Client configuration.") -/** - * Allowlist of modules that will be exposed publicly in generated crates - */ -val DefaultPublicModules = setOf( - ErrorsModule, - OperationsModule, - ModelsModule, - InputsModule, - OutputsModule, - ConfigModule, -).associateBy { it.name } +val UnconstrainedModule = + RustModule.private("unconstrained", "Unconstrained types for constrained shapes.") +val ConstrainedModule = + RustModule.private("constrained", "Constrained types for constrained shapes.") /** * Finalize all the writers by: @@ -200,12 +192,11 @@ fun WriterDelegator.finalize( model: Model, manifestCustomizations: ManifestCustomizations, libRsCustomizations: List, - modules: List, features: List, requireDocs: Boolean, ) { this.useFileWriter("src/lib.rs", "crate::lib") { - LibRsGenerator(settings, model, modules, libRsCustomizations, requireDocs).render(it) + LibRsGenerator(settings, model, libRsCustomizations, requireDocs).render(it) } val cargoDependencies = mergeDependencyFeatures( this.dependencies.map { RustDependency.fromSymbolDependency(it) } 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 baabdc7b077..ebaa19856ca 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 @@ -16,12 +16,14 @@ import software.amazon.smithy.rust.codegen.core.rustlang.CratesIo import software.amazon.smithy.rust.codegen.core.rustlang.DependencyLocation import software.amazon.smithy.rust.codegen.core.rustlang.DependencyScope import software.amazon.smithy.rust.codegen.core.rustlang.InlineDependency +import software.amazon.smithy.rust.codegen.core.rustlang.InlineDependency.Companion.constrained import software.amazon.smithy.rust.codegen.core.rustlang.Local import software.amazon.smithy.rust.codegen.core.rustlang.RustDependency import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustType import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.asType import software.amazon.smithy.rust.codegen.core.rustlang.rustInlineTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.util.orNull @@ -256,14 +258,14 @@ data class RuntimeType(val name: String?, val dependency: RustDependency?, val n ) } + fun ConstrainedTrait() = constrained().asType().member("Constrained") + fun MaybeConstrained() = constrained().asType().member("MaybeConstrained") + fun ProtocolTestHelper(runtimeConfig: RuntimeConfig, func: String): RuntimeType = RuntimeType( func, CargoDependency.smithyProtocolTestHelpers(runtimeConfig), "aws_smithy_protocol_test", ) - fun ConstrainedTrait() = RuntimeType("Constrained", InlineDependency.constrained(), namespace = "crate::constrained") - fun MaybeConstrained() = RuntimeType("MaybeConstrained", InlineDependency.constrained(), namespace = "crate::constrained") - val http = CargoDependency.Http.toType() fun Http(path: String): RuntimeType = RuntimeType(name = path, dependency = CargoDependency.Http, namespace = "http") @@ -313,7 +315,7 @@ data class RuntimeType(val name: String?, val dependency: RustDependency?, val n fun forInlineFun(name: String, module: RustModule, func: Writable) = RuntimeType( name = name, dependency = InlineDependency(name, module, listOf(), func), - namespace = "crate::${module.name}", + namespace = module.fullyQualifiedPath(), ) fun parseResponse(runtimeConfig: RuntimeConfig) = RuntimeType( diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitor.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitor.kt index 1ca4c2ebf25..d2e66a87aa9 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitor.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitor.kt @@ -38,6 +38,7 @@ import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.EnumDefinition import software.amazon.smithy.model.traits.EnumTrait import software.amazon.smithy.model.traits.ErrorTrait +import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustType import software.amazon.smithy.rust.codegen.core.rustlang.stripOuter import software.amazon.smithy.rust.codegen.core.smithy.traits.RustBoxTrait @@ -69,24 +70,6 @@ data class SymbolVisitorConfig( val nullabilityCheckMode: CheckMode, ) -/** - * Container type for the file a symbol should be written to - * - * Downstream code uses symbol location to determine which file to use acquiring a writer - */ -data class SymbolLocation(val namespace: String) { - val filename = "$namespace.rs" -} - -val Models = SymbolLocation(ModelsModule.name) -val Errors = SymbolLocation(ErrorsModule.name) -val Operations = SymbolLocation(OperationsModule.name) -val Serializers = SymbolLocation("serializer") -val Inputs = SymbolLocation(InputsModule.name) -val Outputs = SymbolLocation(OutputsModule.name) -val Unconstrained = SymbolLocation("unconstrained") -val Constrained = SymbolLocation("constrained") - /** * Make the Rust type of a symbol optional (hold `Option`) * @@ -152,15 +135,16 @@ fun Symbol.mapRustType(f: (RustType) -> RustType): Symbol { } /** Set the symbolLocation for this symbol builder */ -fun Symbol.Builder.locatedIn(symbolLocation: SymbolLocation): Symbol.Builder { +fun Symbol.Builder.locatedIn(rustModule: RustModule.LeafModule): Symbol.Builder { val currentRustType = this.build().rustType() check(currentRustType is RustType.Opaque) { "Only `Opaque` can have their namespace updated" } - val newRustType = currentRustType.copy(namespace = "crate::${symbolLocation.namespace}") - return this.definitionFile("src/${symbolLocation.filename}") - .namespace("crate::${symbolLocation.namespace}", "::") + val newRustType = currentRustType.copy(namespace = rustModule.fullyQualifiedPath()) + return this.definitionFile(rustModule.definitionFile()) + .namespace(rustModule.fullyQualifiedPath(), "::") .rustType(newRustType) + .module(rustModule) } /** @@ -274,7 +258,7 @@ open class SymbolVisitor( override fun stringShape(shape: StringShape): Symbol { return if (shape.hasTrait()) { val rustType = RustType.Opaque(shape.contextName(serviceShape).toPascalCase()) - symbolBuilder(shape, rustType).locatedIn(Models).build() + symbolBuilder(shape, rustType).locatedIn(ModelsModule).build() } else { simpleShape(shape) } @@ -325,7 +309,7 @@ open class SymbolVisitor( .replaceFirstChar { it.uppercase() }, ), ) - .locatedIn(Operations) + .locatedIn(OperationsModule) .build() } @@ -346,16 +330,16 @@ open class SymbolVisitor( } val builder = symbolBuilder(shape, RustType.Opaque(name)) return when { - isError -> builder.locatedIn(Errors) - isInput -> builder.locatedIn(Inputs) - isOutput -> builder.locatedIn(Outputs) - else -> builder.locatedIn(Models) + isError -> builder.locatedIn(ErrorsModule) + isInput -> builder.locatedIn(InputsModule) + isOutput -> builder.locatedIn(OutputsModule) + else -> builder.locatedIn(ModelsModule) }.build() } override fun unionShape(shape: UnionShape): Symbol { val name = shape.contextName(serviceShape).toPascalCase() - val builder = symbolBuilder(shape, RustType.Opaque(name)).locatedIn(Models) + val builder = symbolBuilder(shape, RustType.Opaque(name)).locatedIn(ModelsModule) return builder.build() } @@ -399,13 +383,15 @@ fun symbolBuilder(shape: Shape?, rustType: RustType): Symbol.Builder { fun handleOptionality(symbol: Symbol, member: MemberShape, nullableIndex: NullableIndex, nullabilityCheckMode: CheckMode): Symbol = symbol.letIf(nullableIndex.isMemberNullable(member, nullabilityCheckMode)) { symbol.makeOptional() } -// TODO(chore): Move this to a useful place private const val RUST_TYPE_KEY = "rusttype" +private const val RUST_MODULE_KEY = "rustmodule" private const val SHAPE_KEY = "shape" private const val SYMBOL_DEFAULT = "symboldefault" private const val RENAMED_FROM_KEY = "renamedfrom" fun Symbol.Builder.rustType(rustType: RustType): Symbol.Builder = this.putProperty(RUST_TYPE_KEY, rustType) +fun Symbol.Builder.module(module: RustModule.LeafModule): Symbol.Builder = this.putProperty(RUST_MODULE_KEY, module) +fun Symbol.module(): RustModule.LeafModule = this.expectProperty(RUST_MODULE_KEY, RustModule.LeafModule::class.java) fun Symbol.Builder.renamedFrom(name: String): Symbol.Builder { return this.putProperty(RENAMED_FROM_KEY, name) diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGenerator.kt index 89bae1ea493..df39a50841a 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/BuilderGenerator.kt @@ -14,6 +14,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords import software.amazon.smithy.rust.codegen.core.rustlang.RustType import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.core.rustlang.Visibility import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.asArgument import software.amazon.smithy.rust.codegen.core.rustlang.asOptional @@ -36,7 +37,9 @@ import software.amazon.smithy.rust.codegen.core.smithy.canUseDefault import software.amazon.smithy.rust.codegen.core.smithy.defaultValue import software.amazon.smithy.rust.codegen.core.smithy.expectRustMetadata import software.amazon.smithy.rust.codegen.core.smithy.isOptional +import software.amazon.smithy.rust.codegen.core.smithy.locatedIn import software.amazon.smithy.rust.codegen.core.smithy.makeOptional +import software.amazon.smithy.rust.codegen.core.smithy.module import software.amazon.smithy.rust.codegen.core.smithy.rustType import software.amazon.smithy.rust.codegen.core.smithy.traits.SyntheticInputTrait import software.amazon.smithy.rust.codegen.core.util.dq @@ -53,12 +56,12 @@ fun builderSymbolFn(symbolProvider: RustSymbolProvider): (StructureShape) -> Sym fun StructureShape.builderSymbol(symbolProvider: RustSymbolProvider): Symbol { val structureSymbol = symbolProvider.toSymbol(this) val builderNamespace = RustReservedWords.escapeIfNeeded(structureSymbol.name.toSnakeCase()) - val rustType = RustType.Opaque("Builder", "${structureSymbol.namespace}::$builderNamespace") + val module = RustModule.new(builderNamespace, visibility = Visibility.PUBLIC, parent = structureSymbol.module(), inline = true) + val rustType = RustType.Opaque("Builder", module.fullyQualifiedPath()) return Symbol.builder() .rustType(rustType) .name(rustType.name) - .namespace(rustType.namespace, "::") - .definitionFile(structureSymbol.definitionFile) + .locatedIn(module) .build() } @@ -112,7 +115,7 @@ class BuilderGenerator( val symbol = symbolProvider.toSymbol(shape) writer.docs("See #D.", symbol) val segments = shape.builderSymbol(symbolProvider).namespace.split("::") - writer.withModule(RustModule.public(segments.last())) { + writer.withInlineModule(shape.builderSymbol(symbolProvider).module()) { renderBuilder(this) } } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/LibRsGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/LibRsGenerator.kt index 3be71980804..c15d545e6b6 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/LibRsGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/LibRsGenerator.kt @@ -7,7 +7,6 @@ package software.amazon.smithy.rust.codegen.core.smithy.generators import software.amazon.smithy.model.Model import software.amazon.smithy.model.traits.DocumentationTrait -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.containerDocs import software.amazon.smithy.rust.codegen.core.rustlang.escape @@ -33,7 +32,6 @@ typealias LibRsCustomization = NamedSectionGenerator class LibRsGenerator( private val settings: CoreRustSettings, private val model: Model, - private val modules: List, private val customizations: List, private val requireDocs: Boolean, ) { @@ -66,7 +64,6 @@ class LibRsGenerator( // TODO(docs): Automated feature documentation } - modules.forEach { it.render(writer) } customizations.forEach { it.section(LibRsSection.Body(model))(writer) } } } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt index c5bc50ed42e..eb250a05f71 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/Rust.kt @@ -18,7 +18,6 @@ import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.traits.EnumDefinition import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.core.rustlang.RustDependency -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.raw @@ -85,6 +84,9 @@ object TestWorkspace { version = "0.0.1" """.trimIndent(), ) + // ensure there at least an empty lib.rs file to avoid broken crates + newProject.resolve("src").mkdirs() + newProject.resolve("src/lib.rs").writeText("") subprojects.add(newProject.name) generate() return newProject @@ -190,14 +192,6 @@ fun RustWriter.unitTest( return rustBlock("fn $name()", *args, block = block) } -val DefaultTestPublicModules = setOf( - RustModule.Error, - RustModule.Model, - RustModule.Input, - RustModule.Output, - RustModule.Config, -).associateBy { it.name } - /** * WriterDelegator used for test purposes * @@ -211,7 +205,6 @@ class TestWriterDelegator( RustCrate( fileManifest, symbolProvider, - DefaultTestPublicModules, codegenConfig, ) { val baseDir: Path = fileManifest.baseDir @@ -219,6 +212,8 @@ class TestWriterDelegator( fun printGeneratedFiles() { fileManifest.printGeneratedFiles() } + + fun generatedFiles() = fileManifest.files.map { baseDir.relativize(it) } } fun FileManifest.printGeneratedFiles() { diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/InlineDependencyTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/InlineDependencyTest.kt index 1d8448c3060..938e5306ce9 100644 --- a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/InlineDependencyTest.kt +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/InlineDependencyTest.kt @@ -5,10 +5,15 @@ package software.amazon.smithy.rust.codegen.core.rustlang +import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import org.junit.jupiter.api.Test +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest +import software.amazon.smithy.rust.codegen.core.testutil.unitTest +import kotlin.io.path.pathString internal class InlineDependencyTest { private fun makeDep(name: String) = InlineDependency(name, RustModule.private("module")) { @@ -28,17 +33,66 @@ internal class InlineDependencyTest { @Test fun `locate dependencies from the inlineable module`() { val dep = InlineDependency.idempotencyToken() - val testWriter = RustWriter.root() - testWriter.addDependency(CargoDependency.FastRand) - testWriter.withModule(dep.module.copy(rustMetadata = RustMetadata(visibility = Visibility.PUBLIC))) { - dep.renderer(this) + val testProject = TestWorkspace.testProject() + testProject.unitTest { + rustTemplate( + """ + use #{idempotency}::uuid_v4; + let res = uuid_v4(0); + assert_eq!(res, "00000000-0000-4000-8000-000000000000"); + + """, + "idempotency" to dep.asType(), + ) } - testWriter.compileAndTest( - """ - use crate::idempotency_token::uuid_v4; - let res = uuid_v4(0); - assert_eq!(res, "00000000-0000-4000-8000-000000000000"); - """, - ) + testProject.compileAndTest() + } + + @Test + fun `nested dependency modules`() { + val a = RustModule.public("a") + val b = RustModule.public("b", parent = a) + val c = RustModule.public("c", parent = b) + val type = RuntimeType.forInlineFun("forty2", c) { + rust( + """ + pub fn forty2() -> usize { 42 } + """, + ) + } + val crate = TestWorkspace.testProject() + crate.lib { + unitTest("use_nested_module") { + rustTemplate("assert_eq!(42, #{forty2}())", "forty2" to type) + } + } + crate.compileAndTest() + val generatedFiles = crate.generatedFiles().map { it.pathString } + assert(generatedFiles.contains("src/a.rs")) { generatedFiles } + assert(generatedFiles.contains("src/a/b.rs")) { generatedFiles } + assert(generatedFiles.contains("src/a/b/c.rs")) { generatedFiles } + } + + @Test + fun `prevent the creation of duplicate modules`() { + val root = RustModule.private("parent") + // create a child module with no docs + val child1 = RustModule.private("child", parent = root) + val child2 = RustModule.public("child", parent = root) + val crate = TestWorkspace.testProject() + crate.withModule(child1) { } + shouldThrow { + crate.withModule(child2) {} + } + + shouldThrow { + // can't make one with docs when the old one had no docs + crate.withModule(RustModule.private("child", documentation = "docs", parent = root)) {} + } + + // but making an identical module is fine + val identicalChild = RustModule.private("child", parent = root) + crate.withModule(identicalChild) {} + identicalChild.fullyQualifiedPath() shouldBe "crate::parent::child" } } diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriterTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriterTest.kt index 4c18a23c2e5..b3a8e6529c8 100644 --- a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriterTest.kt +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriterTest.kt @@ -27,7 +27,7 @@ class RustWriterTest { fun `inner modules correctly handle dependencies`() { val sut = RustWriter.forModule("parent") val requestBuilder = RuntimeType.HttpRequestBuilder - sut.withModule(RustModule.public("inner")) { + sut.withInlineModule(RustModule.new("inner", visibility = Visibility.PUBLIC, inline = true)) { rustBlock("fn build(builder: #T)", requestBuilder) { } } diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/StructureGeneratorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/StructureGeneratorTest.kt index 110e7f35a98..fcf242dcc4b 100644 --- a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/StructureGeneratorTest.kt +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/StructureGeneratorTest.kt @@ -13,10 +13,10 @@ import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.rust.codegen.core.rustlang.Attribute import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter -import software.amazon.smithy.rust.codegen.core.rustlang.docs import software.amazon.smithy.rust.codegen.core.rustlang.raw import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock +import software.amazon.smithy.rust.codegen.core.smithy.ModelsModule import software.amazon.smithy.rust.codegen.core.smithy.transformers.RecursiveShapeBoxer import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel @@ -100,20 +100,21 @@ class StructureGeneratorTest { @Test fun `generate structures with public fields`() { + val project = TestWorkspace.testProject() val provider = testSymbolProvider(model) - val writer = RustWriter.root() - writer.rust("##![allow(deprecated)]") - writer.withModule(RustModule.public("model")) { + + project.lib { rust("##![allow(deprecated)]") } + project.withModule(ModelsModule) { val innerGenerator = StructureGenerator(model, provider, this, inner) innerGenerator.render() } - writer.withModule(RustModule.public("structs")) { + project.withModule(RustModule.public("structs")) { val generator = StructureGenerator(model, provider, this, struct) generator.render() } // By putting the test in another module, it can't access the struct // fields if they are private - writer.withModule(RustModule.public("inline")) { + project.unitTest { raw("#[test]") rustBlock("fn test_public_fields()") { write( @@ -124,7 +125,7 @@ class StructureGeneratorTest { ) } } - writer.compileAndTest() + project.compileAndTest() } @Test @@ -179,15 +180,16 @@ class StructureGeneratorTest { nested2: Inner }""".asSmithyModel() val provider = testSymbolProvider(model) - val writer = RustWriter.root() - writer.docs("module docs") - Attribute.Custom("deny(missing_docs)").render(writer) - writer.withModule(RustModule.public("model")) { + val project = TestWorkspace.testProject(provider) + project.lib { + Attribute.Custom("deny(missing_docs)").render(this) + } + project.withModule(ModelsModule) { StructureGenerator(model, provider, this, model.lookup("com.test#Inner")).render() StructureGenerator(model, provider, this, model.lookup("com.test#MyStruct")).render() } - writer.compileAndTest() + project.compileAndTest() } @Test @@ -224,9 +226,9 @@ class StructureGeneratorTest { structure Qux {} """.asSmithyModel() val provider = testSymbolProvider(model) - val writer = RustWriter.root() - writer.rust("##![allow(deprecated)]") - writer.withModule(RustModule.public("model")) { + val project = TestWorkspace.testProject(provider) + project.lib { rust("##![allow(deprecated)]") } + project.withModule(ModelsModule) { StructureGenerator(model, provider, this, model.lookup("test#Foo")).render() StructureGenerator(model, provider, this, model.lookup("test#Bar")).render() StructureGenerator(model, provider, this, model.lookup("test#Baz")).render() @@ -234,7 +236,7 @@ class StructureGeneratorTest { } // turn on clippy to check the semver-compliant version of `since`. - writer.compileAndTest(clippy = true) + project.compileAndTest(runClippy = true) } @Test @@ -257,15 +259,15 @@ class StructureGeneratorTest { structure Bar {} """.asSmithyModel() val provider = testSymbolProvider(model) - val writer = RustWriter.root() - writer.rust("##![allow(deprecated)]") - writer.withModule(RustModule.public("model")) { + val project = TestWorkspace.testProject(provider) + project.lib { rust("##![allow(deprecated)]") } + project.withModule(ModelsModule) { StructureGenerator(model, provider, this, model.lookup("test#Nested")).render() StructureGenerator(model, provider, this, model.lookup("test#Foo")).render() StructureGenerator(model, provider, this, model.lookup("test#Bar")).render() } - writer.compileAndTest() + project.compileAndTest() } @Test diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/UnionGeneratorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/UnionGeneratorTest.kt index 043941bfe18..d2ea8b9de11 100644 --- a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/UnionGeneratorTest.kt +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/UnionGeneratorTest.kt @@ -8,9 +8,10 @@ package software.amazon.smithy.rust.codegen.core.smithy.generators import io.kotest.matchers.string.shouldContain import org.junit.jupiter.api.Test import software.amazon.smithy.codegen.core.SymbolProvider -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule 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.smithy.ModelsModule +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 import software.amazon.smithy.rust.codegen.core.testutil.testSymbolProvider @@ -112,16 +113,16 @@ class UnionGeneratorTest { @deprecated union Bar { x: Integer } """.asSmithyModel() - val provider: SymbolProvider = testSymbolProvider(model) - val writer = RustWriter.root() - writer.rust("##![allow(deprecated)]") - writer.withModule(RustModule.public("model")) { + val provider = testSymbolProvider(model) + val project = TestWorkspace.testProject(provider) + project.lib { rust("##![allow(deprecated)]") } + project.withModule(ModelsModule) { UnionGenerator(model, provider, this, model.lookup("test#Nested")).render() UnionGenerator(model, provider, this, model.lookup("test#Foo")).render() UnionGenerator(model, provider, this, model.lookup("test#Bar")).render() } - writer.compileAndTest() + project.compileAndTest() } private fun generateUnion(modelSmithy: String, unionName: String = "MyUnion", unknownVariant: Boolean = true): RustWriter { diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/error/CombinedErrorGeneratorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/error/CombinedErrorGeneratorTest.kt index d84e50b5cc5..30838e82da4 100644 --- a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/error/CombinedErrorGeneratorTest.kt +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/error/CombinedErrorGeneratorTest.kt @@ -7,7 +7,7 @@ package software.amazon.smithy.rust.codegen.core.smithy.generators.error import org.junit.jupiter.api.Test import software.amazon.smithy.model.shapes.StructureShape -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule +import software.amazon.smithy.rust.codegen.core.smithy.ErrorsModule import software.amazon.smithy.rust.codegen.core.smithy.transformers.OperationNormalizer import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel @@ -50,7 +50,7 @@ class CombinedErrorGeneratorTest { @Test fun `generates combined error enums`() { val project = TestWorkspace.testProject(symbolProvider) - project.withModule(RustModule.public("error")) { + project.withModule(ErrorsModule) { listOf("FooException", "ComplexError", "InvalidGreeting", "Deprecated").forEach { model.lookup("error#$it").renderWithModelBuilder(model, symbolProvider, this) } @@ -90,8 +90,6 @@ class CombinedErrorGeneratorTest { """, ) - println("file:///${project.baseDir}/src/lib.rs") - println("file:///${project.baseDir}/src/error.rs") project.compileAndTest() } } diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/error/TopLevelErrorGeneratorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/error/TopLevelErrorGeneratorTest.kt index d407af3fe44..a3d8fc364a2 100644 --- a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/error/TopLevelErrorGeneratorTest.kt +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/error/TopLevelErrorGeneratorTest.kt @@ -15,7 +15,6 @@ import software.amazon.smithy.rust.codegen.core.smithy.CodegenTarget import software.amazon.smithy.rust.codegen.core.smithy.CoreRustSettings import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.generators.StructureGenerator -import software.amazon.smithy.rust.codegen.core.testutil.DefaultTestPublicModules import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.generatePluginContext import software.amazon.smithy.rust.codegen.core.testutil.testSymbolProvider @@ -75,7 +74,6 @@ internal class TopLevelErrorGeneratorTest { val rustCrate = RustCrate( pluginContext.fileManifest, symbolProvider, - DefaultTestPublicModules, codegenContext.settings.codegenConfig, ) diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt index 0eac9dd1961..c9d73999f33 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt @@ -23,7 +23,6 @@ import software.amazon.smithy.rust.codegen.core.smithy.SymbolVisitorConfig import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerEnumGenerator import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerServiceGenerator import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerStructureGenerator -import software.amazon.smithy.rust.codegen.server.smithy.DefaultServerPublicModules import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenVisitor import software.amazon.smithy.rust.codegen.server.smithy.ServerSymbolProviders @@ -98,7 +97,7 @@ class PythonServerCodegenVisitor( ) // Override `rustCrate` which carries the symbolProvider. - rustCrate = RustCrate(context.fileManifest, codegenContext.symbolProvider, DefaultServerPublicModules, settings.codegenConfig) + rustCrate = RustCrate(context.fileManifest, codegenContext.symbolProvider, settings.codegenConfig) // Override `protocolGenerator` which carries the symbolProvider. protocolGenerator = protocolGeneratorFactory.buildProtocolGenerator(codegenContext) } diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt index 4431b5f0a45..63fd61e1c9f 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt @@ -13,9 +13,9 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate 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.Errors -import software.amazon.smithy.rust.codegen.core.smithy.Inputs -import software.amazon.smithy.rust.codegen.core.smithy.Outputs +import software.amazon.smithy.rust.codegen.core.smithy.ErrorsModule +import software.amazon.smithy.rust.codegen.core.smithy.InputsModule +import software.amazon.smithy.rust.codegen.core.smithy.OutputsModule import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rust.codegen.core.util.inputShape @@ -337,12 +337,12 @@ class PythonApplicationGenerator( ) writer.rust( """ - /// from $libName import ${Inputs.namespace} - /// from $libName import ${Outputs.namespace} + /// from $libName import ${InputsModule.name} + /// from $libName import ${OutputsModule.name} """.trimIndent(), ) if (operations.any { it.errors.isNotEmpty() }) { - writer.rust("""/// from $libName import ${Errors.namespace}""".trimIndent()) + writer.rust("""/// from $libName import ${ErrorsModule.name}""".trimIndent()) } writer.rust( """ @@ -396,8 +396,8 @@ class PythonApplicationGenerator( private fun OperationShape.signature(): String { val inputSymbol = symbolProvider.toSymbol(inputShape(model)) val outputSymbol = symbolProvider.toSymbol(outputShape(model)) - val inputT = "${Inputs.namespace}::${inputSymbol.name}" - val outputT = "${Outputs.namespace}::${outputSymbol.name}" + val inputT = "${InputsModule.name}::${inputSymbol.name}" + val outputT = "${OutputsModule.name}::${outputSymbol.name}" val operationName = symbolProvider.toSymbol(this).name.toSnakeCase() return "@app.$operationName\n/// def $operationName(input: $inputT, ctx: Context) -> $outputT" } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstrainedShapeSymbolProvider.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstrainedShapeSymbolProvider.kt index 92ff5faf77e..625f3b5fa64 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstrainedShapeSymbolProvider.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstrainedShapeSymbolProvider.kt @@ -16,7 +16,7 @@ import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.traits.LengthTrait import software.amazon.smithy.rust.codegen.core.rustlang.RustType -import software.amazon.smithy.rust.codegen.core.smithy.Models +import software.amazon.smithy.rust.codegen.core.smithy.ModelsModule import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.WrappingSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.contextName @@ -58,7 +58,7 @@ class ConstrainedShapeSymbolProvider( check(shape is MapShape) val rustType = RustType.Opaque(shape.contextName(serviceShape).toPascalCase()) - return symbolBuilder(shape, rustType).locatedIn(Models).build() + return symbolBuilder(shape, rustType).locatedIn(ModelsModule).build() } override fun toSymbol(shape: Shape): Symbol { @@ -98,7 +98,7 @@ class ConstrainedShapeSymbolProvider( is StringShape -> { if (shape.isDirectlyConstrained(base)) { val rustType = RustType.Opaque(shape.contextName(serviceShape).toPascalCase()) - symbolBuilder(shape, rustType).locatedIn(Models).build() + symbolBuilder(shape, rustType).locatedIn(ModelsModule).build() } else { base.toSymbol(shape) } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstraintViolationSymbolProvider.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstraintViolationSymbolProvider.kt index 119db964524..e2b6e23f01d 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstraintViolationSymbolProvider.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstraintViolationSymbolProvider.kt @@ -14,12 +14,16 @@ import software.amazon.smithy.model.shapes.Shape 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.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords import software.amazon.smithy.rust.codegen.core.rustlang.RustType -import software.amazon.smithy.rust.codegen.core.smithy.Models +import software.amazon.smithy.rust.codegen.core.rustlang.Visibility +import software.amazon.smithy.rust.codegen.core.smithy.ModelsModule import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.WrappingSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.contextName +import software.amazon.smithy.rust.codegen.core.smithy.locatedIn +import software.amazon.smithy.rust.codegen.core.smithy.module import software.amazon.smithy.rust.codegen.core.smithy.rustType import software.amazon.smithy.rust.codegen.core.util.toSnakeCase import software.amazon.smithy.rust.codegen.server.smithy.generators.serverBuilderSymbol @@ -60,27 +64,32 @@ import software.amazon.smithy.rust.codegen.server.smithy.generators.serverBuilde class ConstraintViolationSymbolProvider( private val base: RustSymbolProvider, private val model: Model, - private val serviceShape: ServiceShape, private val publicConstrainedTypes: Boolean, + private val serviceShape: ServiceShape, ) : WrappingSymbolProvider(base) { private val constraintViolationName = "ConstraintViolation" + private val visibility = when (publicConstrainedTypes) { + true -> Visibility.PUBLIC + false -> Visibility.PUBCRATE + } + + private fun Shape.shapeModule() = RustModule.new( + // need to use the context name so we get the correct name for maps + name = RustReservedWords.escapeIfNeeded(this.contextName(serviceShape)).toSnakeCase(), + visibility = visibility, + parent = ModelsModule, + inline = true, + ) private fun constraintViolationSymbolForCollectionOrMapOrUnionShape(shape: Shape): Symbol { check(shape is CollectionShape || shape is MapShape || shape is UnionShape) - val symbol = base.toSymbol(shape) - val constraintViolationNamespace = - "${symbol.namespace.let { it.ifEmpty { "crate::${Models.namespace}" } }}::${ - RustReservedWords.escapeIfNeeded( - shape.contextName(serviceShape).toSnakeCase(), - ) - }" - val rustType = RustType.Opaque(constraintViolationName, constraintViolationNamespace) + val module = shape.shapeModule() + val rustType = RustType.Opaque(constraintViolationName, module.fullyQualifiedPath()) return Symbol.builder() .rustType(rustType) .name(rustType.name) - .namespace(rustType.namespace, "::") - .definitionFile(symbol.definitionFile) + .locatedIn(module) .build() } @@ -91,32 +100,29 @@ class ConstraintViolationSymbolProvider( is MapShape, is CollectionShape, is UnionShape -> { constraintViolationSymbolForCollectionOrMapOrUnionShape(shape) } + is StructureShape -> { val builderSymbol = shape.serverBuilderSymbol(base, pubCrate = !publicConstrainedTypes) - val namespace = builderSymbol.namespace - val rustType = RustType.Opaque(constraintViolationName, namespace) + val module = builderSymbol.module() + val rustType = RustType.Opaque(constraintViolationName, module.fullyQualifiedPath()) Symbol.builder() .rustType(rustType) .name(rustType.name) - .namespace(rustType.namespace, "::") - .definitionFile(builderSymbol.definitionFile) + .locatedIn(module) .build() } + is StringShape -> { - val namespace = "crate::${Models.namespace}::${ - RustReservedWords.escapeIfNeeded( - shape.contextName(serviceShape).toSnakeCase(), - ) - }" - val rustType = RustType.Opaque(constraintViolationName, namespace) + val module = shape.shapeModule() + val rustType = RustType.Opaque(constraintViolationName, module.fullyQualifiedPath()) Symbol.builder() .rustType(rustType) .name(rustType.name) - .namespace(rustType.namespace, "::") - .definitionFile(Models.filename) + .locatedIn(module) .build() } + else -> TODO("Constraint traits on other shapes not implemented yet: $shape") } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/Constraints.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/Constraints.kt index 82102be18a0..fe5b66ad821 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/Constraints.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/Constraints.kt @@ -21,6 +21,7 @@ import software.amazon.smithy.model.traits.LengthTrait import software.amazon.smithy.model.traits.PatternTrait import software.amazon.smithy.model.traits.RangeTrait import software.amazon.smithy.model.traits.RequiredTrait +import software.amazon.smithy.model.traits.Trait import software.amazon.smithy.model.traits.UniqueItemsTrait import software.amazon.smithy.rust.codegen.core.smithy.isOptional import software.amazon.smithy.rust.codegen.core.util.UNREACHABLE @@ -42,6 +43,8 @@ fun Shape.hasConstraintTrait() = hasTrait() || hasTrait() +val supportedStringConstraintTraits: List> = listOf(LengthTrait::class.java, PatternTrait::class.java) + /** * We say a shape is _directly_ constrained if: * @@ -65,8 +68,9 @@ fun Shape.isDirectlyConstrained(symbolProvider: SymbolProvider): Boolean = when // `required`, so we can't use `member.isOptional` here. this.members().map { symbolProvider.toSymbol(it) }.any { !it.isOptional() } } + is MapShape -> this.hasTrait() - is StringShape -> this.hasTrait() || this.hasTrait() + is StringShape -> this.hasTrait() || supportedStringConstraintTraits.any { this.hasTrait(it) } else -> false } @@ -91,7 +95,7 @@ fun MemberShape.targetCanReachConstrainedShape(model: Model, symbolProvider: Sym fun Shape.hasPublicConstrainedWrapperTupleType(model: Model, publicConstrainedTypes: Boolean): Boolean = when (this) { is MapShape -> publicConstrainedTypes && this.hasTrait() - is StringShape -> !this.hasTrait() && (publicConstrainedTypes && this.hasTrait()) + is StringShape -> !this.hasTrait() && (publicConstrainedTypes && supportedStringConstraintTraits.any(this::hasTrait)) is MemberShape -> model.expectShape(this.target).hasPublicConstrainedWrapperTupleType(model, publicConstrainedTypes) else -> false } @@ -125,7 +129,9 @@ fun Shape.typeNameContainsNonPublicType( publicConstrainedTypes: Boolean, ): Boolean = !publicConstrainedTypes && when (this) { is SimpleShape -> wouldHaveConstrainedWrapperTupleTypeWerePublicConstrainedTypesEnabled(model) - is MemberShape -> model.expectShape(this.target).typeNameContainsNonPublicType(model, symbolProvider, publicConstrainedTypes) + is MemberShape -> model.expectShape(this.target) + .typeNameContainsNonPublicType(model, symbolProvider, publicConstrainedTypes) + is CollectionShape -> this.canReachConstrainedShape(model, symbolProvider) is MapShape -> this.canReachConstrainedShape(model, symbolProvider) is StructureShape, is UnionShape -> false diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/PatternTraitValidationErrorMessage.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/PatternTraitValidationErrorMessage.kt new file mode 100644 index 00000000000..8bb3cb648e1 --- /dev/null +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/PatternTraitValidationErrorMessage.kt @@ -0,0 +1,12 @@ +/* + * 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 software.amazon.smithy.model.traits.PatternTrait + +@Suppress("UnusedReceiverParameter") +fun PatternTrait.validationErrorMessage(): String = + "Value {} at '{}' failed to satisfy constraint: Member must satisfy regular expression pattern: {}" diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/PubCrateConstrainedShapeSymbolProvider.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/PubCrateConstrainedShapeSymbolProvider.kt index e63e18c7ac4..bb818eaf078 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/PubCrateConstrainedShapeSymbolProvider.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/PubCrateConstrainedShapeSymbolProvider.kt @@ -16,13 +16,16 @@ import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.SimpleShape import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.UnionShape +import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords import software.amazon.smithy.rust.codegen.core.rustlang.RustType -import software.amazon.smithy.rust.codegen.core.smithy.Constrained +import software.amazon.smithy.rust.codegen.core.rustlang.Visibility +import software.amazon.smithy.rust.codegen.core.smithy.ConstrainedModule import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.WrappingSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.handleOptionality import software.amazon.smithy.rust.codegen.core.smithy.handleRustBoxing +import software.amazon.smithy.rust.codegen.core.smithy.locatedIn import software.amazon.smithy.rust.codegen.core.smithy.rustType import software.amazon.smithy.rust.codegen.core.util.PANIC import software.amazon.smithy.rust.codegen.core.util.toPascalCase @@ -68,13 +71,17 @@ class PubCrateConstrainedShapeSymbolProvider( check(shape is CollectionShape || shape is MapShape) val name = constrainedTypeNameForCollectionOrMapShape(shape, serviceShape) - val namespace = "crate::${Constrained.namespace}::${RustReservedWords.escapeIfNeeded(name.toSnakeCase())}" - val rustType = RustType.Opaque(name, namespace) + val module = RustModule.new( + RustReservedWords.escapeIfNeeded(name.toSnakeCase()), + visibility = Visibility.PUBCRATE, + parent = ConstrainedModule, + inline = true, + ) + val rustType = RustType.Opaque(name, module.fullyQualifiedPath()) return Symbol.builder() .rustType(rustType) .name(rustType.name) - .namespace(rustType.namespace, "::") - .definitionFile(Constrained.filename) + .locatedIn(module) .build() } @@ -88,6 +95,7 @@ class PubCrateConstrainedShapeSymbolProvider( is CollectionShape, is MapShape -> { constrainedSymbolForCollectionOrMapShape(shape) } + is MemberShape -> { require(model.expectShape(shape.container).isStructureShape) { "This arm is only exercised by `ServerBuilderGenerator`" @@ -101,13 +109,20 @@ class PubCrateConstrainedShapeSymbolProvider( } else { val targetSymbol = this.toSymbol(targetShape) // Handle boxing first so we end up with `Option>`, not `Box>`. - handleOptionality(handleRustBoxing(targetSymbol, shape), shape, nullableIndex, base.config().nullabilityCheckMode) + handleOptionality( + handleRustBoxing(targetSymbol, shape), + shape, + nullableIndex, + base.config().nullabilityCheckMode, + ) } } + is StructureShape, is UnionShape -> { // Structure shapes and union shapes always generate a [RustType.Opaque] constrained type. base.toSymbol(shape) } + else -> { check(shape is SimpleShape) // The rest of the shape types are simple shapes, which are impossible to be transitively but not diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/PubCrateConstraintViolationSymbolProvider.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/PubCrateConstraintViolationSymbolProvider.kt index 05a8d635a4b..4e7b5ab6c72 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/PubCrateConstraintViolationSymbolProvider.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/PubCrateConstraintViolationSymbolProvider.kt @@ -8,8 +8,11 @@ package software.amazon.smithy.rust.codegen.server.smithy import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustType import software.amazon.smithy.rust.codegen.core.smithy.WrappingSymbolProvider +import software.amazon.smithy.rust.codegen.core.smithy.locatedIn +import software.amazon.smithy.rust.codegen.core.smithy.module import software.amazon.smithy.rust.codegen.core.smithy.rustType /** @@ -28,10 +31,11 @@ class PubCrateConstraintViolationSymbolProvider( return baseSymbol } val baseRustType = baseSymbol.rustType() - val newNamespace = baseSymbol.namespace + "_internal" + val oldModule = baseSymbol.module() as RustModule.LeafModule + val newModule = oldModule.copy(name = oldModule.name + "_internal") return baseSymbol.toBuilder() - .rustType(RustType.Opaque(baseRustType.name, newNamespace)) - .namespace(newNamespace, baseSymbol.namespaceDelimiter) + .rustType(RustType.Opaque(baseRustType.name, newModule.fullyQualifiedPath())) + .locatedIn(newModule) .build() } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCargoDependency.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCargoDependency.kt index 6058519abfe..651ff1b56c2 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCargoDependency.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCargoDependency.kt @@ -8,6 +8,7 @@ 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.DependencyScope import software.amazon.smithy.rust.codegen.core.rustlang.InlineDependency +import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig /** @@ -25,6 +26,7 @@ object ServerCargoDependency { val PinProjectLite: CargoDependency = CargoDependency("pin-project-lite", CratesIo("0.2")) val Tower: CargoDependency = CargoDependency("tower", CratesIo("0.4")) val TokioDev: CargoDependency = CargoDependency("tokio", CratesIo("1.8.4"), scope = DependencyScope.Dev) + val Regex: CargoDependency = CargoDependency("regex", CratesIo("1.5.5")) fun SmithyHttpServer(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("http-server") } @@ -43,7 +45,8 @@ object ServerCargoDependency { object ServerInlineDependency { fun serverOperationHandler(runtimeConfig: RuntimeConfig): InlineDependency = InlineDependency.forRustFile( - "server_operation_handler_trait", + RustModule.private("server_operation_handler_trait"), + "/inlineable/src/server_operation_handler_trait.rs", ServerCargoDependency.SmithyHttpServer(runtimeConfig), CargoDependency.Http, ServerCargoDependency.PinProjectLite, diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt index 8a3c5649bc9..92f9bafa3bb 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt @@ -24,15 +24,14 @@ import software.amazon.smithy.model.traits.EnumTrait import software.amazon.smithy.model.traits.LengthTrait import software.amazon.smithy.model.transform.ModelTransformer import software.amazon.smithy.rust.codegen.client.smithy.customize.RustCodegenDecorator -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.smithy.CodegenTarget -import software.amazon.smithy.rust.codegen.core.smithy.Constrained +import software.amazon.smithy.rust.codegen.core.smithy.ConstrainedModule import software.amazon.smithy.rust.codegen.core.smithy.CoreRustSettings import software.amazon.smithy.rust.codegen.core.smithy.ModelsModule import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.SymbolVisitorConfig -import software.amazon.smithy.rust.codegen.core.smithy.Unconstrained +import software.amazon.smithy.rust.codegen.core.smithy.UnconstrainedModule import software.amazon.smithy.rust.codegen.core.smithy.generators.StructureGenerator import software.amazon.smithy.rust.codegen.core.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.core.smithy.generators.implBlock @@ -66,13 +65,6 @@ import software.amazon.smithy.rust.codegen.server.smithy.transformers.RemoveEbsM import software.amazon.smithy.rust.codegen.server.smithy.transformers.ShapesReachableFromOperationInputTagger import java.util.logging.Logger -val DefaultServerPublicModules = setOf( - RustModule.Error, - RustModule.Model, - RustModule.Input, - RustModule.Output, -).associateBy { it.name } - /** * Entrypoint for server-side code generation. This class will walk the in-memory model and * generate all the needed types by calling the accept() function on the available shapes. @@ -91,10 +83,6 @@ open class ServerCodegenVisitor( protected var codegenContext: ServerCodegenContext protected var protocolGeneratorFactory: ProtocolGeneratorFactory protected var protocolGenerator: ServerProtocolGenerator - private val unconstrainedModule = - RustModule.private(Unconstrained.namespace, "Unconstrained types for constrained shapes.") - private val constrainedModule = - RustModule.private(Constrained.namespace, "Constrained types for constrained shapes.") init { val symbolVisitorConfig = @@ -138,7 +126,7 @@ open class ServerCodegenVisitor( serverSymbolProviders.pubCrateConstrainedShapeSymbolProvider, ) - rustCrate = RustCrate(context.fileManifest, codegenContext.symbolProvider, DefaultServerPublicModules, settings.codegenConfig) + rustCrate = RustCrate(context.fileManifest, codegenContext.symbolProvider, settings.codegenConfig) protocolGenerator = protocolGeneratorFactory.buildProtocolGenerator(codegenContext) } @@ -294,7 +282,7 @@ open class ServerCodegenVisitor( ) ) { logger.info("[rust-server-codegen] Generating an unconstrained type for collection shape $shape") - rustCrate.withModule(unconstrainedModule) unconstrainedModuleWriter@{ + rustCrate.withModule(UnconstrainedModule) unconstrainedModuleWriter@{ rustCrate.withModule(ModelsModule) modelsModuleWriter@{ UnconstrainedCollectionGenerator( codegenContext, @@ -306,7 +294,7 @@ open class ServerCodegenVisitor( } logger.info("[rust-server-codegen] Generating a constrained type for collection shape $shape") - rustCrate.withModule(constrainedModule) { + rustCrate.withModule(ConstrainedModule) { PubCrateConstrainedCollectionGenerator(codegenContext, this, shape).render() } } @@ -320,13 +308,13 @@ open class ServerCodegenVisitor( ) if (renderUnconstrainedMap) { logger.info("[rust-server-codegen] Generating an unconstrained type for map $shape") - rustCrate.withModule(unconstrainedModule) { + rustCrate.withModule(UnconstrainedModule) { UnconstrainedMapGenerator(codegenContext, this, shape).render() } if (!shape.isDirectlyConstrained(codegenContext.symbolProvider)) { logger.info("[rust-server-codegen] Generating a constrained type for map $shape") - rustCrate.withModule(constrainedModule) { + rustCrate.withModule(ConstrainedModule) { PubCrateConstrainedMapGenerator(codegenContext, this, shape).render() } } @@ -410,7 +398,7 @@ open class ServerCodegenVisitor( ) ) { logger.info("[rust-server-codegen] Generating an unconstrained type for union shape $shape") - rustCrate.withModule(unconstrainedModule) unconstrainedModuleWriter@{ + rustCrate.withModule(UnconstrainedModule) unconstrainedModuleWriter@{ rustCrate.withModule(ModelsModule) modelsModuleWriter@{ UnconstrainedUnionGenerator( codegenContext, diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerSymbolProviders.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerSymbolProviders.kt index e2b77c90fd7..0e368d85175 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerSymbolProviders.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerSymbolProviders.kt @@ -46,7 +46,7 @@ class ServerSymbolProviders private constructor( symbolVisitorConfig, false, ), - model, service, publicConstrainedTypes, + model, publicConstrainedTypes, service, ), pubCrateConstrainedShapeSymbolProvider = PubCrateConstrainedShapeSymbolProvider( baseSymbolProvider, @@ -56,8 +56,8 @@ class ServerSymbolProviders private constructor( constraintViolationSymbolProvider = ConstraintViolationSymbolProvider( baseSymbolProvider, model, - service, publicConstrainedTypes, + service, ), ) } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/UnconstrainedShapeSymbolProvider.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/UnconstrainedShapeSymbolProvider.kt index 9fa2182e6b2..5beb183c816 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/UnconstrainedShapeSymbolProvider.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/UnconstrainedShapeSymbolProvider.kt @@ -16,14 +16,18 @@ import software.amazon.smithy.model.shapes.Shape 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.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords import software.amazon.smithy.rust.codegen.core.rustlang.RustType +import software.amazon.smithy.rust.codegen.core.rustlang.Visibility import software.amazon.smithy.rust.codegen.core.smithy.Default import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider -import software.amazon.smithy.rust.codegen.core.smithy.Unconstrained +import software.amazon.smithy.rust.codegen.core.smithy.UnconstrainedModule import software.amazon.smithy.rust.codegen.core.smithy.WrappingSymbolProvider +import software.amazon.smithy.rust.codegen.core.smithy.contextName import software.amazon.smithy.rust.codegen.core.smithy.handleOptionality import software.amazon.smithy.rust.codegen.core.smithy.handleRustBoxing +import software.amazon.smithy.rust.codegen.core.smithy.locatedIn import software.amazon.smithy.rust.codegen.core.smithy.rustType import software.amazon.smithy.rust.codegen.core.smithy.setDefault import software.amazon.smithy.rust.codegen.core.smithy.symbolBuilder @@ -75,22 +79,39 @@ import software.amazon.smithy.rust.codegen.server.smithy.generators.serverBuilde class UnconstrainedShapeSymbolProvider( private val base: RustSymbolProvider, private val model: Model, - private val serviceShape: ServiceShape, private val publicConstrainedTypes: Boolean, + private val serviceShape: ServiceShape, ) : WrappingSymbolProvider(base) { private val nullableIndex = NullableIndex.of(model) + /** + * Unconstrained type names are always suffixed with `Unconstrained` for clarity, even though we could dispense with it + * given that they all live inside the `unconstrained` module, so they don't collide with the constrained types. + */ + private fun unconstrainedTypeNameForCollectionOrMapOrUnionShape(shape: Shape): String { + check(shape is CollectionShape || shape is MapShape || shape is UnionShape) + // Normally, one could use the base symbol provider's name. However, in this case, the name will be `Vec` or + // `HashMap` because the symbol provider _does not_ newtype the shapes. However, for unconstrained shapes, + // we need to introduce a newtype that preserves the original name of the shape from smithy. To handle that, + // we load the name of the shape directly from the model prior to add `Unconstrained`. + return RustReservedWords.escapeIfNeeded(shape.contextName(serviceShape).toPascalCase() + "Unconstrained") + } + private fun unconstrainedSymbolForCollectionOrMapOrUnionShape(shape: Shape): Symbol { check(shape is CollectionShape || shape is MapShape || shape is UnionShape) - val name = unconstrainedTypeNameForCollectionOrMapOrUnionShape(shape, serviceShape) - val namespace = "crate::${Unconstrained.namespace}::${RustReservedWords.escapeIfNeeded(name.toSnakeCase())}" - val rustType = RustType.Opaque(name, namespace) + val name = unconstrainedTypeNameForCollectionOrMapOrUnionShape(shape) + val module = RustModule.new( + RustReservedWords.escapeIfNeeded(name.toSnakeCase()), + visibility = Visibility.PUBCRATE, + parent = UnconstrainedModule, + inline = true, + ) + val rustType = RustType.Opaque(name, module.fullyQualifiedPath()) return Symbol.builder() .rustType(rustType) .name(rustType.name) - .namespace(rustType.namespace, "::") - .definitionFile(Unconstrained.filename) + .locatedIn(module) .build() } @@ -103,6 +124,7 @@ class UnconstrainedShapeSymbolProvider( base.toSymbol(shape) } } + is MapShape -> { if (shape.canReachConstrainedShape(model, base)) { unconstrainedSymbolForCollectionOrMapOrUnionShape(shape) @@ -110,6 +132,7 @@ class UnconstrainedShapeSymbolProvider( base.toSymbol(shape) } } + is StructureShape -> { if (shape.canReachConstrainedShape(model, base)) { shape.serverBuilderSymbol(base, !publicConstrainedTypes) @@ -117,6 +140,7 @@ class UnconstrainedShapeSymbolProvider( base.toSymbol(shape) } } + is UnionShape -> { if (shape.canReachConstrainedShape(model, base)) { unconstrainedSymbolForCollectionOrMapOrUnionShape(shape) @@ -124,6 +148,7 @@ class UnconstrainedShapeSymbolProvider( base.toSymbol(shape) } } + is MemberShape -> { // There are only two cases where we use this symbol provider on a member shape. // @@ -138,13 +163,19 @@ class UnconstrainedShapeSymbolProvider( val targetShape = model.expectShape(shape.target) val targetSymbol = this.toSymbol(targetShape) // Handle boxing first so we end up with `Option>`, not `Box>`. - handleOptionality(handleRustBoxing(targetSymbol, shape), shape, nullableIndex, base.config().nullabilityCheckMode) + handleOptionality( + handleRustBoxing(targetSymbol, shape), + shape, + nullableIndex, + base.config().nullabilityCheckMode, + ) } else { base.toSymbol(shape) } // TODO(https://github.com/awslabs/smithy-rs/issues/1401) Constraint traits on member shapes are not // implemented yet. } + is StringShape -> { if (shape.canReachConstrainedShape(model, base)) { symbolBuilder(shape, RustType.String).setDefault(Default.RustDefault).build() @@ -152,15 +183,7 @@ class UnconstrainedShapeSymbolProvider( base.toSymbol(shape) } } + else -> base.toSymbol(shape) } } - -/** - * Unconstrained type names are always suffixed with `Unconstrained` for clarity, even though we could dispense with it - * given that they all live inside the `unconstrained` module, so they don't collide with the constrained types. - */ -fun unconstrainedTypeNameForCollectionOrMapOrUnionShape(shape: Shape, serviceShape: ServiceShape): String { - check(shape is CollectionShape || shape is MapShape || shape is UnionShape) - return "${shape.id.getName(serviceShape).toPascalCase()}Unconstrained" -} diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt index 871a8d364c5..4535616bfff 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraints.kt @@ -16,7 +16,6 @@ import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.SetShape import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.ShapeId -import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.EnumTrait import software.amazon.smithy.model.traits.LengthTrait @@ -42,7 +41,7 @@ private sealed class UnsupportedConstraintMessageKind { This is not supported in the smithy-rs server SDK. ${ if (willSupport) "It will be supported in the future." else "" } See the tracking issue ($trackingIssue). - If you want to go ahead and generate the server SDK ignoring unsupported constraint traits, set the key `ignoreUnsupportedConstraintTraits` + If you want to go ahead and generate the server SDK ignoring unsupported constraint traits, set the key `ignoreUnsupportedConstraints` inside the `runtimeConfig.codegenConfig` JSON object in your `smithy-build.json` to `true`. """.trimIndent().replace("\n", " ") @@ -86,10 +85,6 @@ private sealed class UnsupportedConstraintMessageKind { level, buildMessageShapeHasUnsupportedConstraintTrait(shape, lengthTrait, constraintTraitsUberIssue), ) - is UnsupportedPatternTraitOnStringShape -> LogMessage( - level, - buildMessageShapeHasUnsupportedConstraintTrait(shape, patternTrait, constraintTraitsUberIssue), - ) is UnsupportedRangeTraitOnShape -> LogMessage( level, buildMessageShapeHasUnsupportedConstraintTrait(shape, rangeTrait, constraintTraitsUberIssue), @@ -106,7 +101,6 @@ private data class UnsupportedConstraintOnMemberShape(val shape: MemberShape, va private data class UnsupportedConstraintOnShapeReachableViaAnEventStream(val shape: Shape, val constraintTrait: Trait) : UnsupportedConstraintMessageKind() private data class UnsupportedLengthTraitOnStreamingBlobShape(val shape: BlobShape, val lengthTrait: LengthTrait, val streamingTrait: StreamingTrait) : UnsupportedConstraintMessageKind() private data class UnsupportedLengthTraitOnCollectionOrOnBlobShape(val shape: Shape, val lengthTrait: LengthTrait) : UnsupportedConstraintMessageKind() -private data class UnsupportedPatternTraitOnStringShape(val shape: Shape, val patternTrait: PatternTrait) : UnsupportedConstraintMessageKind() private data class UnsupportedRangeTraitOnShape(val shape: Shape, val rangeTrait: RangeTrait) : UnsupportedConstraintMessageKind() private data class UnsupportedUniqueItemsTraitOnShape(val shape: Shape, val uniqueItemsTrait: UniqueItemsTrait) : UnsupportedConstraintMessageKind() @@ -146,20 +140,20 @@ fun validateOperationsWithConstrainedInputHaveValidationExceptionAttached(model: LogMessage( Level.SEVERE, """ - Operation ${it.shape.id} takes in input that is constrained - (https://awslabs.github.io/smithy/2.0/spec/constraint-traits.html), and as such can fail with a validation + Operation ${it.shape.id} takes in input that is constrained + (https://awslabs.github.io/smithy/2.0/spec/constraint-traits.html), and as such can fail with a validation exception. You must model this behavior in the operation shape in your model file. """.trimIndent().replace("\n", "") + """ - - ```smithy - use smithy.framework#ValidationException - - operation ${it.shape.id.name} { - ... - errors: [..., ValidationException] // <-- Add this. - } - ``` + + ```smithy + use smithy.framework#ValidationException + + operation ${it.shape.id.name} { + ... + errors: [..., ValidationException] // <-- Add this. + } + ``` """.trimIndent(), ) } @@ -214,17 +208,7 @@ fun validateUnsupportedConstraints(model: Model, service: ServiceShape, codegenC .map { UnsupportedLengthTraitOnCollectionOrOnBlobShape(it, it.expectTrait()) } .toSet() - // 5. Pattern trait on string shapes is used. It has not been implemented yet. - // TODO(https://github.com/awslabs/smithy-rs/issues/1401) - val unsupportedPatternTraitOnStringShapeSet = walker - .walkShapes(service) - .asSequence() - .filterIsInstance() - .filterMapShapesToTraits(setOf(PatternTrait::class.java)) - .map { (shape, patternTrait) -> UnsupportedPatternTraitOnStringShape(shape, patternTrait as PatternTrait) } - .toSet() - - // 6. Range trait on any shape is used. It has not been implemented yet. + // 5. Range trait on any shape is used. It has not been implemented yet. // TODO(https://github.com/awslabs/smithy-rs/issues/1401) val unsupportedRangeTraitOnShapeSet = walker .walkShapes(service) @@ -247,7 +231,6 @@ fun validateUnsupportedConstraints(model: Model, service: ServiceShape, codegenC unsupportedLengthTraitOnStreamingBlobShapeSet.map { it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints) } + unsupportedConstraintOnShapeReachableViaAnEventStreamSet.map { it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints) } + unsupportedLengthTraitOnCollectionOrOnBlobShapeSet.map { it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints) } + - unsupportedPatternTraitOnStringShapeSet.map { it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints) } + unsupportedRangeTraitOnShapeSet.map { it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints) } + unsupportedUniqueItemsTraitOnShapeSet.map { it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints) } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGenerator.kt index a63801cce44..898f98f17da 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGenerator.kt @@ -5,26 +5,33 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators +import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.traits.LengthTrait +import software.amazon.smithy.model.traits.PatternTrait +import software.amazon.smithy.model.traits.Trait import software.amazon.smithy.rust.codegen.core.rustlang.Attribute import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustType import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.Visibility +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.docs import software.amazon.smithy.rust.codegen.core.rustlang.documentShape +import software.amazon.smithy.rust.codegen.core.rustlang.join import software.amazon.smithy.rust.codegen.core.rustlang.render import software.amazon.smithy.rust.codegen.core.rustlang.rust -import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock -import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate 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.smithy.makeMaybeConstrained -import software.amazon.smithy.rust.codegen.core.util.expectTrait +import software.amazon.smithy.rust.codegen.core.smithy.module +import software.amazon.smithy.rust.codegen.core.util.PANIC +import software.amazon.smithy.rust.codegen.core.util.orNull import software.amazon.smithy.rust.codegen.core.util.redactIfNecessary import software.amazon.smithy.rust.codegen.server.smithy.PubCrateConstraintViolationSymbolProvider +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.supportedStringConstraintTraits import software.amazon.smithy.rust.codegen.server.smithy.traits.isReachableFromOperationInput import software.amazon.smithy.rust.codegen.server.smithy.validationErrorMessage @@ -49,23 +56,47 @@ class ConstrainedStringGenerator( PubCrateConstraintViolationSymbolProvider(this) } } + private val constraintsInfo: List = + supportedStringConstraintTraits + .mapNotNull { shape.getTrait(it).orNull() } + .map(StringTraitInfo::fromTrait) + .map(StringTraitInfo::toTraitInfo) - fun render() { - val lengthTrait = shape.expectTrait() + private fun renderTryFrom(inner: String, name: String, constraintViolation: Symbol) { + writer.rustTemplate( + """ + impl $name { + #{ValidationFunctions:W} + } + """, + "ValidationFunctions" to constraintsInfo.map { it.validationFunctionDefinition(constraintViolation) }.join("\n"), + ) + + writer.rustTemplate( + """ + impl #{TryFrom}<$inner> for $name { + type Error = #{ConstraintViolation}; + + /// ${rustDocsTryFromMethod(name, inner)} + fn try_from(value: $inner) -> Result { + #{TryFromChecks:W} + Ok(Self(value)) + } + } + """, + "TryFrom" to RuntimeType.TryFrom, + "ConstraintViolation" to constraintViolation, + "TryFromChecks" to constraintsInfo.map { it.tryFromCheck }.join("\n"), + ) + } + + fun render() { val symbol = constrainedShapeSymbolProvider.toSymbol(shape) val name = symbol.name val inner = RustType.String.render() val constraintViolation = constraintViolationSymbolProvider.toSymbol(shape) - val condition = if (lengthTrait.min.isPresent && lengthTrait.max.isPresent) { - "(${lengthTrait.min.get()}..=${lengthTrait.max.get()}).contains(&length)" - } else if (lengthTrait.min.isPresent) { - "${lengthTrait.min.get()} <= length" - } else { - "length <= ${lengthTrait.max.get()}" - } - val constrainedTypeVisibility = if (publicConstrainedTypes) { Visibility.PUBLIC } else { @@ -85,55 +116,47 @@ class ConstrainedStringGenerator( if (constrainedTypeVisibility == Visibility.PUBCRATE) { Attribute.AllowUnused.render(writer) } - writer.rustTemplate( + writer.rust( """ impl $name { /// Extracts a string slice containing the entire underlying `String`. pub fn as_str(&self) -> &str { &self.0 } - + /// ${rustDocsInnerMethod(inner)} pub fn inner(&self) -> &$inner { &self.0 } - + /// ${rustDocsIntoInnerMethod(inner)} pub fn into_inner(self) -> $inner { self.0 } - } - + }""", + ) + + renderTryFrom(inner, name, constraintViolation) + + writer.rustTemplate( + """ impl #{ConstrainedTrait} for $name { type Unconstrained = $inner; } - + impl #{From}<$inner> for #{MaybeConstrained} { fn from(value: $inner) -> Self { Self::Unconstrained(value) } } - + impl #{Display} for $name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { ${shape.redactIfNecessary(model, "self.0")}.fmt(f) } } - - impl #{TryFrom}<$inner> for $name { - type Error = #{ConstraintViolation}; - - /// ${rustDocsTryFromMethod(name, inner)} - fn try_from(value: $inner) -> Result { - let length = value.chars().count(); - if $condition { - Ok(Self(value)) - } else { - Err(#{ConstraintViolation}::Length(length)) - } - } - } - + + impl #{From}<$name> for $inner { fn from(value: $name) -> Self { value.into_inner() @@ -145,39 +168,166 @@ class ConstrainedStringGenerator( "MaybeConstrained" to symbol.makeMaybeConstrained(), "Display" to RuntimeType.Display, "From" to RuntimeType.From, - "TryFrom" to RuntimeType.TryFrom, ) - val constraintViolationModuleName = constraintViolation.namespace.split(constraintViolation.namespaceDelimiter).last() - writer.withModule(RustModule(constraintViolationModuleName, RustMetadata(visibility = constrainedTypeVisibility))) { + writer.withInlineModule(constraintViolation.module()) { + renderConstraintViolationEnum(this, shape, constraintViolation) + } + } + + private fun renderConstraintViolationEnum(writer: RustWriter, shape: StringShape, constraintViolation: Symbol) { + writer.rustTemplate( + """ + ##[derive(Debug, PartialEq)] + pub enum ${constraintViolation.name} { + #{Variants:W} + } + """, + "Variants" to constraintsInfo.map { it.constraintViolationVariant }.join(",\n"), + ) + + if (shape.isReachableFromOperationInput()) { + writer.rustTemplate( + """ + impl ${constraintViolation.name} { + pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { + match self { + #{ValidationExceptionFields:W} + } + } + } + """, + "String" to RuntimeType.String, + "ValidationExceptionFields" to constraintsInfo.map { it.asValidationExceptionField }.join("\n"), + ) + } + } +} +private data class Length(val lengthTrait: LengthTrait) : StringTraitInfo() { + override fun toTraitInfo(): TraitInfo { + return TraitInfo( + { rust("Self::check_length(&value)?;") }, + { + docs("Error when a string doesn't satisfy its `@length` requirements.") + rust("Length(usize)") + }, + { + rust( + """ + Self::Length(length) => crate::model::ValidationExceptionField { + message: format!("${lengthTrait.validationErrorMessage()}", length, &path), + path, + }, + """, + ) + }, + this::renderValidationFunction, + ) + } + + /** + * Renders a `check_length` function to validate the string matches the + * required length indicated by the `@length` trait. + */ + private fun renderValidationFunction(constraintViolation: Symbol): Writable { + return { + val condition = if (lengthTrait.min.isPresent && lengthTrait.max.isPresent) { + "(${lengthTrait.min.get()}..=${lengthTrait.max.get()}).contains(&length)" + } else if (lengthTrait.min.isPresent) { + "${lengthTrait.min.get()} <= length" + } else { + "length <= ${lengthTrait.max.get()}" + } + rust( """ - ##[derive(Debug, PartialEq)] - pub enum ${constraintViolation.name} { - Length(usize), + fn check_length(string: &str) -> Result<(), $constraintViolation> { + let length = string.chars().count(); + + if $condition { + Ok(()) + } else { + Err($constraintViolation::Length(length)) + } } """, ) + } + } +} - if (shape.isReachableFromOperationInput()) { - rustBlock("impl ${constraintViolation.name}") { - rustBlockTemplate( - "pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField", - "String" to RuntimeType.String, - ) { - rustBlock("match self") { - rust( - """ - Self::Length(length) => crate::model::ValidationExceptionField { - message: format!("${lengthTrait.validationErrorMessage()}", length, &path), - path, - }, - """, - ) - } +private data class Pattern(val patternTrait: PatternTrait) : StringTraitInfo() { + override fun toTraitInfo(): TraitInfo { + val pattern = patternTrait.pattern + + return TraitInfo( + { rust("let value = Self::check_pattern(value)?;") }, + { + docs("Error when a string doesn't satisfy its `@pattern`.") + docs("Contains the String that failed the pattern.") + rust("Pattern(String)") + }, + { + rust( + """ + Self::Pattern(string) => crate::model::ValidationExceptionField { + message: format!("${patternTrait.validationErrorMessage()}", &string, &path, r##"$pattern"##), + path + }, + """, + ) + }, + this::renderValidationFunction, + ) + } + + /** + * Renders a `check_pattern` function to validate the string matches the + * supplied regex in the `@pattern` trait. + */ + private fun renderValidationFunction(constraintViolation: Symbol): Writable { + val pattern = patternTrait.pattern + val errorMessageForUnsupportedRegex = """The regular expression $pattern is not supported by the `regex` crate; feel free to file an issue under https://github.com/awslabs/smithy-rs/issues for support""" + + return { + rustTemplate( + """ + fn check_pattern(string: String) -> Result { + let regex = Self::compile_regex(); + + if regex.is_match(&string) { + Ok(string) + } else { + Err($constraintViolation::Pattern(string)) } } - } + + pub fn compile_regex() -> &'static #{Regex}::Regex { + static REGEX: #{OnceCell}::sync::Lazy<#{Regex}::Regex> = #{OnceCell}::sync::Lazy::new(|| #{Regex}::Regex::new(r##"$pattern"##).expect(r##"$errorMessageForUnsupportedRegex"##)); + + ®EX + } + """, + "Regex" to ServerCargoDependency.Regex.toType(), + "OnceCell" to ServerCargoDependency.OnceCell.toType(), + ) } } } + +private sealed class StringTraitInfo { + companion object { + fun fromTrait(trait: Trait): StringTraitInfo = + when (trait) { + is PatternTrait -> { + Pattern(trait) + } + is LengthTrait -> { + Length(trait) + } + else -> PANIC("StringTraitInfo.fromTrait called with unsupported trait $trait") + } + } + + abstract fun toTraitInfo(): TraitInfo +} diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/DocHandlerGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/DocHandlerGenerator.kt index 6407c24f1b4..d9a052b2802 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/DocHandlerGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/DocHandlerGenerator.kt @@ -12,9 +12,9 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rust 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.CodegenTarget -import software.amazon.smithy.rust.codegen.core.smithy.Errors -import software.amazon.smithy.rust.codegen.core.smithy.Inputs -import software.amazon.smithy.rust.codegen.core.smithy.Outputs +import software.amazon.smithy.rust.codegen.core.smithy.ErrorsModule +import software.amazon.smithy.rust.codegen.core.smithy.InputsModule +import software.amazon.smithy.rust.codegen.core.smithy.OutputsModule import software.amazon.smithy.rust.codegen.core.smithy.generators.error.errorSymbol import software.amazon.smithy.rust.codegen.core.util.inputShape import software.amazon.smithy.rust.codegen.core.util.outputShape @@ -23,7 +23,7 @@ import software.amazon.smithy.rust.codegen.core.util.toSnakeCase /** Generates a stub for use within documentation. */ -class DocHandlerGenerator(private val operation: OperationShape, private val commentToken: String = "//", private val codegenContext: CodegenContext) { +class DocHandlerGenerator(private val operation: OperationShape, private val commentToken: String = "//", codegenContext: CodegenContext) { private val model = codegenContext.model private val symbolProvider = codegenContext.symbolProvider private val crateName = codegenContext.settings.moduleName.toSnakeCase() @@ -44,12 +44,12 @@ class DocHandlerGenerator(private val operation: OperationShape, private val com return writable { if (!errors.isEmpty()) { - rust("$commentToken ## use $crateName::${Errors.namespace}::${errorSymbol.name};") + rust("$commentToken ## use $crateName::${ErrorsModule.name}::${errorSymbol.name};") } rust( """ - $commentToken ## use $crateName::${Inputs.namespace}::${inputSymbol.name}; - $commentToken ## use $crateName::${Outputs.namespace}::${outputSymbol.name}; + $commentToken ## use $crateName::${InputsModule.name}::${inputSymbol.name}; + $commentToken ## use $crateName::${OutputsModule.name}::${outputSymbol.name}; $commentToken async fn handler(input: ${inputSymbol.name}) -> $outputT { $commentToken todo!() $commentToken } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/MapConstraintViolationGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/MapConstraintViolationGenerator.kt index 684d83322fd..cfcf5a53e1e 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/MapConstraintViolationGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/MapConstraintViolationGenerator.kt @@ -8,8 +8,6 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import software.amazon.smithy.model.shapes.MapShape import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.traits.LengthTrait -import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.Visibility import software.amazon.smithy.rust.codegen.core.rustlang.rust @@ -17,6 +15,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate 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.smithy.module import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.server.smithy.PubCrateConstraintViolationSymbolProvider @@ -63,12 +62,7 @@ class MapConstraintViolationGenerator( } else { Visibility.PUBCRATE } - modelsModuleWriter.withModule( - RustModule( - constraintViolationSymbol.namespace.split(constraintViolationSymbol.namespaceDelimiter).last(), - RustMetadata(visibility = constraintViolationVisibility), - ), - ) { + modelsModuleWriter.withInlineModule(constraintViolationSymbol.module()) { // TODO(https://github.com/awslabs/smithy-rs/issues/1401) We should really have two `ConstraintViolation` // types here. One will just have variants for each constraint trait on the map shape, for use by the user. // The other one will have variants if the shape's key or value is directly or transitively constrained, diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/PubCrateConstrainedCollectionGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/PubCrateConstrainedCollectionGenerator.kt index b789c2166df..66380dd6383 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/PubCrateConstrainedCollectionGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/PubCrateConstrainedCollectionGenerator.kt @@ -7,12 +7,10 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import software.amazon.smithy.model.shapes.CollectionShape import software.amazon.smithy.model.shapes.MapShape -import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter -import software.amazon.smithy.rust.codegen.core.rustlang.Visibility 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.smithy.module import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.canReachConstrainedShape import software.amazon.smithy.rust.codegen.server.smithy.isDirectlyConstrained @@ -54,7 +52,6 @@ class PubCrateConstrainedCollectionGenerator( val constrainedSymbol = pubCrateConstrainedShapeSymbolProvider.toSymbol(shape) val unconstrainedSymbol = unconstrainedShapeSymbolProvider.toSymbol(shape) - val moduleName = constrainedSymbol.namespace.split(constrainedSymbol.namespaceDelimiter).last() val name = constrainedSymbol.name val innerShape = model.expectShape(shape.member.target) val innerConstrainedSymbol = if (innerShape.isTransitivelyButNotDirectlyConstrained(model, symbolProvider)) { @@ -71,7 +68,7 @@ class PubCrateConstrainedCollectionGenerator( "From" to RuntimeType.From, ) - writer.withModule(RustModule(moduleName, RustMetadata(visibility = Visibility.PUBCRATE))) { + writer.withInlineModule(constrainedSymbol.module()) { rustTemplate( """ ##[derive(Debug, Clone)] @@ -105,38 +102,45 @@ class PubCrateConstrainedCollectionGenerator( """ impl #{From}<#{Symbol}> for $name { fn from(v: #{Symbol}) -> Self { - ${ if (innerNeedsConstraining) { + ${ + if (innerNeedsConstraining) { "Self(v.into_iter().map(|item| item.into()).collect())" } else { "Self(v)" - } } + } + } } } impl #{From}<$name> for #{Symbol} { fn from(v: $name) -> Self { - ${ if (innerNeedsConstraining) { + ${ + if (innerNeedsConstraining) { "v.0.into_iter().map(|item| item.into()).collect()" } else { "v.0" - } } + } + } } } """, *codegenScope, ) } else { - val innerNeedsConversion = innerShape.typeNameContainsNonPublicType(model, symbolProvider, publicConstrainedTypes) + val innerNeedsConversion = + innerShape.typeNameContainsNonPublicType(model, symbolProvider, publicConstrainedTypes) rustTemplate( """ impl #{From}<$name> for #{Symbol} { fn from(v: $name) -> Self { - ${ if (innerNeedsConversion) { + ${ + if (innerNeedsConversion) { "v.0.into_iter().map(|item| item.into()).collect()" } else { "v.0" - } } + } + } } } """, diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/PubCrateConstrainedMapGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/PubCrateConstrainedMapGenerator.kt index 591b11b7ed0..d11bcad6b8d 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/PubCrateConstrainedMapGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/PubCrateConstrainedMapGenerator.kt @@ -8,12 +8,10 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import software.amazon.smithy.model.shapes.CollectionShape import software.amazon.smithy.model.shapes.MapShape import software.amazon.smithy.model.shapes.StringShape -import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter -import software.amazon.smithy.rust.codegen.core.rustlang.Visibility 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.smithy.module import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.canReachConstrainedShape import software.amazon.smithy.rust.codegen.server.smithy.isDirectlyConstrained @@ -52,7 +50,6 @@ class PubCrateConstrainedMapGenerator( val symbol = symbolProvider.toSymbol(shape) val unconstrainedSymbol = unconstrainedShapeSymbolProvider.toSymbol(shape) val constrainedSymbol = pubCrateConstrainedShapeSymbolProvider.toSymbol(shape) - val moduleName = constrainedSymbol.namespace.split(constrainedSymbol.namespaceDelimiter).last() val name = constrainedSymbol.name val keyShape = model.expectShape(shape.key.target, StringShape::class.java) val valueShape = model.expectShape(shape.value.target) @@ -72,7 +69,7 @@ class PubCrateConstrainedMapGenerator( "From" to RuntimeType.From, ) - writer.withModule(RustModule(moduleName, RustMetadata(visibility = Visibility.PUBCRATE))) { + writer.withInlineModule(constrainedSymbol.module()) { rustTemplate( """ ##[derive(Debug, Clone)] diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt index 6f319f6719d..9923cc3c7de 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt @@ -12,8 +12,6 @@ import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.rust.codegen.core.rustlang.Attribute -import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustType import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.Visibility @@ -38,6 +36,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.makeMaybeConstrained import software.amazon.smithy.rust.codegen.core.smithy.makeOptional import software.amazon.smithy.rust.codegen.core.smithy.makeRustBoxed import software.amazon.smithy.rust.codegen.core.smithy.mapRustType +import software.amazon.smithy.rust.codegen.core.smithy.module import software.amazon.smithy.rust.codegen.core.smithy.rustType import software.amazon.smithy.rust.codegen.core.smithy.traits.SyntheticInputTrait import software.amazon.smithy.rust.codegen.core.util.hasTrait @@ -120,7 +119,6 @@ class ServerBuilderGenerator( private val members: List = shape.allMembers.values.toList() private val structureSymbol = symbolProvider.toSymbol(shape) private val builderSymbol = shape.serverBuilderSymbol(codegenContext) - private val moduleName = builderSymbol.namespace.split(builderSymbol.namespaceDelimiter).last() private val isBuilderFallible = hasFallibleBuilder(shape, model, symbolProvider, takeInUnconstrainedTypes) private val serverBuilderConstraintViolations = ServerBuilderConstraintViolations(codegenContext, shape, takeInUnconstrainedTypes) @@ -135,7 +133,7 @@ class ServerBuilderGenerator( fun render(writer: RustWriter) { writer.docs("See #D.", structureSymbol) - writer.withModule(RustModule(moduleName, RustMetadata(visibility = visibility))) { + writer.withInlineModule(builderSymbol.module()) { renderBuilder(this) } } @@ -396,7 +394,7 @@ class ServerBuilderGenerator( """ impl #{TryFrom} for #{Structure} { type Error = ConstraintViolation; - + fn try_from(builder: Builder) -> Result { builder.build() } @@ -487,7 +485,7 @@ class ServerBuilderGenerator( #{MaybeConstrained}::Constrained(x) => Ok(Box::new(x)), #{MaybeConstrained}::Unconstrained(x) => Ok(Box::new(x.try_into()?)), }) - .map(|res| + .map(|res| res${ if (constrainedTypeHoldsFinalType(member)) "" else ".map(|v| v.into())" } .map_err(|err| ConstraintViolation::${constraintViolation.name()}(Box::new(err))) ) @@ -502,7 +500,7 @@ class ServerBuilderGenerator( #{MaybeConstrained}::Constrained(x) => Ok(x), #{MaybeConstrained}::Unconstrained(x) => x.try_into(), }) - .map(|res| + .map(|res| res${if (constrainedTypeHoldsFinalType(member)) "" else ".map(|v| v.into())"} .map_err(ConstraintViolation::${constraintViolation.name()}) ) diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorWithoutPublicConstrainedTypes.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorWithoutPublicConstrainedTypes.kt index 897bdc1166a..e342d5c8cfd 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorWithoutPublicConstrainedTypes.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorWithoutPublicConstrainedTypes.kt @@ -9,7 +9,6 @@ import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.StructureShape -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.Visibility import software.amazon.smithy.rust.codegen.core.rustlang.conditionalBlock @@ -25,6 +24,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.expectRustMetadata import software.amazon.smithy.rust.codegen.core.smithy.isOptional import software.amazon.smithy.rust.codegen.core.smithy.makeOptional +import software.amazon.smithy.rust.codegen.core.smithy.module import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.ServerRuntimeType @@ -83,7 +83,7 @@ class ServerBuilderGeneratorWithoutPublicConstrainedTypes( fun render(writer: RustWriter) { writer.docs("See #D.", structureSymbol) - writer.withModule(RustModule.public(moduleName)) { + writer.withInlineModule(builderSymbol.module()) { renderBuilder(this) } } @@ -208,7 +208,7 @@ class ServerBuilderGeneratorWithoutPublicConstrainedTypes( """ impl #{TryFrom} for #{Structure} { type Error = ConstraintViolation; - + fn try_from(builder: Builder) -> Result { builder.build() } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderSymbol.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderSymbol.kt index a8ee7fd8f67..9720717383c 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderSymbol.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderSymbol.kt @@ -8,14 +8,20 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords import software.amazon.smithy.rust.codegen.core.rustlang.RustType +import software.amazon.smithy.rust.codegen.core.rustlang.Visibility +import software.amazon.smithy.rust.codegen.core.smithy.module import software.amazon.smithy.rust.codegen.core.smithy.rustType import software.amazon.smithy.rust.codegen.core.util.toSnakeCase import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext fun StructureShape.serverBuilderSymbol(codegenContext: ServerCodegenContext): Symbol = - this.serverBuilderSymbol(codegenContext.symbolProvider, !codegenContext.settings.codegenConfig.publicConstrainedTypes) + this.serverBuilderSymbol( + codegenContext.symbolProvider, + !codegenContext.settings.codegenConfig.publicConstrainedTypes, + ) fun StructureShape.serverBuilderSymbol(symbolProvider: SymbolProvider, pubCrate: Boolean): Symbol { val structureSymbol = symbolProvider.toSymbol(this) @@ -25,11 +31,15 @@ fun StructureShape.serverBuilderSymbol(symbolProvider: SymbolProvider, pubCrate: } else { "" } - val rustType = RustType.Opaque("Builder", "${structureSymbol.namespace}::$builderNamespace") + val visibility = when (pubCrate) { + true -> Visibility.PUBCRATE + false -> Visibility.PUBLIC + } + val builderModule = RustModule.new(builderNamespace, visibility, parent = structureSymbol.module(), inline = true) + val rustType = RustType.Opaque("Builder", builderModule.fullyQualifiedPath()) return Symbol.builder() .rustType(rustType) .name(rustType.name) - .namespace(rustType.namespace, "::") - .definitionFile(structureSymbol.definitionFile) + .module(builderModule) .build() } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerEnumGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerEnumGenerator.kt index 1514750cda4..893f4d14f48 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerEnumGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerEnumGenerator.kt @@ -13,6 +13,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.smithy.CodegenTarget import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.generators.EnumGenerator +import software.amazon.smithy.rust.codegen.core.smithy.module import software.amazon.smithy.rust.codegen.core.util.dq import software.amazon.smithy.rust.codegen.core.util.expectTrait import software.amazon.smithy.rust.codegen.server.smithy.PubCrateConstraintViolationSymbolProvider @@ -42,8 +43,8 @@ open class ServerEnumGenerator( ) override fun renderFromForStr() { - writer.withModule( - RustModule.public(constraintViolationSymbol.namespace.split(constraintViolationSymbol.namespaceDelimiter).last()), + writer.withInlineModule( + constraintViolationSymbol.module() as RustModule.LeafModule, ) { rustTemplate( """ diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationRegistryGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationRegistryGenerator.kt index e6e1c1ac400..18dfbd869d3 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationRegistryGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationRegistryGenerator.kt @@ -22,9 +22,9 @@ import software.amazon.smithy.rust.codegen.core.rustlang.withBlockTemplate 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.CodegenTarget -import software.amazon.smithy.rust.codegen.core.smithy.Errors -import software.amazon.smithy.rust.codegen.core.smithy.Inputs -import software.amazon.smithy.rust.codegen.core.smithy.Outputs +import software.amazon.smithy.rust.codegen.core.smithy.ErrorsModule +import software.amazon.smithy.rust.codegen.core.smithy.InputsModule +import software.amazon.smithy.rust.codegen.core.smithy.OutputsModule import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.generators.error.errorSymbol import software.amazon.smithy.rust.codegen.core.util.getTrait @@ -86,9 +86,9 @@ class ServerOperationRegistryGenerator( private fun renderOperationRegistryRustDocs(writer: RustWriter) { val inputOutputErrorsImport = if (operations.any { it.errors.isNotEmpty() }) { - "/// use ${crateName.toSnakeCase()}::{${Inputs.namespace}, ${Outputs.namespace}, ${Errors.namespace}};" + "/// use ${crateName.toSnakeCase()}::{${InputsModule.name}, ${OutputsModule.name}, ${ErrorsModule.name}};" } else { - "/// use ${crateName.toSnakeCase()}::{${Inputs.namespace}, ${Outputs.namespace}};" + "/// use ${crateName.toSnakeCase()}::{${InputsModule.name}, ${OutputsModule.name}};" } writer.rustTemplate( @@ -379,12 +379,13 @@ ${operationImplementationStubs(operations)} val outputSymbol = symbolProvider.toSymbol(outputShape(model)) val errorSymbol = errorSymbol(model, symbolProvider, CodegenTarget.SERVER) - val inputT = "${Inputs.namespace}::${inputSymbol.name}" - val t = "${Outputs.namespace}::${outputSymbol.name}" + // using module names here to avoid generating `crate::...` since we've already added the import + val inputT = "${InputsModule.name}::${inputSymbol.name}" + val t = "${OutputsModule.name}::${outputSymbol.name}" val outputT = if (errors.isEmpty()) { t } else { - val e = "${Errors.namespace}::${errorSymbol.name}" + val e = "${ErrorsModule.name}::${errorSymbol.name}" "Result<$t, $e>" } 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 b5b471b4961..e4082484836 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 @@ -75,7 +75,7 @@ open class ServerServiceGenerator( // TODO(https://github.com/awslabs/smithy-rs/issues/1707): Remove, this is temporary. rustCrate.withModule( - RustModule( + RustModule.LeafModule( "operation_shape", RustMetadata( visibility = Visibility.PUBLIC, @@ -90,7 +90,7 @@ open class ServerServiceGenerator( // TODO(https://github.com/awslabs/smithy-rs/issues/1707): Remove, this is temporary. rustCrate.withModule( - RustModule("service", RustMetadata(visibility = Visibility.PUBLIC, additionalAttributes = listOf(Attribute.DocHidden)), null), + RustModule.LeafModule("service", RustMetadata(visibility = Visibility.PUBLIC, additionalAttributes = listOf(Attribute.DocHidden)), null), ) { ServerServiceGeneratorV2( codegenContext, diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/TraitInfo.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/TraitInfo.kt new file mode 100644 index 00000000000..3390382ba16 --- /dev/null +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/TraitInfo.kt @@ -0,0 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy.generators + +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rust + +/** + * Information needed to render a constraint trait as Rust code. + */ +data class TraitInfo( + val tryFromCheck: Writable, + val constraintViolationVariant: Writable, + val asValidationExceptionField: Writable, + val validationFunctionDefinition: (constraintViolation: Symbol) -> Writable, +) diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGenerator.kt index 602cbb7aafc..33a33ecff04 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedCollectionGenerator.kt @@ -6,13 +6,11 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import software.amazon.smithy.model.shapes.CollectionShape -import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter -import software.amazon.smithy.rust.codegen.core.rustlang.Visibility 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.smithy.makeMaybeConstrained +import software.amazon.smithy.rust.codegen.core.smithy.module import software.amazon.smithy.rust.codegen.server.smithy.PubCrateConstraintViolationSymbolProvider import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.canReachConstrainedShape @@ -53,7 +51,6 @@ class UnconstrainedCollectionGenerator( check(shape.canReachConstrainedShape(model, symbolProvider)) val symbol = unconstrainedShapeSymbolProvider.toSymbol(shape) - val module = symbol.namespace.split(symbol.namespaceDelimiter).last() val name = symbol.name val innerShape = model.expectShape(shape.member.target) val innerUnconstrainedSymbol = unconstrainedShapeSymbolProvider.toSymbol(innerShape) @@ -62,21 +59,21 @@ class UnconstrainedCollectionGenerator( val constraintViolationName = constraintViolationSymbol.name val innerConstraintViolationSymbol = constraintViolationSymbolProvider.toSymbol(innerShape) - unconstrainedModuleWriter.withModule(RustModule(module, RustMetadata(visibility = Visibility.PUBCRATE))) { + unconstrainedModuleWriter.withInlineModule(symbol.module()) { rustTemplate( """ ##[derive(Debug, Clone)] pub(crate) struct $name(pub(crate) std::vec::Vec<#{InnerUnconstrainedSymbol}>); - + impl From<$name> for #{MaybeConstrained} { fn from(value: $name) -> Self { Self::Unconstrained(value) } } - + impl #{TryFrom}<$name> for #{ConstrainedSymbol} { type Error = #{ConstraintViolationSymbol}; - + fn try_from(value: $name) -> Result { let res: Result<_, (usize, #{InnerConstraintViolationSymbol})> = value .0 @@ -84,7 +81,7 @@ class UnconstrainedCollectionGenerator( .enumerate() .map(|(idx, inner)| inner.try_into().map_err(|inner_violation| (idx, inner_violation))) .collect(); - res.map(Self) + res.map(Self) .map_err(|(idx, inner_violation)| #{ConstraintViolationSymbol}(idx, inner_violation)) } } @@ -98,17 +95,7 @@ class UnconstrainedCollectionGenerator( ) } - val constraintViolationVisibility = if (publicConstrainedTypes) { - Visibility.PUBLIC - } else { - Visibility.PUBCRATE - } - modelsModuleWriter.withModule( - RustModule( - constraintViolationSymbol.namespace.split(constraintViolationSymbol.namespaceDelimiter).last(), - RustMetadata(visibility = constraintViolationVisibility), - ), - ) { + modelsModuleWriter.withInlineModule(constraintViolationSymbol.module()) { // The first component of the tuple struct is the index in the collection where the first constraint // violation was found. rustTemplate( diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGenerator.kt index 4d47eb62290..3d1f2a3898d 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedMapGenerator.kt @@ -7,16 +7,14 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import software.amazon.smithy.model.shapes.MapShape import software.amazon.smithy.model.shapes.StringShape -import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter -import software.amazon.smithy.rust.codegen.core.rustlang.Visibility 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.rustBlock 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.makeMaybeConstrained +import software.amazon.smithy.rust.codegen.core.smithy.module import software.amazon.smithy.rust.codegen.server.smithy.PubCrateConstraintViolationSymbolProvider import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.canReachConstrainedShape @@ -66,22 +64,21 @@ class UnconstrainedMapGenerator( fun render() { check(shape.canReachConstrainedShape(model, symbolProvider)) - val module = symbol.namespace.split(symbol.namespaceDelimiter).last() val keySymbol = unconstrainedShapeSymbolProvider.toSymbol(keyShape) val valueSymbol = unconstrainedShapeSymbolProvider.toSymbol(valueShape) - unconstrainedModuleWriter.withModule(RustModule(module, RustMetadata(visibility = Visibility.PUBCRATE))) { + unconstrainedModuleWriter.withInlineModule(symbol.module()) { rustTemplate( """ ##[derive(Debug, Clone)] pub(crate) struct $name(pub(crate) std::collections::HashMap<#{KeySymbol}, #{ValueSymbol}>); - + impl From<$name> for #{MaybeConstrained} { fn from(value: $name) -> Self { Self::Unconstrained(value) } } - + """, "KeySymbol" to keySymbol, "ValueSymbol" to valueSymbol, @@ -185,7 +182,7 @@ class UnconstrainedMapGenerator( // ``` rustTemplate( """ - let hm: std::collections::HashMap<#{KeySymbol}, #{ValueSymbol}> = + let hm: std::collections::HashMap<#{KeySymbol}, #{ValueSymbol}> = hm.into_iter().map(|(k, v)| (k, v.into())).collect(); """, "KeySymbol" to symbolProvider.toSymbol(keyShape), diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedUnionGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedUnionGenerator.kt index dd470daf9e2..022fc92650b 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedUnionGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedUnionGenerator.kt @@ -10,8 +10,6 @@ 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.Attribute -import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.Visibility import software.amazon.smithy.rust.codegen.core.rustlang.rust @@ -24,6 +22,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.makeMaybeConstrained import software.amazon.smithy.rust.codegen.core.smithy.makeRustBoxed +import software.amazon.smithy.rust.codegen.core.smithy.module import software.amazon.smithy.rust.codegen.core.smithy.traits.RustBoxTrait import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.core.util.letIf @@ -71,13 +70,12 @@ class UnconstrainedUnionGenerator( fun render() { check(shape.canReachConstrainedShape(model, symbolProvider)) - val moduleName = symbol.namespace.split(symbol.namespaceDelimiter).last() val name = symbol.name val constrainedSymbol = pubCrateConstrainedShapeSymbolProvider.toSymbol(shape) val constraintViolationSymbol = constraintViolationSymbolProvider.toSymbol(shape) val constraintViolationName = constraintViolationSymbol.name - unconstrainedModuleWriter.withModule(RustModule(moduleName, RustMetadata(visibility = Visibility.PUBCRATE))) { + unconstrainedModuleWriter.withInlineModule(symbol.module()) { rustBlock( """ ##[allow(clippy::enum_variant_names)] @@ -97,7 +95,7 @@ class UnconstrainedUnionGenerator( """ impl #{TryFrom}<$name> for #{ConstrainedSymbol} { type Error = #{ConstraintViolationSymbol}; - + fn try_from(value: $name) -> Result { #{body:W} } @@ -115,7 +113,7 @@ class UnconstrainedUnionGenerator( impl #{ConstrainedTrait} for #{ConstrainedSymbol} { type Unconstrained = #{UnconstrainedSymbol}; } - + impl From<#{UnconstrainedSymbol}> for #{MaybeConstrained} { fn from(value: #{UnconstrainedSymbol}) -> Self { Self::Unconstrained(value) @@ -133,14 +131,11 @@ class UnconstrainedUnionGenerator( } else { Visibility.PUBCRATE } - modelsModuleWriter.withModule( - RustModule( - constraintViolationSymbol.namespace.split(constraintViolationSymbol.namespaceDelimiter).last(), - RustMetadata(visibility = constraintViolationVisibility), - ), + modelsModuleWriter.withInlineModule( + constraintViolationSymbol.module(), ) { Attribute.Derives(setOf(RuntimeType.Debug, RuntimeType.PartialEq)).render(this) - rustBlock("pub${ if (constraintViolationVisibility == Visibility.PUBCRATE) " (crate)" else "" } enum $constraintViolationName") { + rustBlock("pub${if (constraintViolationVisibility == Visibility.PUBCRATE) " (crate)" else ""} enum $constraintViolationName") { constraintViolations().forEach { renderConstraintViolation(this, it) } } 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 cf981edb73c..fdc1078a4f4 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 @@ -18,6 +18,7 @@ import software.amazon.smithy.model.traits.ErrorTrait import software.amazon.smithy.protocoltests.traits.AppliesTo import software.amazon.smithy.protocoltests.traits.HttpMalformedRequestTestCase import software.amazon.smithy.protocoltests.traits.HttpMalformedRequestTestsTrait +import software.amazon.smithy.protocoltests.traits.HttpMalformedResponseBodyDefinition import software.amazon.smithy.protocoltests.traits.HttpMalformedResponseDefinition import software.amazon.smithy.protocoltests.traits.HttpRequestTestCase import software.amazon.smithy.protocoltests.traits.HttpRequestTestsTrait @@ -175,7 +176,7 @@ class ServerProtocolTestGenerator( } } - val module = RustModule( + val module = RustModule.LeafModule( PROTOCOL_TEST_HELPER_MODULE_NAME, RustMetadata( additionalAttributes = listOf( @@ -184,9 +185,10 @@ class ServerProtocolTestGenerator( ), visibility = Visibility.PUBCRATE, ), + inline = true, ) - writer.withModule(module) { + writer.withInlineModule(module) { rustTemplate( """ use #{Tower}::Service as _; @@ -252,7 +254,7 @@ class ServerProtocolTestGenerator( if (allTests.isNotEmpty()) { val operationName = operationSymbol.name - val module = RustModule( + val module = RustModule.LeafModule( "server_${operationName.toSnakeCase()}_test", RustMetadata( additionalAttributes = listOf( @@ -261,8 +263,9 @@ class ServerProtocolTestGenerator( ), visibility = Visibility.PRIVATE, ), + inline = true, ) - writer.withModule(module) { + writer.withInlineModule(module) { renderAllTestCases(operationShape, allTests) } } @@ -339,8 +342,13 @@ class ServerProtocolTestGenerator( } is TestCase.MalformedRequestTest -> { - // We haven't found any broken `HttpMalformedRequestTest`s yet. - it + val howToFixIt = BrokenMalformedRequestTests[Pair(codegenContext.serviceShape.id.toString(), it.id)] + if (howToFixIt == null) { + it + } else { + val fixed = howToFixIt(it.testCase) + TestCase.MalformedRequestTest(fixed) + } } } } @@ -969,17 +977,6 @@ class ServerProtocolTestGenerator( FailingTest(RestJsonValidation, "RestJsonMalformedPatternStringOverride_case1", TestType.MalformedRequest), FailingTest(RestJsonValidation, "RestJsonMalformedPatternUnionOverride_case0", TestType.MalformedRequest), FailingTest(RestJsonValidation, "RestJsonMalformedPatternUnionOverride_case1", TestType.MalformedRequest), - FailingTest(RestJsonValidation, "RestJsonMalformedPatternList_case0", TestType.MalformedRequest), - FailingTest(RestJsonValidation, "RestJsonMalformedPatternList_case1", TestType.MalformedRequest), - FailingTest(RestJsonValidation, "RestJsonMalformedPatternMapKey_case0", TestType.MalformedRequest), - FailingTest(RestJsonValidation, "RestJsonMalformedPatternMapKey_case1", TestType.MalformedRequest), - FailingTest(RestJsonValidation, "RestJsonMalformedPatternMapValue_case0", TestType.MalformedRequest), - FailingTest(RestJsonValidation, "RestJsonMalformedPatternMapValue_case1", TestType.MalformedRequest), - FailingTest(RestJsonValidation, "RestJsonMalformedPatternReDOSString", TestType.MalformedRequest), - FailingTest(RestJsonValidation, "RestJsonMalformedPatternString_case0", TestType.MalformedRequest), - FailingTest(RestJsonValidation, "RestJsonMalformedPatternString_case1", TestType.MalformedRequest), - FailingTest(RestJsonValidation, "RestJsonMalformedPatternUnion_case0", TestType.MalformedRequest), - FailingTest(RestJsonValidation, "RestJsonMalformedPatternUnion_case1", TestType.MalformedRequest), FailingTest(RestJsonValidation, "RestJsonMalformedRangeByteOverride_case0", TestType.MalformedRequest), FailingTest(RestJsonValidation, "RestJsonMalformedRangeByteOverride_case1", TestType.MalformedRequest), FailingTest(RestJsonValidation, "RestJsonMalformedRangeFloatOverride_case0", TestType.MalformedRequest), @@ -1176,6 +1173,27 @@ class ServerProtocolTestGenerator( private fun fixRestJsonComplexErrorWithNoMessage(testCase: HttpResponseTestCase): HttpResponseTestCase = testCase.toBuilder().putHeader("X-Amzn-Errortype", "aws.protocoltests.restjson#ComplexError").build() + // TODO(https://github.com/awslabs/smithy/issues/1506) + private fun fixRestJsonMalformedPatternReDOSString(testCase: HttpMalformedRequestTestCase): HttpMalformedRequestTestCase { + val brokenResponse = testCase.response + val brokenBody = brokenResponse.body.get() + val fixedBody = HttpMalformedResponseBodyDefinition.builder() + .mediaType(brokenBody.mediaType) + .contents( + """ + { + "message" : "1 validation error detected. Value 000000000000000000000000000000000000000000000000000000000000000000000000000000000000! at '/evilString' failed to satisfy constraint: Member must satisfy regular expression pattern: ^([0-9]+)+${'$'}", + "fieldList" : [{"message": "Value 000000000000000000000000000000000000000000000000000000000000000000000000000000000000! at '/evilString' failed to satisfy constraint: Member must satisfy regular expression pattern: ^([0-9]+)+${'$'}", "path": "/evilString"}] + } + """.trimIndent(), + ) + .build() + + return testCase.toBuilder() + .response(brokenResponse.toBuilder().body(fixedBody).build()) + .build() + } + // These are tests whose definitions in the `awslabs/smithy` repository are wrong. // This is because they have not been written from a server perspective, and as such the expected `params` field is incomplete. // TODO(https://github.com/awslabs/smithy-rs/issues/1288): Contribute a PR to fix them upstream. @@ -1258,5 +1276,11 @@ class ServerProtocolTestGenerator( Pair(RestJson, "RestJsonEmptyComplexErrorWithNoMessage") to ::fixRestJsonEmptyComplexErrorWithNoMessage, Pair(RestJson, "RestJsonComplexErrorWithNoMessage") to ::fixRestJsonComplexErrorWithNoMessage, ) + + private val BrokenMalformedRequestTests: Map, KFunction1> = + // TODO(https://github.com/awslabs/smithy/issues/1506) + mapOf( + Pair(RestJsonValidation, "RestJsonMalformedPatternReDOSString") to ::fixRestJsonMalformedPatternReDOSString, + ) } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstraintsTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstraintsTest.kt index 80e2d93dae4..76821a90dd1 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstraintsTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ConstraintsTest.kt @@ -26,63 +26,58 @@ class ConstraintsTest { version: "123", operations: [TestOperation] } - + operation TestOperation { input: TestInputOutput, output: TestInputOutput, } - + structure TestInputOutput { map: MapA, - recursive: RecursiveShape } - + structure RecursiveShape { shape: RecursiveShape, mapB: MapB } - + @length(min: 1, max: 69) map MapA { key: String, value: MapB } - + map MapB { key: String, value: StructureA } - + @uniqueItems list ListA { member: MyString } - + @pattern("\\w+") string MyString - + @length(min: 1, max: 69) string LengthString - + structure StructureA { @range(min: 1, max: 69) int: Integer, - @required string: String } - + // This shape is not in the service closure. structure StructureB { @pattern("\\w+") patternString: String, - @required requiredString: String, - mapA: MapA, - @length(min: 1, max: 5) mapAPrecedence: MapA } @@ -94,7 +89,6 @@ class ConstraintsTest { private val mapA = model.lookup("test#MapA") private val mapB = model.lookup("test#MapB") private val listA = model.lookup("test#ListA") - private val myString = model.lookup("test#MyString") private val lengthString = model.lookup("test#LengthString") private val structA = model.lookup("test#StructureA") private val structAInt = model.lookup("test#StructureA\$int") @@ -114,7 +108,7 @@ class ConstraintsTest { @Test fun `it should not detect unsupported constrained traits as constrained`() { - listOf(structAInt, structAString, myString).forAll { + listOf(structAInt, structAString).forAll { it.isDirectlyConstrained(symbolProvider) shouldBe false } } @@ -123,9 +117,7 @@ class ConstraintsTest { fun `it should evaluate reachability of constrained shapes`() { mapA.canReachConstrainedShape(model, symbolProvider) shouldBe true structAInt.canReachConstrainedShape(model, symbolProvider) shouldBe false - - // This should be true when we start supporting the `pattern` trait on string shapes. - listA.canReachConstrainedShape(model, symbolProvider) shouldBe false + listA.canReachConstrainedShape(model, symbolProvider) shouldBe true // All of these eventually reach `StructureA`, which is constrained because one of its members is `required`. testInputOutput.canReachConstrainedShape(model, symbolProvider) shouldBe true diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitorTest.kt index bbb697d93ef..e51d5b813af 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitorTest.kt @@ -13,7 +13,6 @@ import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.generatePluginContext import software.amazon.smithy.rust.codegen.server.smithy.customizations.ServerRequiredCustomizations import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocolGenerator -import kotlin.io.path.createDirectory import kotlin.io.path.writeText class ServerCodegenVisitorTest { @@ -46,7 +45,6 @@ class ServerCodegenVisitorTest { } """.asSmithyModel(smithyVersion = "2.0") val (ctx, testDir) = generatePluginContext(model) - testDir.resolve("src").createDirectory() testDir.resolve("src/main.rs").writeText("fn main() {}") val codegenDecorator: CombinedCodegenDecorator = CombinedCodegenDecorator.fromClasspath(ctx, ServerRequiredCustomizations()) diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraintsAreNotUsedTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraintsAreNotUsedTest.kt index 78cec7408be..4c3fa0efe15 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraintsAreNotUsedTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ValidateUnsupportedConstraintsAreNotUsedTest.kt @@ -22,12 +22,12 @@ internal class ValidateUnsupportedConstraintsAreNotUsedTest { private val baseModel = """ namespace test - + service TestService { version: "123", operations: [TestOperation] } - + operation TestOperation { input: TestInputOutput, output: TestInputOutput, @@ -44,7 +44,7 @@ internal class ValidateUnsupportedConstraintsAreNotUsedTest { val model = """ $baseModel - + structure TestInputOutput { @required requiredString: String @@ -62,7 +62,7 @@ internal class ValidateUnsupportedConstraintsAreNotUsedTest { val model = """ $baseModel - + structure TestInputOutput { @length(min: 1, max: 69) lengthString: String @@ -79,7 +79,7 @@ internal class ValidateUnsupportedConstraintsAreNotUsedTest { val model = """ $baseModel - + structure TestInputOutput { @required string: String @@ -93,12 +93,12 @@ internal class ValidateUnsupportedConstraintsAreNotUsedTest { private val constraintTraitOnStreamingBlobShapeModel = """ $baseModel - + structure TestInputOutput { @required streamingBlob: StreamingBlob } - + @streaming @length(min: 69) blob StreamingBlob @@ -123,20 +123,20 @@ internal class ValidateUnsupportedConstraintsAreNotUsedTest { val model = """ $baseModel - + structure TestInputOutput { eventStream: EventStream } - + @streaming union EventStream { message: Message } - + structure Message { lengthString: LengthString } - + @length(min: 1) string LengthString """.asSmithyModel() @@ -155,17 +155,17 @@ internal class ValidateUnsupportedConstraintsAreNotUsedTest { val model = """ $baseModel - + structure TestInputOutput { collection: LengthCollection, blob: LengthBlob } - + @length(min: 1) list LengthCollection { member: String } - + @length(min: 1) blob LengthBlob """.asSmithyModel() @@ -176,35 +176,16 @@ internal class ValidateUnsupportedConstraintsAreNotUsedTest { validationResult.messages.forSome { it.message shouldContain "The blob shape `test#LengthBlob` has the constraint trait `smithy.api#length` attached" } } - @Test - fun `it should detect when the pattern trait on string shapes is used`() { - val model = - """ - $baseModel - - structure TestInputOutput { - patternString: PatternString - } - - @pattern("^[A-Za-z]+$") - string PatternString - """.asSmithyModel() - val validationResult = validateModel(model) - - validationResult.messages shouldHaveSize 1 - validationResult.messages[0].message shouldContain "The string shape `test#PatternString` has the constraint trait `smithy.api#pattern` attached" - } - @Test fun `it should detect when the range trait is used`() { val model = """ $baseModel - + structure TestInputOutput { rangeInteger: RangeInteger } - + @range(min: 1) integer RangeInteger """.asSmithyModel() diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGeneratorTest.kt index 75db6303f7a..ddf9a53d073 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedStringGeneratorTest.kt @@ -25,7 +25,6 @@ import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCode import java.util.stream.Stream class ConstrainedStringGeneratorTest { - data class TestCase(val model: Model, val validString: String, val invalidString: String) class ConstrainedStringGeneratorTestProvider : ArgumentsProvider { @@ -44,11 +43,20 @@ class ConstrainedStringGeneratorTest { "👍👍👍", // These three emojis are three Unicode scalar values. "👍👍👍👍", ), + Triple("@pattern(\"^[a-z]+$\")", "valid", "123 invalid"), + Triple( + """ + @length(min: 3, max: 10) + @pattern("^a string$") + """, + "a string", "an invalid string", + ), + Triple("@pattern(\"123\")", "some pattern 123 in the middle", "no pattern at all"), ).map { TestCase( """ namespace test - + ${it.first} string ConstrainedString """.asSmithyModel(), @@ -116,10 +124,10 @@ class ConstrainedStringGeneratorTest { fun `type should not be constructible without using a constructor`() { val model = """ namespace test - + @length(min: 1, max: 69) string ConstrainedString - """.asSmithyModel() + """.asSmithyModel() val constrainedStringShape = model.lookup("test#ConstrainedString") val codegenContext = serverTestCodegenContext(model) @@ -136,14 +144,14 @@ class ConstrainedStringGeneratorTest { fun `Display implementation`() { val model = """ namespace test - + @length(min: 1, max: 69) string ConstrainedString - + @sensitive @length(min: 1, max: 78) string SensitiveConstrainedString - """.asSmithyModel() + """.asSmithyModel() val constrainedStringShape = model.lookup("test#ConstrainedString") val sensitiveConstrainedStringShape = model.lookup("test#SensitiveConstrainedString") diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerCombinedErrorGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerCombinedErrorGeneratorTest.kt index d414aa63fc7..74658c3a674 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerCombinedErrorGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerCombinedErrorGeneratorTest.kt @@ -7,7 +7,7 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import org.junit.jupiter.api.Test import software.amazon.smithy.model.shapes.StructureShape -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule +import software.amazon.smithy.rust.codegen.core.smithy.ErrorsModule import software.amazon.smithy.rust.codegen.core.smithy.generators.error.ServerCombinedErrorGenerator import software.amazon.smithy.rust.codegen.core.smithy.transformers.OperationNormalizer import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace @@ -52,7 +52,7 @@ class ServerCombinedErrorGeneratorTest { @Test fun `generates combined error enums`() { val project = TestWorkspace.testProject(symbolProvider) - project.withModule(RustModule.public("error")) { + project.withModule(ErrorsModule) { listOf("FooException", "ComplexError", "InvalidGreeting", "Deprecated").forEach { model.lookup("error#$it").serverRenderWithModelBuilder(model, symbolProvider, this) } 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 42774274d9d..50543d67260 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 @@ -8,8 +8,9 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import org.junit.jupiter.api.Test import software.amazon.smithy.model.shapes.ListShape import software.amazon.smithy.model.shapes.StructureShape -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule +import software.amazon.smithy.rust.codegen.core.smithy.ConstrainedModule import software.amazon.smithy.rust.codegen.core.smithy.ModelsModule +import software.amazon.smithy.rust.codegen.core.smithy.UnconstrainedModule 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 @@ -28,15 +29,15 @@ class UnconstrainedCollectionGeneratorTest { list ListA { member: ListB } - + list ListB { member: StructureC } - + structure StructureC { @required int: Integer, - + @required string: String } @@ -49,16 +50,16 @@ class UnconstrainedCollectionGeneratorTest { val project = TestWorkspace.testProject(symbolProvider) - project.withModule(RustModule.public("model")) { + project.withModule(ModelsModule) { model.lookup("test#StructureC").serverRenderWithModelBuilder(model, symbolProvider, this) } - project.withModule(RustModule.private("constrained")) { + project.withModule(ConstrainedModule) { listOf(listA, listB).forEach { PubCrateConstrainedCollectionGenerator(codegenContext, this, it).render() } } - project.withModule(RustModule.private("unconstrained")) unconstrainedModuleWriter@{ + project.withModule(UnconstrainedModule) unconstrainedModuleWriter@{ project.withModule(ModelsModule) modelsModuleWriter@{ listOf(listA, listB).forEach { UnconstrainedCollectionGenerator( @@ -72,53 +73,53 @@ class UnconstrainedCollectionGeneratorTest { this@unconstrainedModuleWriter.unitTest( name = "list_a_unconstrained_fail_to_constrain_with_first_error", test = """ - let c_builder1 = crate::model::StructureC::builder().int(69); - let c_builder2 = crate::model::StructureC::builder().string("david".to_owned()); - let list_b_unconstrained = list_b_unconstrained::ListBUnconstrained(vec![c_builder1, c_builder2]); - let list_a_unconstrained = list_a_unconstrained::ListAUnconstrained(vec![list_b_unconstrained]); - - let expected_err = - crate::model::list_a::ConstraintViolation(0, crate::model::list_b::ConstraintViolation( - 0, crate::model::structure_c::ConstraintViolation::MissingString, - )); - - assert_eq!( - expected_err, - crate::constrained::list_a_constrained::ListAConstrained::try_from(list_a_unconstrained).unwrap_err() - ); + let c_builder1 = crate::model::StructureC::builder().int(69); + let c_builder2 = crate::model::StructureC::builder().string("david".to_owned()); + let list_b_unconstrained = list_b_unconstrained::ListBUnconstrained(vec![c_builder1, c_builder2]); + let list_a_unconstrained = list_a_unconstrained::ListAUnconstrained(vec![list_b_unconstrained]); + + let expected_err = + crate::model::list_a::ConstraintViolation(0, crate::model::list_b::ConstraintViolation( + 0, crate::model::structure_c::ConstraintViolation::MissingString, + )); + + assert_eq!( + expected_err, + crate::constrained::list_a_constrained::ListAConstrained::try_from(list_a_unconstrained).unwrap_err() + ); """, ) this@unconstrainedModuleWriter.unitTest( name = "list_a_unconstrained_succeed_to_constrain", test = """ - let c_builder = crate::model::StructureC::builder().int(69).string(String::from("david")); - let list_b_unconstrained = list_b_unconstrained::ListBUnconstrained(vec![c_builder]); - let list_a_unconstrained = list_a_unconstrained::ListAUnconstrained(vec![list_b_unconstrained]); - - let expected: Vec> = vec![vec![crate::model::StructureC { - string: "david".to_owned(), - int: 69 - }]]; - let actual: Vec> = - crate::constrained::list_a_constrained::ListAConstrained::try_from(list_a_unconstrained).unwrap().into(); - - assert_eq!(expected, actual); + let c_builder = crate::model::StructureC::builder().int(69).string(String::from("david")); + let list_b_unconstrained = list_b_unconstrained::ListBUnconstrained(vec![c_builder]); + let list_a_unconstrained = list_a_unconstrained::ListAUnconstrained(vec![list_b_unconstrained]); + + let expected: Vec> = vec![vec![crate::model::StructureC { + string: "david".to_owned(), + int: 69 + }]]; + let actual: Vec> = + crate::constrained::list_a_constrained::ListAConstrained::try_from(list_a_unconstrained).unwrap().into(); + + assert_eq!(expected, actual); """, ) this@unconstrainedModuleWriter.unitTest( name = "list_a_unconstrained_converts_into_constrained", test = """ - let c_builder = crate::model::StructureC::builder(); - let list_b_unconstrained = list_b_unconstrained::ListBUnconstrained(vec![c_builder]); - let list_a_unconstrained = list_a_unconstrained::ListAUnconstrained(vec![list_b_unconstrained]); + let c_builder = crate::model::StructureC::builder(); + let list_b_unconstrained = list_b_unconstrained::ListBUnconstrained(vec![c_builder]); + let list_a_unconstrained = list_a_unconstrained::ListAUnconstrained(vec![list_b_unconstrained]); - let _list_a: crate::constrained::MaybeConstrained = list_a_unconstrained.into(); + let _list_a: crate::constrained::MaybeConstrained = list_a_unconstrained.into(); """, ) - project.compileAndTest() } } + project.compileAndTest() } } 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 a5877b7c007..75e176be39d 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 @@ -8,8 +8,9 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import org.junit.jupiter.api.Test import software.amazon.smithy.model.shapes.MapShape import software.amazon.smithy.model.shapes.StructureShape -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule +import software.amazon.smithy.rust.codegen.core.smithy.ConstrainedModule import software.amazon.smithy.rust.codegen.core.smithy.ModelsModule +import software.amazon.smithy.rust.codegen.core.smithy.UnconstrainedModule 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 @@ -29,16 +30,16 @@ class UnconstrainedMapGeneratorTest { key: String, value: MapB } - + map MapB { key: String, value: StructureC } - + structure StructureC { @required int: Integer, - + @required string: String } @@ -49,18 +50,18 @@ class UnconstrainedMapGeneratorTest { val mapA = model.lookup("test#MapA") val mapB = model.lookup("test#MapB") - val project = TestWorkspace.testProject(symbolProvider) + val project = TestWorkspace.testProject(symbolProvider, debugMode = true) - project.withModule(RustModule.public("model")) { + project.withModule(ModelsModule) { model.lookup("test#StructureC").serverRenderWithModelBuilder(model, symbolProvider, this) } - project.withModule(RustModule.private("constrained")) { + project.withModule(ConstrainedModule) { listOf(mapA, mapB).forEach { PubCrateConstrainedMapGenerator(codegenContext, this, it).render() } } - project.withModule(RustModule.private("unconstrained")) unconstrainedModuleWriter@{ + project.withModule(UnconstrainedModule) unconstrainedModuleWriter@{ project.withModule(ModelsModule) modelsModuleWriter@{ listOf(mapA, mapB).forEach { UnconstrainedMapGenerator(codegenContext, this@unconstrainedModuleWriter, it).render() @@ -100,65 +101,65 @@ class UnconstrainedMapGeneratorTest { crate::model::structure_c::ConstraintViolation::MissingInt, ) ); - + let actual_err = crate::constrained::map_a_constrained::MapAConstrained::try_from(map_a_unconstrained).unwrap_err(); assert!(actual_err == missing_string_expected_err || actual_err == missing_int_expected_err); - """, + """, ) this@unconstrainedModuleWriter.unitTest( name = "map_a_unconstrained_succeed_to_constrain", test = """ - let c_builder = crate::model::StructureC::builder().int(69).string(String::from("david")); - let map_b_unconstrained = map_b_unconstrained::MapBUnconstrained( - std::collections::HashMap::from([ - (String::from("KeyB"), c_builder), - ]) - ); - let map_a_unconstrained = map_a_unconstrained::MapAUnconstrained( - std::collections::HashMap::from([ - (String::from("KeyA"), map_b_unconstrained), - ]) - ); - - let expected = std::collections::HashMap::from([ - (String::from("KeyA"), std::collections::HashMap::from([ - (String::from("KeyB"), crate::model::StructureC { - int: 69, - string: String::from("david") - }), - ])) - ]); - - assert_eq!( - expected, - crate::constrained::map_a_constrained::MapAConstrained::try_from(map_a_unconstrained).unwrap().into() - ); + let c_builder = crate::model::StructureC::builder().int(69).string(String::from("david")); + let map_b_unconstrained = map_b_unconstrained::MapBUnconstrained( + std::collections::HashMap::from([ + (String::from("KeyB"), c_builder), + ]) + ); + let map_a_unconstrained = map_a_unconstrained::MapAUnconstrained( + std::collections::HashMap::from([ + (String::from("KeyA"), map_b_unconstrained), + ]) + ); + + let expected = std::collections::HashMap::from([ + (String::from("KeyA"), std::collections::HashMap::from([ + (String::from("KeyB"), crate::model::StructureC { + int: 69, + string: String::from("david") + }), + ])) + ]); + + assert_eq!( + expected, + crate::constrained::map_a_constrained::MapAConstrained::try_from(map_a_unconstrained).unwrap().into() + ); """, ) this@unconstrainedModuleWriter.unitTest( name = "map_a_unconstrained_converts_into_constrained", test = """ - let c_builder = crate::model::StructureC::builder(); - let map_b_unconstrained = map_b_unconstrained::MapBUnconstrained( - std::collections::HashMap::from([ - (String::from("KeyB"), c_builder), - ]) - ); - let map_a_unconstrained = map_a_unconstrained::MapAUnconstrained( - std::collections::HashMap::from([ - (String::from("KeyA"), map_b_unconstrained), - ]) - ); - - let _map_a: crate::constrained::MaybeConstrained = map_a_unconstrained.into(); + let c_builder = crate::model::StructureC::builder(); + let map_b_unconstrained = map_b_unconstrained::MapBUnconstrained( + std::collections::HashMap::from([ + (String::from("KeyB"), c_builder), + ]) + ); + let map_a_unconstrained = map_a_unconstrained::MapAUnconstrained( + std::collections::HashMap::from([ + (String::from("KeyA"), map_b_unconstrained), + ]) + ); + + let _map_a: crate::constrained::MaybeConstrained = map_a_unconstrained.into(); """, ) - - project.compileAndTest() } } + + project.compileAndTest() } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedUnionGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedUnionGeneratorTest.kt index f31285de980..4b5eca2d1e4 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedUnionGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/UnconstrainedUnionGeneratorTest.kt @@ -8,8 +8,8 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import org.junit.jupiter.api.Test import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.UnionShape -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.smithy.ModelsModule +import software.amazon.smithy.rust.codegen.core.smithy.UnconstrainedModule import software.amazon.smithy.rust.codegen.core.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel @@ -29,7 +29,7 @@ class UnconstrainedUnionGeneratorTest { union Union { structure: Structure } - + structure Structure { @required requiredMember: String @@ -42,14 +42,14 @@ class UnconstrainedUnionGeneratorTest { val project = TestWorkspace.testProject(symbolProvider) - project.withModule(RustModule.public("model")) { + project.withModule(ModelsModule) { model.lookup("test#Structure").serverRenderWithModelBuilder(model, symbolProvider, this) } project.withModule(ModelsModule) { UnionGenerator(model, symbolProvider, this, unionShape, renderUnknownVariant = false).render() } - project.withModule(RustModule.private("unconstrained")) unconstrainedModuleWriter@{ + project.withModule(UnconstrainedModule) unconstrainedModuleWriter@{ project.withModule(ModelsModule) modelsModuleWriter@{ UnconstrainedUnionGenerator(codegenContext, this@unconstrainedModuleWriter, this@modelsModuleWriter, unionShape).render() @@ -67,7 +67,7 @@ class UnconstrainedUnionGeneratorTest { expected_err, crate::model::Union::try_from(union_unconstrained).unwrap_err() ); - """, + """, ) this@unconstrainedModuleWriter.unitTest( @@ -82,7 +82,7 @@ class UnconstrainedUnionGeneratorTest { let actual: crate::model::Union = crate::model::Union::try_from(union_unconstrained).unwrap(); assert_eq!(expected, actual); - """, + """, ) this@unconstrainedModuleWriter.unitTest( @@ -93,10 +93,10 @@ class UnconstrainedUnionGeneratorTest { let _union: crate::constrained::MaybeConstrained = union_unconstrained.into(); - """, + """, ) - project.compileAndTest() } } + project.compileAndTest() } } diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/EventStreamTestTools.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/EventStreamTestTools.kt index 52d312ffa96..71e425eb384 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/EventStreamTestTools.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/EventStreamTestTools.kt @@ -21,6 +21,8 @@ import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.CodegenTarget +import software.amazon.smithy.rust.codegen.core.smithy.ErrorsModule +import software.amazon.smithy.rust.codegen.core.smithy.ModelsModule import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.generators.BuilderGenerator import software.amazon.smithy.rust.codegen.core.smithy.generators.StructureGenerator @@ -355,7 +357,7 @@ object EventStreamTestTools { } val project = TestWorkspace.testProject(symbolProvider) val operationSymbol = symbolProvider.toSymbol(operationShape) - project.withModule(RustModule.public("error")) { + project.withModule(ErrorsModule) { val errors = model.shapes() .filter { shape -> shape.isStructureShape && shape.hasTrait() } .map { it.asStructureShape().get() } @@ -373,11 +375,11 @@ object EventStreamTestTools { } } } - project.withModule(RustModule.public("model")) { + project.withModule(ModelsModule) { val inputOutput = model.lookup("test#TestStreamInputOutput") recursivelyGenerateModels(model, symbolProvider, inputOutput, this, testCase.target) } - project.withModule(RustModule.public("output")) { + project.withModule(RustModule.Output) { operationShape.outputShape(model).renderWithModelBuilder(model, symbolProvider, this) } return TestEventStreamProject(model, serviceShape, operationShape, unionShape, symbolProvider, project)