Skip to content

Commit a8ee954

Browse files
authored
feat(kernel): Normalize empty structs to undefined (#416)
Upon deserializing objects from a "native" front-end (Java, .NET, ...), the kernel now interprets an empty value for a struct type (aka data type) as `undefined`, since front-ends that perform keyword lifting such as Python will not be able to distinguish between "no argument passed" and "an empty argument passed". Fixes #411
1 parent 0fb5f7c commit a8ee954

File tree

19 files changed

+453
-4
lines changed

19 files changed

+453
-4
lines changed

packages/jsii-calc/lib/compliance.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1587,7 +1587,7 @@ export class ConsumersOfThisCrazyTypeSystem {
15871587

15881588
//
15891589
// Ensure the JSII kernel can pass "this" out to JSII remotes from within the constructor (this is dirty, but possible)
1590-
///
1590+
//
15911591
export abstract class PartiallyInitializedThisConsumer {
15921592
public abstract consumePartiallyInitializedThis(obj: ConstructorPassesThisOut, dt: Date, ev: AllTypesEnum): string;
15931593
}
@@ -1600,3 +1600,20 @@ export class ConstructorPassesThisOut {
16001600
}
16011601
}
16021602
}
1603+
1604+
//
1605+
// Consumes a possibly empty struct and verifies it is turned to undefined when passed
1606+
// See: https://github.com/awslabs/jsii/issues/411
1607+
//
1608+
export class OptionalStructConsumer {
1609+
public readonly parameterWasUndefined: boolean;
1610+
public readonly fieldValue?: string;
1611+
1612+
constructor(optionalStruct?: OptionalStruct) {
1613+
this.parameterWasUndefined = optionalStruct === undefined;
1614+
this.fieldValue = optionalStruct && optionalStruct.field;
1615+
}
1616+
}
1617+
export interface OptionalStruct {
1618+
readonly field?: string;
1619+
}

packages/jsii-calc/test/assembly.jsii

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3343,6 +3343,59 @@
33433343
}
33443344
]
33453345
},
3346+
"jsii-calc.OptionalStruct": {
3347+
"assembly": "jsii-calc",
3348+
"datatype": true,
3349+
"fqn": "jsii-calc.OptionalStruct",
3350+
"kind": "interface",
3351+
"name": "OptionalStruct",
3352+
"properties": [
3353+
{
3354+
"abstract": true,
3355+
"immutable": true,
3356+
"name": "field",
3357+
"type": {
3358+
"optional": true,
3359+
"primitive": "string"
3360+
}
3361+
}
3362+
]
3363+
},
3364+
"jsii-calc.OptionalStructConsumer": {
3365+
"assembly": "jsii-calc",
3366+
"fqn": "jsii-calc.OptionalStructConsumer",
3367+
"initializer": {
3368+
"initializer": true,
3369+
"parameters": [
3370+
{
3371+
"name": "optionalStruct",
3372+
"type": {
3373+
"fqn": "jsii-calc.OptionalStruct",
3374+
"optional": true
3375+
}
3376+
}
3377+
]
3378+
},
3379+
"kind": "class",
3380+
"name": "OptionalStructConsumer",
3381+
"properties": [
3382+
{
3383+
"immutable": true,
3384+
"name": "parameterWasUndefined",
3385+
"type": {
3386+
"primitive": "boolean"
3387+
}
3388+
},
3389+
{
3390+
"immutable": true,
3391+
"name": "fieldValue",
3392+
"type": {
3393+
"optional": true,
3394+
"primitive": "string"
3395+
}
3396+
}
3397+
]
3398+
},
33463399
"jsii-calc.OverrideReturnsObject": {
33473400
"assembly": "jsii-calc",
33483401
"fqn": "jsii-calc.OverrideReturnsObject",
@@ -4580,5 +4633,5 @@
45804633
}
45814634
},
45824635
"version": "0.8.2",
4583-
"fingerprint": "72ya8nGgXRz4NmrkTbtbKD06Kk++josvz4i1aenPmvI="
4636+
"fingerprint": "QQVEfUkkaxXMbXiD6wDVqdim8HdLW5L8CElwn+WdzUA="
45844637
}

