Skip to content

Commit

Permalink
feat(kernel): Normalize empty structs to undefined (#416)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
RomainMuller authored Apr 2, 2019
1 parent 0fb5f7c commit a8ee954
Show file tree
Hide file tree
Showing 19 changed files with 453 additions and 4 deletions.
19 changes: 18 additions & 1 deletion packages/jsii-calc/lib/compliance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1587,7 +1587,7 @@ export class ConsumersOfThisCrazyTypeSystem {

//
// Ensure the JSII kernel can pass "this" out to JSII remotes from within the constructor (this is dirty, but possible)
///
//
export abstract class PartiallyInitializedThisConsumer {
public abstract consumePartiallyInitializedThis(obj: ConstructorPassesThisOut, dt: Date, ev: AllTypesEnum): string;
}
Expand All @@ -1600,3 +1600,20 @@ export class ConstructorPassesThisOut {
}
}
}

//
// Consumes a possibly empty struct and verifies it is turned to undefined when passed
// See: https://github.com/awslabs/jsii/issues/411
//
export class OptionalStructConsumer {
public readonly parameterWasUndefined: boolean;
public readonly fieldValue?: string;

constructor(optionalStruct?: OptionalStruct) {
this.parameterWasUndefined = optionalStruct === undefined;
this.fieldValue = optionalStruct && optionalStruct.field;
}
}
export interface OptionalStruct {
readonly field?: string;
}
55 changes: 54 additions & 1 deletion packages/jsii-calc/test/assembly.jsii
Original file line number Diff line number Diff line change
Expand Up @@ -3343,6 +3343,59 @@
}
]
},
"jsii-calc.OptionalStruct": {
"assembly": "jsii-calc",
"datatype": true,
"fqn": "jsii-calc.OptionalStruct",
"kind": "interface",
"name": "OptionalStruct",
"properties": [
{
"abstract": true,
"immutable": true,
"name": "field",
"type": {
"optional": true,
"primitive": "string"
}
}
]
},
"jsii-calc.OptionalStructConsumer": {
"assembly": "jsii-calc",
"fqn": "jsii-calc.OptionalStructConsumer",
"initializer": {
"initializer": true,
"parameters": [
{
"name": "optionalStruct",
"type": {
"fqn": "jsii-calc.OptionalStruct",
"optional": true
}
}
]
},
"kind": "class",
"name": "OptionalStructConsumer",
"properties": [
{
"immutable": true,
"name": "parameterWasUndefined",
"type": {
"primitive": "boolean"
}
},
{
"immutable": true,
"name": "fieldValue",
"type": {
"optional": true,
"primitive": "string"
}
}
]
},
"jsii-calc.OverrideReturnsObject": {
"assembly": "jsii-calc",
"fqn": "jsii-calc.OverrideReturnsObject",
Expand Down Expand Up @@ -4580,5 +4633,5 @@
}
},
"version": "0.8.2",
"fingerprint": "72ya8nGgXRz4NmrkTbtbKD06Kk++josvz4i1aenPmvI="
"fingerprint": "QQVEfUkkaxXMbXiD6wDVqdim8HdLW5L8CElwn+WdzUA="
}
4 changes: 4 additions & 0 deletions packages/jsii-kernel/lib/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ export const SERIALIZERS: {[k: string]: Serializer} = {
return host.objects.registerObject(value, wireFqn);
},
deserialize(value, type, host) {
if (typeof value === 'object' && Object.keys(value || {}).length === 0) {
// Treat empty structs as `undefined` (see https://github.com/awslabs/jsii/issues/411)
value = undefined;
}
if (nullAndOk(value, type)) { return undefined; }

if (typeof value !== 'object' || value == null) {
Expand Down
14 changes: 14 additions & 0 deletions packages/jsii-kernel/test/test.kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,20 @@ defineTest('Object ID does not get re-allocated when the constructor passes "thi
test.equal(classRef[api.TOKEN_REF], 'jsii-calc.ConstructorPassesThisOut@10001');
});

defineTest('struct: empty object is turned to undefined by deserialization', async (test, sandbox) => {
const object = sandbox.create({ fqn: 'jsii-calc.OptionalStructConsumer', args: [{}] });
const result = sandbox.get({ objref: object, property: 'parameterWasUndefined' });
test.ok(result.value, 'The parameter was undefined within the constructor');
});

defineTest('struct: non-empty object deserializes properly', async (test, sandbox) => {
const objref = sandbox.create({ fqn: 'jsii-calc.OptionalStructConsumer', args: [{ field: 'foo' }] });
const result = sandbox.get({ objref, property: 'parameterWasUndefined' });
test.ok(!result.value, 'The parameter was not undefined within the constructor');
const field = sandbox.get({ objref, property: 'fieldValue' });
test.equal('foo', field.value);
});

// =================================================================================================

const testNames: { [name: string]: boolean } = { };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3343,6 +3343,59 @@
}
]
},
"jsii-calc.OptionalStruct": {
"assembly": "jsii-calc",
"datatype": true,
"fqn": "jsii-calc.OptionalStruct",
"kind": "interface",
"name": "OptionalStruct",
"properties": [
{
"abstract": true,
"immutable": true,
"name": "field",
"type": {
"optional": true,
"primitive": "string"
}
}
]
},
"jsii-calc.OptionalStructConsumer": {
"assembly": "jsii-calc",
"fqn": "jsii-calc.OptionalStructConsumer",
"initializer": {
"initializer": true,
"parameters": [
{
"name": "optionalStruct",
"type": {
"fqn": "jsii-calc.OptionalStruct",
"optional": true
}
}
]
},
"kind": "class",
"name": "OptionalStructConsumer",
"properties": [
{
"immutable": true,
"name": "parameterWasUndefined",
"type": {
"primitive": "boolean"
}
},
{
"immutable": true,
"name": "fieldValue",
"type": {
"optional": true,
"primitive": "string"
}
}
]
},
"jsii-calc.OverrideReturnsObject": {
"assembly": "jsii-calc",
"fqn": "jsii-calc.OverrideReturnsObject",
Expand Down Expand Up @@ -4580,5 +4633,5 @@
}
},
"version": "0.8.2",
"fingerprint": "72ya8nGgXRz4NmrkTbtbKD06Kk++josvz4i1aenPmvI="
"fingerprint": "QQVEfUkkaxXMbXiD6wDVqdim8HdLW5L8CElwn+WdzUA="
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Amazon.JSII.Runtime.Deputy;

