From 89bdd2977acc20ec9b8daae9fc98c2b1e600acb0 Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Mon, 14 Aug 2023 13:12:12 -0400 Subject: [PATCH] feat: add support for sub-expressions and list indexing in JMESPath (#912) --- .../KotlinJmespathExpressionVisitor.kt | 39 ++++++-- .../model/waiter-operations.smithy | 95 +++++++++++++++++++ .../src/test/kotlin/com/test/WaiterTest.kt | 42 +++++++- 3 files changed, 165 insertions(+), 11 deletions(-) diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/KotlinJmespathExpressionVisitor.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/KotlinJmespathExpressionVisitor.kt index b37e7d05f..8e848e7df 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/KotlinJmespathExpressionVisitor.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/KotlinJmespathExpressionVisitor.kt @@ -14,13 +14,14 @@ import software.amazon.smithy.kotlin.codegen.core.CodegenContext import software.amazon.smithy.kotlin.codegen.core.KotlinWriter import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes import software.amazon.smithy.kotlin.codegen.core.withBlock -import software.amazon.smithy.kotlin.codegen.model.* +import software.amazon.smithy.kotlin.codegen.model.hasTrait +import software.amazon.smithy.kotlin.codegen.model.isEnum +import software.amazon.smithy.kotlin.codegen.model.targetOrSelf import software.amazon.smithy.kotlin.codegen.model.traits.OperationInput import software.amazon.smithy.kotlin.codegen.model.traits.OperationOutput import software.amazon.smithy.kotlin.codegen.utils.dq import software.amazon.smithy.kotlin.codegen.utils.getOrNull import software.amazon.smithy.kotlin.codegen.utils.toCamelCase -import software.amazon.smithy.model.knowledge.NullableIndex import software.amazon.smithy.model.shapes.ListShape import software.amazon.smithy.model.shapes.MapShape import software.amazon.smithy.model.shapes.MemberShape @@ -49,8 +50,6 @@ class KotlinJmespathExpressionVisitor( ) : ExpressionVisitor { private val tempVars = mutableSetOf() - private val nullableIndex = NullableIndex(ctx.model) - // tracks the current shape on which the visitor is operating private val shapeCursor = ArrayDeque(listOf(shape)) @@ -308,14 +307,35 @@ class KotlinJmespathExpressionVisitor( } override fun visitSubexpression(expression: Subexpression): VisitedExpression { - val left = expression.left.accept(this) + val leftName = expression.left.accept(this).identifier - val ret = when (val right = expression.right) { - is FieldExpression -> subfield(right, left.identifier) + return when (val right = expression.right) { + is FieldExpression -> subfield(right, leftName) + is IndexExpression -> index(right, leftName) + is Subexpression -> subexpression(right, leftName) else -> throw CodegenException("Subexpression type $right is unsupported") } + } + + private fun subexpression(expression: Subexpression, parentName: String): VisitedExpression { + val leftName = when (val left = expression.left) { + is FieldExpression -> subfield(left, parentName).identifier + is Subexpression -> subexpression(left, parentName).identifier + else -> throw CodegenException("Subexpression type $left is unsupported") + } + + return when (val right = expression.right) { + is FieldExpression -> subfield(right, leftName) + is Subexpression -> subexpression(right, leftName) + is IndexExpression -> index(right, leftName) + else -> throw CodegenException("Subexpression type $right is unsupported") + } + } - return ret + private fun index(expression: IndexExpression, parentName: String): VisitedExpression { + shapeCursor.addLast(currentShape.targetOrSelf(ctx.model).targetMemberOrSelf) + val index = if (expression.index < 0) "$parentName.size${expression.index}" else expression.index + return VisitedExpression(addTempVar("index", "$parentName?.get($index)")) } private val Shape.isEnumList: Boolean @@ -336,8 +356,7 @@ class KotlinJmespathExpressionVisitor( private val Shape.isNullable: Boolean get() = this is MemberShape && - ctx.model.expectShape(target).let { !it.hasTrait() && !it.hasTrait() } && - nullableIndex.isMemberNullable(this, NullableIndex.CheckMode.CLIENT_ZERO_VALUE_V1_NO_INPUT) + ctx.model.expectShape(target).let { !it.hasTrait() && !it.hasTrait() } private val Shape.targetMemberOrSelf: Shape get() = when (val target = targetOrSelf(ctx.model)) { diff --git a/tests/codegen/waiter-tests/model/waiter-operations.smithy b/tests/codegen/waiter-tests/model/waiter-operations.smithy index 76418e270..a82b5e00f 100644 --- a/tests/codegen/waiter-tests/model/waiter-operations.smithy +++ b/tests/codegen/waiter-tests/model/waiter-operations.smithy @@ -193,6 +193,92 @@ service WaitersTestService { ] }, + // list indexing + BooleanListIndexZeroEquals: { + acceptors: [ + { + state: "success", + matcher: { + output: { + path: "lists.booleans[0]", + expected: "true", + comparator: "booleanEquals" + } + } + } + ] + }, + BooleanListIndexOneEquals: { + acceptors: [ + { + state: "success", + matcher: { + output: { + path: "lists.booleans[1]", + expected: "true", + comparator: "booleanEquals" + } + } + } + ] + }, + BooleanListIndexNegativeTwoEquals: { + acceptors: [ + { + state: "success", + matcher: { + output: { + path: "lists.booleans[-2]", + expected: "true", + comparator: "booleanEquals" + } + } + } + ] + }, + TwoDimensionalBooleanListIndexZeroZeroEquals: { + acceptors: [ + { + state: "success", + matcher: { + output: { + path: "twoDimensionalLists.booleansList[0][0]", + expected: "true", + comparator: "booleanEquals" + } + } + } + ] + }, + StructListIndexOneStringsIndexZeroEquals: { + acceptors: [ + { + state: "success", + matcher: { + output: { + path: "lists.structs[1].strings[0]", + expected: "foo", + comparator: "stringEquals" + } + } + } + ] + }, + StructListIndexOneSubStructsIndexZeroSubStructPrimitivesBooleanEquals: { + acceptors: [ + { + state: "success", + matcher: { + output: { + path: "lists.structs[1].subStructs[0].subStructPrimitives.boolean", + expected: "true", + comparator: "booleanEquals" + } + } + } + ] + }, + // anyStringEquals StringListAnyStringEquals: { acceptors: [ @@ -764,6 +850,7 @@ structure GetEntityRequest { structure GetEntityResponse { primitives: EntityPrimitives, lists: EntityLists, + twoDimensionalLists: TwoDimensionalEntityLists, maps: EntityMaps, } @@ -800,6 +887,10 @@ structure EntityLists { structs: StructList, } +structure TwoDimensionalEntityLists { + booleansList: TwoDimensionalBooleanList, +} + structure EntityMaps { booleans: BooleanMap, strings: StringMap, @@ -823,6 +914,10 @@ list BooleanList { member: Boolean, } +list TwoDimensionalBooleanList { + member: BooleanList, +} + list StringList { member: String, } diff --git a/tests/codegen/waiter-tests/src/test/kotlin/com/test/WaiterTest.kt b/tests/codegen/waiter-tests/src/test/kotlin/com/test/WaiterTest.kt index b6d29324c..d302fab59 100644 --- a/tests/codegen/waiter-tests/src/test/kotlin/com/test/WaiterTest.kt +++ b/tests/codegen/waiter-tests/src/test/kotlin/com/test/WaiterTest.kt @@ -12,7 +12,8 @@ import com.test.model.Enum import com.test.waiters.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) class WaiterTest { @@ -107,6 +108,45 @@ class WaiterTest { GetEntityResponse { primitives = EntityPrimitives { intEnum = IntEnum.One } }, ) + // list indexing + @Test fun testBooleanListIndexZeroEquals() = successTest( + WaitersTestClient::waitUntilBooleanListIndexZeroEquals, + GetEntityResponse { lists = EntityLists { booleans = listOf(false) } }, + GetEntityResponse { lists = EntityLists { booleans = listOf(true) } }, + ) + + @Test fun testBooleanListIndexOneEquals() = successTest( + WaitersTestClient::waitUntilBooleanListIndexOneEquals, + GetEntityResponse { lists = EntityLists { booleans = listOf(false, false) } }, + GetEntityResponse { lists = EntityLists { booleans = listOf(true, false) } }, + GetEntityResponse { lists = EntityLists { booleans = listOf(false, true) } }, + ) + + @Test fun testBooleanListIndexNegativeTwoEquals() = successTest( + WaitersTestClient::waitUntilBooleanListIndexNegativeTwoEquals, + GetEntityResponse { lists = EntityLists { booleans = listOf(false, false) } }, + GetEntityResponse { lists = EntityLists { booleans = listOf(false, true) } }, + GetEntityResponse { lists = EntityLists { booleans = listOf(true, false) } }, + ) + + @Test fun testTwoDimensionalBooleanListIndexZeroZeroEquals() = successTest( + WaitersTestClient::waitUntilTwoDimensionalBooleanListIndexZeroZeroEquals, + GetEntityResponse { twoDimensionalLists = TwoDimensionalEntityLists { booleansList = listOf(listOf(false)) } }, + GetEntityResponse { twoDimensionalLists = TwoDimensionalEntityLists { booleansList = listOf(listOf(true)) } }, + ) + + @Test fun testStructListIndexOneStringsIndexZeroEquals() = successTest( + WaitersTestClient::waitUntilStructListIndexOneStringsIndexZeroEquals, + GetEntityResponse { lists = EntityLists { structs = listOf(Struct { strings = listOf("bar") }, Struct { strings = listOf("bar") }) } }, + GetEntityResponse { lists = EntityLists { structs = listOf(Struct { strings = listOf("bar") }, Struct { strings = listOf("foo") }) } }, + ) + + @Test fun testStructListIndexOneSubStructsIndexZeroSubStructPrimitivesBooleanEquals() = successTest( + WaitersTestClient::waitUntilStructListIndexOneSubStructsIndexZeroSubStructPrimitivesBooleanEquals, + GetEntityResponse { lists = EntityLists { structs = listOf(Struct { }, Struct { subStructs = listOf(SubStruct { subStructPrimitives = EntityPrimitives { boolean = false } }) }) } }, + GetEntityResponse { lists = EntityLists { structs = listOf(Struct { }, Struct { subStructs = listOf(SubStruct { subStructPrimitives = EntityPrimitives { boolean = true } }) }) } }, + ) + // anyStringEquals @Test fun testStringListAnyListStringEquals() = successTest( WaitersTestClient::waitUntilStringListAnyStringEquals,