Skip to content

Commit

Permalink
feat(java): dynamic type checking for union-typed parameters (#3703)
Browse files Browse the repository at this point in the history
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
comcalvi committed Aug 30, 2022
1 parent 23a5baa commit 26ca47c
Show file tree
Hide file tree
Showing 10 changed files with 1,083 additions and 17 deletions.
3 changes: 1 addition & 2 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -1272,8 +1272,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/66279577?v=4",
"profile": "https://github.com/comcalvi",
"contributions": [
"code",
"bug"
"code"
]
},
{
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<tr>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
Expand Down
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());
}
}
4 changes: 3 additions & 1 deletion packages/@jsii/java-runtime/lib/index.ts
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');
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();
}
}
3 changes: 2 additions & 1 deletion packages/jsii-pacmak/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ cat > lib/version.ts <<HERE
// Generated at $(date -u +"%Y-%m-%dT%H:%M:%SZ") by generate.sh
/** The short version number for this JSII compiler (e.g: \`X.Y.Z\`) */
export const VERSION = '${VERSION}';
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
export const VERSION: string = '${VERSION}';
/** The qualified version number for this JSII compiler (e.g: \`X.Y.Z (build #######)\`) */
export const VERSION_DESC = '${VERSION} (build ${commit:0:7}${suffix:-})';
Expand Down
Loading

0 comments on commit 26ca47c

Please sign in to comment.