namespace Amazon.JSII.Tests.CalculatorNamespace
{
[JsiiInterface(typeof(IOptionalStruct), "jsii-calc.OptionalStruct")]
public interface IOptionalStruct
{
[JsiiProperty("field", "{\"primitive\":\"string\",\"optional\":true}")]
string Field
{
get;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Amazon.JSII.Runtime.Deputy;

namespace Amazon.JSII.Tests.CalculatorNamespace
{
[JsiiByValue]
public class OptionalStruct : IOptionalStruct
{
[JsiiProperty("field", "{\"primitive\":\"string\",\"optional\":true}", true)]
public string Field
{
get;
set;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Amazon.JSII.Runtime.Deputy;

namespace Amazon.JSII.Tests.CalculatorNamespace
{
[JsiiClass(typeof(OptionalStructConsumer), "jsii-calc.OptionalStructConsumer", "[{\"name\":\"optionalStruct\",\"type\":{\"fqn\":\"jsii-calc.OptionalStruct\",\"optional\":true}}]")]
public class OptionalStructConsumer : DeputyBase
{
public OptionalStructConsumer(IOptionalStruct optionalStruct): base(new DeputyProps(new object[]{optionalStruct}))
{
}

protected OptionalStructConsumer(ByRefValue reference): base(reference)
{
}

protected OptionalStructConsumer(DeputyProps props): base(props)
{
}

[JsiiProperty("parameterWasUndefined", "{\"primitive\":\"boolean\"}")]
public virtual bool ParameterWasUndefined
{
get => GetInstanceProperty<bool>();
}

[JsiiProperty("fieldValue", "{\"primitive\":\"string\",\"optional\":true}")]
public virtual string FieldValue
{
get => GetInstanceProperty<string>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Amazon.JSII.Runtime.Deputy;

namespace Amazon.JSII.Tests.CalculatorNamespace
{
[JsiiTypeProxy(typeof(IOptionalStruct), "jsii-calc.OptionalStruct")]
internal sealed class OptionalStructProxy : DeputyBase, IOptionalStruct
{
private OptionalStructProxy(ByRefValue reference): base(reference)
{
}

[JsiiProperty("field", "{\"primitive\":\"string\",\"optional\":true}")]
public string Field
{
get => GetInstanceProperty<string>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ protected Class<?> resolveClass(final String fqn) throws ClassNotFoundException
case "jsii-calc.NumberGenerator": return software.amazon.jsii.tests.calculator.NumberGenerator.class;
case "jsii-calc.ObjectRefsInCollections": return software.amazon.jsii.tests.calculator.ObjectRefsInCollections.class;
case "jsii-calc.OptionalConstructorArgument": return software.amazon.jsii.tests.calculator.OptionalConstructorArgument.class;
case "jsii-calc.OptionalStruct": return software.amazon.jsii.tests.calculator.OptionalStruct.class;
case "jsii-calc.OptionalStructConsumer": return software.amazon.jsii.tests.calculator.OptionalStructConsumer.class;
case "jsii-calc.OverrideReturnsObject": return software.amazon.jsii.tests.calculator.OverrideReturnsObject.class;
case "jsii-calc.PartiallyInitializedThisConsumer": return software.amazon.jsii.tests.calculator.PartiallyInitializedThisConsumer.class;
case "jsii-calc.Polymorphism": return software.amazon.jsii.tests.calculator.Polymorphism.class;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package software.amazon.jsii.tests.calculator;

@javax.annotation.Generated(value = "jsii-pacmak")
public interface OptionalStruct extends software.amazon.jsii.JsiiSerializable {
java.lang.String getField();

/**
* @return a {@link Builder} of {@link OptionalStruct}
*/
static Builder builder() {
return new Builder();
}

/**
* A builder for {@link OptionalStruct}
*/
final class Builder {
@javax.annotation.Nullable
private java.lang.String _field;

/**
* Sets the value of Field
* @param value the value to be set
* @return {@code this}
*/
public Builder withField(@javax.annotation.Nullable final java.lang.String value) {
this._field = value;
return this;
}

/**
* Builds the configured instance.
* @return a new instance of {@link OptionalStruct}
* @throws NullPointerException if any required attribute was not provided
*/
public OptionalStruct build() {
return new OptionalStruct() {
@javax.annotation.Nullable
private final java.lang.String $field = _field;

@Override
public java.lang.String getField() {
return this.$field;
}

public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() {
com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE;
com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode();
obj.set("field", om.valueToTree(this.getField()));
return obj;
}

};
}
}

/**
* A proxy class which represents a concrete javascript instance of this type.
*/
final static class Jsii$Proxy extends software.amazon.jsii.JsiiObject implements software.amazon.jsii.tests.calculator.OptionalStruct {
protected Jsii$Proxy(final software.amazon.jsii.JsiiObject.InitializationMode mode) {
super(mode);
}

@Override
@javax.annotation.Nullable
public java.lang.String getField() {
return this.jsiiGet("field", java.lang.String.class);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package software.amazon.jsii.tests.calculator;

@javax.annotation.Generated(value = "jsii-pacmak")
@software.amazon.jsii.Jsii(module = software.amazon.jsii.tests.calculator.$Module.class, fqn = "jsii-calc.OptionalStructConsumer")
public class OptionalStructConsumer extends software.amazon.jsii.JsiiObject {
protected OptionalStructConsumer(final software.amazon.jsii.JsiiObject.InitializationMode mode) {
super(mode);
}
public OptionalStructConsumer(@javax.annotation.Nullable final software.amazon.jsii.tests.calculator.OptionalStruct optionalStruct) {
super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii);
software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this, java.util.stream.Stream.of(optionalStruct).toArray());
}
public OptionalStructConsumer() {
super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii);
software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this);
}

public java.lang.Boolean getParameterWasUndefined() {
return this.jsiiGet("parameterWasUndefined", java.lang.Boolean.class);
}

@javax.annotation.Nullable
public java.lang.String getFieldValue() {
return this.jsiiGet("fieldValue", java.lang.String.class);
}
}
Loading

0 comments on commit a8ee954

Please sign in to comment.