1
1
import static java.util.stream.Collectors.toList;
2
+ import static java.util.stream.Collectors.toMap;
2
3
3
4
import java.util.ArrayList;
4
5
import java.util.Collection;
5
6
import java.util.Collections;
7
+ import java.util.HashMap;
6
8
import java.util.List;
9
+ import java.util.Map;
7
10
import java.util.Objects;
8
11
import java.util.function.Function;
9
12
import software.amazon.awssdk.annotations.SdkInternalApi;
@@ -58,6 +61,11 @@ public final class JmesPathRuntime {
58
61
*/
59
62
private List<Object> listValue;
60
63
64
+ /**
65
+ * The value if this is an {@link Type#MAP} (or null otherwise).
66
+ */
67
+ private Map<Object, Object> mapValue;
68
+
61
69
/**
62
70
* The value if this is an {@link Type#BOOLEAN} (or null otherwise).
63
71
*/
@@ -73,6 +81,16 @@ public final class JmesPathRuntime {
73
81
this.isProjection = projection;
74
82
}
75
83
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
+
76
94
/**
77
95
* Create a non-projection value, where the value type is determined reflectively.
78
96
*/
@@ -93,6 +111,9 @@ public final class JmesPathRuntime {
93
111
} else if (value instanceof Collection) {
94
112
this.type = Type.LIST;
95
113
this.listValue = new ArrayList<>(((Collection<?>) value));
114
+ } else if (value instanceof Map) {
115
+ this.type = Type.MAP;
116
+ this.mapValue = new HashMap<>((Map<?, ?>) value);
96
117
} else if (value instanceof Boolean) {
97
118
this.type = Type.BOOLEAN;
98
119
this.booleanValue = (Boolean) value;
@@ -108,6 +129,13 @@ public final class JmesPathRuntime {
108
129
return new Value(values, true);
109
130
}
110
131
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
+
111
139
/**
112
140
* Retrieve the actual value that this represents (this will be the same value passed to the constructor).
113
141
*/
@@ -119,6 +147,7 @@ public final class JmesPathRuntime {
119
147
case STRING: return stringValue;
120
148
case BOOLEAN: return booleanValue;
121
149
case LIST: return listValue;
150
+ case MAP: return mapValue;
122
151
default: throw new IllegalStateException();
123
152
}
124
153
}
@@ -138,6 +167,21 @@ public final class JmesPathRuntime {
138
167
return Collections.singletonList(value());
139
168
}
140
169
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
+
141
185
/**
142
186
* Retrieve the actual value that this represents, as a Boolean.
143
187
* Note that only null, boolean and string types are supported.
@@ -176,7 +220,7 @@ public final class JmesPathRuntime {
176
220
177
221
/**
178
222
* 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.
180
224
* If the value has a different type, the code makes a best effort to return a single element
181
225
* list of String. See {@code stringValue}.
182
226
*/
@@ -204,6 +248,39 @@ public final class JmesPathRuntime {
204
248
return Collections.singletonList(stringValue());
205
249
}
206
250
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
+
207
284
/**
208
285
* Convert this value to a new constant value, discarding the current value.
209
286
*/
@@ -230,6 +307,10 @@ public final class JmesPathRuntime {
230
307
return newProjection(listValue);
231
308
}
232
309
310
+ if (type == Type.MAP) {
311
+ return newProjection(mapValue);
312
+ }
313
+
233
314
if (type == Value.Type.POJO) {
234
315
return newProjection(pojoValue.sdkFields().stream().map(f -> f.getValueOrDefault(pojoValue))
235
316
.filter(Objects::nonNull).collect(toList()));
@@ -300,19 +381,31 @@ public final class JmesPathRuntime {
300
381
return NULL_VALUE;
301
382
}
302
383
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);
305
394
}
306
395
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);
316
409
}
317
410
318
411
/**
@@ -335,6 +428,10 @@ public final class JmesPathRuntime {
335
428
return new Value(Math.toIntExact(listValue.size()));
336
429
}
337
430
431
+ if (type == Type.MAP) {
432
+ return new Value(Math.toIntExact(mapValue.size()));
433
+ }
434
+
338
435
throw new IllegalArgumentException("Unsupported type for length function: " + type);
339
436
}
340
437
@@ -347,6 +444,10 @@ public final class JmesPathRuntime {
347
444
return new Value(pojoValue.sdkFields().stream().map(SdkField::memberName).collect(toList()));
348
445
}
349
446
447
+ if (type == Type.MAP) {
448
+ return new Value(mapValue.keySet());
449
+ }
450
+
350
451
throw new IllegalArgumentException("Unsupported type for keys function: " + type);
351
452
}
352
453
@@ -367,8 +468,14 @@ public final class JmesPathRuntime {
367
468
return new Value(stringValue.contains(rhs.stringValue));
368
469
}
369
470
471
+ Object value = rhs.value();
472
+
370
473
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));
372
479
}
373
480
374
481
throw new IllegalArgumentException("Unsupported type for contains function: " + type);
@@ -430,6 +537,32 @@ public final class JmesPathRuntime {
430
537
return new Value(result);
431
538
}
432
539
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
+
433
566
/**
434
567
* Perform an OR comparison between this value and another one: https://jmespath.org/specification.html#or-expressions
435
568
*/
@@ -464,6 +597,8 @@ public final class JmesPathRuntime {
464
597
return !pojoValue.sdkFields().isEmpty();
465
598
case LIST:
466
599
return !listValue.isEmpty();
600
+ case MAP:
601
+ return !mapValue.isEmpty();
467
602
case STRING:
468
603
return !stringValue.isEmpty();
469
604
case BOOLEAN:
@@ -474,16 +609,21 @@ public final class JmesPathRuntime {
474
609
}
475
610
476
611
/**
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.
478
614
*/
479
615
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");
487
627
}
488
628
489
629
/**
@@ -492,6 +632,7 @@ public final class JmesPathRuntime {
492
632
private enum Type {
493
633
POJO,
494
634
LIST,
635
+ MAP,
495
636
BOOLEAN,
496
637
STRING,
497
638
INTEGER,
0 commit comments