packages/jsii-kernel/lib/serialization.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ export const SERIALIZERS: {[k: string]: Serializer} = {
274274
return host.objects.registerObject(value, wireFqn);
275275
},
276276
deserialize(value, type, host) {
277+
if (typeof value === 'object' && Object.keys(value || {}).length === 0) {
278+
// Treat empty structs as `undefined` (see https://github.com/awslabs/jsii/issues/411)
279+
value = undefined;
280+
}
277281
if (nullAndOk(value, type)) { return undefined; }
278282

279283
if (typeof value !== 'object' || value == null) {

packages/jsii-kernel/test/test.kernel.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,20 @@ defineTest('Object ID does not get re-allocated when the constructor passes "thi
11121112
test.equal(classRef[api.TOKEN_REF], 'jsii-calc.ConstructorPassesThisOut@10001');
11131113
});
11141114

1115+
defineTest('struct: empty object is turned to undefined by deserialization', async (test, sandbox) => {
1116+
const object = sandbox.create({ fqn: 'jsii-calc.OptionalStructConsumer', args: [{}] });
1117+
const result = sandbox.get({ objref: object, property: 'parameterWasUndefined' });
1118+
test.ok(result.value, 'The parameter was undefined within the constructor');
1119+
});
1120+
1121+
defineTest('struct: non-empty object deserializes properly', async (test, sandbox) => {
1122+
const objref = sandbox.create({ fqn: 'jsii-calc.OptionalStructConsumer', args: [{ field: 'foo' }] });
1123+
const result = sandbox.get({ objref, property: 'parameterWasUndefined' });
1124+
test.ok(!result.value, 'The parameter was not undefined within the constructor');
1125+
const field = sandbox.get({ objref, property: 'fieldValue' });
1126+
test.equal('foo', field.value);
1127+
});
1128+
11151129
// =================================================================================================
11161130

11171131
const testNames: { [name: string]: boolean } = { };

packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3343,6 +3343,59 @@
33433343
}
33443344
]
33453345
},
3346+
"jsii-calc.OptionalStruct": {
3347+
"assembly": "jsii-calc",
3348+
"datatype": true,
3349+
"fqn": "jsii-calc.OptionalStruct",
3350+
"kind": "interface",
3351+
"name": "OptionalStruct",
3352+
"properties": [
3353+
{
3354+
"abstract": true,
3355+
"immutable": true,
3356+
"name": "field",
3357+
"type": {
3358+
"optional": true,
3359+
"primitive": "string"
3360+
}
3361+
}
3362+
]
3363+
},
3364+
"jsii-calc.OptionalStructConsumer": {
3365+
"assembly": "jsii-calc",
3366+
"fqn": "jsii-calc.OptionalStructConsumer",
3367+
"initializer": {
3368+
"initializer": true,
3369+
"parameters": [
3370+
{
3371+
"name": "optionalStruct",
3372+
"type": {
3373+
"fqn": "jsii-calc.OptionalStruct",
3374+
"optional": true
3375+
}
3376+
}
3377+
]
3378+
},
3379+
"kind": "class",
3380+
"name": "OptionalStructConsumer",
3381+
"properties": [
3382+
{
3383+
"immutable": true,
3384+
"name": "parameterWasUndefined",
3385+
"type": {
3386+
"primitive": "boolean"
3387+
}
3388+
},
3389+
{
3390+
"immutable": true,
3391+
"name": "fieldValue",
3392+
"type": {
3393+
"optional": true,
3394+
"primitive": "string"
3395+
}
3396+
}
3397+
]
3398+
},
33463399
"jsii-calc.OverrideReturnsObject": {
33473400
"assembly": "jsii-calc",
33483401
"fqn": "jsii-calc.OverrideReturnsObject",
@@ -4580,5 +4633,5 @@
45804633
}
45814634
},
45824635
"version": "0.8.2",
4583-
"fingerprint": "72ya8nGgXRz4NmrkTbtbKD06Kk++josvz4i1aenPmvI="
4636+
"fingerprint": "QQVEfUkkaxXMbXiD6wDVqdim8HdLW5L8CElwn+WdzUA="
45844637
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Amazon.JSII.Runtime.Deputy;
2+
3+
namespace Amazon.JSII.Tests.CalculatorNamespace
4+
{
5+
[JsiiInterface(typeof(IOptionalStruct), "jsii-calc.OptionalStruct")]
6+
public interface IOptionalStruct
7+
{
8+
[JsiiProperty("field", "{\"primitive\":\"string\",\"optional\":true}")]
9+
string Field
10+
{
11+
get;
12+
}
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Amazon.JSII.Runtime.Deputy;
2+
3+
namespace Amazon.JSII.Tests.CalculatorNamespace
4+
{
5+
[JsiiByValue]
6+
public class OptionalStruct : IOptionalStruct
7+
{
8+
[JsiiProperty("field", "{\"primitive\":\"string\",\"optional\":true}", true)]
9+
public string Field
10+
{
11+
get;
12+
set;
13+
}
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Amazon.JSII.Runtime.Deputy;
2+
3+
namespace Amazon.JSII.Tests.CalculatorNamespace
4+
{
5+
[JsiiClass(typeof(OptionalStructConsumer), "jsii-calc.OptionalStructConsumer", "[{\"name\":\"optionalStruct\",\"type\":{\"fqn\":\"jsii-calc.OptionalStruct\",\"optional\":true}}]")]
6+
public class OptionalStructConsumer : DeputyBase
7+
{
8+
public OptionalStructConsumer(IOptionalStruct optionalStruct): base(new DeputyProps(new object[]{optionalStruct}))
9+
{
10+
}
11+
12+
protected OptionalStructConsumer(ByRefValue reference): base(reference)
13+
{
14+
}
15+
16+
protected OptionalStructConsumer(DeputyProps props): base(props)
17+
{
18+
}
19+
20+
[JsiiProperty("parameterWasUndefined", "{\"primitive\":\"boolean\"}")]
21+
public virtual bool ParameterWasUndefined
22+
{
23+
get => GetInstanceProperty<bool>();
24+
}
25+
26+
[JsiiProperty("fieldValue", "{\"primitive\":\"string\",\"optional\":true}")]
27+
public virtual string FieldValue
28+
{
29+
get => GetInstanceProperty<string>();
30+
}
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Amazon.JSII.Runtime.Deputy;
2+
3+
namespace Amazon.JSII.Tests.CalculatorNamespace
4+
{
5+
[JsiiTypeProxy(typeof(IOptionalStruct), "jsii-calc.OptionalStruct")]
6+
internal sealed class OptionalStructProxy : DeputyBase, IOptionalStruct
7+
{
8+
private OptionalStructProxy(ByRefValue reference): base(reference)
9+
{
10+
}
11+
12+
[JsiiProperty("field", "{\"primitive\":\"string\",\"optional\":true}")]
13+
public string Field
14+
{
15+
get => GetInstanceProperty<string>();
16+
}
17+
}
18+
}

packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ protected Class<?> resolveClass(final String fqn) throws ClassNotFoundException
9292
case "jsii-calc.NumberGenerator": return software.amazon.jsii.tests.calculator.NumberGenerator.class;
9393
case "jsii-calc.ObjectRefsInCollections": return software.amazon.jsii.tests.calculator.ObjectRefsInCollections.class;
9494
case "jsii-calc.OptionalConstructorArgument": return software.amazon.jsii.tests.calculator.OptionalConstructorArgument.class;
95+
case "jsii-calc.OptionalStruct": return software.amazon.jsii.tests.calculator.OptionalStruct.class;
96+
case "jsii-calc.OptionalStructConsumer": return software.amazon.jsii.tests.calculator.OptionalStructConsumer.class;
9597
case "jsii-calc.OverrideReturnsObject": return software.amazon.jsii.tests.calculator.OverrideReturnsObject.class;
9698
case "jsii-calc.PartiallyInitializedThisConsumer": return software.amazon.jsii.tests.calculator.PartiallyInitializedThisConsumer.class;
9799
case "jsii-calc.Polymorphism": return software.amazon.jsii.tests.calculator.Polymorphism.class;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package software.amazon.jsii.tests.calculator;
2+
3+
@javax.annotation.Generated(value = "jsii-pacmak")
4+
public interface OptionalStruct extends software.amazon.jsii.JsiiSerializable {
5+
java.lang.String getField();
6+
7+
/**
8+
* @return a {@link Builder} of {@link OptionalStruct}
9+
*/
10+
static Builder builder() {
11+
return new Builder();
12+
}
13+
14+
/**
15+
* A builder for {@link OptionalStruct}
16+
*/
17+
final class Builder {
18+
@javax.annotation.Nullable
19+
private java.lang.String _field;
20+
21+
/**
22+
* Sets the value of Field
23+
* @param value the value to be set
24+
* @return {@code this}
25+
*/
26+
public Builder withField(@javax.annotation.Nullable final java.lang.String value) {
27+
this._field = value;
28+
return this;
29+
}
30+
31+
/**
32+
* Builds the configured instance.
33+
* @return a new instance of {@link OptionalStruct}
34+
* @throws NullPointerException if any required attribute was not provided
35+
*/
36+
public OptionalStruct build() {
37+
return new OptionalStruct() {
38+
@javax.annotation.Nullable
39+
private final java.lang.String $field = _field;
40+
41+
@Override
42+
public java.lang.String getField() {
43+
return this.$field;
44+
}
45+
46+
public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() {
47+
com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE;
48+
com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode();
49+
obj.set("field", om.valueToTree(this.getField()));
50+
return obj;
51+
}
52+
53+
};
54+
}
55+
}
56+
57+
/**
58+
* A proxy class which represents a concrete javascript instance of this type.
59+
*/
60+
final static class Jsii$Proxy extends software.amazon.jsii.JsiiObject implements software.amazon.jsii.tests.calculator.OptionalStruct {
61+
protected Jsii$Proxy(final software.amazon.jsii.JsiiObject.InitializationMode mode) {
62+
super(mode);
63+
}
64+
65+
@Override
66+
@javax.annotation.Nullable
67+
public java.lang.String getField() {
68+
return this.jsiiGet("field", java.lang.String.class);
69+
}
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package software.amazon.jsii.tests.calculator;
2+
3+
@javax.annotation.Generated(value = "jsii-pacmak")
4+
@software.amazon.jsii.Jsii(module = software.amazon.jsii.tests.calculator.$Module.class, fqn = "jsii-calc.OptionalStructConsumer")
5+
public class OptionalStructConsumer extends software.amazon.jsii.JsiiObject {
6+
protected OptionalStructConsumer(final software.amazon.jsii.JsiiObject.InitializationMode mode) {
7+
super(mode);
8+
}
9+
public OptionalStructConsumer(@javax.annotation.Nullable final software.amazon.jsii.tests.calculator.OptionalStruct optionalStruct) {
10+
super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii);
11+
software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this, java.util.stream.Stream.of(optionalStruct).toArray());
12+
}
13+
public OptionalStructConsumer() {
14+
super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii);
15+
software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this);
16+
}
17+
18+
public java.lang.Boolean getParameterWasUndefined() {
19+
return this.jsiiGet("parameterWasUndefined", java.lang.Boolean.class);
20+
}
21+
22+
@javax.annotation.Nullable
23+
public java.lang.String getFieldValue() {
24+
return this.jsiiGet("fieldValue", java.lang.String.class);
25+
}
26+
}

0 commit comments

Comments
 (0)