Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support @default #1879

Merged
merged 12 commits into from
Dec 23, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,18 @@ class ServerBuilderConstraintViolations(
nonExhaustive: Boolean,
shouldRenderAsValidationExceptionFieldList: Boolean,
) {
check(all.isNotEmpty()) {
"Attempted to render constraint violations for the builder for structure shape ${shape.id}, but calculation of the constraint violations resulted in no variants"
}

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)

Expand All @@ -93,7 +98,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ 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.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.RuntimeType
import software.amazon.smithy.rust.codegen.core.smithy.expectRustMetadata
import software.amazon.smithy.rust.codegen.core.smithy.isOptional
Expand Down Expand Up @@ -99,15 +98,33 @@ class ServerBuilderGenerator(
model: Model,
symbolProvider: SymbolProvider,
takeInUnconstrainedTypes: Boolean,
): Boolean =
if (takeInUnconstrainedTypes) {
structureShape.canReachConstrainedShape(model, symbolProvider)
): Boolean {
val members = structureShape.members()
fun isOptional(member: MemberShape) = symbolProvider.toSymbol(member).isOptional()
fun hasDefault(member: MemberShape) = member.hasNonNullDefault()
fun isNotConstrained(member: MemberShape) = !member.canReachConstrainedShape(model, symbolProvider)

val notFallible = members.all {
if (structureShape.isReachableFromOperationInput()) {
// When deserializing an input structure, constraints might not be satisfied by the data in the
// incoming request.
// For this builder not to be fallible, no members must be constrained (constraints in input must
// always be checked) and all members must _either_ be optional (no need to set it; not required)
// or have a default value.
isNotConstrained(it) && (isOptional(it) || hasDefault(it))
} else {
// This structure will be constructed manually by the user.
// Constraints will have to be dealt with before members are set in the builder.
isOptional(it) || hasDefault(it)
}
}

return if (takeInUnconstrainedTypes) {
!notFallible && structureShape.canReachConstrainedShape(model, symbolProvider)
} else {
structureShape
.members()
.map { symbolProvider.toSymbol(it) }
.any { !it.isOptional() }
!notFallible
}
}
}

private val takeInUnconstrainedTypes = shape.isReachableFromOperationInput()
Expand Down Expand Up @@ -497,67 +514,84 @@ class ServerBuilderGenerator(

withBlock("$memberName: self.$memberName", ",") {
// Write the modifier(s).

// 1. Enforce constraint traits of data from incoming requests.
serverBuilderConstraintViolations.builderConstraintViolationForMember(member)?.also { constraintViolation ->
val hasBox = builderMemberSymbol(member)
.mapRustType { it.stripOuter<RustType.Option>() }
.isRustBoxed()
if (hasBox) {
rustTemplate(
"""
.map(|v| match *v {
#{MaybeConstrained}::Constrained(x) => Ok(Box::new(x)),
#{MaybeConstrained}::Unconstrained(x) => Ok(Box::new(x.try_into()?)),
})
.map(|res|
res${ if (constrainedTypeHoldsFinalType(member)) "" else ".map(|v| v.into())" }
.map_err(|err| ConstraintViolation::${constraintViolation.name()}(Box::new(err)))
)
.transpose()?
""",
*codegenScope,
)
} else {
rustTemplate(
"""
.map(|v| match v {
#{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,
)

// 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.
// We've just checked the constraints hold by going through the non-public
// constrained type, but the user wants to work with the unconstrained type, so we have to
// unwrap it.
if (!publicConstrainedTypes && member.wouldHaveConstrainedWrapperTupleTypeWerePublicConstrainedTypesEnabled(model)) {
rust(
".map(|v: #T| v.into())",
constrainedShapeSymbolProvider.toSymbol(model.expectShape(member.target)),
)
}
}
enforceConstraints(this, member, constraintViolation)
}
serverBuilderConstraintViolations.forMember(member)?.also {
rust(".ok_or(ConstraintViolation::${it.name()})?")

if (member.hasNonNullDefault()) {
david-perez marked this conversation as resolved.
Show resolved Hide resolved
// 2a. If a `@default` value is modeled and the user did not set a value, fall back to using the
// default value.
generateFallbackCodeToDefaultValue(
this,
member,
model,
runtimeConfig,
symbolProvider,
publicConstrainedTypes,
)
} else {
// 2b. If the member is `@required` and has no `@default` value, the user must set a value;
// otherwise, we fail with a `ConstraintViolation::Missing*` variant.
serverBuilderConstraintViolations.forMember(member)?.also {
rust(".ok_or(ConstraintViolation::${it.name()})?")
}
}
}
}
}
}
}

fun buildFnReturnType(isBuilderFallible: Boolean, structureSymbol: Symbol) = writable {
if (isBuilderFallible) {
rust("Result<#T, ConstraintViolation>", structureSymbol)
} else {
rust("#T", structureSymbol)
private fun enforceConstraints(writer: RustWriter, member: MemberShape, constraintViolation: ConstraintViolation) {
// This member is constrained. Enforce the constraint traits on the value set in the builder.
// The code is slightly different in case the member is recursive, since it will be wrapped in
// `std::boxed::Box`.
val hasBox = builderMemberSymbol(member)
.mapRustType { it.stripOuter<RustType.Option>() }
.isRustBoxed()
if (hasBox) {
writer.rustTemplate(
"""
.map(|v| match *v {
#{MaybeConstrained}::Constrained(x) => Ok(Box::new(x)),
#{MaybeConstrained}::Unconstrained(x) => Ok(Box::new(x.try_into()?)),
})
.map(|res|
res${ if (constrainedTypeHoldsFinalType(member)) "" else ".map(|v| v.into())" }
.map_err(|err| ConstraintViolation::${constraintViolation.name()}(Box::new(err)))
)
.transpose()?
""",
*codegenScope,
)
} else {
writer.rustTemplate(
"""
.map(|v| match v {
#{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,
)
}

// 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.
// We've just checked the constraints hold by going through the non-public
// constrained type, but the user wants to work with the unconstrained type, so we have to
// unwrap it.
if (!publicConstrainedTypes && member.wouldHaveConstrainedWrapperTupleTypeWerePublicConstrainedTypesEnabled(model)) {
writer.rust(
".map(|v: #T| v.into())",
constrainedShapeSymbolProvider.toSymbol(model.expectShape(member.target)),
)
}
}
}
Loading