Skip to content

Commit 26ca47c

Browse files
authored
feat(java): dynamic type checking for union-typed parameters (#3703)
Add runtime type checks for Java around type unions to provide better error messages for developers when union types are being used. Only performed if `software.amazon.jsii.Configuration.getRuntimeTypeChecking()` is `true`, which it is by default. --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
1 parent 23a5baa commit 26ca47c

File tree

10 files changed

+1083
-17
lines changed

10 files changed

+1083
-17
lines changed

.all-contributorsrc

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,8 +1272,7 @@
12721272
"avatar_url": "https://avatars.githubusercontent.com/u/66279577?v=4",
12731273
"profile": "https://github.com/comcalvi",
12741274
"contributions": [
1275-
"code",
1276-
"bug"
1275+
"code"
12771276
]
12781277
},
12791278
{

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
8484
<tr>
8585
<td align="center"><a href="http://bdawg.org/"><img src="https://avatars1.githubusercontent.com/u/92937?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Breland Miley</b></sub></a><br /><a href="https://github.com/aws/jsii/commits?author=mindstorms6" title="Code">💻</a></td>
8686
<td align="center"><a href="https://github.com/CaerusKaru"><img src="https://avatars3.githubusercontent.com/u/416563?v=4?s=100" width="100px;" alt=""/><br /><sub><b>CaerusKaru</b></sub></a><br /><a href="https://github.com/aws/jsii/commits?author=CaerusKaru" title="Code">💻</a> <a href="https://github.com/aws/jsii/pulls?q=is%3Apr+author%3ACaerusKaru" title="Maintenance">🚧</a></td>
87-
<td align="center"><a href="https://github.com/comcalvi"><img src="https://avatars.githubusercontent.com/u/66279577?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Calvin Combs</b></sub></a><br /><a href="https://github.com/aws/jsii/commits?author=comcalvi" title="Code">💻</a> <a href="https://github.com/aws/jsii/issues?q=author%3Acomcalvi+label%3Abug" title="Bug reports">🐛</a></td>
87+
<td align="center"><a href="https://github.com/comcalvi"><img src="https://avatars.githubusercontent.com/u/66279577?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Calvin Combs</b></sub></a><br /><a href="https://github.com/aws/jsii/commits?author=comcalvi" title="Code">💻</a></td>
8888
<td align="center"><a href="https://camilobermudez85.github.io/"><img src="https://avatars0.githubusercontent.com/u/7834055?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Camilo Bermúdez</b></sub></a><br /><a href="https://github.com/aws/jsii/issues?q=author%3Acamilobermudez85+label%3Abug" title="Bug reports">🐛</a></td>
8989
<td align="center"><a href="https://github.com/campionfellin"><img src="https://avatars3.githubusercontent.com/u/11984923?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Campion Fellin</b></sub></a><br /><a href="https://github.com/aws/jsii/commits?author=campionfellin" title="Code">💻</a></td>
9090
<td align="center"><a href="https://github.com/carterv"><img src="https://avatars2.githubusercontent.com/u/1551538?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Carter Van Deuren</b></sub></a><br /><a href="https://github.com/aws/jsii/issues?q=author%3Acarterv+label%3Abug" title="Bug reports">🐛</a></td>
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package software.amazon.jsii.testing;
2+
3+
import org.junit.jupiter.api.Test;
4+
import software.amazon.jsii.JsiiException;
5+
import software.amazon.jsii.JsiiObject;
6+
import software.amazon.jsii.tests.calculator.*;
7+
import software.amazon.jsii.tests.calculator.anonymous.*;
8+
import software.amazon.jsii.tests.calculator.anonymous.UseOptions;
9+
10+
import java.util.ArrayList;
11+
import java.util.Arrays;
12+
import java.util.Collection;
13+
import java.util.Collections;
14+
import java.util.HashMap;
15+
import java.util.Map;
16+
17+
import static org.junit.jupiter.api.Assertions.*;
18+
19+
public class TypeCheckingTest {
20+
private static class StructAImplementer implements StructA, StructB {
21+
public String requiredString;
22+
23+
StructAImplementer(String param) {
24+
requiredString = param;
25+
}
26+
27+
public void setRequiredString(String param) {
28+
requiredString = param;
29+
}
30+
31+
public String getRequiredString() {
32+
return requiredString;
33+
}
34+
}
35+
36+
@Test
37+
public void constructor() {
38+
HashMap<String, Object> map = new HashMap<String, Object>();
39+
map.put("good", new StructAImplementer("present"));
40+
map.put("bad", "Not a StructA or StructB");
41+
ArrayList<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
42+
list.add(map);
43+
Exception e = assertThrows(IllegalArgumentException.class, () ->
44+
{
45+
new ClassWithCollectionOfUnions(list);
46+
});
47+
48+
assertEquals("Expected unionProperty.get(0).get(\"bad\") to be one of: software.amazon.jsii.tests.calculator.StructA, software.amazon.jsii.tests.calculator.StructB; received class java.lang.String", e.getMessage());
49+
}
50+
51+
@Test
52+
public void anonymousObjectIsValid()
53+
{
54+
Object anonymousObject = UseOptions.provide("A");
55+
assertEquals(JsiiObject.class, anonymousObject.getClass());
56+
assertEquals("A", UseOptions.consume(anonymousObject));
57+
}
58+
59+
@Test
60+
public void nestedUnion() {
61+
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () ->
62+
{
63+
ArrayList<Object> list = new ArrayList<Object>();
64+
list.add(1337.42);
65+
new ClassWithNestedUnion(list);
66+
});
67+
assertEquals("Expected unionProperty.get(0) to be one of: java.util.Map<java.lang.String, java.lang.Object>, java.util.List<java.lang.Object>; received class java.lang.Double", e.getMessage());
68+
69+
e = assertThrows(IllegalArgumentException.class, () ->
70+
{
71+
ArrayList<Object> list = new ArrayList<Object>();
72+
ArrayList<Object> nestedList = new ArrayList<Object>();
73+
nestedList.add(new StructAImplementer("required"));
74+
nestedList.add(1337.42);
75+
list.add(nestedList);
76+
new ClassWithNestedUnion(list);
77+
});
78+
assertEquals("Expected unionProperty.get(0).get(1) to be one of: software.amazon.jsii.tests.calculator.StructA, software.amazon.jsii.tests.calculator.StructB; received class java.lang.Double", e.getMessage());
79+
80+
e = assertThrows(IllegalArgumentException.class, () ->
81+
{
82+
HashMap<String, Object> map = new HashMap<String, Object>();
83+
ArrayList<Object> list = new ArrayList<Object>();
84+
map.put("good", new StructAImplementer("present"));
85+
map.put("bad", "Not a StructA or StructB");
86+
list.add(map);
87+
new ClassWithNestedUnion(list);
88+
});
89+
assertEquals("Expected unionProperty.get(0).get(\"bad\") to be one of: software.amazon.jsii.tests.calculator.StructA, software.amazon.jsii.tests.calculator.StructB; received class java.lang.String", e.getMessage());
90+
}
91+
92+
@Test
93+
public void keysAreTypeChecked() {
94+
HashMap<Object, Object> map = new HashMap<Object, Object>();
95+
ArrayList<Object> list = new ArrayList<Object>();
96+
map.put("good", new StructAImplementer("present"));
97+
map.put(1337.42, new StructAImplementer("present"));
98+
list.add(map);
99+
100+
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> {
101+
new ClassWithNestedUnion(list);
102+
});
103+
assertEquals("Expected unionProperty.get(0).keySet() to contain class String; received class java.lang.Double", e.getMessage());
104+
}
105+
106+
@Test
107+
public void variadic() {
108+
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () ->
109+
{
110+
new VariadicTypeUnion(new StructAImplementer("present"), 1337.42);
111+
});
112+
assertEquals("Expected union[1] to be one of: software.amazon.jsii.tests.calculator.StructA, software.amazon.jsii.tests.calculator.StructB; received class java.lang.Double", e.getMessage());
113+
}
114+
115+
@Test
116+
public void setter() {
117+
HashMap<String, Object> map = new HashMap<String, Object>();
118+
map.put("good", new StructAImplementer("present"));
119+
map.put("bad", "Not a StructA or StructB");
120+
ArrayList<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
121+
ClassWithCollectionOfUnions subject = new ClassWithCollectionOfUnions(list);
122+
list.add(map);
123+
124+
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> {
125+
subject.setUnionProperty(list);
126+
});
127+
assertEquals("Expected value.get(0).get(\"bad\") to be one of: software.amazon.jsii.tests.calculator.StructA, software.amazon.jsii.tests.calculator.StructB; received class java.lang.String", e.getMessage());
128+
}
129+
130+
@Test
131+
public void staticMethod()
132+
{
133+
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> {
134+
StructUnionConsumer.isStructA("Not a StructA");
135+
});
136+
assertEquals("Expected struct to be one of: software.amazon.jsii.tests.calculator.StructA, software.amazon.jsii.tests.calculator.StructB; received class java.lang.String", e.getMessage());
137+
}
138+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import * as path from 'path';
22

