Skip to content

Commit ffb5a87

Browse files
authored
feat: add support for multi select hash to JMESPath visitor (#931)
1 parent 21aff4e commit ffb5a87

File tree

5 files changed

+312
-22
lines changed

5 files changed

+312
-22
lines changed

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/AcceptorGenerator.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,12 @@ private fun KotlinWriter.renderPathAcceptor(wi: WaiterInfo, directive: String, i
8989
val comparison = when (matcher.comparator!!) {
9090
PathComparator.STRING_EQUALS -> "${actual.identifier} == ${expected.dq()}"
9191
PathComparator.BOOLEAN_EQUALS -> "${actual.identifier} == ${expected.toBoolean()}"
92-
PathComparator.ANY_STRING_EQUALS -> "${actual.identifier}?.any { it == ${expected.dq()} } ?: false"
92+
PathComparator.ANY_STRING_EQUALS -> "(${actual.identifier} as List<String>?)?.any { it == ${expected.dq()} } ?: false"
9393

9494
// NOTE: the isNotEmpty check is necessary because the waiter spec says that `allStringEquals` requires
9595
// at least one value unlike Kotlin's `all` which returns true if the collection is empty
9696
PathComparator.ALL_STRING_EQUALS ->
97-
"!${actual.identifier}.isNullOrEmpty() && ${actual.identifier}.all { it == ${expected.dq()} }"
97+
"!(${actual.identifier} as List<String>).isNullOrEmpty() && ${actual.identifier}.all { it == ${expected.dq()} }"
9898
}
9999
write(comparison)
100100
}

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/KotlinJmespathExpressionVisitor.kt

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class KotlinJmespathExpressionVisitor(
9696
shapeCursor.removeLast()
9797

9898
val innerCollector = when (right) {
99-
is MultiSelectListExpression -> innerResult.identifier // Already a list
99+
is MultiSelectListExpression, is MultiSelectHashExpression -> innerResult.identifier // Already a list
100100
else -> "listOfNotNull(${innerResult.identifier})"
101101
}
102102
writer.write(innerCollector)
@@ -107,26 +107,28 @@ class KotlinJmespathExpressionVisitor(
107107

108108
private fun subfield(expression: FieldExpression, parentName: String): VisitedExpression {
109109
val member = currentShape.targetOrSelf(ctx.model).getMember(expression.name).getOrNull()
110-
?: throw CodegenException("reference to nonexistent member '${expression.name}' of shape $currentShape")
111110

112111
val name = expression.name.toCamelCase()
113112
val nameExpr = ensureNullGuard(currentShape, name)
114113

115-
val memberTarget = ctx.model.expectShape(member.target)
116-
val unwrapExpr = when {
117-
memberTarget.isEnum -> "value"
118-
memberTarget.isEnumList -> "map { it.value }"
119-
memberTarget.isEnumMap -> "mapValues { (_, v) -> v.value }"
120-
memberTarget.isBlobShape || memberTarget.isTimestampShape ->
121-
throw CodegenException("acceptor behavior for shape type ${memberTarget.type} is undefined")
122-
else -> null
114+
val unwrapExpr = member?.let {
115+
val memberTarget = ctx.model.expectShape(member.target)
116+
when {
117+
memberTarget.isEnum -> "value"
118+
memberTarget.isEnumList -> "map { it.value }"
119+
memberTarget.isEnumMap -> "mapValues { (_, v) -> v.value }"
120+
memberTarget.isBlobShape || memberTarget.isTimestampShape ->
121+
throw CodegenException("acceptor behavior for shape type ${memberTarget.type} is undefined")
122+
else -> null
123+
}
123124
}
125+
124126
val codegen = buildString {
125127
append("$parentName$nameExpr")
126128
unwrapExpr?.let { append(ensureNullGuard(member, it)) }
127129
}
128130

129-
shapeCursor.addLast(member)
131+
member?.let { shapeCursor.addLast(it) }
130132
return VisitedExpression(addTempVar(name, codegen), member)
131133
}
132134

@@ -244,20 +246,32 @@ class KotlinJmespathExpressionVisitor(
244246
}
245247

246248
override fun visitMultiSelectHash(expression: MultiSelectHashExpression): VisitedExpression {
247-
throw CodegenException("MultiSelectHashExpression is unsupported")
249+
val properties = expression.expressions.keys.joinToString { "val $it: T" }
250+
writer.write("class Selection<T>($properties)")
251+
252+
val listName = bestTempVarName("multiSelect")
253+
writer.withBlock("val $listName = listOfNotNull(", ")") {
254+
withBlock("run {", "}") {
255+
val identifiers = expression.expressions.toList().joinToString { addTempVar(it.first, it.second.accept(this@KotlinJmespathExpressionVisitor).identifier) }
256+
write("Selection($identifiers)")
257+
}
258+
}
259+
return VisitedExpression(listName, currentShape)
248260
}
249261

250262
override fun visitMultiSelectList(expression: MultiSelectListExpression): VisitedExpression {
251263
val listName = bestTempVarName("multiSelect")
252264
writer.openBlock("val #L = listOfNotNull(", listName)
265+
writer.openBlock("listOfNotNull(")
253266

254267
expression.expressions.forEach {
255268
writer.openBlock("run {")
256-
val inner = acceptSubexpression(it)
269+
val inner = it.accept(this)
257270
writer.write(inner.identifier)
258271
writer.closeBlock("},")
259272
}
260273

274+
writer.closeBlock(")")
261275
writer.closeBlock(")")
262276
return VisitedExpression(listName, currentShape)
263277
}
@@ -328,8 +342,10 @@ class KotlinJmespathExpressionVisitor(
328342
if (expression.stop.asInt < 0) "$parentName.size${expression.stop.asInt}" else expression.stop.asInt
329343
}
330344

345+
val sliceExpr = ensureNullGuard(currentShape, "slice($startIndex..<$stopIndex step ${expression.step}")
346+
331347
writer.write("@OptIn(ExperimentalStdlibApi::class)")
332-
val slicedListName = addTempVar("slicedList", "$parentName?.slice($startIndex..<$stopIndex step ${expression.step})")
348+
val slicedListName = addTempVar("slicedList", "$parentName$sliceExpr)")
333349
return VisitedExpression(slicedListName, currentShape)
334350
}
335351

@@ -357,9 +373,13 @@ class KotlinJmespathExpressionVisitor(
357373
}
358374

359375
private fun index(expression: IndexExpression, parentName: String): VisitedExpression {
360-
shapeCursor.addLast(currentShape.targetOrSelf(ctx.model).targetMemberOrSelf)
361376
val index = if (expression.index < 0) "$parentName.size${expression.index}" else expression.index
362-
return VisitedExpression(addTempVar("index", "$parentName?.get($index)"))
377+
378+
shapeCursor.addLast(currentShape.targetOrSelf(ctx.model).targetMemberOrSelf)
379+
val indexExpr = ensureNullGuard(currentShape, "get($index)")
380+
shapeCursor.removeLast()
381+
382+
return VisitedExpression(addTempVar("index", "$parentName$indexExpr"))
363383
}
364384

365385
private val Shape.isEnumList: Boolean

codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/AcceptorGeneratorTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,12 @@ class AcceptorGeneratorTest {
7777
InputOutputAcceptor(RetryDirective.TerminateAndSucceed) {
7878
val output = it.output
7979
val tags = output.tags
80-
!tags.isNullOrEmpty() && tags.all { it == "foo" }
80+
!(tags as List<String>).isNullOrEmpty() && tags.all { it == "foo" }
8181
},
8282
InputOutputAcceptor(RetryDirective.TerminateAndSucceed) {
8383
val output = it.output
8484
val tags = output.tags
85-
tags?.any { it == "foo" } ?: false
85+
(tags as List<String>?)?.any { it == "foo" } ?: false
8686
},
8787
)
8888
""".trimIndent()

tests/codegen/waiter-tests/model/waiter-operations.smithy

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,107 @@ service WaitersTestService {
525525
]
526526
},
527527

528+
// multi select list
529+
StructListStringMultiSelectList: {
530+
acceptors: [
531+
{
532+
state: "success",
533+
matcher: {
534+
output: {
535+
path: "lists.structs[].[primitives.string][0][0]",
536+
expected: "foo",
537+
comparator: "stringEquals"
538+
}
539+
}
540+
}
541+
]
542+
},
543+
StructListStringListMultiSelectList: {
544+
acceptors: [
545+
{
546+
state: "success",
547+
matcher: {
548+
output: {
549+
path: "lists.structs[].[primitives.string, primitives.string][1]",
550+
expected: "foo",
551+
comparator: "allStringEquals"
552+
}
553+
}
554+
}
555+
]
556+
},
557+
StructListSubStructPrimitivesBooleanMultiSelectList: {
558+
acceptors: [
559+
{
560+
state: "success",
561+
matcher: {
562+
output: {
563+
path: "lists.structs[].[subStructs][1][0][0].subStructPrimitives.boolean",
564+
expected: "true",
565+
comparator: "booleanEquals"
566+
}
567+
}
568+
}
569+
]
570+
},
571+
572+
StructListStringMultiSelectHash: {
573+
acceptors: [
574+
{
575+
state: "success",
576+
matcher: {
577+
output: {
578+
path: "(lists.structs[].{x: primitives.string, y: strings})[0].x",
579+
expected: "foo"
580+
comparator: "stringEquals"
581+
}
582+
}
583+
}
584+
]
585+
},
586+
StructListStringsMultiSelectHash: {
587+
acceptors: [
588+
{
589+
state: "success",
590+
matcher: {
591+
output: {
592+
path: "(lists.structs[].{x: primitives.string, y: strings})[1].y",
593+
expected: "foo"
594+
comparator: "allStringEquals"
595+
}
596+
}
597+
}
598+
]
599+
},
600+
StructListStringsAnyMultiSelectHash: {
601+
acceptors: [
602+
{
603+
state: "success",
604+
matcher: {
605+
output: {
606+
path: "(lists.structs[].{x: primitives.string, y: strings})[1].y",
607+
expected: "foo"
608+
comparator: "anyStringEquals"
609+
}
610+
}
611+
}
612+
]
613+
},
614+
StructListSubStructPrimitivesBooleanMultiSelectHash: {
615+
acceptors: [
616+
{
617+
state: "success",
618+
matcher: {
619+
output: {
620+
path: "(lists.structs[].{x: subStructs})[0].x[0].subStructPrimitives.boolean",
621+
expected: "true"
622+
comparator: "booleanEquals"
623+
}
624+
}
625+
}
626+
]
627+
},
628+
528629
// function: contains, list
529630
BooleanListContains: {
530631
acceptors: [

0 commit comments

Comments
 (0)