Unverified Commit 52cb0fc8 authored by david-perez's avatar david-perez Committed by GitHub
Browse files

Add support for `@uniqueItems` (#2232)

This commit adds support for the `@uniqueItems` trait on `list` shapes
in server SDKs. Requests with duplicate values for `list` shapes
constrained with `@uniqueItems` will be rejected by servers.
parent 1e9e8774
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -56,3 +56,9 @@ let result = cache
references = ["smithy-rs#2246"]
meta = { "breaking" = false, "tada" = false, "bug" = false }
author = "ysaito1001"

[[smithy-rs]]
message = "The [`@uniqueItems`](https://smithy.io/2.0/spec/constraint-traits.html#uniqueitems-trait) trait on `list` shapes is now supported in server SDKs."
references = ["smithy-rs#2232", "smithy-rs#1670"]
meta = { "breaking" = false, "tada" = true, "bug" = false, "target" = "server"}
author = "david-perez"
+90 −144
Original line number Diff line number Diff line
@@ -227,10 +227,8 @@ structure ConstrainedHttpBoundShapesOperationInputOutput {
    // @httpHeader("X-Length-MediaType")
    // lengthStringHeaderWithMediaType: MediaTypeLengthString,

    // 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")
    // lengthStringSetHeader: SetOfLengthString,
    @httpHeader("X-Length-Set")
    lengthStringSetHeader: SetOfLengthString,

    @httpHeader("X-List-Length-String")
    listLengthStringHeader: ListOfLengthString,
@@ -238,34 +236,27 @@ structure ConstrainedHttpBoundShapesOperationInputOutput {
    @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.
    // @httpHeader("X-Range-Integer-Set")
    // rangeIntegerSetHeader: SetOfRangeInteger,
    // @httpHeader("X-Range-Short-Set")
    // rangeShortSetHeader: SetOfShortInteger,
    // @httpHeader("X-Range-Long-Set")
    // rangeLongSetHeader: SetOfRangeLong,
    // @httpHeader("X-Range-Byte-Set")
    // rangeByteSetHeader: SetOfByteInteger,
    @httpHeader("X-Length-Set-Pattern-String")
    lengthSetPatternStringHeader: LengthSetOfPatternString,

    @httpHeader("X-Range-Integer-List")
    rangeIntegerListHeader: ListOfRangeInteger,
    @httpHeader("X-Range-Byte-Set")
    rangeByteSetHeader: SetOfRangeByte,
    @httpHeader("X-Range-Short-Set")
    rangeShortSetHeader: SetOfRangeShort,
    @httpHeader("X-Range-Integer-Set")
    rangeIntegerSetHeader: SetOfRangeInteger,
    @httpHeader("X-Range-Long-Set")
    rangeLongSetHeader: SetOfRangeLong,

    @httpHeader("X-Range-Byte-List")
    rangeByteListHeader: ListOfRangeByte,
    @httpHeader("X-Range-Short-List")
    rangeShortListHeader: ListOfRangeShort,

    @httpHeader("X-Range-Integer-List")
    rangeIntegerListHeader: ListOfRangeInteger,
    @httpHeader("X-Range-Long-List")
    rangeLongListHeader: ListOfRangeLong,

    @httpHeader("X-Range-Byte-List")
    rangeByteListHeader: ListOfRangeByte,

    // TODO(https://github.com/awslabs/smithy-rs/issues/1431)
    // @httpHeader("X-Enum")
    //enumStringHeader: EnumString,
@@ -276,17 +267,15 @@ structure ConstrainedHttpBoundShapesOperationInputOutput {
    @httpQuery("lengthString")
    lengthStringQuery: LengthString,

    @httpQuery("rangeInteger")
    rangeIntegerQuery: RangeInteger,

    @httpQuery("rangeByte")
    rangeByteQuery: RangeByte,
    @httpQuery("rangeShort")
    rangeShortQuery: RangeShort,

    @httpQuery("rangeInteger")
    rangeIntegerQuery: RangeInteger,
    @httpQuery("rangeLong")
    rangeLongQuery: RangeLong,

    @httpQuery("rangeByte")
    rangeByteQuery: RangeByte,

    @httpQuery("enumString")
    enumStringQuery: EnumString,
@@ -297,33 +286,26 @@ structure ConstrainedHttpBoundShapesOperationInputOutput {
    @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")
    // lengthStringSetQuery: SetOfLengthString,

    @httpQuery("rangeIntegerList")
    rangeIntegerListQuery: ListOfRangeInteger,
    @httpQuery("lengthStringSet")
    lengthStringSetQuery: SetOfLengthString,

    @httpQuery("rangeByteList")
    rangeByteListQuery: ListOfRangeByte,
    @httpQuery("rangeShortList")
    rangeShortListQuery: ListOfRangeShort,

    @httpQuery("rangeIntegerList")
    rangeIntegerListQuery: ListOfRangeInteger,
    @httpQuery("rangeLongList")
    rangeLongListQuery: ListOfRangeLong,

    @httpQuery("rangeByteList")
    rangeByteListQuery: ListOfRangeByte,

    // 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("rangeIntegerSet")
    // rangeIntegerSetQuery: SetOfRangeInteger,
    // @httpQuery("rangeShortSet")
    // rangeShortSetQuery: SetOfRangeShort,
    // @httpQuery("rangeLongSet")
    // rangeLongSetQuery: SetOfRangeLong,
    // @httpQuery("rangeByteSet")
    // rangeByteSetQuery: SetOfRangeByte,
    @httpQuery("rangeByteSet")
    rangeByteSetQuery: SetOfRangeByte,
    @httpQuery("rangeShortSet")
    rangeShortSetQuery: SetOfRangeShort,
    @httpQuery("rangeIntegerSet")
    rangeIntegerSetQuery: SetOfRangeInteger,
    @httpQuery("rangeLongSet")
    rangeLongSetQuery: SetOfRangeLong,

    @httpQuery("enumStringList")
    enumStringListQuery: ListOfEnumString,
@@ -473,9 +455,7 @@ structure ConA {
    conBList: ConBList,
    lengthList: LengthList,

    // 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.
    // conBSet: ConBSet,
    conBSet: ConBSet,

    conBMap: ConBMap,
    lengthMap: LengthMap,
@@ -490,9 +470,7 @@ structure ConA {
    enumString: EnumString,

    listOfLengthString: ListOfLengthString,
    // 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.
    // setOfLengthString: SetOfLengthString,
    setOfLengthString: SetOfLengthString,
    mapOfLengthString: MapOfLengthString,

    listOfLengthBlob: ListOfLengthBlob,
@@ -502,27 +480,19 @@ structure ConA {
    mapOfLengthBlob: MapOfLengthBlob,

    listOfRangeInteger: ListOfRangeInteger,
    // 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.
    // setOfRangeInteger: SetOfRangeInteger,
    setOfRangeInteger: SetOfRangeInteger,
    mapOfRangeInteger: MapOfRangeInteger,

    listOfRangeShort: ListOfRangeShort,
    // 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.
    // setOfRangeShort: SetOfRangeShort,
    setOfRangeShort: SetOfRangeShort,
    mapOfRangeShort: MapOfRangeShort,

    listOfRangeLong: ListOfRangeLong,
    // 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.
    // setOfRangeLong: SetOfRangeLong,
    setOfRangeLong: SetOfRangeLong,
    mapOfRangeLong: MapOfRangeLong,

    listOfRangeByte: ListOfRangeByte,
    // 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.
    // setOfRangeByte: SetOfRangeByte,
    setOfRangeByte: SetOfRangeByte,
    mapOfRangeByte: MapOfRangeByte,

    nonStreamingBlob: NonStreamingBlob
@@ -530,27 +500,26 @@ structure ConA {
    patternString: PatternString,
    mapOfPatternString: MapOfPatternString,
    listOfPatternString: ListOfPatternString,
    // TODO(https://github.com/awslabs/smithy-rs/issues/1401): a `set` shape is
    //  just a `list` shape with `uniqueItems`, which hasn't been implemented yet.
    // setOfPatternString: SetOfPatternString,
    setOfPatternString: SetOfPatternString,

    lengthLengthPatternString: LengthPatternString,
    mapOfLengthPatternString: MapOfLengthPatternString,
    listOfLengthPatternString: ListOfLengthPatternString
    // TODO(https://github.com/awslabs/smithy-rs/issues/1401): a `set` shape is
    //  just a `list` shape with `uniqueItems`, which hasn't been implemented yet.
    // setOfLengthPatternString: SetOfLengthPatternString,
    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,
    lengthSetOfPatternString: LengthSetOfPatternString,
}

@uniqueItems
list UniqueItemsList {
    member: String
}

@sparse
map SparseMap {
    key: String,
    value: LengthString
    value: UniqueItemsList
}

@sparse
@@ -628,10 +597,7 @@ map MapOfListOfLengthPatternString {

map MapOfSetOfLengthString {
    key: LengthString,
    // 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.
    // value: SetOfLengthString,
    value: ListOfLengthString
    value: SetOfLengthString,
}

map MapOfLengthListOfPatternString {
@@ -639,33 +605,25 @@ map MapOfLengthListOfPatternString {
    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 {
//     key: String,
//     value: SetOfRangeInteger,
// }
map MapOfSetOfRangeInteger {
    key: String,
    value: SetOfRangeInteger,
}

// 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 MapOfSetOfRangeShort {
//     key: String,
//     value: SetOfRangeShort,
// }
map MapOfSetOfRangeShort {
    key: String,
    value: SetOfRangeShort,
}

// 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 MapOfSetOfRangeLong {
//     key: String,
//     value: SetOfRangeLong,
// }
map MapOfSetOfRangeLong {
    key: String,
    value: SetOfRangeLong,
}

// 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 MapOfSetOfRangeByte {
//     key: String,
//     value: SetOfRangeByte,
// }
map MapOfSetOfRangeByte {
    key: String,
    value: SetOfRangeByte,
}

@length(min: 2, max: 8)
list LengthListOfLengthString {
@@ -767,9 +725,7 @@ union ConstrainedUnion {

    constrainedStructure: ConB,
    conBList: ConBList,
    // 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.
    // conBSet: ConBSet,
    conBSet: ConBSet,
    conBMap: ConBMap,
}

@@ -814,45 +770,37 @@ list ListOfLengthString {
    member: LengthString
}

set SetOfRangeInteger {
    member: RangeInteger
}

list ListOfLengthBlob {
    member: LengthBlob
}

// 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.
// set SetOfRangeInteger {
//     member: RangeInteger
// }

list ListOfRangeInteger {
    member: RangeInteger
}

// 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.
// set SetOfRangeShort {
//     member: RangeShort
// }
set SetOfRangeShort {
    member: RangeShort
}

list ListOfRangeShort {
    member: RangeShort
}

// 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.
// set SetOfRangeLong {
//     member: RangeLong
// }
set SetOfRangeLong {
    member: RangeLong
}

list ListOfRangeLong {
    member: RangeLong
}

// 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.
// set SetOfRangeByte {
//     member: RangeByte
// }
set SetOfRangeByte {
    member: RangeByte
}

list ListOfRangeByte {
    member: RangeByte
@@ -918,15 +866,13 @@ list LengthList {
    member: String
}

// 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.
// set ConBSet {
//     member: ConBSetInner
// }
//
// set ConBSetInner {
//     member: String
// }
set ConBSet {
    member: ConBSetInner
}

set ConBSetInner {
    member: String
}

map MapOfPatternString {
    key: PatternString,
+110 −0
Original line number Diff line number Diff line
// TODO(https://github.com/awslabs/smithy/issues/1541): This file is
// temporary; these tests should really be in awslabs/smithy, but they were removed.
// They have been written starting off from the commit that removed them:
// https://github.com/awslabs/smithy/commit/1b5737a873033a101066b3d92b9e11d4ed3587c7

$version: "1.0"

namespace com.amazonaws.constraints

use aws.protocols#restJson1
use smithy.framework#ValidationException
use smithy.test#httpMalformedRequestTests

@restJson1
service UniqueItemsService {
    operations: [
        MalformedUniqueItems
    ]
}

@http(uri: "/MalformedUniqueItems", method: "POST")
operation MalformedUniqueItems {
    input: MalformedUniqueItemsInput,
    errors: [ValidationException]
}

apply MalformedUniqueItems @httpMalformedRequestTests([
    {
        id: "RestJsonMalformedUniqueItemsDuplicateItems",
        documentation: """
        When the list has duplicated items, the response should be a 400
        ValidationException.""",
        protocol: restJson1,
        request: {
            method: "POST",
            uri: "/MalformedUniqueItems",
            body: """
            { "set" : ["a", "a", "b", "c"] }""",
            headers: {
                "content-type": "application/json"
            }
        },
        response: {
            code: 400,
            headers: {
                "x-amzn-errortype": "ValidationException"
            },
            body: {
                mediaType: "application/json",
                assertion: {
                    contents: """
                    { "message" : "1 validation error detected. Value with repeated values at indices [0, 1] at '/set' failed to satisfy constraint: Member must have unique values",
                      "fieldList" : [{"message": "Value with repeated values at indices [0, 1] at '/set' failed to satisfy constraint: Member must have unique values", "path": "/set"}]}"""
                }
            }
        }
    },
    {
        id: "RestJsonMalformedUniqueItemsDuplicateBlobs",
        documentation: """
        When the list has duplicated blobs, the response should be a 400
        ValidationException.""",
        protocol: restJson1,
        request: {
            method: "POST",
            uri: "/MalformedUniqueItems",
            body: """
            { "complexSet" : [{"foo": true, "blob": "YmxvYg=="}, {"foo": true, "blob": "b3RoZXJibG9i"}, {"foo": true, "blob": "YmxvYg=="}] }""",
            headers: {
                "content-type": "application/json"
            }
        },
        response: {
            code: 400,
            headers: {
                "x-amzn-errortype": "ValidationException"
            },
            body: {
                mediaType: "application/json",
                assertion: {
                    contents: """
                    { "message" : "1 validation error detected. Value with repeated values at indices [0, 2] at '/complexSet' failed to satisfy constraint: Member must have unique values",
                      "fieldList" : [{"message": "Value with repeated values at indices [0, 2] at '/complexSet' failed to satisfy constraint: Member must have unique values", "path": "/complexSet"}]}"""
                }
            }
        }
    },
    // Note that the previously existing `RestJsonMalformedUniqueItemsNullItem` test is already covered by `RestJsonMalformedUniqueItemsNullItem`.
    // See https://github.com/awslabs/smithy/issues/1577#issuecomment-1397069720.
])

structure MalformedUniqueItemsInput {
    set: SimpleSet,
    complexSet: ComplexSet
}

@uniqueItems
list SimpleSet {
    member: String
}

@uniqueItems
list ComplexSet {
    member: ComplexSetStruct
}

structure ComplexSetStruct {
    foo: Boolean,
    blob: Blob
}
+19 −1
Original line number Diff line number Diff line
@@ -46,6 +46,8 @@ 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.customize.NamedSectionGenerator
import software.amazon.smithy.rust.codegen.core.smithy.customize.Section
import software.amazon.smithy.rust.codegen.core.smithy.isOptional
import software.amazon.smithy.rust.codegen.core.smithy.rustType
import software.amazon.smithy.rust.codegen.core.util.dq
@@ -55,7 +57,19 @@ import software.amazon.smithy.rust.codegen.core.util.isTargetUnit
import software.amazon.smithy.rust.codegen.core.util.letIf

/**
 * Instantiator generates code to instantiate a given Shape given a `Node` representing the value.
 * Class describing an instantiator section that can be used in a customization.
 */
sealed class InstantiatorSection(name: String) : Section(name) {
    data class AfterInstantiatingValue(val shape: Shape) : InstantiatorSection("AfterInstantiatingValue")
}

/**
 * Customization for the instantiator.
 */
typealias InstantiatorCustomization = NamedSectionGenerator<InstantiatorSection>

/**
 * Instantiator generates code to instantiate a given shape given a `Node` representing the value.
 *
 * This is only used during protocol test generation.
 */
@@ -72,6 +86,7 @@ open class Instantiator(
    private val enumFromStringFn: (Symbol, String) -> Writable,
    /** Fill out required fields with a default value. **/
    private val defaultsForRequiredFields: Boolean = false,
    private val customizations: List<InstantiatorCustomization> = listOf(),
) {
    data class Ctx(
        // The `http` crate requires that headers be lowercase, but Smithy protocol tests
@@ -281,6 +296,9 @@ open class Instantiator(
                rust(",")
            }
        }
        for (customization in customizations) {
            customization.section(InstantiatorSection.AfterInstantiatingValue(shape))(writer)
        }
    }

    private fun renderString(writer: RustWriter, shape: StringShape, arg: StringNode) {
+11 −3
Original line number Diff line number Diff line
@@ -45,10 +45,16 @@ val allCodegenTests = "../codegen-core/common-test-models".let { commonModels ->
        ),
        CodegenTest("com.amazonaws.simple#SimpleService", "simple", imports = listOf("$commonModels/simple.smithy")),
        CodegenTest(
            "com.amazonaws.constraints#ConstraintsService", "constraints_without_public_constrained_types",
            "com.amazonaws.constraints#ConstraintsService",
            "constraints_without_public_constrained_types",
            imports = listOf("$commonModels/constraints.smithy"),
            extraConfig = """, "codegen": { "publicConstrainedTypes": false } """,
        ),
        CodegenTest(
            "com.amazonaws.constraints#UniqueItemsService",
            "unique_items",
            imports = listOf("$commonModels/unique-items.smithy"),
        ),
        CodegenTest(
            "com.amazonaws.constraints#ConstraintsService",
            "constraints",
@@ -66,11 +72,13 @@ val allCodegenTests = "../codegen-core/common-test-models".let { commonModels ->
            imports = listOf("$commonModels/rest-json-extras.smithy"),
        ),
        CodegenTest(
            "aws.protocoltests.restjson.validation#RestJsonValidation", "rest_json_validation",
            "aws.protocoltests.restjson.validation#RestJsonValidation",
            "rest_json_validation",
            extraConfig = """, "codegen": { "ignoreUnsupportedConstraints": true } """,
        ),
        CodegenTest(
            "aws.protocoltests.extras.restjson.validation#MalformedRangeValidation", "malformed_range_extras",
            "aws.protocoltests.extras.restjson.validation#MalformedRangeValidation",
            "malformed_range_extras",
            extraConfig = """, "codegen": { "ignoreUnsupportedConstraints": true } """,
            imports = listOf("$commonModels/malformed-range-extras.smithy"),
        ),
Loading