Skip to content

Commit e1d56a7

Browse files
mtdowlingadwsingh
authored andcommitted
Add context param providers
1 parent 3bde20c commit e1d56a7

9 files changed

Lines changed: 487 additions & 75 deletions

File tree

client/client-rulesengine/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ extra["moduleName"] = "software.amazon.smithy.java.client.endpointrules"
1010

1111
dependencies {
1212
api(project(":client:client-core"))
13+
api(project(":jmespath"))
1314
api(libs.smithy.rules)
1415
implementation(project(":logging"))
16+
17+
testImplementation(project(":aws:client:aws-client-awsjson"))
18+
testImplementation(project(":client:dynamic-client"))
1519
}
1620

1721
jmh {
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.client.rulesengine;
7+
8+
import java.util.ArrayList;
9+
import java.util.HashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.concurrent.ConcurrentHashMap;
13+
import java.util.concurrent.ConcurrentMap;
14+
import software.amazon.smithy.java.core.schema.ApiOperation;
15+
import software.amazon.smithy.java.core.schema.Schema;
16+
import software.amazon.smithy.java.core.schema.SerializableStruct;
17+
import software.amazon.smithy.java.core.serde.document.Document;
18+
import software.amazon.smithy.java.jmespath.JMESPathDocumentQuery;
19+
import software.amazon.smithy.jmespath.JmespathExpression;
20+
import software.amazon.smithy.model.shapes.ShapeId;
21+
22+
/**
23+
* Provides context parameters from operations using {@code smithy.rules#contextParam},
24+
* {@code smithy.rules#operationContextParams}, and {@code smithy.rules#staticContextParams} traits.
25+
*
26+
* <p>The results of finding operation context parameters from an operation are cached and reused over the life of
27+
* a client per/operation.
28+
*/
29+
sealed interface ContextProvider {
30+
31+
void addContext(ApiOperation<?, ?> operation, SerializableStruct input, Map<String, Object> params);
32+
33+
final class OrchestratingProvider implements ContextProvider {
34+
private final ConcurrentMap<ShapeId, ContextProvider> PROVIDERS = new ConcurrentHashMap<>();
35+
36+
@Override
37+
public void addContext(ApiOperation<?, ?> operation, SerializableStruct input, Map<String, Object> params) {
38+
var provider = PROVIDERS.get(operation.schema().id());
39+
if (provider == null) {
40+
provider = createProvider(operation);
41+
var fresh = PROVIDERS.putIfAbsent(operation.schema().id(), provider);
42+
if (fresh != null) {
43+
provider = fresh;
44+
}
45+
}
46+
provider.addContext(operation, input, params);
47+
}
48+
49+
private ContextProvider createProvider(ApiOperation<?, ?> operation) {
50+
List<ContextProvider> providers = new ArrayList<>();
51+
var operationSchema = operation.schema();
52+
var inputSchema = operation.inputSchema();
53+
ContextParamProvider.compute(providers, inputSchema);
54+
ContextPathProvider.compute(providers, operationSchema);
55+
StaticParamsProvider.compute(providers, operationSchema); // overrides everything else
56+
return MultiContextParamProvider.from(providers);
57+
}
58+
}
59+
60+
// Find the smithy.rules#staticContextParams on the operation.
61+
final class StaticParamsProvider implements ContextProvider {
62+
private final Map<String, Object> params;
63+
64+
StaticParamsProvider(Map<String, Object> params) {
65+
this.params = params;
66+
}
67+
68+
@Override
69+
public void addContext(ApiOperation<?, ?> operation, SerializableStruct input, Map<String, Object> params) {
70+
params.putAll(this.params);
71+
}
72+
73+
static void compute(List<ContextProvider> providers, Schema operation) {
74+
var staticParamsTrait = operation.getTrait(EndpointRulesPlugin.STATIC_CONTEXT_PARAMS_TRAIT);
75+
if (staticParamsTrait == null) {
76+
return;
77+
}
78+
79+
Map<String, Object> result = new HashMap<>(staticParamsTrait.getParameters().size());
80+
for (var entry : staticParamsTrait.getParameters().entrySet()) {
81+
result.put(entry.getKey(), EndpointUtils.convertNode(entry.getValue().getValue()));
82+
}
83+
84+
providers.add(new StaticParamsProvider(result));
85+
}
86+
}
87+
88+
// Find smithy.rules#contextParam trait on operation input members.
89+
final class ContextParamProvider implements ContextProvider {
90+
private final Schema member;
91+
private final String name;
92+
93+
ContextParamProvider(Schema member, String name) {
94+
this.member = member;
95+
this.name = name;
96+
}
97+
98+
@Override
99+
public void addContext(ApiOperation<?, ?> operation, SerializableStruct input, Map<String, Object> params) {
100+
var value = input.getMemberValue(member);
101+
if (value != null) {
102+
params.put(name, value);
103+
}
104+
}
105+
106+
static void compute(List<ContextProvider> providers, Schema inputSchema) {
107+
for (var member : inputSchema.members()) {
108+
var ctxTrait = member.getTrait(EndpointRulesPlugin.CONTEXT_PARAM_TRAIT);
109+
if (ctxTrait != null) {
110+
providers.add(new ContextParamProvider(member, ctxTrait.getName()));
111+
}
112+
}
113+
}
114+
}
115+
116+
// Find the smithy.rules#operationContextParams trait on the operation and each JMESPath to extract.
117+
// TODO: I wish we didn't have to convert input to a document and could use the struct directly.
118+
// We'd need to add a new code path to the jmespath module, something like JMESPathStructQuery.
119+
final class ContextPathProvider implements ContextProvider {
120+
121+
private final String name;
122+
private final JmespathExpression jp;
123+
124+
ContextPathProvider(String name, JmespathExpression jp) {
125+
this.name = name;
126+
this.jp = jp;
127+
}
128+
129+
@Override
130+
public void addContext(ApiOperation<?, ?> operation, SerializableStruct input, Map<String, Object> params) {
131+
var doc = Document.of(input);
132+
var result = JMESPathDocumentQuery.query(jp, doc);
133+
if (result != null) {
134+
params.put(name, result.asObject());
135+
}
136+
}
137+
138+
static void compute(List<ContextProvider> providers, Schema operation) {
139+
var params = operation.getTrait(EndpointRulesPlugin.OPERATION_CONTEXT_PARAMS_TRAIT);
140+
if (params == null) {
141+
return;
142+
}
143+
144+
for (var param : params.getParameters().entrySet()) {
145+
var name = param.getKey();
146+
var path = param.getValue().getPath();
147+
var jp = JmespathExpression.parse(path);
148+
providers.add(new ContextPathProvider(name, jp));
149+
}
150+
}
151+
}
152+
153+
// Applies multiple context providers.
154+
final class MultiContextParamProvider implements ContextProvider {
155+
private final List<ContextProvider> providers;
156+
157+
MultiContextParamProvider(List<ContextProvider> providers) {
158+
this.providers = providers;
159+
}
160+
161+
static ContextProvider from(List<ContextProvider> providers) {
162+
return providers.size() == 1 ? providers.get(0) : new MultiContextParamProvider(providers);
163+
}
164+
165+
@Override
166+
public void addContext(ApiOperation<?, ?> operation, SerializableStruct input, Map<String, Object> params) {
167+
for (ContextProvider provider : providers) {
168+
provider.addContext(operation, input, params);
169+
}
170+
}
171+
}
172+
}

client/client-rulesengine/src/main/java/software/amazon/smithy/java/client/rulesengine/EndpointRulesResolver.java

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,67 +8,37 @@
88
import java.util.HashMap;
99
import java.util.Map;
1010
import java.util.concurrent.CompletableFuture;
11-
import java.util.concurrent.ConcurrentHashMap;
12-
import java.util.concurrent.ConcurrentMap;
1311
import software.amazon.smithy.java.client.core.endpoint.Endpoint;
1412
import software.amazon.smithy.java.client.core.endpoint.EndpointResolver;
1513
import software.amazon.smithy.java.client.core.endpoint.EndpointResolverParams;
16-
import software.amazon.smithy.java.core.schema.Schema;
17-
import software.amazon.smithy.model.shapes.ShapeId;
14+
import software.amazon.smithy.java.core.schema.ApiOperation;
15+
import software.amazon.smithy.java.core.schema.SerializableStruct;
1816

1917
/**
2018
* Endpoint resolver that uses the endpoint rules engine.
2119
*/
2220
final class EndpointRulesResolver implements EndpointResolver {
2321

2422
private final RulesProgram program;
25-
private final ConcurrentMap<ShapeId, Map<String, Object>> STATIC_PARAMS = new ConcurrentHashMap<>();
23+
private final ContextProvider operationContextParams = new ContextProvider.OrchestratingProvider();
2624

2725
EndpointRulesResolver(RulesProgram program) {
2826
this.program = program;
2927
}
3028

3129
@Override
3230
public CompletableFuture<Endpoint> resolveEndpoint(EndpointResolverParams params) {
33-
var operation = params.operation().schema();
34-
var endpointParams = createEndpointParams(operation);
35-
3631
try {
32+
var endpointParams = createEndpointParams(params.operation(), params.inputValue());
3733
return CompletableFuture.completedFuture(program.resolveEndpoint(params.context(), endpointParams));
3834
} catch (RulesEvaluationError e) {
3935
return CompletableFuture.failedFuture(e);
4036
}
4137
}
4238

43-
private Map<String, Object> createEndpointParams(Schema operation) {
44-
var staticParams = getStaticParams(operation);
45-
// TODO: Grab input from RulesEnginePlugin.OPERATION_CONTEXT_PARAMS_TRAIT
46-
// TODO: Grab input from RulesEnginePlugin.CONTEXT_PARAM_TRAIT
47-
return new HashMap<>(staticParams);
48-
}
49-
50-
private Map<String, Object> getStaticParams(Schema operation) {
51-
var id = operation.id();
52-
var staticParams = STATIC_PARAMS.get(id);
53-
if (staticParams != null) {
54-
return staticParams;
55-
} else {
56-
staticParams = computeStaticParams(operation);
57-
var fresh = STATIC_PARAMS.putIfAbsent(id, staticParams);
58-
return fresh == null ? staticParams : fresh;
59-
}
60-
}
61-
62-
private Map<String, Object> computeStaticParams(Schema operation) {
63-
var staticParamsTrait = operation.getTrait(EndpointRulesPlugin.STATIC_CONTEXT_PARAMS_TRAIT);
64-
if (staticParamsTrait == null) {
65-
return Map.of();
66-
}
67-
68-
Map<String, Object> result = new HashMap<>(staticParamsTrait.getParameters().size());
69-
for (var entry : staticParamsTrait.getParameters().entrySet()) {
70-
result.put(entry.getKey(), EndpointUtils.convertNodeInput(entry.getValue().getValue()));
71-
}
72-
return result;
39+
private Map<String, Object> createEndpointParams(ApiOperation<?, ?> operation, SerializableStruct input) {
40+
Map<String, Object> params = new HashMap<>();
41+
operationContextParams.addContext(operation, input, params);
42+
return params;
7343
}
7444
}

client/client-rulesengine/src/main/java/software/amazon/smithy/java/client/rulesengine/EndpointUtils.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import software.amazon.smithy.model.node.ArrayNode;
1414
import software.amazon.smithy.model.node.BooleanNode;
1515
import software.amazon.smithy.model.node.Node;
16+
import software.amazon.smithy.model.node.NumberNode;
17+
import software.amazon.smithy.model.node.ObjectNode;
1618
import software.amazon.smithy.model.node.StringNode;
1719
import software.amazon.smithy.rulesengine.language.evaluation.value.ArrayValue;
1820
import software.amazon.smithy.rulesengine.language.evaluation.value.BooleanValue;
@@ -28,20 +30,38 @@ final class EndpointUtils {
2830
private EndpointUtils() {}
2931

3032
// "The type of the value MUST be either a string, boolean or an array of string."
31-
static Object convertNodeInput(Node value) {
33+
static Object convertNode(Node value, boolean allowAllTypes) {
3234
if (value instanceof StringNode s) {
3335
return s.getValue();
3436
} else if (value instanceof BooleanNode b) {
3537
return b.getValue();
3638
} else if (value instanceof ArrayNode a) {
3739
List<Object> result = new ArrayList<>(a.size());
3840
for (var e : a.getElements()) {
39-
result.add(convertNodeInput(e));
41+
result.add(convertNode(e, allowAllTypes));
4042
}
4143
return result;
42-
} else {
43-
throw new RulesEvaluationError("Unsupported endpoint ruleset parameter: " + value);
44+
} else if (allowAllTypes) {
45+
if (value instanceof NumberNode n) {
46+
return n.getValue();
47+
} else if (value instanceof ObjectNode o) {
48+
var result = new HashMap<String, Object>(o.size());
49+
for (var e : o.getStringMap().entrySet()) {
50+
result.put(e.getKey(), convertNode(e.getValue(), allowAllTypes));
51+
}
52+
return result;
53+
} else if (value.isNullNode()) {
54+
return null;
55+
} else {
56+
throw new RulesEvaluationError("Unsupported endpoint ruleset parameter type: " + value);
57+
}
4458
}
59+
60+
throw new RulesEvaluationError("Unsupported endpoint ruleset parameter: " + value);
61+
}
62+
63+
static Object convertNode(Node value) {
64+
return convertNode(value, false);
4565
}
4666

4767
static Object convertInputParamValue(Value value) {

client/client-rulesengine/src/test/java/software/amazon/smithy/java/client/rulesengine/EndpointRulesResolverTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@ public Schema schema() {
102102

103103
@Override
104104
public Schema inputSchema() {
105-
return null;
105+
return Schema.structureBuilder(ShapeId.from("smithy.example#FooInput")).build();
106106
}
107107

108108
@Override
109109
public Schema outputSchema() {
110-
return null;
110+
return Schema.structureBuilder(ShapeId.from("smithy.example#FooOutput")).build();
111111
}
112112

113113
@Override

client/client-rulesengine/src/test/java/software/amazon/smithy/java/client/rulesengine/EndpointUtilsTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public void getsUriParts() throws Exception {
136136
@ParameterizedTest
137137
@MethodSource("convertsNodeInputsProvider")
138138
void convertsNodeInputs(Node value, Object object) {
139-
var converted = EndpointUtils.convertNodeInput(value);
139+
var converted = EndpointUtils.convertNode(value);
140140

141141
assertThat(converted, equalTo(object));
142142
}
@@ -152,6 +152,6 @@ public static List<Arguments> convertsNodeInputsProvider() {
152152

153153
@Test
154154
public void throwsOnUnsupportNodeInput() {
155-
Assertions.assertThrows(RulesEvaluationError.class, () -> EndpointUtils.convertNodeInput(Node.from(1)));
155+
Assertions.assertThrows(RulesEvaluationError.class, () -> EndpointUtils.convertNode(Node.from(1)));
156156
}
157157
}

0 commit comments

Comments
 (0)