Skip to content

Commit ca206c8

Browse files
authored
feat: add functions (keys, values, merge) to JMESPath visitor (#948)
1 parent c236d00 commit ca206c8

File tree

13 files changed

+460
-24
lines changed

13 files changed

+460
-24
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} as List<String>?)?.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} as List<String>).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: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,12 @@ class KotlinJmespathExpressionVisitor(
105105
return VisitedExpression(outerName, leftShape, innerResult.shape)
106106
}
107107

108-
private fun subfield(expression: FieldExpression, parentName: String): VisitedExpression {
108+
private fun subfield(expression: FieldExpression, parentName: String, isObject: Boolean = false): VisitedExpression {
109109
val member = currentShape.targetOrSelf(ctx.model).getMember(expression.name).getOrNull()
110110

111111
val name = expression.name.toCamelCase()
112-
val nameExpr = ensureNullGuard(currentShape, name)
112+
// User created objects are represented as hash maps in code-gen and are marked by `isObject`
113+
val nameExpr = if (isObject) "[\"$name\"]" else ensureNullGuard(currentShape, name)
113114

114115
val unwrapExpr = member?.let {
115116
val memberTarget = ctx.model.expectShape(member.target)
@@ -210,17 +211,20 @@ class KotlinJmespathExpressionVisitor(
210211
codegenReq(arguments.size == 1) { "Unexpected number of arguments to $this" }
211212
return acceptSubexpression(this.arguments[0])
212213
}
213-
214214
private fun FunctionExpression.twoArgs(): Pair<VisitedExpression, VisitedExpression> {
215215
codegenReq(arguments.size == 2) { "Unexpected number of arguments to $this" }
216216
return acceptSubexpression(this.arguments[0]) to acceptSubexpression(this.arguments[1])
217217
}
218218

219-
private fun VisitedExpression.dotFunction(expression: FunctionExpression, expr: String, elvisExpr: String? = null): VisitedExpression {
219+
private fun FunctionExpression.args(): List<VisitedExpression> =
220+
this.arguments.map { acceptSubexpression(it) }
221+
222+
private fun VisitedExpression.dotFunction(expression: FunctionExpression, expr: String, elvisExpr: String? = null, isObject: Boolean = false): VisitedExpression {
220223
val dotFunctionExpr = ensureNullGuard(shape, expr, elvisExpr)
221-
val ident = addTempVar(expression.name, "$identifier$dotFunctionExpr")
224+
val ident = addTempVar(expression.name.toCamelCase(), "$identifier$dotFunctionExpr")
222225

223-
return VisitedExpression(ident, shape)
226+
shape?.let { shapeCursor.addLast(shape) }
227+
return VisitedExpression(ident, shape, isObject = isObject)
224228
}
225229

226230
override fun visitFunction(expression: FunctionExpression): VisitedExpression = when (expression.name) {
@@ -267,6 +271,21 @@ class KotlinJmespathExpressionVisitor(
267271
subject.dotFunction(expression, "endsWith(${suffix.identifier})")
268272
}
269273

274+
"keys" -> {
275+
val obj = expression.singleArg()
276+
VisitedExpression(addTempVar("keys", obj.getKeys()))
277+
}
278+
279+
"values" -> {
280+
val obj = expression.singleArg()
281+
VisitedExpression(addTempVar("values", obj.getValues()))
282+
}
283+
284+
"merge" -> {
285+
val objects = expression.args()
286+
VisitedExpression(addTempVar("merge", objects.mergeProperties()), isObject = true)
287+
}
288+
270289
else -> throw CodegenException("Unknown function type in $expression")
271290
}
272291

@@ -391,22 +410,22 @@ class KotlinJmespathExpressionVisitor(
391410
}
392411

393412
override fun visitSubexpression(expression: Subexpression): VisitedExpression {
394-
val leftName = expression.left.accept(this).identifier
395-
return processRightSubexpression(expression.right, leftName)
413+
val left = expression.left.accept(this)
414+
return processRightSubexpression(expression.right, left.identifier, left.isObject)
396415
}
397416

398417
private fun subexpression(expression: Subexpression, parentName: String): VisitedExpression {
399-
val leftName = when (val left = expression.left) {
400-
is FieldExpression -> subfield(left, parentName).identifier
401-
is Subexpression -> subexpression(left, parentName).identifier
418+
val left = when (val left = expression.left) {
419+
is FieldExpression -> subfield(left, parentName)
420+
is Subexpression -> subexpression(left, parentName)
402421
else -> throw CodegenException("Subexpression type $left is unsupported")
403422
}
404-
return processRightSubexpression(expression.right, leftName)
423+
return processRightSubexpression(expression.right, left.identifier, left.isObject)
405424
}
406425

407-
private fun processRightSubexpression(expression: JmespathExpression, leftName: String): VisitedExpression =
426+
private fun processRightSubexpression(expression: JmespathExpression, leftName: String, isObject: Boolean = false): VisitedExpression =
408427
when (expression) {
409-
is FieldExpression -> subfield(expression, leftName)
428+
is FieldExpression -> subfield(expression, leftName, isObject)
410429
is IndexExpression -> index(expression, leftName)
411430
is Subexpression -> subexpression(expression, leftName)
412431
is ProjectionExpression -> projection(expression, leftName)
@@ -415,10 +434,7 @@ class KotlinJmespathExpressionVisitor(
415434

416435
private fun index(expression: IndexExpression, parentName: String): VisitedExpression {
417436
val index = if (expression.index < 0) "$parentName.size${expression.index}" else expression.index
418-
419-
shapeCursor.addLast(currentShape.targetOrSelf(ctx.model).targetMemberOrSelf)
420-
val indexExpr = ensureNullGuard(currentShape, "get($index)")
421-
shapeCursor.removeLast()
437+
val indexExpr = ensureNullGuard(currentShape.targetMemberOrSelf, "get($index)")
422438

423439
return VisitedExpression(addTempVar("index", "$parentName$indexExpr"), currentShape)
424440
}
@@ -439,6 +455,33 @@ class KotlinJmespathExpressionVisitor(
439455
".$expr"
440456
}
441457

458+
private fun VisitedExpression.getKeys(): String {
459+
val keys = this.shape?.targetOrSelf(ctx.model)?.allMembers
460+
?.keys?.joinToString(", ", "listOf(", ")") { "\"$it\"" }
461+
return keys ?: "listOf<String>()"
462+
}
463+
464+
private fun VisitedExpression.getValues(): String {
465+
val values = this.shape?.targetOrSelf(ctx.model)?.allMembers?.keys
466+
?.joinToString(", ", "listOf(", ")") { "${this.identifier}${ensureNullGuard(this.shape, it)}" }
467+
return values ?: "listOf<String>()"
468+
}
469+
470+
private fun List<VisitedExpression>.mergeProperties(): String {
471+
val union = addTempVar("union", "HashMap<String, Any?>()")
472+
473+
forEach { obj ->
474+
val keys = addTempVar("keys", obj.getKeys())
475+
val values = addTempVar("values", obj.getValues())
476+
477+
writer.withBlock("for(i in $keys.indices){", "}") {
478+
write("union[$keys[i]] = $values[i]")
479+
}
480+
}
481+
482+
return union
483+
}
484+
442485
private val Shape.isNullable: Boolean
443486
get() = this is MemberShape &&
444487
ctx.model.expectShape(target).let { !it.hasTrait<OperationInput>() && !it.hasTrait<OperationOutput>() }
@@ -461,5 +504,9 @@ class KotlinJmespathExpressionVisitor(
461504
* `foo[].bar[].baz.qux`, the shape that backs the identifier (and therefore determines overall nullability)
462505
* is `foo`, but the shape that needs carried through to subfield expressions in the following projection
463506
* is the target of `bar`, such that its subfields `baz` and `qux` can be recognized.
507+
* @param nullable Boolean to indicate that a visited expression is nullable. Shape is used for this mostly but sometimes an
508+
* expression is nullable for reasons that are not shape related
509+
* @param isObject Boolean to indicate that a visited expression results in an object. Objects are represented as hash maps
510+
* because it is not possible to construct a class at runtime
464511
*/
465-
data class VisitedExpression(val identifier: String, val shape: Shape? = null, val projected: Shape? = null, val nullable: Boolean = false)
512+
data class VisitedExpression(val identifier: String, val shape: Shape? = null, val projected: Shape? = null, val nullable: Boolean = false, val isObject: Boolean = false)

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 as List<String>).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 as List<String>?)?.any { it == "foo" } ?: false
85+
(tags as List<String?>?)?.any { it == "foo" } ?: false
8686
},
8787
)
8888
""".trimIndent()

runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Convenience.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
package aws.smithy.kotlin.runtime.util
77

88
import aws.smithy.kotlin.runtime.InternalApi
9-
import kotlin.jvm.JvmName
109

1110
/**
1211
* Determines the length of a collection. This is a synonym for [Collection.size].
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
$version: "2"
2+
namespace com.test
3+
4+
use smithy.waiters#waitable
5+
6+
@waitable(
7+
KeysFunctionPrimitivesStringEquals: {
8+
acceptors: [
9+
{
10+
state: "success",
11+
matcher: {
12+
output: {
13+
path: "keys(primitives)",
14+
expected: "string",
15+
comparator: "anyStringEquals"
16+
}
17+
}
18+
}
19+
]
20+
},
21+
KeysFunctionPrimitivesIntegerEquals: {
22+
acceptors: [
23+
{
24+
state: "success",
25+
matcher: {
26+
output: {
27+
path: "keys(primitives)",
28+
expected: "integer",
29+
comparator: "anyStringEquals"
30+
}
31+
}
32+
}
33+
]
34+
},
35+
)
36+
@readonly
37+
@http(method: "GET", uri: "/keys/{name}", code: 200)
38+
operation GetFunctionKeysEquals {
39+
input: GetEntityRequest,
40+
output: GetEntityResponse,
41+
errors: [NotFound],
42+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
$version: "2"
2+
namespace com.test
3+
4+
use smithy.waiters#waitable
5+
6+
@suppress(["WaitableTraitJmespathProblem"])
7+
@waitable(
8+
MergeFunctionOverrideObjectsOneEquals: {
9+
acceptors: [
10+
{
11+
state: "success",
12+
matcher: {
13+
output: {
14+
path: "merge(objectOne, objectTwo).valueOne",
15+
expected: "foo",
16+
comparator: "stringEquals"
17+
}
18+
}
19+
}
20+
]
21+
},
22+
MergeFunctionOverrideObjectsTwoEquals: {
23+
acceptors: [
24+
{
25+
state: "success",
26+
matcher: {
27+
output: {
28+
path: "merge(objectOne, objectTwo).valueTwo",
29+
expected: "bar",
30+
comparator: "stringEquals"
31+
}
32+
}
33+
}
34+
]
35+
},
36+
MergeFunctionOverrideObjectsThreeEquals: {
37+
acceptors: [
38+
{
39+
state: "success",
40+
matcher: {
41+
output: {
42+
path: "merge(objectOne, objectTwo).valueThree",
43+
expected: "baz",
44+
comparator: "stringEquals"
45+
}
46+
}
47+
}
48+
]
49+
},
50+
)
51+
@readonly
52+
@http(method: "GET", uri: "/merge/{name}", code: 200)
53+
operation GetFunctionMergeEquals {
54+
input: GetEntityRequest,
55+
output: GetEntityResponse,
56+
errors: [NotFound],
57+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
$version: "2"
2+
namespace com.test
3+
4+
use smithy.waiters#waitable
5+
6+
@waitable(
7+
ValuesFunctionSampleValuesEquals: {
8+
acceptors: [
9+
{
10+
state: "success",
11+
matcher: {
12+
output: {
13+
path: "values(sampleValues)",
14+
expected: "foo",
15+
comparator: "allStringEquals"
16+
}
17+
}
18+
}
19+
]
20+
},
21+
ValuesFunctionAnySampleValuesEquals: {
22+
acceptors: [
23+
{
24+
state: "success",
25+
matcher: {
26+
output: {
27+
path: "values(sampleValues)",
28+
expected: "foo",
29+
comparator: "anyStringEquals"
30+
}
31+
}
32+
}
33+
]
34+
},
35+
)
36+
@readonly
37+
@http(method: "GET", uri: "/values/{name}", code: 200)
38+
operation GetFunctionValuesEquals {
39+
input: GetEntityRequest,
40+
output: GetEntityResponse,
41+
errors: [NotFound],
42+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ structure GetEntityResponse {
1818
lists: EntityLists,
1919
twoDimensionalLists: TwoDimensionalEntityLists,
2020
maps: EntityMaps,
21+
sampleValues: Values,
22+
objectOne: Values,
23+
objectTwo: Values,
2124
}
2225

2326
structure EntityPrimitives {
@@ -163,3 +166,9 @@ list SubStructList {
163166
structure SubStruct {
164167
subStructPrimitives: EntityPrimitives,
165168
}
169+
170+
structure Values {
171+
valueOne: String
172+
valueTwo: String
173+
valueThree: String
174+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,8 @@ service WaitersTestService {
2020
GetFunctionJoinEquals,
2121
GetFunctionStartsWithEquals,
2222
GetFunctionEndsWithEquals,
23+
GetFunctionKeysEquals,
24+
GetFunctionValuesEquals,
25+
GetFunctionMergeEquals,
2326
]
2427
}

tests/codegen/waiter-tests/src/main/kotlin/com/test/DefaultWaitersTestClient.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ class DefaultWaitersTestClient<T>(resultList: List<Result<T>>) : WaitersTestClie
5252

5353
override suspend fun getFunctionEndsWithEquals(input: GetFunctionEndsWithEqualsRequest): GetFunctionEndsWithEqualsResponse = findSuccess()
5454

55+
override suspend fun getFunctionKeysEquals(input: GetFunctionKeysEqualsRequest): GetFunctionKeysEqualsResponse = findSuccess()
56+
57+
override suspend fun getFunctionValuesEquals(input: GetFunctionValuesEqualsRequest): GetFunctionValuesEqualsResponse = findSuccess()
58+
59+
override suspend fun getFunctionMergeEquals(input: GetFunctionMergeEqualsRequest): GetFunctionMergeEqualsResponse = findSuccess()
60+
5561
private fun <Response> findSuccess(): Response {
5662
val nextResult = results.next()
5763
@Suppress("UNCHECKED_CAST")

0 commit comments

Comments
 (0)