-
Notifications
You must be signed in to change notification settings - Fork 244
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
10 changed files
with
1,083 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
...ava-runtime-test/project/src/test/java/software/amazon/jsii/testing/TypeCheckingTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
package software.amazon.jsii.testing; | ||
|
||
import org.junit.jupiter.api.Test; | ||
import software.amazon.jsii.JsiiException; | ||
import software.amazon.jsii.JsiiObject; | ||
import software.amazon.jsii.tests.calculator.*; | ||
import software.amazon.jsii.tests.calculator.anonymous.*; | ||
import software.amazon.jsii.tests.calculator.anonymous.UseOptions; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
import static org.junit.jupiter.api.Assertions.*; | ||
|
||
public class TypeCheckingTest { | ||
private static class StructAImplementer implements StructA, StructB { | ||
public String requiredString; | ||
|
||
StructAImplementer(String param) { | ||
requiredString = param; | ||
} | ||
|
||
public void setRequiredString(String param) { | ||
requiredString = param; | ||
} | ||
|
||
public String getRequiredString() { | ||
return requiredString; | ||
} | ||
} | ||
|
||
@Test | ||
public void constructor() { | ||
HashMap<String, Object> map = new HashMap<String, Object>(); | ||
map.put("good", new StructAImplementer("present")); | ||
map.put("bad", "Not a StructA or StructB"); | ||
ArrayList<Map<String, Object>> list = new ArrayList<Map<String, Object>>(); | ||
list.add(map); | ||
Exception e = assertThrows(IllegalArgumentException.class, () -> | ||
{ | ||
new ClassWithCollectionOfUnions(list); | ||
}); | ||
|
||
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()); | ||
} | ||
|
||
@Test | ||
public void anonymousObjectIsValid() | ||
{ | ||
Object anonymousObject = UseOptions.provide("A"); | ||
assertEquals(JsiiObject.class, anonymousObject.getClass()); | ||
assertEquals("A", UseOptions.consume(anonymousObject)); | ||
} | ||
|
||
@Test | ||
public void nestedUnion() { | ||
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> | ||
{ | ||
ArrayList<Object> list = new ArrayList<Object>(); | ||
list.add(1337.42); | ||
new ClassWithNestedUnion(list); | ||
}); | ||
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()); | ||
|
||
e = assertThrows(IllegalArgumentException.class, () -> | ||
{ | ||
ArrayList<Object> list = new ArrayList<Object>(); | ||
ArrayList<Object> nestedList = new ArrayList<Object>(); | ||
nestedList.add(new StructAImplementer("required")); | ||
nestedList.add(1337.42); | ||
list.add(nestedList); | ||
new ClassWithNestedUnion(list); | ||
}); | ||
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()); | ||
|
||
e = assertThrows(IllegalArgumentException.class, () -> | ||
{ | ||
HashMap<String, Object> map = new HashMap<String, Object>(); | ||
ArrayList<Object> list = new ArrayList<Object>(); | ||
map.put("good", new StructAImplementer("present")); | ||
map.put("bad", "Not a StructA or StructB"); | ||
list.add(map); | ||
new ClassWithNestedUnion(list); | ||
}); | ||
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()); | ||
} | ||
|
||
@Test | ||
public void keysAreTypeChecked() { | ||
HashMap<Object, Object> map = new HashMap<Object, Object>(); | ||
ArrayList<Object> list = new ArrayList<Object>(); | ||
map.put("good", new StructAImplementer("present")); | ||
map.put(1337.42, new StructAImplementer("present")); | ||
list.add(map); | ||
|
||
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> { | ||
new ClassWithNestedUnion(list); | ||
}); | ||
assertEquals("Expected unionProperty.get(0).keySet() to contain class String; received class java.lang.Double", e.getMessage()); | ||
} | ||
|
||
@Test | ||
public void variadic() { | ||
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> | ||
{ | ||
new VariadicTypeUnion(new StructAImplementer("present"), 1337.42); | ||
}); | ||
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()); | ||
} | ||
|
||
@Test | ||
public void setter() { | ||
HashMap<String, Object> map = new HashMap<String, Object>(); | ||
map.put("good", new StructAImplementer("present")); | ||
map.put("bad", "Not a StructA or StructB"); | ||
ArrayList<Map<String, Object>> list = new ArrayList<Map<String, Object>>(); | ||
ClassWithCollectionOfUnions subject = new ClassWithCollectionOfUnions(list); | ||
list.add(map); | ||
|
||
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> { | ||
subject.setUnionProperty(list); | ||
}); | ||
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()); | ||
} | ||
|
||
@Test | ||
public void staticMethod() | ||
{ | ||
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> { | ||
StructUnionConsumer.isStructA("Not a StructA"); | ||
}); | ||
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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
import * as path from 'path'; | ||
|
||
const version = require('../package.json').version; | ||
|
||
export const maven = { | ||
groupId: 'software.amazon.jsii', | ||
artifactId: 'jsii-runtime', | ||
version: require('../package.json').version.replace(/\+.+$/, '') | ||
version: version === '0.0.0' ? '0.0.0-SNAPSHOT' : version.replace(/\+.+$/, ''), | ||
}; | ||
|
||
export const repository = path.resolve(__dirname, '../maven-repo'); |
38 changes: 38 additions & 0 deletions
38
packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/Configuration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package software.amazon.jsii; | ||
|
||
/** | ||
* Runtime configuration flags available for the Java jsii runtime. | ||
*/ | ||
public final class Configuration { | ||
private static boolean runtimeTypeChecking = true; | ||
|
||
/** | ||
* Determines whether runtime type checking will be performed in places where | ||
* APIs accept {@link java.lang.Object} but the underlying model actually | ||
* uses a type union. | ||
* | ||
* Disabling this configuration allows to stop paying the runtime cost of type | ||
* checking, however it will produce degraded error messages in case of a | ||
* developer error. | ||
*/ | ||
public static boolean getRuntimeTypeChecking() { | ||
return Configuration.runtimeTypeChecking; | ||
} | ||
|
||
/** | ||
* Specifies whether runtime type checking will be performed in places where | ||
* APIs accept {@link java.lang.Object} but the underlying model actually | ||
* uses a type union. | ||
* | ||
* Disabling this configuration allows to stop paying the runtime cost of type | ||
* checking, however it will produce degraded error messages in case of a | ||
* developer error. | ||
*/ | ||
public void setRuntimeTypeChecking(final boolean value) { | ||
Configuration.runtimeTypeChecking = value; | ||
} | ||
|
||
private Configuration(){ | ||
throw new UnsupportedOperationException(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.