Skip to content

Commit e4a58c3

Browse files
authored
Add support for operationContextParams Endpoints trait (#3755)
## Motivation and Context <!--- Why is this change required? What problem does it solve? --> <!--- If it fixes an open issue, please link to the issue here --> We have to support the new [`operationContextParams` trait](https://smithy.io/2.0/additional-specs/rules-engine/parameters.html#smithy-rules-operationcontextparams-trait) for endpoint resolution. This trait specifies JMESPath expressions for selecting parameter data from the operation's input type. ## Description <!--- Describe your changes in detail --> * Add codegen support for the [JMESPath `keys`](https://jmespath.org/specification.html#keys) function (required by the trait [spec](https://smithy.io/2.0/additional-specs/rules-engine/parameters.html#smithy-rules-operationcontextparams-trait)) * Add codegen support for the trait itself. This is achieved by generating `get_param_name` functions for each param specified in `operationContextParams`. These functions pull the data out of the input object and it is added to the endpoint params in the `${operationName}EndpointParamsInterceptor` ## Testing <!--- Please describe in detail how you tested your changes --> <!--- Include details of your testing environment, and the tests you ran to --> <!--- see how your change affects other areas of the code, etc. --> Updated the existing test suite for JMESPath codegen to test the `keys` function. Updated the existing EndpointsDecoratorTest with an `operationContextParams` trait specifying one param of each supported type to test the codegen. ## Checklist <!--- If a checkbox below is not applicable, then please DELETE it rather than leaving it unchecked --> - [x] I have updated `CHANGELOG.next.toml` if I made changes to the smithy-rs codegen or runtime crates ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent 2313eb9 commit e4a58c3

File tree

6 files changed

+249
-25
lines changed

6 files changed

+249
-25
lines changed

CHANGELOG.next.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,16 @@
99
# message = "Fix typos in module documentation for generated crates"
1010
# references = ["smithy-rs#920"]
1111
# meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"}
12-
# author = "rcoh"
12+
# author = "rcoh"
13+
14+
[[smithy-rs]]
15+
message = "Support `stringArray` type in endpoints params"
16+
references = ["smithy-rs#3742"]
17+
meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client"}
18+
author = "landonxjames"
19+
20+
[[smithy-rs]]
21+
message = "Add support for `operationContextParams` Endpoints trait"
22+
references = ["smithy-rs#3755"]
23+
meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client"}
24+
author = "landonxjames"

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointParamsGenerator.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ internal class EndpointParamsGenerator(
120120
fun memberName(parameterName: String) = Identifier.of(parameterName).rustName()
121121

122122
fun setterName(parameterName: String) = "set_${memberName(parameterName)}"
123+
124+
fun getterName(parameterName: String) = "get_${memberName(parameterName)}"
123125
}
124126

125127
fun paramsStruct(): RuntimeType =
@@ -230,7 +232,9 @@ internal class EndpointParamsGenerator(
230232

231233
private fun generateEndpointParamsBuilder(rustWriter: RustWriter) {
232234
rustWriter.docs("Builder for [`Params`]")
233-
Attribute(derive(RuntimeType.Debug, RuntimeType.Default, RuntimeType.PartialEq, RuntimeType.Clone)).render(rustWriter)
235+
Attribute(derive(RuntimeType.Debug, RuntimeType.Default, RuntimeType.PartialEq, RuntimeType.Clone)).render(
236+
rustWriter,
237+
)
234238
rustWriter.rustBlock("pub struct ParamsBuilder") {
235239
parameters.toList().forEach { parameter ->
236240
val name = parameter.memberName()
@@ -253,7 +257,8 @@ internal class EndpointParamsGenerator(
253257
rustBlockTemplate("#{Params}", "Params" to paramsStruct()) {
254258
parameters.toList().forEach { parameter ->
255259
rust("${parameter.memberName()}: self.${parameter.memberName()}")
256-
parameter.default.orNull()?.also { default -> rust(".or_else(||Some(${value(default)}))") }
260+
parameter.default.orNull()
261+
?.also { default -> rust(".or_else(||Some(${value(default)}))") }
257262
if (parameter.isRequired) {
258263
rustTemplate(
259264
".ok_or_else(||#{Error}::missing(${parameter.memberName().dq()}))?",

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointParamsInterceptorGenerator.kt

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package software.amazon.smithy.rust.codegen.client.smithy.endpoint.generators
77

8+
import software.amazon.smithy.jmespath.JmespathExpression
89
import software.amazon.smithy.model.node.ArrayNode
910
import software.amazon.smithy.model.node.BooleanNode
1011
import software.amazon.smithy.model.node.Node
@@ -20,16 +21,23 @@ import software.amazon.smithy.rust.codegen.client.smithy.endpoint.rustName
2021
import software.amazon.smithy.rust.codegen.client.smithy.generators.EndpointTraitBindings
2122
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.configParamNewtype
2223
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.loadFromConfigBag
24+
import software.amazon.smithy.rust.codegen.client.smithy.generators.waiters.RustJmespathShapeTraversalGenerator
25+
import software.amazon.smithy.rust.codegen.client.smithy.generators.waiters.TraversalBinding
26+
import software.amazon.smithy.rust.codegen.client.smithy.generators.waiters.TraversedShape
2327
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency
28+
import software.amazon.smithy.rust.codegen.core.rustlang.RustType
2429
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
2530
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
31+
import software.amazon.smithy.rust.codegen.core.rustlang.asRef
2632
import software.amazon.smithy.rust.codegen.core.rustlang.rust
33+
import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate
2734
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
2835
import software.amazon.smithy.rust.codegen.core.rustlang.withBlockTemplate
2936
import software.amazon.smithy.rust.codegen.core.rustlang.writable
3037
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
3138
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
3239
import software.amazon.smithy.rust.codegen.core.smithy.generators.enforceRequired
40+
import software.amazon.smithy.rust.codegen.core.smithy.rustType
3341
import software.amazon.smithy.rust.codegen.core.util.PANIC
3442
import software.amazon.smithy.rust.codegen.core.util.dq
3543
import software.amazon.smithy.rust.codegen.core.util.inputShape
@@ -103,10 +111,16 @@ class EndpointParamsInterceptorGenerator(
103111
#{Ok}(())
104112
}
105113
}
114+
115+
// The get_* functions below are generated from JMESPath expressions in the
116+
// operationContextParams trait. They target the operation's input shape.
117+
118+
#{jmespath_getters}
106119
""",
107120
*codegenScope,
108121
"endpoint_prefix" to endpointPrefix(operationShape),
109122
"param_setters" to paramSetters(operationShape, endpointTypesGenerator.params),
123+
"jmespath_getters" to jmesPathGetters(operationShape),
110124
)
111125
}
112126

@@ -140,6 +154,33 @@ class EndpointParamsInterceptorGenerator(
140154
rust(".$setterName(#W)", value)
141155
}
142156

157+
idx.getOperationContextParams(operationShape).orNull()?.parameters?.forEach { (name, param) ->
158+
val setterName = EndpointParamsGenerator.setterName(name)
159+
val getterName = EndpointParamsGenerator.getterName(name)
160+
val pathValue = param.path
161+
val pathExpression = JmespathExpression.parse(pathValue)
162+
val pathTraversal =
163+
RustJmespathShapeTraversalGenerator(codegenContext).generate(
164+
pathExpression,
165+
listOf(
166+
TraversalBinding.Global(
167+
"input",
168+
TraversedShape.from(model, operationShape.inputShape(model)),
169+
),
170+
),
171+
)
172+
173+
when (pathTraversal.outputType) {
174+
is RustType.Vec -> {
175+
rust(".$setterName($getterName(_input))")
176+
}
177+
178+
else -> {
179+
rust(".$setterName($getterName(_input).cloned())")
180+
}
181+
}
182+
}
183+
143184
// lastly, allow these to be overridden by members
144185
memberParams.forEach { (memberShape, param) ->
145186
val memberName = codegenContext.symbolProvider.toMemberName(memberShape)
@@ -151,6 +192,39 @@ class EndpointParamsInterceptorGenerator(
151192
}
152193
}
153194

195+
private fun jmesPathGetters(operationShape: OperationShape) =
196+
writable {
197+
val idx = ContextIndex.of(codegenContext.model)
198+
val inputShape = operationShape.inputShape(codegenContext.model)
199+
val input = symbolProvider.toSymbol(inputShape)
200+
201+
idx.getOperationContextParams(operationShape).orNull()?.parameters?.forEach { (name, param) ->
202+
val getterName = EndpointParamsGenerator.getterName(name)
203+
val pathValue = param.path
204+
val pathExpression = JmespathExpression.parse(pathValue)
205+
val pathTraversal =
206+
RustJmespathShapeTraversalGenerator(codegenContext).generate(
207+
pathExpression,
208+
listOf(
209+
TraversalBinding.Global(
210+
"input",
211+
TraversedShape.from(model, operationShape.inputShape(model)),
212+
),
213+
),
214+
)
215+
216+
rust("// Generated from JMESPath Expression: $pathValue")
217+
rustBlockTemplate(
218+
"fn $getterName(input: #{Input}) -> Option<#{Ret}>",
219+
"Input" to input.rustType().asRef(),
220+
"Ret" to pathTraversal.outputType,
221+
) {
222+
pathTraversal.output(this)
223+
rust("Some(${pathTraversal.identifier})")
224+
}
225+
}
226+
}
227+
154228
private fun Node.toWritable(): Writable {
155229
val node = this
156230
return writable {

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/waiters/RustJmespathShapeTraversalGenerator.kt

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ data class GeneratedExpression(
128128

129129
internal fun isStringOrEnum(): Boolean = isString() || isEnum()
130130

131+
internal fun isObject(): Boolean = outputShape is TraversedShape.Object
132+
131133
/** Dereferences this expression if it is a reference. */
132134
internal fun dereference(namer: SafeNamer): GeneratedExpression =
133135
if (outputType is RustType.Reference) {
@@ -278,7 +280,7 @@ class JmesPathTraversalCodegenBugException(msg: String?, what: Throwable? = null
278280
* - Object projections
279281
* - Multi-select lists (but only when every item in the list is the exact same type)
280282
* - And/or/not boolean operations
281-
* - Functions `contains` and `length`. The `keys` function may be supported in the future.
283+
* - Functions `contains`, `length`, and `keys`.
282284
*/
283285
class RustJmespathShapeTraversalGenerator(
284286
codegenContext: ClientCodegenContext,
@@ -429,6 +431,41 @@ class RustJmespathShapeTraversalGenerator(
429431
}
430432
}
431433

434+
"keys" -> {
435+
if (expr.arguments.size != 1) {
436+
throw InvalidJmesPathTraversalException("Keys function takes exactly one argument")
437+
}
438+
val arg = generate(expr.arguments[0], bindings)
439+
if (!arg.isObject()) {
440+
throw InvalidJmesPathTraversalException("Argument to `keys` function must be an object type")
441+
}
442+
GeneratedExpression(
443+
identifier = ident,
444+
outputType = RustType.Vec(RustType.String),
445+
outputShape = TraversedShape.Array(null, TraversedShape.String(null)),
446+
output =
447+
writable {
448+
arg.output(this)
449+
val outputShape = arg.outputShape.shape
450+
when (outputShape) {
451+
is StructureShape -> {
452+
// Can't iterate a struct in Rust so source the keys from smithy
453+
val keys =
454+
outputShape.allMembers.keys.joinToString(",") { "${it.dq()}.to_string()" }
455+
rust("let $ident = vec![$keys];")
456+
}
457+
458+
is MapShape -> {
459+
rust("let $ident = ${arg.identifier}.keys().map(Clone::clone).collect::<Vec<String>>();")
460+
}
461+
462+
else ->
463+
throw UnsupportedJmesPathException("The shape type for an input to the keys function must be a struct or a map, got ${outputShape?.type}")
464+
}
465+
},
466+
)
467+
}
468+
432469
else -> throw UnsupportedJmesPathException("The `${expr.name}` function is not supported by smithy-rs")
433470
}
434471
}

0 commit comments

Comments
 (0)