diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index aa9ae8ab73..3b6d0210a1 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -242,6 +242,7 @@ message = """ * The `length` trait on `string` shapes. * The `length` trait on `map` shapes. +* The `length` trait on `list` shapes. * The `range` trait on `byte` shapes. * The `range` trait on `short` shapes. * The `range` trait on `integer` shapes. @@ -252,7 +253,7 @@ Upon receiving a request that violates the modeled constraints, the server SDK w 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", "smithy-rs#2005", "smithy-rs#1998", "smithy-rs#2034", "smithy-rs#2036"] +references = ["smithy-rs#1199", "smithy-rs#1342", "smithy-rs#1401", "smithy-rs#1998", "smithy-rs#2005", "smithy-rs#2028", "smithy-rs#2034", "smithy-rs#2036"] meta = { "breaking" = true, "tada" = true, "bug" = false, "target" = "server" } author = "david-perez" diff --git a/codegen-core/common-test-models/constraints.smithy b/codegen-core/common-test-models/constraints.smithy index 322abf1d07..3e6ea4bf2e 100644 --- a/codegen-core/common-test-models/constraints.smithy +++ b/codegen-core/common-test-models/constraints.smithy @@ -21,6 +21,7 @@ service ConstraintsService { QueryParamsTargetingMapOfLengthStringOperation, QueryParamsTargetingMapOfListOfLengthStringOperation, QueryParamsTargetingMapOfSetOfLengthStringOperation, + QueryParamsTargetingMapOfLengthListOfPatternStringOperation, QueryParamsTargetingMapOfListOfEnumStringOperation, QueryParamsTargetingMapOfPatternStringOperation, @@ -101,6 +102,13 @@ operation QueryParamsTargetingMapOfSetOfLengthStringOperation { errors: [ValidationException] } +@http(uri: "/query-params-targeting-map-of-length-list-of-pattern-string-operation", method: "POST") +operation QueryParamsTargetingMapOfLengthListOfPatternStringOperation { + input: QueryParamsTargetingMapOfLengthListOfPatternStringOperationInputOutput, + output: QueryParamsTargetingMapOfLengthListOfPatternStringOperationInputOutput, + errors: [ValidationException] +} + @http(uri: "/query-params-targeting-map-of-list-of-enum-string-operation", method: "POST") operation QueryParamsTargetingMapOfListOfEnumStringOperation { input: QueryParamsTargetingMapOfListOfEnumStringOperationInputOutput, @@ -225,8 +233,16 @@ structure ConstrainedHttpBoundShapesOperationInputOutput { // @httpHeader("X-Length-Set") // lengthStringSetHeader: SetOfLengthString, - @httpHeader("X-Length-List") - lengthStringListHeader: ListOfLengthString, + @httpHeader("X-List-Length-String") + listLengthStringHeader: ListOfLengthString, + + @httpHeader("X-Length-List-Pattern-String") + lengthListPatternStringHeader: LengthListOfPatternString, + + // 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. + // @httpHeader("X-Length-Set-Pattern-String") + // lengthSetPatternStringHeader: LengthSetOfPatternString, // 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. @@ -279,6 +295,9 @@ structure ConstrainedHttpBoundShapesOperationInputOutput { @httpQuery("lengthStringList") lengthStringListQuery: ListOfLengthString, + @httpQuery("lengthListPatternString") + lengthListPatternStringQuery: LengthListOfPatternString, + // 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. // @httpQuery("lengthStringSet") @@ -366,6 +385,11 @@ structure QueryParamsTargetingMapOfSetOfLengthStringOperationInputOutput { mapOfSetOfLengthString: MapOfSetOfLengthString } +structure QueryParamsTargetingMapOfLengthListOfPatternStringOperationInputOutput { + @httpQueryParams + mapOfLengthListOfPatternString: MapOfLengthListOfPatternString +} + structure QueryParamsTargetingMapOfListOfEnumStringOperationInputOutput { @httpQueryParams mapOfListOfEnumString: MapOfListOfEnumString @@ -501,6 +525,11 @@ structure ConA { // 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, + + lengthListOfPatternString: LengthListOfPatternString, + // 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. + // lengthSetOfPatternString: LengthSetOfPatternString, } map MapOfLengthString { @@ -561,6 +590,11 @@ map MapOfSetOfLengthString { value: ListOfLengthString } +map MapOfLengthListOfPatternString { + key: PatternString, + value: LengthListOfPatternString +} + // 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. // map MapOfSetOfRangeInteger { @@ -706,6 +740,11 @@ set SetOfLengthPatternString { member: LengthPatternString } +@length(min: 5, max: 9) +set LengthSetOfPatternString { + member: PatternString +} + list ListOfLengthString { member: LengthString } @@ -762,6 +801,11 @@ list ListOfLengthPatternString { member: LengthPatternString } +@length(min: 12, max: 39) +list LengthListOfPatternString { + member: PatternString +} + structure ConB { @required nice: String, diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt index 120798258f..f7172b271b 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt @@ -325,7 +325,23 @@ fun RustType.isCopy(): Boolean = when (this) { enum class Visibility { PRIVATE, PUBCRATE, - PUBLIC + PUBLIC; + + companion object { + fun publicIf(condition: Boolean, ifNot: Visibility): Visibility = + if (condition) { + PUBLIC + } else { + ifNot + } + } + + fun toRustQualifier(): String = + when (this) { + PRIVATE -> "" + PUBCRATE -> "pub(crate)" + PUBLIC -> "pub" + } } /** 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 d2e66a87aa..29ce9bced2 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 @@ -346,7 +346,7 @@ open class SymbolVisitor( override fun memberShape(shape: MemberShape): Symbol { val target = model.expectShape(shape.target) - // Handle boxing first so we end up with Option>, not Box>. + // Handle boxing first, so we end up with Option>, not Box>. return handleOptionality( handleRustBoxing(toSymbol(target), shape), shape, diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/http/HttpBindingGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/http/HttpBindingGenerator.kt index 8ff3880c17..1dfd208ace 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/http/HttpBindingGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/http/HttpBindingGenerator.kt @@ -590,7 +590,14 @@ class HttpBindingGenerator( renderErrorMessage: (String) -> Writable, ) { val loopVariable = ValueExpression.Reference(safeName("inner")) - rustBlock("for ${loopVariable.name} in ${value.asRef()}") { + val context = HeaderValueSerializationContext(value, shape) + for (customization in customizations) { + customization.section( + HttpBindingSection.BeforeRenderingHeaderValue(context), + )(this) + } + + rustBlock("for ${loopVariable.name} in ${context.valueExpression.asRef()}") { this.renderHeaderValue( headerName, loopVariable, diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/JsonSerializerGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/JsonSerializerGenerator.kt index c7805fc468..a599d9d0bf 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/JsonSerializerGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/JsonSerializerGenerator.kt @@ -62,8 +62,8 @@ sealed class JsonSerializerSection(name: String) : Section(name) { JsonSerializerSection("ServerError") /** Manipulate the serializer context for a map prior to it being serialized. **/ - data class BeforeIteratingOverMap(val shape: MapShape, val context: JsonSerializerGenerator.Context) : - JsonSerializerSection("BeforeIteratingOverMap") + data class BeforeIteratingOverMapOrCollection(val shape: Shape, val context: JsonSerializerGenerator.Context) : + JsonSerializerSection("BeforeIteratingOverMapOrCollection") /** Manipulate the serializer context for a non-null member prior to it being serialized. **/ data class BeforeSerializingNonNullMember(val shape: Shape, val context: JsonSerializerGenerator.MemberContext) : @@ -90,7 +90,7 @@ class JsonSerializerGenerator( private val jsonName: (MemberShape) -> String, private val customizations: List = listOf(), ) : StructuredDataSerializerGenerator { - data class Context( + data class Context( /** Expression that retrieves a JsonValueWriter from either a JsonObjectWriter or JsonArrayWriter */ val writerExpression: String, /** Expression representing the value to write to the JsonValueWriter */ @@ -455,6 +455,9 @@ class JsonSerializerGenerator( private fun RustWriter.serializeCollection(context: Context) { val itemName = safeName("item") + for (customization in customizations) { + customization.section(JsonSerializerSection.BeforeIteratingOverMapOrCollection(context.shape, context))(this) + } rustBlock("for $itemName in ${context.valueExpression.asRef()}") { serializeMember(MemberContext.collectionMember(context, itemName)) } @@ -464,7 +467,7 @@ class JsonSerializerGenerator( val keyName = safeName("key") val valueName = safeName("value") for (customization in customizations) { - customization.section(JsonSerializerSection.BeforeIteratingOverMap(context.shape, context))( + customization.section(JsonSerializerSection.BeforeIteratingOverMapOrCollection(context.shape, context))( this, ) } 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 c614e03007..8fc17fa232 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 @@ -30,6 +30,7 @@ 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.symbolBuilder import software.amazon.smithy.rust.codegen.core.util.hasTrait +import software.amazon.smithy.rust.codegen.core.util.orNull import software.amazon.smithy.rust.codegen.core.util.toPascalCase /** @@ -58,8 +59,8 @@ class ConstrainedShapeSymbolProvider( ) : WrappingSymbolProvider(base) { private val nullableIndex = NullableIndex.of(model) - private fun publicConstrainedSymbolForMapShape(shape: Shape): Symbol { - check(shape is MapShape) + private fun publicConstrainedSymbolForMapOrCollectionShape(shape: Shape): Symbol { + check(shape is MapShape || shape is CollectionShape) val rustType = RustType.Opaque(shape.contextName(serviceShape).toPascalCase()) return symbolBuilder(shape, rustType).locatedIn(ModelsModule).build() @@ -72,13 +73,13 @@ class ConstrainedShapeSymbolProvider( // (constraint trait precedence). val target = model.expectShape(shape.target) val targetSymbol = this.toSymbol(target) - // Handle boxing first so we end up with `Option>`, not `Box>`. + // Handle boxing first, so we end up with `Option>`, not `Box>`. handleOptionality(handleRustBoxing(targetSymbol, shape), shape, nullableIndex, base.config().nullabilityCheckMode) } is MapShape -> { if (shape.isDirectlyConstrained(base)) { check(shape.hasTrait()) { "Only the `length` constraint trait can be applied to maps" } - publicConstrainedSymbolForMapShape(shape) + publicConstrainedSymbolForMapOrCollectionShape(shape) } else { val keySymbol = this.toSymbol(shape.key) val valueSymbol = this.toSymbol(shape.value) @@ -89,11 +90,9 @@ class ConstrainedShapeSymbolProvider( } } is CollectionShape -> { - // TODO(https://github.com/awslabs/smithy-rs/issues/1401) Both arms return the same because we haven't - // implemented any constraint trait on collection shapes yet. if (shape.isDirectlyConstrained(base)) { - val inner = this.toSymbol(shape.member) - symbolBuilder(shape, RustType.Vec(inner.rustType())).addReference(inner).build() + check(constrainedCollectionCheck(shape)) { "Only the `length` constraint trait can be applied to lists" } + publicConstrainedSymbolForMapOrCollectionShape(shape) } else { val inner = this.toSymbol(shape.member) symbolBuilder(shape, RustType.Vec(inner.rustType())).addReference(inner).build() @@ -112,4 +111,16 @@ class ConstrainedShapeSymbolProvider( else -> base.toSymbol(shape) } } + + /** + * Checks that the collection: + * - Has at least 1 supported constraint applied to it, and + * - That it has no unsupported constraints applied. + */ + private fun constrainedCollectionCheck(shape: CollectionShape): Boolean { + val supportedConstraintTraits = supportedCollectionConstraintTraits.mapNotNull { shape.getTrait(it).orNull() }.toSet() + val allConstraintTraits = allConstraintTraits.mapNotNull { shape.getTrait(it).orNull() }.toSet() + + return supportedConstraintTraits.isNotEmpty() && allConstraintTraits.subtract(supportedConstraintTraits).isEmpty() + } } 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 c90e28384f..8544fc1eb1 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 @@ -25,8 +25,8 @@ 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.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.isOptional import software.amazon.smithy.rust.codegen.core.util.UNREACHABLE import software.amazon.smithy.rust.codegen.core.util.hasTrait @@ -40,14 +40,27 @@ import software.amazon.smithy.rust.codegen.core.util.hasTrait * we support it or not_. */ fun Shape.hasConstraintTrait() = - hasTrait() || - hasTrait() || - hasTrait() || - hasTrait() || - hasTrait() || - hasTrait() + allConstraintTraits.any(this::hasTrait) -val supportedStringConstraintTraits: List> = listOf(LengthTrait::class.java, PatternTrait::class.java) +val allConstraintTraits = setOf( + LengthTrait::class.java, + PatternTrait::class.java, + RangeTrait::class.java, + UniqueItemsTrait::class.java, + EnumTrait::class.java, + RequiredTrait::class.java, +) + +val supportedStringConstraintTraits = setOf(LengthTrait::class.java, PatternTrait::class.java) + +/** + * Supported constraint traits for the `list` and `set` shapes. + */ +val supportedCollectionConstraintTraits = setOf( + LengthTrait::class.java, + // TODO(https://github.com/awslabs/smithy-rs/issues/1670): Not yet supported. + // UniqueItemsTrait::class.java +) /** * We say a shape is _directly_ constrained if: @@ -75,6 +88,7 @@ fun Shape.isDirectlyConstrained(symbolProvider: SymbolProvider): Boolean = when is MapShape -> this.hasTrait() is StringShape -> this.hasTrait() || supportedStringConstraintTraits.any { this.hasTrait(it) } + is CollectionShape -> supportedCollectionConstraintTraits.any { this.hasTrait(it) } is IntegerShape, is ShortShape, is LongShape, is ByteShape -> this.hasTrait() else -> false } @@ -99,6 +113,7 @@ fun MemberShape.targetCanReachConstrainedShape(model: Model, symbolProvider: Sym model.expectShape(this.target).canReachConstrainedShape(model, symbolProvider) fun Shape.hasPublicConstrainedWrapperTupleType(model: Model, publicConstrainedTypes: Boolean): Boolean = when (this) { + is CollectionShape -> publicConstrainedTypes && supportedCollectionConstraintTraits.any(this::hasTrait) is MapShape -> publicConstrainedTypes && this.hasTrait() is StringShape -> !this.hasTrait() && (publicConstrainedTypes && supportedStringConstraintTraits.any(this::hasTrait)) is IntegerShape, is ShortShape, is LongShape, is ByteShape -> publicConstrainedTypes && this.hasTrait() 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 bb818eaf07..3674185520 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 @@ -108,7 +108,7 @@ class PubCrateConstrainedShapeSymbolProvider( base.toSymbol(shape) } else { val targetSymbol = this.toSymbol(targetShape) - // Handle boxing first so we end up with `Option>`, not `Box>`. + // Handle boxing first, so we end up with `Option>`, not `Box>`. handleOptionality( handleRustBoxing(targetSymbol, shape), shape, 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 23d3e6f0f0..07009c3eb9 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 @@ -46,6 +46,9 @@ import software.amazon.smithy.rust.codegen.core.smithy.transformers.RecursiveSha import software.amazon.smithy.rust.codegen.core.util.CommandFailed import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.core.util.runCommand +import software.amazon.smithy.rust.codegen.server.smithy.generators.CollectionConstraintViolationGenerator +import software.amazon.smithy.rust.codegen.server.smithy.generators.CollectionTraitInfo +import software.amazon.smithy.rust.codegen.server.smithy.generators.ConstrainedCollectionGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.ConstrainedMapGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.ConstrainedNumberGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.ConstrainedStringGenerator @@ -281,26 +284,47 @@ open class ServerCodegenVisitor( override fun setShape(shape: SetShape) = collectionShape(shape) private fun collectionShape(shape: CollectionShape) { - if (shape.isReachableFromOperationInput() && shape.canReachConstrainedShape( + val renderUnconstrainedList = + shape.isReachableFromOperationInput() && shape.canReachConstrainedShape( model, codegenContext.symbolProvider, ) - ) { + val isDirectlyConstrained = shape.isDirectlyConstrained(codegenContext.symbolProvider) + + if (renderUnconstrainedList) { logger.info("[rust-server-codegen] Generating an unconstrained type for collection shape $shape") - rustCrate.withModule(UnconstrainedModule) unconstrainedModuleWriter@{ - rustCrate.withModule(ModelsModule) modelsModuleWriter@{ - UnconstrainedCollectionGenerator( - codegenContext, - this@unconstrainedModuleWriter, - this@modelsModuleWriter, - shape, - ).render() + rustCrate.withModule(UnconstrainedModule) { + UnconstrainedCollectionGenerator( + codegenContext, + this, + shape, + ).render() + } + + if (!isDirectlyConstrained) { + logger.info("[rust-server-codegen] Generating a constrained type for collection shape $shape") + rustCrate.withModule(ConstrainedModule) { + PubCrateConstrainedCollectionGenerator(codegenContext, this, shape).render() } } + } - logger.info("[rust-server-codegen] Generating a constrained type for collection shape $shape") - rustCrate.withModule(ConstrainedModule) { - PubCrateConstrainedCollectionGenerator(codegenContext, this, shape).render() + val constraintsInfo = CollectionTraitInfo.fromShape(shape) + if (isDirectlyConstrained) { + rustCrate.withModule(ModelsModule) { + ConstrainedCollectionGenerator( + codegenContext, + this, + shape, + constraintsInfo, + if (renderUnconstrainedList) codegenContext.unconstrainedShapeSymbolProvider.toSymbol(shape) else null, + ).render() + } + } + + if (isDirectlyConstrained || renderUnconstrainedList) { + rustCrate.withModule(ModelsModule) { + CollectionConstraintViolationGenerator(codegenContext, this, shape, constraintsInfo).render() } } } @@ -311,13 +335,15 @@ open class ServerCodegenVisitor( model, codegenContext.symbolProvider, ) + val isDirectlyConstrained = shape.isDirectlyConstrained(codegenContext.symbolProvider) + if (renderUnconstrainedMap) { logger.info("[rust-server-codegen] Generating an unconstrained type for map $shape") rustCrate.withModule(UnconstrainedModule) { UnconstrainedMapGenerator(codegenContext, this, shape).render() } - if (!shape.isDirectlyConstrained(codegenContext.symbolProvider)) { + if (!isDirectlyConstrained) { logger.info("[rust-server-codegen] Generating a constrained type for map $shape") rustCrate.withModule(ConstrainedModule) { PubCrateConstrainedMapGenerator(codegenContext, this, shape).render() @@ -325,7 +351,6 @@ open class ServerCodegenVisitor( } } - val isDirectlyConstrained = shape.isDirectlyConstrained(codegenContext.symbolProvider) if (isDirectlyConstrained) { rustCrate.withModule(ModelsModule) { ConstrainedMapGenerator( 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 5beb183c81..3da3129387 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 @@ -162,7 +162,7 @@ class UnconstrainedShapeSymbolProvider( if (shape.targetCanReachConstrainedShape(model, base)) { val targetShape = model.expectShape(shape.target) val targetSymbol = this.toSymbol(targetShape) - // Handle boxing first so we end up with `Option>`, not `Box>`. + // Handle boxing first, so we end up with `Option>`, not `Box>`. handleOptionality( handleRustBoxing(targetSymbol, shape), shape, 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 8155b1d8ab..6f48632df0 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 @@ -9,7 +9,6 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.neighbor.Walker import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.ByteShape -import software.amazon.smithy.model.shapes.CollectionShape import software.amazon.smithy.model.shapes.EnumShape import software.amazon.smithy.model.shapes.IntegerShape import software.amazon.smithy.model.shapes.LongShape @@ -21,15 +20,14 @@ import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.shapes.ShortShape import software.amazon.smithy.model.shapes.UnionShape -import software.amazon.smithy.model.traits.EnumTrait 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.StreamingTrait import software.amazon.smithy.model.traits.Trait import software.amazon.smithy.model.traits.UniqueItemsTrait import software.amazon.smithy.rust.codegen.core.util.expectTrait +import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.core.util.inputShape import software.amazon.smithy.rust.codegen.core.util.orNull @@ -92,7 +90,7 @@ private sealed class UnsupportedConstraintMessageKind { ), ) - is UnsupportedLengthTraitOnCollectionOrOnBlobShape -> LogMessage( + is UnsupportedLengthTraitOnBlobShape -> LogMessage( level, buildMessageShapeHasUnsupportedConstraintTrait(shape, lengthTrait, constraintTraitsUberIssue), ) @@ -123,7 +121,7 @@ private data class UnsupportedLengthTraitOnStreamingBlobShape( val streamingTrait: StreamingTrait, ) : UnsupportedConstraintMessageKind() -private data class UnsupportedLengthTraitOnCollectionOrOnBlobShape(val shape: Shape, val lengthTrait: LengthTrait) : +private data class UnsupportedLengthTraitOnBlobShape(val shape: Shape, val lengthTrait: LengthTrait) : UnsupportedConstraintMessageKind() private data class UnsupportedRangeTraitOnShape(val shape: Shape, val rangeTrait: RangeTrait) : @@ -135,14 +133,6 @@ private data class UnsupportedUniqueItemsTraitOnShape(val shape: Shape, val uniq data class LogMessage(val level: Level, val message: String) data class ValidationResult(val shouldAbort: Boolean, val messages: List) -private val allConstraintTraits = setOf( - LengthTrait::class.java, - PatternTrait::class.java, - RangeTrait::class.java, - UniqueItemsTrait::class.java, - EnumTrait::class.java, - RequiredTrait::class.java, -) private val unsupportedConstraintsOnMemberShapes = allConstraintTraits - RequiredTrait::class.java fun validateOperationsWithConstrainedInputHaveValidationExceptionAttached( @@ -233,14 +223,15 @@ fun validateUnsupportedConstraints( .map { (shape, trait) -> UnsupportedConstraintOnShapeReachableViaAnEventStream(shape, trait) } .toSet() - // 4. Length trait on collection shapes or on blob shapes is used. It has not been implemented yet for these target types. + // 4. Length trait on blob shapes is used. It has not been implemented yet. // TODO(https://github.com/awslabs/smithy-rs/issues/1401) - val unsupportedLengthTraitOnCollectionOrOnBlobShapeSet = walker + val unsupportedLengthTraitOnBlobShapeSet = walker .walkShapes(service) .asSequence() - .filter { it is CollectionShape || it is BlobShape } - .filter { it.hasTrait() } - .map { UnsupportedLengthTraitOnCollectionOrOnBlobShape(it, it.expectTrait()) } + .filterIsInstance() + .mapNotNull { + it.getTrait()?.let { trait -> UnsupportedLengthTraitOnBlobShape(it, trait) } + } .toSet() // 5. Range trait used on unsupported shapes. @@ -271,7 +262,7 @@ fun validateUnsupportedConstraints( unsupportedConstraintOnMemberShapeSet.map { it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints) } + unsupportedLengthTraitOnStreamingBlobShapeSet.map { it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints) } + unsupportedConstraintOnShapeReachableViaAnEventStreamSet.map { it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints) } + - unsupportedLengthTraitOnCollectionOrOnBlobShapeSet.map { it.intoLogMessage(codegenConfig.ignoreUnsupportedConstraints) } + + unsupportedLengthTraitOnBlobShapeSet.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/customizations/BeforeIteratingOverMapJsonCustomization.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/BeforeIteratingOverMapOrCollectionJsonCustomization.kt similarity index 67% rename from codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/BeforeIteratingOverMapJsonCustomization.kt rename to codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/BeforeIteratingOverMapOrCollectionJsonCustomization.kt index 9fad74c044..8dabd4ac02 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/BeforeIteratingOverMapJsonCustomization.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/BeforeIteratingOverMapOrCollectionJsonCustomization.kt @@ -5,8 +5,9 @@ package software.amazon.smithy.rust.codegen.server.smithy.customizations +import software.amazon.smithy.model.shapes.CollectionShape +import software.amazon.smithy.model.shapes.MapShape 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.writable import software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize.JsonSerializerCustomization import software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize.JsonSerializerSection @@ -15,12 +16,14 @@ import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.workingWithPublicConstrainedWrapperTupleType /** - * A customization to, just before we iterate over a _constrained_ map shape in a JSON serializer, unwrap the wrapper - * newtype and take a shared reference to the actual `std::collections::HashMap` within it. + * A customization to, just before we iterate over a _constrained_ map or collection shape in a JSON serializer, + * unwrap the wrapper newtype and take a shared reference to the actual value within it. + * That value will be a `std::collections::HashMap` for map shapes, and a `std::vec::Vec` for collection shapes. */ -class BeforeIteratingOverMapJsonCustomization(private val codegenContext: ServerCodegenContext) : JsonSerializerCustomization() { +class BeforeIteratingOverMapOrCollectionJsonCustomization(private val codegenContext: ServerCodegenContext) : JsonSerializerCustomization() { override fun section(section: JsonSerializerSection): Writable = when (section) { - is JsonSerializerSection.BeforeIteratingOverMap -> writable { + is JsonSerializerSection.BeforeIteratingOverMapOrCollection -> writable { + check(section.shape is CollectionShape || section.shape is MapShape) if (workingWithPublicConstrainedWrapperTupleType( section.shape, codegenContext.model, diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/CollectionConstraintViolationGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/CollectionConstraintViolationGenerator.kt new file mode 100644 index 0000000000..7867b045c4 --- /dev/null +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/CollectionConstraintViolationGenerator.kt @@ -0,0 +1,106 @@ +/* + * 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.model.shapes.CollectionShape +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.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.PubCrateConstraintViolationSymbolProvider +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.traits.isReachableFromOperationInput + +class CollectionConstraintViolationGenerator( + codegenContext: ServerCodegenContext, + private val modelsModuleWriter: RustWriter, + private val shape: CollectionShape, + private val constraintsInfo: List, +) { + private val model = codegenContext.model + private val symbolProvider = codegenContext.symbolProvider + private val publicConstrainedTypes = codegenContext.settings.codegenConfig.publicConstrainedTypes + private val constraintViolationSymbolProvider = + with(codegenContext.constraintViolationSymbolProvider) { + if (publicConstrainedTypes) { + this + } else { + PubCrateConstraintViolationSymbolProvider(this) + } + } + + fun render() { + val memberShape = model.expectShape(shape.member.target) + val constraintViolationSymbol = constraintViolationSymbolProvider.toSymbol(shape) + val constraintViolationName = constraintViolationSymbol.name + val isMemberConstrained = memberShape.canReachConstrainedShape(model, symbolProvider) + val constraintViolationVisibility = Visibility.publicIf(publicConstrainedTypes, Visibility.PUBCRATE) + + modelsModuleWriter.withInlineModule(constraintViolationSymbol.module()) { + val constraintViolationVariants = constraintsInfo.map { it.constraintViolationVariant }.toMutableList() + if (isMemberConstrained) { + constraintViolationVariants += { + rustTemplate( + """ + /// Constraint violation error when an element doesn't satisfy its own constraints. + /// The first component of the tuple is the index in the collection where the + /// first constraint violation was found. + ##[doc(hidden)] + Member(usize, #{MemberConstraintViolationSymbol}) + """, + "MemberConstraintViolationSymbol" to constraintViolationSymbolProvider.toSymbol(memberShape), + ) + } + } + + // 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 collection shape, for use by the user. + // The other one will have variants if the shape's member is directly or transitively constrained, + // and is for use by the framework. + rustTemplate( + """ + ##[derive(Debug, PartialEq)] + ${constraintViolationVisibility.toRustQualifier()} enum $constraintViolationName { + #{ConstraintViolationVariants:W} + } + """, + "ConstraintViolationVariants" to constraintViolationVariants.join(",\n"), + ) + + if (shape.isReachableFromOperationInput()) { + val validationExceptionFields = constraintsInfo.map { it.asValidationExceptionField }.toMutableList() + if (isMemberConstrained) { + validationExceptionFields += { + rust( + """ + Self::Member(index, member_constraint_violation) => + member_constraint_violation.as_validation_exception_field(path + "/" + &index.to_string()) + """, + ) + } + } + + rustTemplate( + """ + impl $constraintViolationName { + pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { + match self { + #{AsValidationExceptionFields:W} + } + } + } + """, + "String" to RuntimeType.String, + "AsValidationExceptionFields" to validationExceptionFields.join("\n"), + ) + } + } + } +} diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGenerator.kt new file mode 100644 index 0000000000..857b6aa829 --- /dev/null +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGenerator.kt @@ -0,0 +1,230 @@ +/* + * 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.model.shapes.CollectionShape +import software.amazon.smithy.model.traits.LengthTrait +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.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.documentShape +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.rustTemplate +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.util.PANIC +import software.amazon.smithy.rust.codegen.core.util.orNull +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.supportedCollectionConstraintTraits +import software.amazon.smithy.rust.codegen.server.smithy.validationErrorMessage + +/** + * [ConstrainedCollectionGenerator] generates a wrapper tuple newtype holding a constrained `std::vec::Vec`. + * This type can be built from unconstrained values, yielding a `ConstraintViolation` when the input does not satisfy + * the constraints. + * + * The [`length`] and [`uniqueItems`] traits are the only constraint traits applicable to list shapes. + * TODO(https://github.com/awslabs/smithy-rs/issues/1401): + * The [`uniqueItems`] trait has not been implemented yet. + * + * If [unconstrainedSymbol] is provided, the `MaybeConstrained` trait is implemented for the constrained type, using the + * [unconstrainedSymbol]'s associated type as the associated type for the trait. + * + * [`length`]: https://smithy.io/2.0/spec/constraint-traits.html#length-trait + * [`uniqueItems`]: https://smithy.io/2.0/spec/constraint-traits.html#smithy-api-uniqueitems-trait + */ +class ConstrainedCollectionGenerator( + val codegenContext: ServerCodegenContext, + val writer: RustWriter, + val shape: CollectionShape, + private val constraintsInfo: List, + private val unconstrainedSymbol: Symbol? = null, +) { + private val model = codegenContext.model + private val constrainedShapeSymbolProvider = codegenContext.constrainedShapeSymbolProvider + private val publicConstrainedTypes = codegenContext.settings.codegenConfig.publicConstrainedTypes + private val constraintViolationSymbolProvider = + with(codegenContext.constraintViolationSymbolProvider) { + if (publicConstrainedTypes) { + this + } else { + PubCrateConstraintViolationSymbolProvider(this) + } + } + private val symbolProvider = codegenContext.symbolProvider + + fun render() { + check(constraintsInfo.isNotEmpty()) { + "`ConstrainedCollectionGenerator` can only be invoked for constrained collections, but this shape was not constrained" + } + + val name = constrainedShapeSymbolProvider.toSymbol(shape).name + val inner = "std::vec::Vec<#{ValueSymbol}>" + val constraintViolation = constraintViolationSymbolProvider.toSymbol(shape) + val constrainedTypeVisibility = Visibility.publicIf(publicConstrainedTypes, Visibility.PUBCRATE) + val constrainedTypeMetadata = RustMetadata( + Attribute.Derives(setOf(RuntimeType.Debug, RuntimeType.Clone, RuntimeType.PartialEq)), + visibility = constrainedTypeVisibility, + ) + + val codegenScope = arrayOf( + "ValueSymbol" to constrainedShapeSymbolProvider.toSymbol(model.expectShape(shape.member.target)), + "From" to RuntimeType.From, + "TryFrom" to RuntimeType.TryFrom, + "ConstraintViolation" to constraintViolation, + ) + + writer.documentShape(shape, model, note = rustDocsNote(name)) + constrainedTypeMetadata.render(writer) + writer.rustTemplate( + """ + struct $name(pub(crate) $inner); + """, + *codegenScope, + ) + if (constrainedTypeVisibility == Visibility.PUBCRATE) { + Attribute.AllowUnused.render(writer) + } + + writer.rustTemplate( + """ + impl $name { + /// ${rustDocsInnerMethod(inner)} + pub fn inner(&self) -> &$inner { + &self.0 + } + + /// ${rustDocsIntoInnerMethod(inner)} + pub fn into_inner(self) -> $inner { + self.0 + } + + #{ValidationFunctions:W} + } + + impl #{TryFrom}<$inner> for $name { + type Error = #{ConstraintViolation}; + + /// ${rustDocsTryFromMethod(name, inner)} + fn try_from(value: $inner) -> Result { + #{ConstraintChecks:W} + + Ok(Self(value)) + } + } + + impl #{From}<$name> for $inner { + fn from(value: $name) -> Self { + value.into_inner() + } + } + """, + *codegenScope, + "ConstraintChecks" to constraintsInfo.map { it.tryFromCheck }.join("\n"), + "ValidationFunctions" to constraintsInfo.map { it.validationFunctionDefinition(constraintViolation, inner) }.join("\n"), + ) + + if (!publicConstrainedTypes && isValueConstrained(shape, model, symbolProvider)) { + writer.rustTemplate( + """ + impl #{From}<$name> for #{FullyUnconstrainedSymbol} { + fn from(value: $name) -> Self { + value + .into_inner() + .into_iter() + .map(|v| v.into()) + .collect() + } + } + """, + *codegenScope, + "FullyUnconstrainedSymbol" to symbolProvider.toSymbol(shape), + ) + } + + if (unconstrainedSymbol != null) { + writer.rustTemplate( + """ + impl #{ConstrainedTrait} for $name { + type Unconstrained = #{UnconstrainedSymbol}; + } + """, + "ConstrainedTrait" to RuntimeType.ConstrainedTrait(), + "UnconstrainedSymbol" to unconstrainedSymbol, + ) + } + } +} + +internal sealed class CollectionTraitInfo { + data class Length(val lengthTrait: LengthTrait) : CollectionTraitInfo() { + override fun toTraitInfo(): TraitInfo = + TraitInfo( + tryFromCheck = { + rust("Self::check_length(value.len())?;") + }, + constraintViolationVariant = { + docs("Constraint violation error when the vector doesn't have the required length") + rust("Length(usize)") + }, + asValidationExceptionField = { + rust( + """ + Self::Length(length) => crate::model::ValidationExceptionField { + message: format!("${lengthTrait.validationErrorMessage()}", length, &path), + path, + }, + """, + ) + }, + validationFunctionDefinition = { constraintViolation, _ -> + { + rustTemplate( + """ + fn check_length(length: usize) -> Result<(), #{ConstraintViolation}> { + if ${lengthTrait.rustCondition("length")} { + Ok(()) + } else { + Err(#{ConstraintViolation}::Length(length)) + } + } + """, + "ConstraintViolation" to constraintViolation, + ) + } + }, + ) + } + + companion object { + private fun fromTrait(trait: Trait): CollectionTraitInfo = + when (trait) { + is LengthTrait -> { + Length(trait) + } + // TODO(https://github.com/awslabs/smithy-rs/issues/1670): Not implemented yet. + // is UniqueItemsTrait -> { + // UniqueItems(trait) + // } + else -> { + PANIC("CollectionTraitInfo.fromTrait called with unsupported trait $trait") + } + } + + fun fromShape(shape: CollectionShape): List = + supportedCollectionConstraintTraits + .mapNotNull { shape.getTrait(it).orNull() } + .map(CollectionTraitInfo::fromTrait) + .map(CollectionTraitInfo::toTraitInfo) + } + + abstract fun toTraitInfo(): TraitInfo +} diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedMapGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedMapGenerator.kt index 677350dd67..69cfafc698 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedMapGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedMapGenerator.kt @@ -58,19 +58,7 @@ class ConstrainedMapGenerator( val inner = "std::collections::HashMap<#{KeySymbol}, #{ValueSymbol}>" 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 { - Visibility.PUBCRATE - } + val constrainedTypeVisibility = Visibility.publicIf(publicConstrainedTypes, Visibility.PUBCRATE) val constrainedTypeMetadata = RustMetadata( Attribute.Derives(setOf(RuntimeType.Debug, RuntimeType.Clone, RuntimeType.PartialEq)), visibility = constrainedTypeVisibility, @@ -97,27 +85,27 @@ class ConstrainedMapGenerator( pub fn inner(&self) -> &$inner { &self.0 } - + /// ${rustDocsIntoInnerMethod(inner)} pub fn into_inner(self) -> $inner { self.0 } } - + impl #{TryFrom}<$inner> for $name { type Error = #{ConstraintViolation}; - + /// ${rustDocsTryFromMethod(name, inner)} fn try_from(value: $inner) -> Result { let length = value.len(); - if $condition { + if ${lengthTrait.rustCondition("length")} { Ok(Self(value)) } else { Err(#{ConstraintViolation}::Length(length)) } } } - + impl #{From}<$name> for $inner { fn from(value: $name) -> Self { value.into_inner() 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 38d1a2acc7..6bf017b61f 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 @@ -198,21 +198,14 @@ private data class Length(val lengthTrait: LengthTrait) : StringTraitInfo() { * Renders a `check_length` function to validate the string matches the * required length indicated by the `@length` trait. */ + @Suppress("UNUSED_PARAMETER") private fun renderValidationFunction(constraintViolation: Symbol, unconstrainedTypeName: String): Writable = { - 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( """ fn check_length(string: &str) -> Result<(), $constraintViolation> { let length = string.chars().count(); - if $condition { + if ${lengthTrait.rustCondition("length")} { Ok(()) } else { Err($constraintViolation::Length(length)) diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/LenghTraitCommon.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/LenghTraitCommon.kt new file mode 100644 index 0000000000..bc074126ee --- /dev/null +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/LenghTraitCommon.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.model.traits.LengthTrait + +fun LengthTrait.rustCondition(lengthVariable: String): String { + val condition = if (min.isPresent && max.isPresent) { + "(${min.get()}..=${max.get()}).contains(&$lengthVariable)" + } else if (min.isPresent) { + "${min.get()} <= $lengthVariable" + } else { + "$lengthVariable <= ${max.get()}" + } + + return condition +} 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 33a33ecff0..b337c6f629 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,6 +6,7 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import software.amazon.smithy.model.shapes.CollectionShape +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.rustTemplate import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType @@ -13,8 +14,9 @@ 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.UnconstrainedShapeSymbolProvider import software.amazon.smithy.rust.codegen.server.smithy.canReachConstrainedShape -import software.amazon.smithy.rust.codegen.server.smithy.traits.isReachableFromOperationInput +import software.amazon.smithy.rust.codegen.server.smithy.isDirectlyConstrained /** * Generates a Rust type for a constrained collection shape that is able to hold values for the corresponding @@ -30,7 +32,6 @@ import software.amazon.smithy.rust.codegen.server.smithy.traits.isReachableFromO class UnconstrainedCollectionGenerator( val codegenContext: ServerCodegenContext, private val unconstrainedModuleWriter: RustWriter, - private val modelsModuleWriter: RustWriter, val shape: CollectionShape, ) { private val model = codegenContext.model @@ -46,6 +47,12 @@ class UnconstrainedCollectionGenerator( PubCrateConstraintViolationSymbolProvider(this) } } + private val constrainedShapeSymbolProvider = codegenContext.constrainedShapeSymbolProvider + private val constrainedSymbol = if (shape.isDirectlyConstrained(symbolProvider)) { + constrainedShapeSymbolProvider.toSymbol(shape) + } else { + pubCrateConstrainedShapeSymbolProvider.toSymbol(shape) + } fun render() { check(shape.canReachConstrainedShape(model, symbolProvider)) @@ -54,9 +61,7 @@ class UnconstrainedCollectionGenerator( val name = symbol.name val innerShape = model.expectShape(shape.member.target) val innerUnconstrainedSymbol = unconstrainedShapeSymbolProvider.toSymbol(innerShape) - val constrainedSymbol = pubCrateConstrainedShapeSymbolProvider.toSymbol(shape) val constraintViolationSymbol = constraintViolationSymbolProvider.toSymbol(shape) - val constraintViolationName = constraintViolationSymbol.name val innerConstraintViolationSymbol = constraintViolationSymbolProvider.toSymbol(innerShape) unconstrainedModuleWriter.withInlineModule(symbol.module()) { @@ -82,7 +87,7 @@ class UnconstrainedCollectionGenerator( .map(|(idx, inner)| inner.try_into().map_err(|inner_violation| (idx, inner_violation))) .collect(); res.map(Self) - .map_err(|(idx, inner_violation)| #{ConstraintViolationSymbol}(idx, inner_violation)) + .map_err(|(idx, inner_violation)| #{ConstraintViolationSymbol}::Member(idx, inner_violation)) } } """, @@ -94,33 +99,5 @@ class UnconstrainedCollectionGenerator( "TryFrom" to RuntimeType.TryFrom, ) } - - 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( - """ - ##[derive(Debug, PartialEq)] - pub struct $constraintViolationName( - pub(crate) usize, - pub(crate) #{InnerConstraintViolationSymbol} - ); - """, - "InnerConstraintViolationSymbol" to innerConstraintViolationSymbol, - ) - - if (shape.isReachableFromOperationInput()) { - rustTemplate( - """ - impl $constraintViolationName { - pub(crate) fn as_validation_exception_field(self, path: #{String}) -> crate::model::ValidationExceptionField { - self.1.as_validation_exception_field(format!("{}/{}", path, &self.0)) - } - } - """, - "String" to RuntimeType.String, - ) - } - } } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/http/ServerResponseBindingGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/http/ServerResponseBindingGenerator.kt index ae564c9c86..e30d9cc633 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/http/ServerResponseBindingGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/http/ServerResponseBindingGenerator.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators.http import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.shapes.ByteShape +import software.amazon.smithy.model.shapes.CollectionShape import software.amazon.smithy.model.shapes.IntegerShape import software.amazon.smithy.model.shapes.LongShape import software.amazon.smithy.model.shapes.OperationShape @@ -88,16 +89,18 @@ class ServerResponseBeforeRenderingHeadersHttpBindingCustomization(val codegenCo HttpBindingCustomization() { override fun section(section: HttpBindingSection): Writable = when (section) { is HttpBindingSection.BeforeRenderingHeaderValue -> writable { - if (workingWithPublicConstrainedWrapperTupleType( - section.context.shape, - codegenContext.model, - codegenContext.settings.codegenConfig.publicConstrainedTypes, - ) - ) { - if (section.context.shape is IntegerShape || section.context.shape is ShortShape || section.context.shape is LongShape || section.context.shape is ByteShape) { - section.context.valueExpression = - ValueExpression.Reference("&${section.context.valueExpression.name.removePrefix("&")}.0") - } + val isIntegral = section.context.shape is ByteShape || section.context.shape is ShortShape || section.context.shape is IntegerShape || section.context.shape is LongShape + val isCollection = section.context.shape is CollectionShape + + val workingWithPublicWrapper = workingWithPublicConstrainedWrapperTupleType( + section.context.shape, + codegenContext.model, + codegenContext.settings.codegenConfig.publicConstrainedTypes, + ) + + if (workingWithPublicWrapper && (isIntegral || isCollection)) { + section.context.valueExpression = + ValueExpression.Reference("&${section.context.valueExpression.name.removePrefix("&")}.0") } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerAwsJson.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerAwsJson.kt index ad50011ede..2a3467cd6b 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerAwsJson.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerAwsJson.kt @@ -21,7 +21,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize.JsonS import software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize.StructuredDataSerializerGenerator import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext -import software.amazon.smithy.rust.codegen.server.smithy.customizations.BeforeIteratingOverMapJsonCustomization +import software.amazon.smithy.rust.codegen.server.smithy.customizations.BeforeIteratingOverMapOrCollectionJsonCustomization import software.amazon.smithy.rust.codegen.server.smithy.customizations.BeforeSerializingMemberJsonCustomization import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerAwsJsonProtocol import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol @@ -95,7 +95,7 @@ class ServerAwsJsonSerializerGenerator( ::awsJsonFieldName, customizations = listOf( ServerAwsJsonError(awsJsonVersion), - BeforeIteratingOverMapJsonCustomization(codegenContext), + BeforeIteratingOverMapOrCollectionJsonCustomization(codegenContext), BeforeSerializingMemberJsonCustomization(codegenContext), ), ), diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerRestJson.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerRestJson.kt index a53bb363da..744ef93bc5 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerRestJson.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerRestJson.kt @@ -13,7 +13,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.protocols.restJsonFieldNa import software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize.JsonSerializerGenerator import software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize.StructuredDataSerializerGenerator import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext -import software.amazon.smithy.rust.codegen.server.smithy.customizations.BeforeIteratingOverMapJsonCustomization +import software.amazon.smithy.rust.codegen.server.smithy.customizations.BeforeIteratingOverMapOrCollectionJsonCustomization import software.amazon.smithy.rust.codegen.server.smithy.customizations.BeforeSerializingMemberJsonCustomization import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerRestJsonProtocol @@ -52,7 +52,7 @@ class ServerRestJsonSerializerGenerator( httpBindingResolver, ::restJsonFieldName, customizations = listOf( - BeforeIteratingOverMapJsonCustomization(codegenContext), + BeforeIteratingOverMapOrCollectionJsonCustomization(codegenContext), BeforeSerializingMemberJsonCustomization(codegenContext), ), ), 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 e75eaacb0b..6cc4f14230 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 @@ -151,29 +151,22 @@ internal class ValidateUnsupportedConstraintsAreNotUsedTest { } @Test - fun `it should detect when the length trait on collection shapes or on blob shapes is used`() { + fun `it should detect when the length trait on blob shapes is used`() { val model = """ $baseModel structure TestInputOutput { - collection: LengthCollection, blob: LengthBlob } - @length(min: 1) - list LengthCollection { - member: String - } - @length(min: 1) blob LengthBlob """.asSmithyModel() val validationResult = validateModel(model) - validationResult.messages shouldHaveSize 2 - validationResult.messages.forSome { it.message shouldContain "The list shape `test#LengthCollection` has the constraint trait `smithy.api#length` attached" } - validationResult.messages.forSome { it.message shouldContain "The blob shape `test#LengthBlob` has the constraint trait `smithy.api#length` attached" } + validationResult.messages shouldHaveSize 1 + validationResult.messages[0].message shouldContain "The blob shape `test#LengthBlob` has the constraint trait `smithy.api#length` attached" } @Test diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGeneratorTest.kt new file mode 100644 index 0000000000..f14ac17284 --- /dev/null +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ConstrainedCollectionGeneratorTest.kt @@ -0,0 +1,177 @@ +/* + * 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 io.kotest.matchers.string.shouldContain +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import org.junit.jupiter.params.provider.ArgumentsSource +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.node.ArrayNode +import software.amazon.smithy.model.shapes.CollectionShape +import software.amazon.smithy.model.shapes.ListShape +import software.amazon.smithy.model.shapes.SetShape +import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter +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.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.UNREACHABLE +import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.transformers.ShapesReachableFromOperationInputTagger +import java.util.stream.Stream + +@Suppress("DEPRECATION") +class ConstrainedCollectionGeneratorTest { + data class TestCase(val model: Model, val validList: ArrayNode, val invalidList: ArrayNode) + + class ConstrainedListGeneratorTestProvider : ArgumentsProvider { + private val testCases = listOf( + // Min and max. + Triple("@length(min: 11, max: 12)", 11, 13), + // Min equal to max. + Triple("@length(min: 11, max: 11)", 11, 12), + // Only min. + Triple("@length(min: 11)", 15, 10), + // Only max. + Triple("@length(max: 11)", 11, 12), + ).map { + val validList = List(it.second, Int::toString) + val invalidList = List(it.third, Int::toString) + + Triple(it.first, ArrayNode.fromStrings(validList), ArrayNode.fromStrings(invalidList)) + }.map { (trait, validList, invalidList) -> + TestCase( + """ + namespace test + + $trait + list ConstrainedList { + member: String + } + + $trait + set ConstrainedSet { + member: String + } + """.asSmithyModel().let(ShapesReachableFromOperationInputTagger::transform), + validList, + invalidList, + ) + } + + override fun provideArguments(context: ExtensionContext?): Stream = + testCases.map { Arguments.of(it) }.stream() + } + + @ParameterizedTest + @ArgumentsSource(ConstrainedListGeneratorTestProvider::class) + fun `it should generate constrained collection types`(testCase: TestCase) { + val constrainedListShape = testCase.model.lookup("test#ConstrainedList") + // 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. + // val constrainedSetShape = testCase.model.lookup("test#ConstrainedSet") + + val codegenContext = serverTestCodegenContext(testCase.model) + val symbolProvider = codegenContext.symbolProvider + + val project = TestWorkspace.testProject(symbolProvider) + + listOf(constrainedListShape /*, constrainedSetShape */).forEach { shape -> + val shapeName = when (shape) { + is ListShape -> "list" + is SetShape -> "set" + else -> UNREACHABLE("Shape is either list or set.") + } + + project.withModule(ModelsModule) { + render(codegenContext, this, shape) + + val instantiator = serverInstantiator(codegenContext) + rustBlock("##[cfg(test)] fn build_valid_$shapeName() -> std::vec::Vec") { + instantiator.render(this, shape, testCase.validList) + } + rustBlock("##[cfg(test)] fn build_invalid_$shapeName() -> std::vec::Vec") { + instantiator.render(this, shape, testCase.invalidList) + } + + unitTest( + name = "try_from_success", + test = """ + let $shapeName = build_valid_$shapeName(); + let _constrained: ConstrainedList = $shapeName.try_into().unwrap(); + """, + ) + unitTest( + name = "try_from_fail", + test = """ + let $shapeName = build_invalid_$shapeName(); + let constrained_res: Result = $shapeName.try_into(); + constrained_res.unwrap_err(); + """, + ) + unitTest( + name = "inner", + test = """ + let $shapeName = build_valid_$shapeName(); + let constrained = ConstrainedList::try_from($shapeName.clone()).unwrap(); + + assert_eq!(constrained.inner(), &$shapeName); + """, + ) + unitTest( + name = "into_inner", + test = """ + let $shapeName = build_valid_$shapeName(); + let constrained = ConstrainedList::try_from($shapeName.clone()).unwrap(); + + assert_eq!(constrained.into_inner(), $shapeName); + """, + ) + } + } + + project.compileAndTest() + } + + @Test + fun `type should not be constructible without using a constructor`() { + val model = """ + namespace test + + @length(min: 1, max: 69) + list ConstrainedList { + member: String + } + """.asSmithyModel().let(ShapesReachableFromOperationInputTagger::transform) + val constrainedCollectionShape = model.lookup("test#ConstrainedList") + + val writer = RustWriter.forModule(ModelsModule.name) + + val codegenContext = serverTestCodegenContext(model) + render(codegenContext, writer, constrainedCollectionShape) + + // Check that the wrapped type is `pub(crate)`. + writer.toString() shouldContain "pub struct ConstrainedList(pub(crate) std::vec::Vec);" + } + + private fun render( + codegenContext: ServerCodegenContext, + writer: RustWriter, + constrainedCollectionShape: CollectionShape, + ) { + val constraintsInfo = CollectionTraitInfo.fromShape(constrainedCollectionShape) + ConstrainedCollectionGenerator(codegenContext, writer, constrainedCollectionShape, constraintsInfo).render() + CollectionConstraintViolationGenerator(codegenContext, writer, constrainedCollectionShape, constraintsInfo).render() + } +} 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 50543d6726..6a02765c9a 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 @@ -65,9 +65,10 @@ class UnconstrainedCollectionGeneratorTest { UnconstrainedCollectionGenerator( codegenContext, this@unconstrainedModuleWriter, - this@modelsModuleWriter, it, ).render() + + CollectionConstraintViolationGenerator(codegenContext, this@modelsModuleWriter, it, listOf()).render() } this@unconstrainedModuleWriter.unitTest( @@ -79,7 +80,7 @@ class UnconstrainedCollectionGeneratorTest { 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( + crate::model::list_a::ConstraintViolation::Member(0, crate::model::list_b::ConstraintViolation::Member( 0, crate::model::structure_c::ConstraintViolation::MissingString, ));