Skip to content

Commit

Permalink
fix(kernel): correctly de-serialize mappings as JSON (#968)
Browse files Browse the repository at this point in the history
When mapping data (e.g. a Python `dict`) was passed through a JSON value,
it would not be deserialized correctly and the `$jsii.map` markers would
remain in the JS-visible map.
  • Loading branch information
RomainMuller committed Nov 11, 2019
1 parent faba0be commit 5d056f4
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ public void PrimitiveTypes()
Assert.Equal(UnixEpoch.AddMilliseconds(123), types.DateProperty);

// json
types.JsonProperty = JObject.Parse(@"{ ""Foo"": 123 }");
Assert.Equal(123d, types.JsonProperty["Foo"].Value<double>());
types.JsonProperty = JObject.Parse(@"{ ""Foo"": { ""Bar"": 123 } }");
Assert.Equal(123d, types.JsonProperty["Foo"]["Bar"].Value<double>());
}

[Fact(DisplayName = Prefix + nameof(Dates))]
Expand Down Expand Up @@ -137,7 +137,7 @@ public void DynamicTypes()
new Dictionary<string, object>
{
{ "World", 123 }
}
}
}
}
};
Expand Down Expand Up @@ -1011,12 +1011,12 @@ public void CorrectlyDeserializesStructUnions()
var a1 = new StructA { RequiredString = "Present!", OptionalNumber = 1337 };
var b0 = new StructB { RequiredString = "Present!", OptionalBoolean = true };
var b1 = new StructB { RequiredString = "Present!", OptionalStructA = a1 };

Assert.True(StructUnionConsumer.IsStructA(a0));
Assert.True(StructUnionConsumer.IsStructA(a1));
Assert.False(StructUnionConsumer.IsStructA(b0));
Assert.False(StructUnionConsumer.IsStructA(b1));

Assert.False(StructUnionConsumer.IsStructB(a0));
Assert.False(StructUnionConsumer.IsStructB(a1));
Assert.True(StructUnionConsumer.IsStructB(b0));
Expand All @@ -1032,7 +1032,7 @@ public void VariadicCallbacksAreHandledCorrectly()
Assert.Equal(new double[]{2d, 3d}, invoker.AsArray(1, 2));
Assert.Equal(new double[]{2d, 3d, 4d}, invoker.AsArray(1, 2, 3));
}

private sealed class OverrideVariadicMethod : VariadicMethod
{
public override double[] AsArray(double first, params double[] others)
Expand All @@ -1047,7 +1047,7 @@ public void OptionalCallbackArgumentsAreHandledCorrectly()
var noOption = new InterfaceWithOptionalMethodArguments();
new OptionalArgumentInvoker(noOption).InvokeWithoutOptional();
Assert.True(noOption.Invoked);

var option = new InterfaceWithOptionalMethodArguments(1337);
new OptionalArgumentInvoker(option).InvokeWithOptional();
Assert.True(option.Invoked);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ public void primitiveTypes() throws IOException {
assertEquals(Instant.ofEpochMilli(123), types.getDateProperty());

// json
types.setJsonProperty((ObjectNode) new ObjectMapper().readTree("{ \"Foo\": 123 }"));
assertEquals(123, types.getJsonProperty().get("Foo").numberValue());
types.setJsonProperty((ObjectNode) new ObjectMapper().readTree("{ \"Foo\": { \"Bar\": 123 } }"));
assertEquals(123, types.getJsonProperty().get("Foo").get("Bar").numberValue());
}

@Test
Expand Down
32 changes: 30 additions & 2 deletions packages/jsii-kernel/lib/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,38 @@ export const SERIALIZERS: {[k: string]: Serializer} = {
// Just whatever. Dates will automatically serialize themselves to strings.
return value;
},
deserialize(value, optionalValue) {
deserialize(value, optionalValue, host) {
// /!\ Top-level "null" will turn to undefined, but any null nested in the value is valid JSON, so it'll stay!
if (nullAndOk(value, optionalValue)) { return undefined; }
return value;

// A mapping object can arrive though here. This would be the case if anything that is valid into a Map<string, ?>
// is passed into a JSON transfer point. Indeed, those are also valid JSON! For example, Python "dicts" will be
// serialized (by the Python runtime) as a $jsii.map (the mapping object). We need to de-serialize that as a
// Map<string, JSON> in order to obtain the correct output behavior here!
if (isWireMap(value)) {
return SERIALIZERS[SerializationClass.Map].deserialize(
value,
{
optional: false,
type: { collection: { kind: spec.CollectionKind.Map, elementtype: { primitive: spec.PrimitiveType.Json } } },
},
host);
}

if (typeof value !== 'object') {
return value;
}

if (Array.isArray(value)) {
return value.map(mapJsonValue);
}

return mapValues(value, mapJsonValue);

function mapJsonValue(toMap: any) {
if (toMap == null) { return toMap; }
return host.recurse(toMap, { type: { primitive: spec.PrimitiveType.Json } });
}
},
},

Expand Down
18 changes: 18 additions & 0 deletions packages/jsii-kernel/test/kernel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,24 @@ defineTest('in/out enum values', (sandbox) => {
expect(sandbox.get({ objref: alltypes, property: 'enumProperty' }).value).toEqual({ '$jsii.enum': 'jsii-calc.AllTypesEnum/THIS_IS_GREAT' });
});

describe('in/out json values', () => {
defineTest('with a plain object', (sandbox) => {
const allTypes = sandbox.create({ fqn: 'jsii-calc.AllTypes' });
sandbox.set({ objref: allTypes, property: 'jsonProperty', value: { foo: 'bar', baz: 1337 } });
expect(sandbox.get({ objref: allTypes, property: 'jsonProperty' }).value).toEqual({ foo: 'bar', baz: 1337 });
});
defineTest('with a simple mapping', (sandbox) => {
const allTypes = sandbox.create({ fqn: 'jsii-calc.AllTypes' });
sandbox.set({ objref: allTypes, property: 'jsonProperty', value: { [api.TOKEN_MAP]: { foo: 'bar', baz: 1337 } } });
expect(sandbox.get({ objref: allTypes, property: 'jsonProperty' }).value).toEqual({ foo: 'bar', baz: 1337 });
});
defineTest('with a nested mapping', (sandbox) => {
const allTypes = sandbox.create({ fqn: 'jsii-calc.AllTypes' });
sandbox.set({ objref: allTypes, property: 'jsonProperty', value: { [api.TOKEN_MAP]: { foo: 'bar', baz: { [api.TOKEN_MAP]: { bazinga: [null, 'Pickle Rick'] } } } } });
expect(sandbox.get({ objref: allTypes, property: 'jsonProperty' }).value).toEqual({ foo: 'bar', baz: { bazinga: [null, 'Pickle Rick'] } });
});
});

defineTest('enum values from @scoped packages awslabs/jsii#138', (sandbox) => {
const objref = sandbox.create({ fqn: 'jsii-calc.ReferenceEnumFromScopedPackage' });

Expand Down
4 changes: 2 additions & 2 deletions packages/jsii-python-runtime/tests/test_compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ def test_primitiveTypes():
assert types.date_property == datetime.fromtimestamp(123 / 1000.0, tz=timezone.utc)

# json
types.json_property = {"Foo": 123}
assert types.json_property.get("Foo") == 123
types.json_property = { "Foo": { "bar": 123 } }
assert types.json_property.get("Foo") == { "bar": 123 }


def test_dates():
Expand Down

0 comments on commit 5d056f4

Please sign in to comment.