Skip to content

Commit cfe975e

Browse files
authored
Add support for Map type to JmesPathRuntime (#5423)
1 parent dac020d commit cfe975e

File tree

6 files changed

+413
-23
lines changed

6 files changed

+413
-23
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Adds support for Map type to JmesPathRuntime"
6+
}

codegen/src/main/resources/software/amazon/awssdk/codegen/jmespath/JmesPathRuntime.java.resource

Lines changed: 162 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import static java.util.stream.Collectors.toList;
2+
import static java.util.stream.Collectors.toMap;
23

34
import java.util.ArrayList;
45
import java.util.Collection;
56
import java.util.Collections;
7+
import java.util.HashMap;
68
import java.util.List;
9+
import java.util.Map;
710
import java.util.Objects;
811
import java.util.function.Function;
912
import software.amazon.awssdk.annotations.SdkInternalApi;
@@ -58,6 +61,11 @@ public final class JmesPathRuntime {
5861
*/
5962
private List<Object> listValue;
6063

64+
/**
65+
* The value if this is an {@link Type#MAP} (or null otherwise).
66+
*/
67+
private Map<Object, Object> mapValue;
68+
6169
/**
6270
* The value if this is an {@link Type#BOOLEAN} (or null otherwise).
6371
*/
@@ -73,6 +81,16 @@ public final class JmesPathRuntime {
7381
this.isProjection = projection;
7482
}
7583

84+
/**
85+
* Create a MAP value, specifying whether this is a projection. This is private and is usually invoked by
86+
* {@link #newProjection(Map)}.
87+
*/
88+
private Value(Map<?, ?> value, boolean projection) {
89+
this.type = Type.MAP;
90+
this.mapValue = new HashMap<>(value);
91+
this.isProjection = projection;
92+
}
93+
7694
/**
7795
* Create a non-projection value, where the value type is determined reflectively.
7896
*/
@@ -93,6 +111,9 @@ public final class JmesPathRuntime {
93111
} else if (value instanceof Collection) {
94112
this.type = Type.LIST;
95113
this.listValue = new ArrayList<>(((Collection<?>) value));
114+
} else if (value instanceof Map) {
115+
this.type = Type.MAP;
116+
this.mapValue = new HashMap<>((Map<?, ?>) value);
96117
} else if (value instanceof Boolean) {
97118
this.type = Type.BOOLEAN;
98119
this.booleanValue = (Boolean) value;
@@ -108,6 +129,13 @@ public final class JmesPathRuntime {
108129
return new Value(values, true);
109130
}
110131

132+
/**
133+
* Create a {@link Type#MAP} with a {@link #isProjection} of true.
134+
*/
135+
private static Value newProjection(Map<?, ?> values) {
136+
return new Value(values, true);
137+
}
138+
111139
/**
112140
* Retrieve the actual value that this represents (this will be the same value passed to the constructor).
113141
*/
@@ -119,6 +147,7 @@ public final class JmesPathRuntime {
119147
case STRING: return stringValue;
120148
case BOOLEAN: return booleanValue;
121149
case LIST: return listValue;
150+
case MAP: return mapValue;
122151
default: throw new IllegalStateException();
123152
}
124153
}
@@ -138,6 +167,21 @@ public final class JmesPathRuntime {
138167
return Collections.singletonList(value());
139168
}
140169

170+
/**
171+
* Retrieve the actual value that this represents, as a map of objects.
172+
*/
173+
private Map<Object, Object> mapValues() {
174+
if (type == Type.NULL) {
175+
return Collections.emptyMap();
176+
}
177+
178+
if (type == Type.MAP) {
179+
return mapValue;
180+
}
181+
182+
throw new IllegalStateException("Must be MAP type to get map values");
183+
}
184+
141185
/**
142186
* Retrieve the actual value that this represents, as a Boolean.
143187
* Note that only null, boolean and string types are supported.
@@ -176,7 +220,7 @@ public final class JmesPathRuntime {
176220

177221
/**
178222
* Retrieve the actual value that this represents, as a list of String.
179-
* Note that if the contents of the list is not String, a value is thrown.
223+
* Note that if the contents of the list is not String, an exception is thrown.
180224
* If the value has a different type, the code makes a best effort to return a single element
181225
* list of String. See {@code stringValue}.
182226
*/
@@ -204,6 +248,39 @@ public final class JmesPathRuntime {
204248
return Collections.singletonList(stringValue());
205249
}
206250

251+
/**
252+
* Retrieve the actual value that this represents, as a map of Strings.
253+
* Note that if the contents of the map are not String, or
254+
* if the value has a different type, an exception is thrown.
255+
*/
256+
public Map<String, String> stringValuesMap() {
257+
if (type == Type.NULL) {
258+
return Collections.emptyMap();
259+
}
260+
261+
if (type == Type.MAP) {
262+
Map<String, String> result = new HashMap<>();
263+
mapValue.forEach((key, value) -> {
264+
Value keyAsValue = new Value(key);
265+
Value entryAsValue = new Value(value);
266+
if (keyAsValue.type != Type.NULL) {
267+
if (!isStringType(keyAsValue) || !isStringType(entryAsValue)) {
268+
throw new IllegalStateException("Keys and values must be String type");
269+
}
270+
result.put(keyAsValue.stringValue, entryAsValue.stringValue);
271+
}
272+
});
273+
274+
return result;
275+
}
276+
277+
throw new IllegalArgumentException("Not of type MAP");
278+
}
279+
280+
private boolean isStringType(Value value) {
281+
return value.type == Type.STRING;
282+
}
283+
207284
/**
208285
* Convert this value to a new constant value, discarding the current value.
209286
*/
@@ -230,6 +307,10 @@ public final class JmesPathRuntime {
230307
return newProjection(listValue);
231308
}
232309

310+
if (type == Type.MAP) {
311+
return newProjection(mapValue);
312+
}
313+
233314
if (type == Value.Type.POJO) {
234315
return newProjection(pojoValue.sdkFields().stream().map(f -> f.getValueOrDefault(pojoValue))
235316
.filter(Objects::nonNull).collect(toList()));
@@ -300,19 +381,31 @@ public final class JmesPathRuntime {
300381
return NULL_VALUE;
301382
}
302383

303-
if (type != Type.LIST) {
304-
throw new IllegalArgumentException("Unsupported type for filter function: " + type);
384+
if (type == Type.LIST) {
385+
List<Object> results = new ArrayList<>();
386+
listValue.forEach(entry -> {
387+
Value entryValue = new Value(entry);
388+
Value predicateResult = predicate.apply(entryValue);
389+
if (predicateResult.isTrue()) {
390+
results.add(entry);
391+
}
392+
});
393+
return new Value(results);
305394
}
306395

307-
List<Object> results = new ArrayList<>();
308-
listValue.forEach(entry -> {
309-
Value entryValue = new Value(entry);
310-
Value predicateResult = predicate.apply(entryValue);
311-
if (predicateResult.isTrue()) {
312-
results.add(entry);
313-
}
314-
});
315-
return new Value(results);
396+
if (type == Type.MAP) {
397+
Map<Object, Object> results = new HashMap<>();
398+
mapValue.forEach((key, entry) -> {
399+
Value entryValue = new Value(entry);
400+
Value predicateResult = predicate.apply(entryValue);
401+
if (predicateResult.isTrue()) {
402+
results.put(key, entry);
403+
}
404+
});
405+
return new Value(results);
406+
}
407+
408+
throw new IllegalArgumentException("Unsupported type for filter function: " + type);
316409
}
317410

318411
/**
@@ -335,6 +428,10 @@ public final class JmesPathRuntime {
335428
return new Value(Math.toIntExact(listValue.size()));
336429
}
337430

431+
if (type == Type.MAP) {
432+
return new Value(Math.toIntExact(mapValue.size()));
433+
}
434+
338435
throw new IllegalArgumentException("Unsupported type for length function: " + type);
339436
}
340437

@@ -347,6 +444,10 @@ public final class JmesPathRuntime {
347444
return new Value(pojoValue.sdkFields().stream().map(SdkField::memberName).collect(toList()));
348445
}
349446

447+
if (type == Type.MAP) {
448+
return new Value(mapValue.keySet());
449+
}
450+
350451
throw new IllegalArgumentException("Unsupported type for keys function: " + type);
351452
}
352453

@@ -367,8 +468,14 @@ public final class JmesPathRuntime {
367468
return new Value(stringValue.contains(rhs.stringValue));
368469
}
369470

471+
Object value = rhs.value();
472+
370473
if (type == Type.LIST) {
371-
return new Value(listValue.stream().anyMatch(v -> Objects.equals(v, rhs.value())));
474+
return new Value(listValue.stream().anyMatch(v -> Objects.equals(v, value)));
475+
}
476+
477+
if (type == Type.MAP) {
478+
return new Value(mapValue.containsValue(value));
372479
}
373480

374481
throw new IllegalArgumentException("Unsupported type for contains function: " + type);
@@ -430,6 +537,32 @@ public final class JmesPathRuntime {
430537
return new Value(result);
431538
}
432539

540+
/**
541+
* Perform a multi-select hash expression on this value:
542+
* https://jmespath.org/specification.html#multiselect-hash
543+
*/
544+
public final Value multiSelectHash(Map<String, Function<Value, Value>> selections) {
545+
if (isProjection) {
546+
return project(v -> v.multiSelectHash(selections));
547+
}
548+
if (type == Type.NULL) {
549+
return NULL_VALUE;
550+
}
551+
if (type != Type.MAP) {
552+
throw new IllegalArgumentException("Multi-select map operation is only supported for maps");
553+
}
554+
555+
Map<String, Object> result = new HashMap<>();
556+
for (Map.Entry<String, Function<Value, Value>> entry : selections.entrySet()) {
557+
String key = entry.getKey();
558+
Function<Value, Value> function = entry.getValue();
559+
Value selectedValue = function.apply(new Value(mapValue.get(key)));
560+
result.put(key, selectedValue.value());
561+
}
562+
563+
return new Value(result);
564+
}
565+
433566
/**
434567
* Perform an OR comparison between this value and another one: https://jmespath.org/specification.html#or-expressions
435568
*/
@@ -464,6 +597,8 @@ public final class JmesPathRuntime {
464597
return !pojoValue.sdkFields().isEmpty();
465598
case LIST:
466599
return !listValue.isEmpty();
600+
case MAP:
601+
return !mapValue.isEmpty();
467602
case STRING:
468603
return !stringValue.isEmpty();
469604
case BOOLEAN:
@@ -474,16 +609,21 @@ public final class JmesPathRuntime {
474609
}
475610

476611
/**
477-
* Project the provided function across all values in this list. Assumes this is a LIST and isProjection is true.
612+
* Project the provided function across all values in this list. Assumes this is a LIST or MAP and isProjection
613+
* is true.
478614
*/
479615
private Value project(Function<Value, Value> functionToApply) {
480-
return new Value(listValue.stream()
481-
.map(Value::new)
482-
.map(functionToApply)
483-
.map(Value::value)
484-
.filter(Objects::nonNull)
485-
.collect(toList()),
486-
true);
616+
if (type == Type.LIST) {
617+
return new Value(listValue.stream().map(Value::new).map(functionToApply).map(Value::value)
618+
.filter(Objects::nonNull).collect(toList()), true);
619+
}
620+
621+
if (type == Type.MAP) {
622+
return new Value(mapValue.values().stream().map(Value::new).map(functionToApply).map(Value::value)
623+
.filter(Objects::nonNull).collect(toList()), true);
624+
}
625+
626+
throw new IllegalArgumentException("Can only project on List or Map types");
487627
}
488628

489629
/**
@@ -492,6 +632,7 @@ public final class JmesPathRuntime {
492632
private enum Type {
493633
POJO,
494634
LIST,
635+
MAP,
495636
BOOLEAN,
496637
STRING,
497638
INTEGER,

test/codegen-generated-classes-test/src/main/resources/codegen-resources/customresponsemetadata/customization.config

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,22 @@
2222
"skipEndpointTestGeneration": true,
2323
"requiredTraitValidationEnabled": true,
2424
"delegateAsyncClientClass": true,
25-
"delegateSyncClientClass": true
25+
"delegateSyncClientClass": true,
26+
"useSraAuth": true,
27+
"endpointParameters": {
28+
"Tables": {
29+
"required": false,
30+
"type": "StringArray"
31+
}
32+
},
33+
"customOperationContextParams": [
34+
{
35+
"operationName": "GetOperationWithMapEndpointParam",
36+
"operationContextParamsMap": {
37+
"Tables": {
38+
"value" : "keys(MapOfStrings)"
39+
}
40+
}
41+
}
42+
]
2643
}

test/codegen-generated-classes-test/src/main/resources/codegen-resources/customresponsemetadata/service-2.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,15 @@
338338
"responseAlgorithms": ["CRC32C", "CRC32", "SHA1", "SHA256"]
339339
}
340340
},
341+
"GetOperationWithMapEndpointParam":{
342+
"name":"GetOperationWithMapEndpointParam",
343+
"http":{
344+
"method":"GET",
345+
"requestUri":"/"
346+
},
347+
"input":{"shape":"GetOperationWithMapEndpointParamInput"},
348+
"output":{"shape":"GetOperationWithMapEndpointParamOutput"}
349+
},
341350
"OperationWithChecksumNonStreaming":{
342351
"name":"OperationWithChecksumNonStreaming",
343352
"http":{
@@ -1114,6 +1123,28 @@
11141123
}
11151124
},
11161125
"payload":"Body"
1126+
},
1127+
"GetOperationWithMapEndpointParamInput": {
1128+
"type": "structure",
1129+
"required":["MapOfStrings"],
1130+
"members": {
1131+
"MapOfStrings":{
1132+
"shape":"MapOfStrings"
1133+
}
1134+
}
1135+
},
1136+
"GetOperationWithMapEndpointParamOutput": {
1137+
"type": "structure",
1138+
"members": {
1139+
"MapOfStrings":{
1140+
"shape":"MapOfStrings"
1141+
}
1142+
}
1143+
},
1144+
"MapOfStrings":{
1145+
"type":"map",
1146+
"key":{"shape":"String"},
1147+
"value":{"shape":"String"}
11171148
}
11181149
}
11191150
}

0 commit comments

Comments
 (0)