Skip to content

Commit

Permalink
Support @default (#1879)
Browse files Browse the repository at this point in the history
* Default trait in server

Signed-off-by: Daniele Ahmed <[email protected]>
Co-authored-by: david-perez <[email protected]>
  • Loading branch information
82marbag and david-perez authored Dec 23, 2022
1 parent 9522cc6 commit 5363c41
Show file tree
Hide file tree
Showing 8 changed files with 738 additions and 74 deletions.
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()) {
// 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

0 comments on commit 5363c41

Please sign in to comment.