diff --git a/codegen-core/common-test-models/constraints.smithy b/codegen-core/common-test-models/constraints.smithy index 138c9e632f..e1f8215877 100644 --- a/codegen-core/common-test-models/constraints.smithy +++ b/codegen-core/common-test-models/constraints.smithy @@ -651,7 +651,7 @@ string LengthPatternString @length(min: 1, max: 69) string MediaTypeLengthString -@range(min: -0, max: 69) +@range(min: -7, max: 69) integer RangeInteger @range(min: -10) diff --git a/codegen-core/common-test-models/simple.smithy b/codegen-core/common-test-models/simple.smithy index 43c4bc6aca..c21aec67b7 100644 --- a/codegen-core/common-test-models/simple.smithy +++ b/codegen-core/common-test-models/simple.smithy @@ -1,4 +1,4 @@ -$version: "1.0" +$version: "2.0" namespace com.amazonaws.simple @@ -12,125 +12,23 @@ use smithy.framework#ValidationException @documentation("A simple service example, with a Service resource that can be registered and a readonly healthcheck") service SimpleService { version: "2022-01-01", - resources: [ - Service, - ], operations: [ - Healthcheck, - StoreServiceBlob, + HealthCheck, ], } -@documentation("Id of the service that will be registered") -string ServiceId - -@documentation("Name of the service that will be registered") -string ServiceName - -@error("client") -@documentation( - """ - Returned when a new resource cannot be created because one already exists. - """ -) -structure ResourceAlreadyExists { - @required - message: String -} - -@documentation("A resource that can register services") -resource Service { - identifiers: { id: ServiceId }, - put: RegisterService, -} - -@idempotent -@http(method: "PUT", uri: "/service/{id}") -@documentation("Service register operation") -@httpRequestTests([ - { - id: "RegisterServiceRequestTest", - protocol: "aws.protocols#restJson1", - uri: "/service/1", - headers: { - "Content-Type": "application/json", - }, - params: { id: "1", name: "TestService" }, - body: "{\"name\":\"TestService\"}", - method: "PUT", - } -]) -@httpResponseTests([ - { - id: "RegisterServiceResponseTest", - protocol: "aws.protocols#restJson1", - params: { id: "1", name: "TestService" }, - body: "{\"id\":\"1\",\"name\":\"TestService\"}", - code: 200, - headers: { - "Content-Length": "31" - } - } -]) -operation RegisterService { - input: RegisterServiceInputRequest, - output: RegisterServiceOutputResponse, - errors: [ResourceAlreadyExists, ValidationException] -} - -@documentation("Service register input structure") -structure RegisterServiceInputRequest { - @required - @httpLabel - id: ServiceId, - name: ServiceName, -} - -@documentation("Service register output structure") -structure RegisterServiceOutputResponse { - @required - id: ServiceId, - name: ServiceName, -} - -@readonly -@http(uri: "/healthcheck", method: "GET") -@documentation("Read-only healthcheck operation") -operation Healthcheck { - input: HealthcheckInputRequest, - output: HealthcheckOutputResponse -} - -@documentation("Service healthcheck output structure") -structure HealthcheckInputRequest { - -} - -@documentation("Service healthcheck input structure") -structure HealthcheckOutputResponse { - -} - -@readonly -@http(method: "POST", uri: "/service/{id}/blob") -@documentation("Stores a blob for a service id") -operation StoreServiceBlob { - input: StoreServiceBlobInput, - output: StoreServiceBlobOutput, +@http(uri: "/", method: "POST") +operation HealthCheck { + input: Input, errors: [ValidationException] } -@documentation("Store a blob for a service id input structure") -structure StoreServiceBlobInput { - @required - @httpLabel - id: ServiceId, - @required - @httpPayload - content: Blob, +@input +structure Input { + @default(15) + int:ConstrainedInteger } -@documentation("Store a blob for a service id output structure") -structure StoreServiceBlobOutput { - -} +@default(15) +@range(min: 10, max: 29) +integer ConstrainedInteger 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 8544fc1eb1..a7af29426e 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 @@ -94,7 +94,7 @@ fun Shape.isDirectlyConstrained(symbolProvider: SymbolProvider): Boolean = when } fun MemberShape.hasConstraintTraitOrTargetHasConstraintTrait(model: Model, symbolProvider: SymbolProvider): Boolean = - this.isDirectlyConstrained(symbolProvider) || (model.expectShape(this.target).isDirectlyConstrained(symbolProvider)) + this.isDirectlyConstrained(symbolProvider) || model.expectShape(this.target).isDirectlyConstrained(symbolProvider) fun Shape.isTransitivelyButNotDirectlyConstrained(model: Model, symbolProvider: SymbolProvider): Boolean = !this.isDirectlyConstrained(symbolProvider) && this.canReachConstrainedShape(model, symbolProvider) 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 5bfb0f98f2..6b4df3204e 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 @@ -29,6 +29,7 @@ object ServerCargoDependency { val Regex: CargoDependency = CargoDependency("regex", CratesIo("1.5.5")) fun smithyHttpServer(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-http-server") + fun smithyTypes(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-types") } /** diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderConstraintViolations.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderConstraintViolations.kt index f2c572eedc..b416af4db7 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderConstraintViolations.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderConstraintViolations.kt @@ -62,13 +62,16 @@ class ServerBuilderConstraintViolations( nonExhaustive: Boolean, shouldRenderAsValidationExceptionFieldList: Boolean, ) { + check(all.isNotEmpty()) + Attribute.Derives(setOf(RuntimeType.Debug, RuntimeType.PartialEq)).render(writer) writer.docs("Holds one variant for each of the ways the builder can fail.") if (nonExhaustive) Attribute.NonExhaustive.render(writer) val constraintViolationSymbolName = constraintViolationSymbolProvider.toSymbol(shape).name - writer.rustBlock("pub${ if (visibility == Visibility.PUBCRATE) " (crate) " else "" } enum $constraintViolationSymbolName") { + writer.rustBlock("pub${if (visibility == Visibility.PUBCRATE) " (crate) " else ""} enum $constraintViolationSymbolName") { renderConstraintViolations(writer) } + renderImplDisplayConstraintViolation(writer) writer.rust("impl #T for ConstraintViolation { }", RuntimeType.StdError) @@ -93,7 +96,7 @@ class ServerBuilderConstraintViolations( fun forMember(member: MemberShape): ConstraintViolation? { check(members.contains(member)) // TODO(https://github.com/awslabs/smithy-rs/issues/1302, https://github.com/awslabs/smithy/issues/1179): See above. - return if (symbolProvider.toSymbol(member).isOptional()) { + return if (symbolProvider.toSymbol(member).isOptional() || member.hasNonNullDefault()) { null } else { ConstraintViolation(member, ConstraintViolationKind.MISSING_MEMBER) 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 facb2c5b0a..59133e1e39 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 @@ -5,16 +5,41 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators +import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.Model +import software.amazon.smithy.model.node.ArrayNode +import software.amazon.smithy.model.node.BooleanNode +import software.amazon.smithy.model.node.NullNode +import software.amazon.smithy.model.node.NumberNode +import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.model.node.StringNode +import software.amazon.smithy.model.shapes.BlobShape +import software.amazon.smithy.model.shapes.BooleanShape +import software.amazon.smithy.model.shapes.ByteShape +import software.amazon.smithy.model.shapes.DocumentShape +import software.amazon.smithy.model.shapes.DoubleShape +import software.amazon.smithy.model.shapes.EnumShape +import software.amazon.smithy.model.shapes.FloatShape +import software.amazon.smithy.model.shapes.IntEnumShape +import software.amazon.smithy.model.shapes.IntegerShape +import software.amazon.smithy.model.shapes.ListShape +import software.amazon.smithy.model.shapes.LongShape +import software.amazon.smithy.model.shapes.MapShape import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.ShortShape +import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.shapes.TimestampShape import software.amazon.smithy.model.shapes.UnionShape +import software.amazon.smithy.model.traits.DefaultTrait +import software.amazon.smithy.model.traits.EnumDefinition import software.amazon.smithy.rust.codegen.core.rustlang.Attribute 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.conditionalBlock import software.amazon.smithy.rust.codegen.core.rustlang.deprecatedShape import software.amazon.smithy.rust.codegen.core.rustlang.docs @@ -28,7 +53,9 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.stripOuter import software.amazon.smithy.rust.codegen.core.rustlang.withBlock import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider 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.isRustBoxed @@ -39,11 +66,15 @@ 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.UNREACHABLE import software.amazon.smithy.rust.codegen.core.util.dq +import software.amazon.smithy.rust.codegen.core.util.expectTrait import software.amazon.smithy.rust.codegen.core.util.hasTrait +import software.amazon.smithy.rust.codegen.core.util.isStreaming import software.amazon.smithy.rust.codegen.core.util.letIf import software.amazon.smithy.rust.codegen.core.util.redactIfNecessary import software.amazon.smithy.rust.codegen.core.util.toSnakeCase +import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.ServerRuntimeType import software.amazon.smithy.rust.codegen.server.smithy.canReachConstrainedShape @@ -99,15 +130,18 @@ class ServerBuilderGenerator( model: Model, symbolProvider: SymbolProvider, takeInUnconstrainedTypes: Boolean, - ): Boolean = - if (takeInUnconstrainedTypes) { - structureShape.canReachConstrainedShape(model, symbolProvider) + ): Boolean { + val members = structureShape.members() + val allOptional = members.all { symbolProvider.toSymbol(it).isOptional() } + val allUnconstrainedDefault = members.all { it.hasNonNullDefault() && !it.canReachConstrainedShape(model, symbolProvider) } + val notFallible = allOptional || allUnconstrainedDefault + + return if (takeInUnconstrainedTypes) { + !notFallible && structureShape.canReachConstrainedShape(model, symbolProvider) } else { - structureShape - .members() - .map { symbolProvider.toSymbol(it) } - .any { !it.isOptional() } + !notFallible } + } } private val takeInUnconstrainedTypes = shape.isReachableFromOperationInput() @@ -142,24 +176,26 @@ class ServerBuilderGenerator( private fun renderBuilder(writer: RustWriter) { if (isBuilderFallible) { - serverBuilderConstraintViolations.render( - writer, - visibility, - nonExhaustive = true, - shouldRenderAsValidationExceptionFieldList = shape.isReachableFromOperationInput(), - ) + if (!members.all { it.hasNonNullDefault() && !it.hasConstraintTraitOrTargetHasConstraintTrait(model, symbolProvider) }) { + serverBuilderConstraintViolations.render( + writer, + visibility, + nonExhaustive = true, + shouldRenderAsValidationExceptionFieldList = shape.isReachableFromOperationInput(), + ) + + // Only generate converter from `ConstraintViolation` into `RequestRejection` if the structure shape is + // an operation input shape. + if (shape.hasTrait()) { + renderImplFromConstraintViolationForRequestRejection(writer) + } - // Only generate converter from `ConstraintViolation` into `RequestRejection` if the structure shape is - // an operation input shape. - if (shape.hasTrait()) { - renderImplFromConstraintViolationForRequestRejection(writer) - } + if (takeInUnconstrainedTypes) { + renderImplFromBuilderForMaybeConstrained(writer) + } - if (takeInUnconstrainedTypes) { - renderImplFromBuilderForMaybeConstrained(writer) + renderTryFromBuilderImpl(writer) } - - renderTryFromBuilderImpl(writer) } else { renderFromBuilderImpl(writer) } @@ -396,10 +432,11 @@ class ServerBuilderGenerator( } private fun renderTryFromBuilderImpl(writer: RustWriter) { + val errorType = if (!isBuilderFallible) "std::convert::Infallible" else "ConstraintViolation" writer.rustTemplate( """ impl #{TryFrom} for #{Structure} { - type Error = ConstraintViolation; + type Error = $errorType; fn try_from(builder: Builder) -> Result { builder.build() @@ -496,6 +533,16 @@ class ServerBuilderGenerator( val memberName = symbolProvider.toMemberName(member) withBlock("$memberName: self.$memberName", ",") { + val wrapDefault: (String) -> String = { + if (member.isStreaming(model)) { + // We set ByteStream to Default::default() until it is easier to use the full namespace for python. + // Use `unwrap_or_default` to make clippy happy. + ".unwrap_or_default()" + } else { + """.unwrap_or_else(|| $it.try_into().expect("This check should have failed at generation time; please file a bug report under https://github.com/awslabs/smithy-rs/issues"))""" + } + } + // Write the modifier(s). serverBuilderConstraintViolations.builderConstraintViolationForMember(member)?.also { constraintViolation -> val hasBox = builderMemberSymbol(member) @@ -523,14 +570,21 @@ class ServerBuilderGenerator( #{MaybeConstrained}::Constrained(x) => Ok(x), #{MaybeConstrained}::Unconstrained(x) => x.try_into(), }) - .map(|res| - res${if (constrainedTypeHoldsFinalType(member)) "" else ".map(|v| v.into())"} - .map_err(ConstraintViolation::${constraintViolation.name()}) - ) - .transpose()? """, *codegenScope, ) + if (isBuilderFallible) { + rustTemplate( + """ + .map(|res| + res${if (constrainedTypeHoldsFinalType(member)) "" else ".map(|v| v.into())"} + .map_err(ConstraintViolation::${constraintViolation.name()}) + ) + .transpose()? + """, + *codegenScope, + ) + } // Constrained types are not public and this is a member shape that would have generated a // public constrained type, were the setting to be enabled. @@ -543,8 +597,32 @@ class ServerBuilderGenerator( constrainedShapeSymbolProvider.toSymbol(model.expectShape(member.target)), ) } + if (member.hasNonNullDefault()) { + rustTemplate( + """#{Default:W}""", + "Default" to renderDefaultBuilder( + model, + runtimeConfig, + symbolProvider, + member, + wrapDefault, + ), + ) + if (!isBuilderFallible) { + // unwrap the Option + rust(".unwrap()") + } + } + } + } ?: run { + if (member.hasNonNullDefault()) { + rustTemplate( + "#{Default:W}", + "Default" to renderDefaultBuilder(model, runtimeConfig, symbolProvider, member, wrapDefault), + ) } } + // This won't run if there is a default value serverBuilderConstraintViolations.forMember(member)?.also { rust(".ok_or(ConstraintViolation::${it.name()})?") } @@ -561,3 +639,121 @@ fun buildFnReturnType(isBuilderFallible: Boolean, structureSymbol: Symbol) = wri rust("#T", structureSymbol) } } + +fun renderDefaultBuilder(model: Model, runtimeConfig: RuntimeConfig, symbolProvider: RustSymbolProvider, member: MemberShape, wrap: (s: String) -> String = { it }): Writable { + return writable { + val node = member.expectTrait().toNode()!! + val name = member.memberName + val types = ServerCargoDependency.smithyTypes(runtimeConfig).toType() + when (val target = model.expectShape(member.target)) { + is EnumShape, is IntEnumShape -> { + val value = when (target) { + is IntEnumShape -> node.expectNumberNode().value + is EnumShape -> node.expectStringNode().value + else -> throw CodegenException("Default value for $name must be of EnumShape or IntEnumShape") + } + val enumValues = when (target) { + is IntEnumShape -> target.enumValues + is EnumShape -> target.enumValues + else -> UNREACHABLE("It must be an [Int]EnumShape, otherwise it'd have failed above") + } + val variant = enumValues + .entries + .filter { entry -> entry.value == value } + .map { entry -> + symbolProvider.toEnumVariantName( + EnumDefinition.builder().name(entry.key).value(entry.value.toString()).build(), + )!! + } + .first() + val symbol = symbolProvider.toSymbol(target) + val result = "$symbol::${variant.name}" + rust(wrap(result)) + } + + is ByteShape -> rust(wrap(node.expectNumberNode().value.toString() + "i8")) + is ShortShape -> rust(wrap(node.expectNumberNode().value.toString() + "i16")) + is IntegerShape -> rust(wrap(node.expectNumberNode().value.toString() + "i32")) + is LongShape -> rust(wrap(node.expectNumberNode().value.toString() + "i64")) + is FloatShape -> rust(wrap(node.expectNumberNode().value.toFloat().toString() + "f32")) + is DoubleShape -> rust(wrap(node.expectNumberNode().value.toDouble().toString() + "f64")) + is BooleanShape -> rust(wrap(node.expectBooleanNode().value.toString())) + is StringShape -> rust(wrap("String::from(${node.expectStringNode().value.dq()})")) + is TimestampShape -> when (node) { + is NumberNode -> rust(wrap(node.expectNumberNode().value.toString())) + is StringNode -> { + val value = node.expectStringNode().value + rustTemplate( + wrap( + """ + #{SmithyTypes}::DateTime::from_str("$value", #{SmithyTypes}::date_time::Format::DateTime) + .expect("default value `$value` cannot be parsed into a valid date time; please file a bug report under https://github.com/awslabs/smithy-rs/issues")""", + ), + "SmithyTypes" to types, + ) + } + + else -> throw CodegenException("Default value for $name is unsupported") + } + + is ListShape -> { + check(node is ArrayNode && node.isEmpty) + rust(wrap("Vec::new()")) + } + + is MapShape -> { + check(node is ObjectNode && node.isEmpty) + rust(wrap("std::collections::HashMap::new()")) + } + + is DocumentShape -> { + when (node) { + is NullNode -> rustTemplate( + "#{SmithyTypes}::Document::Null", + "SmithyTypes" to types, + ) + + is BooleanNode -> rust(wrap(node.value.toString())) + is StringNode -> rust(wrap("String::from(${node.value.dq()})")) + is NumberNode -> { + val value = node.value.toString() + val variant = when (node.value) { + is Float, is Double -> "Float" + else -> if (node.value.toLong() >= 0) "PosInt" else "NegInt" + } + rustTemplate( + wrap( + "#{SmithyTypes}::Document::Number(#{SmithyTypes}::Number::$variant($value))", + ), + "SmithyTypes" to types, + ) + } + + is ArrayNode -> { + check(node.isEmpty) + rust(wrap("Vec::new()")) + } + + is ObjectNode -> { + check(node.isEmpty) + rust(wrap("std::collections::HashMap::new()")) + } + + else -> throw CodegenException("Default value $node for member shape ${member.id} is unsupported or cannot exist; please file a bug report under https://github.com/awslabs/smithy-rs/issues") + } + } + + is BlobShape -> { + val value = if (member.isStreaming(model)) { + /* ByteStream to work in Python and Rust without explicit dependency */ + "Default::default()" + } else { + "Vec::new()" + } + rust(wrap(value)) + } + + else -> throw CodegenException("Default value for $name is unsupported or cannot exist") + } + } +} 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 d83446dc83..58e83735ab 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 @@ -7,6 +7,7 @@ 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.Model import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter @@ -25,8 +26,11 @@ 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.core.util.isStreaming import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.ServerRuntimeType +import software.amazon.smithy.rust.codegen.server.smithy.canReachConstrainedShape +import software.amazon.smithy.rust.codegen.server.smithy.hasConstraintTraitOrTargetHasConstraintTrait /** * Generates a builder for the Rust type associated with the [StructureShape]. @@ -53,23 +57,28 @@ class ServerBuilderGeneratorWithoutPublicConstrainedTypes( * This builder only enforces the `required` trait. */ fun hasFallibleBuilder( + model: Model, structureShape: StructureShape, symbolProvider: SymbolProvider, - ): Boolean = - structureShape - .members() - .map { symbolProvider.toSymbol(it) } - .any { !it.isOptional() } + ): Boolean { + val members = structureShape.members() + val allOptional = members.all { symbolProvider.toSymbol(it).isOptional() } + val allUnconstrainedDefault = members.all { it.hasNonNullDefault() && !it.canReachConstrainedShape(model, symbolProvider) } + val notFallible = allOptional || allUnconstrainedDefault + + return !notFallible + } } private val model = codegenContext.model private val symbolProvider = codegenContext.symbolProvider private val members: List = shape.allMembers.values.toList() + private val runtimeConfig = codegenContext.runtimeConfig private val structureSymbol = symbolProvider.toSymbol(shape) private val builderSymbol = shape.serverBuilderSymbol(symbolProvider, false) private val moduleName = builderSymbol.namespace.split("::").last() - private val isBuilderFallible = hasFallibleBuilder(shape, symbolProvider) + private val isBuilderFallible = hasFallibleBuilder(model, shape, symbolProvider) private val serverBuilderConstraintViolations = ServerBuilderConstraintViolations(codegenContext, shape, builderTakesInUnconstrainedTypes = false) @@ -90,14 +99,16 @@ class ServerBuilderGeneratorWithoutPublicConstrainedTypes( private fun renderBuilder(writer: RustWriter) { if (isBuilderFallible) { - serverBuilderConstraintViolations.render( - writer, - Visibility.PUBLIC, - nonExhaustive = false, - shouldRenderAsValidationExceptionFieldList = false, - ) + if (!members.all { it.hasNonNullDefault() && !it.hasConstraintTraitOrTargetHasConstraintTrait(model, symbolProvider) }) { + serverBuilderConstraintViolations.render( + writer, + Visibility.PUBLIC, + nonExhaustive = false, + shouldRenderAsValidationExceptionFieldList = false, + ) - renderTryFromBuilderImpl(writer) + renderTryFromBuilderImpl(writer) + } } else { renderFromBuilderImpl(writer) } @@ -158,6 +169,12 @@ class ServerBuilderGeneratorWithoutPublicConstrainedTypes( val memberName = symbolProvider.toMemberName(member) withBlock("$memberName: self.$memberName", ",") { + if (member.hasNonNullDefault()) { + val into = if (member.isStreaming(model)) { + "" + } else { ".into()" } + rustTemplate("""#{default:W}""", "default" to renderDefaultBuilder(model, runtimeConfig, symbolProvider, member) { ".unwrap_or_else(|| $it$into)" }) + } serverBuilderConstraintViolations.forMember(member)?.also { rust(".ok_or(ConstraintViolation::${it.name()})?") } @@ -204,10 +221,11 @@ class ServerBuilderGeneratorWithoutPublicConstrainedTypes( } private fun renderTryFromBuilderImpl(writer: RustWriter) { + val errorType = if (!isBuilderFallible) "std::convert::Infallible" else "ConstraintViolation" writer.rustTemplate( """ impl #{TryFrom} for #{Structure} { - type Error = ConstraintViolation; + type Error = $errorType; fn try_from(builder: Builder) -> Result { builder.build() diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderDefaultValuesTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderDefaultValuesTest.kt new file mode 100644 index 0000000000..f4df8daf8e --- /dev/null +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderDefaultValuesTest.kt @@ -0,0 +1,326 @@ +/* + * 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 org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.EnumShape +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.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.core.smithy.generators.StructureGenerator +import software.amazon.smithy.rust.codegen.core.smithy.generators.implBlock +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.unitTest +import software.amazon.smithy.rust.codegen.core.util.dq +import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.core.util.toPascalCase +import software.amazon.smithy.rust.codegen.core.util.toSnakeCase +import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestSymbolProvider +import java.util.stream.Stream + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ServerBuilderDefaultValuesTest { + // When defaults are used, the model will be generated with these default values + private val defaultValues = mapOf( + "Boolean" to "true", + "String" to "foo".dq(), + "Byte" to "5", + "Short" to "55", + "Integer" to "555", + "Long" to "5555", + "Float" to "0.5", + "Double" to "0.55", + "Timestamp" to "1985-04-12T23:20:50.52Z".dq(), + // "BigInteger" to "55555", "BigDecimal" to "0.555", // TODO(https://github.com/awslabs/smithy-rs/issues/312) + "StringList" to "[]", + "IntegerMap" to "{}", + "Language" to "en".dq(), + "DocumentBoolean" to "true", + "DocumentString" to "foo".dq(), + "DocumentNumberPosInt" to "100", + "DocumentNumberNegInt" to "-100", + "DocumentNumberFloat" to "0.1", + "DocumentList" to "[]", + "DocumentMap" to "{}", + ) + + // When the test applies values to validate we honor custom values, use these values + private val customValues = + mapOf( + "Boolean" to "false", + "String" to "bar".dq(), + "Byte" to "6", + "Short" to "66", + "Integer" to "666", + "Long" to "6666", + "Float" to "0.6", + "Double" to "0.66", + "Timestamp" to "2022-11-25T17:30:50.00Z".dq(), + // "BigInteger" to "55555", "BigDecimal" to "0.555", // TODO(https://github.com/awslabs/smithy-rs/issues/312) + "StringList" to "[]", + "IntegerMap" to "{}", + "Language" to "fr".dq(), + "DocumentBoolean" to "false", + "DocumentString" to "bar".dq(), + "DocumentNumberPosInt" to "1000", + "DocumentNumberNegInt" to "-1000", + "DocumentNumberFloat" to "0.01", + "DocumentList" to "[]", + "DocumentMap" to "{}", + ) + + @ParameterizedTest(name = "(#{index}) Generate default value. Required Trait: {1}, nulls: {2}, optionals: {3}") + @MethodSource("localParameters") + fun `default values are generated and builders respect default and overrides`(testConfig: TestConfig, setupFiles: (w: RustWriter, m: Model, s: RustSymbolProvider) -> Unit) { + val initialSetValues = defaultValues.mapValues { if (testConfig.nullDefault) null else it.value } + val model = generateModel(testConfig, initialSetValues) + val symbolProvider = serverTestSymbolProvider(model) + val project = TestWorkspace.testProject(symbolProvider) + project.withModule(RustModule.public("model")) { + setupFiles(this, model, symbolProvider) + + val rustValues = setupRustValuesForTest(testConfig.assertValues) + val applySetters = testConfig.applyDefaultValues + val setters = if (applySetters) structSetters(rustValues, testConfig.nullDefault && !testConfig.requiredTrait) else writable { } + // unwrap when the builder is fallible + // enums are constrained + val unwrapBuilder = if ((testConfig.nullDefault && testConfig.requiredTrait) || (testConfig.applyDefaultValues && !testConfig.nullDefault)) ".unwrap()" else "" + unitTest( + name = "generates_default_required_values", + block = writable { + rustTemplate( + """ + let my_struct = MyStruct::builder() + #{Setters:W} + .build()$unwrapBuilder; + + #{Assertions:W} + """, + "Assertions" to assertions(rustValues, applySetters, testConfig.nullDefault, testConfig.requiredTrait, testConfig.applyDefaultValues), + "Setters" to setters, + ) + }, + ) + } + project.compileAndTest() + } + + private fun setupRustValuesForTest(valuesMap: Map): Map { + return valuesMap + mapOf( + "Byte" to "${valuesMap["Byte"]}i8", + "Short" to "${valuesMap["Short"]}i16", + "Integer" to "${valuesMap["Integer"]}i32", + "Long" to "${valuesMap["Long"]}i64", + "Float" to "${valuesMap["Float"]}f32", + "Double" to "${valuesMap["Double"]}f64", + "Language" to "crate::model::Language::${valuesMap["Language"]!!.replace(""""""", "").toPascalCase()}", + "Timestamp" to """aws_smithy_types::DateTime::from_str(${valuesMap["Timestamp"]}, aws_smithy_types::date_time::Format::DateTime).unwrap()""", + // These must be empty + "StringList" to "Vec::::new()", + "IntegerMap" to "std::collections::HashMap::::new()", + "DocumentList" to "Vec::::new()", + "DocumentMap" to "std::collections::HashMap::::new()", + ) + } + + private fun writeServerBuilderGeneratorWithoutPublicConstrainedTypes(writer: RustWriter, model: Model, symbolProvider: RustSymbolProvider) { + writer.rust("##![allow(deprecated)]") + val struct = model.lookup("com.test#MyStruct") + val codegenContext = serverTestCodegenContext(model) + val builderGenerator = ServerBuilderGeneratorWithoutPublicConstrainedTypes(codegenContext, struct) + + writer.implBlock(struct, symbolProvider) { + builderGenerator.renderConvenienceMethod(writer) + } + builderGenerator.render(writer) + + ServerEnumGenerator(codegenContext, writer, model.lookup("com.test#Language")).render() + StructureGenerator(model, symbolProvider, writer, struct).render() + } + + private fun writeServerBuilderGenerator(writer: RustWriter, model: Model, symbolProvider: RustSymbolProvider) { + writer.rust("##![allow(deprecated)]") + val struct = model.lookup("com.test#MyStruct") + val codegenContext = serverTestCodegenContext(model) + val builderGenerator = ServerBuilderGenerator(codegenContext, struct) + + writer.implBlock(struct, symbolProvider) { + builderGenerator.renderConvenienceMethod(writer) + } + builderGenerator.render(writer) + + ServerEnumGenerator(codegenContext, writer, model.lookup("com.test#Language")).render() + StructureGenerator(model, symbolProvider, writer, struct).render() + } + + private fun structSetters(values: Map, optional: Boolean): Writable { + return writable { + values.entries.forEach { + rust(".${it.key.toSnakeCase()}(") + if (optional) { + rust("Some(") + } + when (it.key) { + "String" -> { + rust("${it.value}.into()") + } + + "DocumentNull" -> + rust("aws_smithy_types::Document::Null") + + "DocumentString" -> { + rust("aws_smithy_types::Document::String(String::from(${it.value}))") + } + + else -> { + if (it.key.startsWith("DocumentNumber")) { + val type = it.key.replace("DocumentNumber", "") + rust("aws_smithy_types::Document::Number(aws_smithy_types::Number::$type(${it.value}))") + } else { + rust("${it.value}.into()") + } + } + } + if (optional) { + rust(")") + } + rust(")") + } + } + } + + private fun assertions(values: Map, hasSetValues: Boolean, hasNullValues: Boolean, requiredTrait: Boolean, hasDefaults: Boolean): Writable { + return writable { + for (it in values.entries) { + rust("assert_eq!(my_struct.${it.key.toSnakeCase()} ") + if (!hasSetValues) { + rust(".is_none(), true);") + continue + } + + val expected = if (it.key == "DocumentNull") { + "aws_smithy_types::Document::Null" + } else if (it.key == "DocumentString") { + "String::from(${it.value}).into()" + } else if (it.key.startsWith("DocumentNumber")) { + val type = it.key.replace("DocumentNumber", "") + "aws_smithy_types::Document::Number(aws_smithy_types::Number::$type(${it.value}))" + } else if (it.key.startsWith("Document")) { + "${it.value}.into()" + } else { + "${it.value}" + } + + if (!requiredTrait && !(hasDefaults && !hasNullValues)) { + rust(".unwrap()") + } + + rust(", $expected);") + } + } + } + + private fun generateModel(testConfig: TestConfig, values: Map): Model { + val requiredTrait = if (testConfig.requiredTrait) "@required" else "" + + val members = values.entries.joinToString(", ") { + val value = if (testConfig.applyDefaultValues) { + "= ${it.value}" + } else if (testConfig.nullDefault) { + "= null" + } else { "" } + """ + $requiredTrait + ${it.key.toPascalCase()}: ${it.key} $value + """ + } + val model = + """ + namespace com.test + use smithy.framework#ValidationException + + structure MyStruct { + $members + } + + enum Language { + EN = "en", + FR = "fr", + } + + list StringList { + member: String + } + + map IntegerMap { + key: String + value: Integer + } + + document DocumentNull + document DocumentBoolean + document DocumentString + document DocumentDecimal + document DocumentNumberNegInt + document DocumentNumberPosInt + document DocumentNumberFloat + document DocumentList + document DocumentMap + """ + return model.asSmithyModel(smithyVersion = "2") + } + + private fun localParameters(): Stream { + val builderWriters = listOf( + ::writeServerBuilderGenerator, + ::writeServerBuilderGeneratorWithoutPublicConstrainedTypes, + ) + return Stream.of( + TestConfig(defaultValues, requiredTrait = false, nullDefault = true, applyDefaultValues = true), + TestConfig(defaultValues, requiredTrait = false, nullDefault = true, applyDefaultValues = false), + + TestConfig(customValues, requiredTrait = false, nullDefault = true, applyDefaultValues = true), + TestConfig(customValues, requiredTrait = false, nullDefault = true, applyDefaultValues = false), + + TestConfig(defaultValues, requiredTrait = true, nullDefault = true, applyDefaultValues = true), + TestConfig(customValues, requiredTrait = true, nullDefault = true, applyDefaultValues = true), + + TestConfig(defaultValues, requiredTrait = false, nullDefault = false, applyDefaultValues = true), + TestConfig(defaultValues, requiredTrait = false, nullDefault = false, applyDefaultValues = false), + + TestConfig(customValues, requiredTrait = false, nullDefault = false, applyDefaultValues = true), + TestConfig(customValues, requiredTrait = false, nullDefault = false, applyDefaultValues = false), + + TestConfig(defaultValues, requiredTrait = true, nullDefault = false, applyDefaultValues = true), + + TestConfig(customValues, requiredTrait = true, nullDefault = false, applyDefaultValues = true), + + ).flatMap { builderWriters.stream().map { builderWriter -> Arguments.of(it, builderWriter) } } + } + + data class TestConfig( + // The values in the setters and assert!() calls + val assertValues: Map, + // Whether to apply @required to all members + val requiredTrait: Boolean, + // Whether to set all members to `null` and force them to be optional + val nullDefault: Boolean, + // Whether to set `assertValues` in the builder + val applyDefaultValues: Boolean, + ) +} diff --git a/rust-runtime/aws-smithy-http-server-python/src/types.rs b/rust-runtime/aws-smithy-http-server-python/src/types.rs index db3aa183da..1ce7779482 100644 --- a/rust-runtime/aws-smithy-http-server-python/src/types.rs +++ b/rust-runtime/aws-smithy-http-server-python/src/types.rs @@ -322,6 +322,12 @@ impl ByteStream { } } +impl Default for ByteStream { + fn default() -> Self { + Self::new(aws_smithy_http::body::SdkBody::from("")) + } +} + /// ByteStream Abstractions. #[pymethods] impl ByteStream {