3+
const version = require('../package.json').version;
4+
35
export const maven = {
46
groupId: 'software.amazon.jsii',
57
artifactId: 'jsii-runtime',
6-
version: require('../package.json').version.replace(/\+.+$/, '')
8+
version: version === '0.0.0' ? '0.0.0-SNAPSHOT' : version.replace(/\+.+$/, ''),
79
};
810

911
export const repository = path.resolve(__dirname, '../maven-repo');
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package software.amazon.jsii;
2+
3+
/**
4+
* Runtime configuration flags available for the Java jsii runtime.
5+
*/
6+
public final class Configuration {
7+
private static boolean runtimeTypeChecking = true;
8+
9+
/**
10+
* Determines whether runtime type checking will be performed in places where
11+
* APIs accept {@link java.lang.Object} but the underlying model actually
12+
* uses a type union.
13+
*
14+
* Disabling this configuration allows to stop paying the runtime cost of type
15+
* checking, however it will produce degraded error messages in case of a
16+
* developer error.
17+
*/
18+
public static boolean getRuntimeTypeChecking() {
19+
return Configuration.runtimeTypeChecking;
20+
}
21+
22+
/**
23+
* Specifies whether runtime type checking will be performed in places where
24+
* APIs accept {@link java.lang.Object} but the underlying model actually
25+
* uses a type union.
26+
*
27+
* Disabling this configuration allows to stop paying the runtime cost of type
28+
* checking, however it will produce degraded error messages in case of a
29+
* developer error.
30+
*/
31+
public void setRuntimeTypeChecking(final boolean value) {
32+
Configuration.runtimeTypeChecking = value;
33+
}
34+
35+
private Configuration(){
36+
throw new UnsupportedOperationException();
37+
}
38+
}

packages/jsii-pacmak/generate.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ cat > lib/version.ts <<HERE
1515
// Generated at $(date -u +"%Y-%m-%dT%H:%M:%SZ") by generate.sh
1616
1717
/** The short version number for this JSII compiler (e.g: \`X.Y.Z\`) */
18-
export const VERSION = '${VERSION}';
18+
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
19+
export const VERSION: string = '${VERSION}';
1920
2021
/** The qualified version number for this JSII compiler (e.g: \`X.Y.Z (build #######)\`) */
2122
export const VERSION_DESC = '${VERSION} (build ${commit:0:7}${suffix:-})';

0 commit comments

Comments
 (0)