Skip to content

Commit 90135f9

Browse files
authored
fix(kernel): validate presence of required struct properties (#591)
Detect when required struct properties are not supplied, so that we can produce useful error messages to untyped jsii languages (in particular, Python). Also check for an array in place of a variadic argument, and throw a hopefully helpful message to hint at a common misuse. Fix the test to be actually variadic, which it wasn't :o.
1 parent 15f77b5 commit 90135f9

11 files changed

Lines changed: 90 additions & 38 deletions

File tree

packages/jsii-calc/lib/compliance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1780,7 +1780,7 @@ export class StructPassing {
17801780
};
17811781
}
17821782

1783-
public static howManyVarArgsDidIPass(_positional: number, inputs: TopLevelStruct[]): number {
1783+
public static howManyVarArgsDidIPass(_positional: number, ...inputs: TopLevelStruct[]): number {
17841784
return inputs.length;
17851785
}
17861786
}

packages/jsii-calc/test/assembly.jsii

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7837,21 +7837,18 @@
78377837
{
78387838
"name": "inputs",
78397839
"type": {
7840-
"collection": {
7841-
"elementtype": {
7842-
"fqn": "jsii-calc.TopLevelStruct"
7843-
},
7844-
"kind": "array"
7845-
}
7846-
}
7840+
"fqn": "jsii-calc.TopLevelStruct"
7841+
},
7842+
"variadic": true
78477843
}
78487844
],
78497845
"returns": {
78507846
"type": {
78517847
"primitive": "number"
78527848
}
78537849
},
7854-
"static": true
7850+
"static": true,
7851+
"variadic": true
78557852
},
78567853
{
78577854
"docs": {
@@ -9055,5 +9052,5 @@
90559052
}
90569053
},
90579054
"version": "0.14.0",
9058-
"fingerprint": "FCuQcrhxNzRu5psy8XKe+fkbvJhTtFAEc2eiULxQP8w="
9055+
"fingerprint": "3KJr12zCrNOW5f2XkO6HxM761PQoUBbT9VLR5+foBOo="
90599056
}

packages/jsii-kernel/lib/serialization.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export const SERIALIZERS: {[k: string]: Serializer} = {
170170
return value;
171171
},
172172
deserialize(value, optionalValue) {
173-
// /!\ Top-level "null" will turn to underfined, but any null nested in the value is valid JSON, so it'll stay!
173+
// /!\ Top-level "null" will turn to undefined, but any null nested in the value is valid JSON, so it'll stay!
174174
if (nullAndOk(value, optionalValue)) { return undefined; }
175175
return value;
176176
},
@@ -258,7 +258,7 @@ export const SERIALIZERS: {[k: string]: Serializer} = {
258258
throw new Error(`Expected object, got ${JSON.stringify(value)}`);
259259
}
260260

261-
// This looks odd, but if an object was originally passed in as a by-ref
261+
// This looks odd, but if an object was originally passed in/out as a by-ref
262262
// class, and it happens to conform to a datatype interface we say we're
263263
// returning, return the actual object instead of the serialized value.
264264
// NOTE: Not entirely sure yet whether this is a bug masquerading as a
@@ -296,16 +296,29 @@ export const SERIALIZERS: {[k: string]: Serializer} = {
296296
throw new Error(`Expected object reference, got ${JSON.stringify(value)}`);
297297
}
298298

299-
// Similarly to other end, we might be getting a reference type where we're
300-
// expecting a value type. Accept this for now.
299+
const namedType = host.lookupType((optionalValue.type as spec.NamedTypeReference).fqn);
300+
const props = propertiesOf(namedType, host.lookupType);
301+
302+
if (Array.isArray(value)) {
303+
throw new Error(`Got an array where a ${namedType.fqn} was expected. Did you mean to pass a variable number of arguments?`);
304+
}
305+
306+
// Similarly to serialization, we might be getting a reference type where we're
307+
// expecting a value type. Accept this for now (but also validate that object
308+
// for presence of the right properties).
301309
if (isObjRef(value)) {
302310
host.debug('Expected value type but got reference type, accepting for now (awslabs/jsii#400)');
303-
return host.objects.findObject(value).instance;
311+
312+
// Return same INSTANCE (shouldn't matter but we don't know for sure that it doesn't)
313+
return validateRequiredProps(
314+
host.objects.findObject(value).instance,
315+
namedType.fqn,
316+
props);
304317
}
305318

306-
const namedType = host.lookupType((optionalValue.type as spec.NamedTypeReference).fqn);
307-
const props = propertiesOf(namedType, host.lookupType);
319+
value = validateRequiredProps(value, namedType.fqn, props);
308320

321+
// Return a dict COPY, we have by-value semantics anyway.
309322
return mapValues(value, (v, key) => {
310323
if (!props[key]) { return undefined; } // Don't map if unknown property
311324
return host.recurse(v, props[key]);
@@ -648,3 +661,16 @@ function isAssignable(actualTypeFqn: string, requiredType: spec.NamedTypeReferen
648661
}
649662
return false;
650663
}
664+
665+
function validateRequiredProps(actualProps: {[key: string]: any}, typeName: string, specProps: {[key: string]: spec.Property}) {
666+
// Check for required properties
667+
const missingRequiredProps = Object.keys(specProps)
668+
.filter(name => !specProps[name].optional)
669+
.filter(name => !(name in actualProps));
670+
671+
if (missingRequiredProps.length > 0) {
672+
throw new Error(`Missing required properties for ${typeName}: ${missingRequiredProps}`);
673+
}
674+
675+
return actualProps;
676+
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,36 @@ defineTest('can set and retrieve union properties', async (test, sandbox) => {
11181118
test.equal(get(sandbox, unionArray[1])('value'), 33);
11191119
});
11201120

1121+
defineTest('require presence of required properties -- top level', async (test, sandbox) => {
1122+
test.throws(() => {
1123+
sandbox.sinvoke({ fqn: 'jsii-calc.StructPassing', method: 'roundTrip', args: [
1124+
123,
1125+
{ incomplete: true }
1126+
]});
1127+
}, /Missing required properties for jsii-calc.TopLevelStruct: required,secondLevel/);
1128+
});
1129+
1130+
defineTest('require presence of required properties -- deeper level', async (test, sandbox) => {
1131+
test.throws(() => {
1132+
sandbox.sinvoke({ fqn: 'jsii-calc.StructPassing', method: 'roundTrip', args: [
1133+
123,
1134+
{
1135+
required: 'abc',
1136+
secondLevel: { alsoIncomplete: true, }
1137+
}
1138+
]});
1139+
}, /Missing required properties for jsii-calc.SecondLevelStruct: deeperRequiredProp/);
1140+
});
1141+
1142+
defineTest('notice when an array is passed instead of varargs', async (test, sandbox) => {
1143+
test.throws(() => {
1144+
sandbox.sinvoke({ fqn: 'jsii-calc.StructPassing', method: 'howManyVarArgsDidIPass', args: [
1145+
123,
1146+
[ { required: 'abc', secondLevel: 6 } ]
1147+
]});
1148+
}, /Got an array where a jsii-calc.TopLevelStruct was expected/);
1149+
});
1150+
11211151
defineTest('Object ID does not get re-allocated when the constructor passes "this" out', async (test, sandbox) => {
11221152
sandbox.callbackHandler = makeSyncCallbackHandler((callback) => {
11231153
test.equal(callback.invoke && callback.invoke.method, 'consumePartiallyInitializedThis');

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7837,21 +7837,18 @@
78377837
{
78387838
"name": "inputs",
78397839
"type": {
7840-
"collection": {
7841-
"elementtype": {
7842-
"fqn": "jsii-calc.TopLevelStruct"
7843-
},
7844-
"kind": "array"
7845-
}
7846-
}
7840+
"fqn": "jsii-calc.TopLevelStruct"
7841+
},
7842+
"variadic": true
78477843
}
78487844
],
78497845
"returns": {
78507846
"type": {
78517847
"primitive": "number"
78527848
}
78537849
},
7854-
"static": true
7850+
"static": true,
7851+
"variadic": true
78557852
},
78567853
{
78577854
"docs": {
@@ -9055,5 +9052,5 @@
90559052
}
90569053
},
90579054
"version": "0.14.0",
9058-
"fingerprint": "FCuQcrhxNzRu5psy8XKe+fkbvJhTtFAEc2eiULxQP8w="
9055+
"fingerprint": "3KJr12zCrNOW5f2XkO6HxM761PQoUBbT9VLR5+foBOo="
90599056
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ protected StructPassing(DeputyProps props): base(props)
1919
}
2020

2121
/// <remarks>stability: Experimental</remarks>
22-
[JsiiMethod(name: "howManyVarArgsDidIPass", returnsJson: "{\"type\":{\"primitive\":\"number\"}}", parametersJson: "[{\"name\":\"_positional\",\"type\":{\"primitive\":\"number\"}},{\"name\":\"inputs\",\"type\":{\"collection\":{\"kind\":\"array\",\"elementtype\":{\"fqn\":\"jsii-calc.TopLevelStruct\"}}}}]")]
23-
public static double HowManyVarArgsDidIPass(double _positional, ITopLevelStruct[] inputs)
22+
[JsiiMethod(name: "howManyVarArgsDidIPass", returnsJson: "{\"type\":{\"primitive\":\"number\"}}", parametersJson: "[{\"name\":\"_positional\",\"type\":{\"primitive\":\"number\"}},{\"name\":\"inputs\",\"variadic\":true,\"type\":{\"fqn\":\"jsii-calc.TopLevelStruct\"}}]")]
23+
public static double HowManyVarArgsDidIPass(double _positional, ITopLevelStruct inputs)
2424
{
2525
return InvokeStaticMethod<double>(typeof(StructPassing), new object[]{_positional, inputs});
2626
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ public StructPassing() {
1919
* EXPERIMENTAL
2020
*/
2121
@software.amazon.jsii.Stability(software.amazon.jsii.Stability.Level.Experimental)
22-
public static java.lang.Number howManyVarArgsDidIPass(final java.lang.Number _positional, final java.util.List<software.amazon.jsii.tests.calculator.TopLevelStruct> inputs) {
23-
return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.StructPassing.class, "howManyVarArgsDidIPass", java.lang.Number.class, new Object[] { java.util.Objects.requireNonNull(_positional, "_positional is required"), java.util.Objects.requireNonNull(inputs, "inputs is required") });
22+
public static java.lang.Number howManyVarArgsDidIPass(final java.lang.Number _positional, final software.amazon.jsii.tests.calculator.TopLevelStruct... inputs) {
23+
return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.StructPassing.class, "howManyVarArgsDidIPass", java.lang.Number.class, java.util.stream.Stream.concat(java.util.Arrays.<Object>stream(new Object[] { java.util.Objects.requireNonNull(_positional, "_positional is required") }), java.util.Arrays.<Object>stream(inputs)).toArray(Object[]::new));
2424
}
2525

2626
/**

packages/jsii-pacmak/test/expected.jsii-calc/python/src/jsii_calc/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5341,15 +5341,15 @@ def __init__(self) -> None:
53415341

53425342
@jsii.member(jsii_name="howManyVarArgsDidIPass")
53435343
@classmethod
5344-
def how_many_var_args_did_i_pass(cls, _positional: jsii.Number, inputs: typing.List["TopLevelStruct"]) -> jsii.Number:
5344+
def how_many_var_args_did_i_pass(cls, _positional: jsii.Number, *inputs: "TopLevelStruct") -> jsii.Number:
53455345
"""
53465346
:param _positional: -
53475347
:param inputs: -
53485348
53495349
stability
53505350
:stability: experimental
53515351
"""
5352-
return jsii.sinvoke(cls, "howManyVarArgsDidIPass", [_positional, inputs])
5352+
return jsii.sinvoke(cls, "howManyVarArgsDidIPass", [_positional, *inputs])
53535353

53545354
@jsii.member(jsii_name="roundTrip")
53555355
@classmethod

packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6436,12 +6436,12 @@ StructPassing
64366436

64376437

64386438

6439-
.. py:staticmethod:: howManyVarArgsDidIPass(_positional, inputs) -> number
6439+
.. py:staticmethod:: howManyVarArgsDidIPass(_positional, *inputs) -> number
64406440
64416441
:param _positional:
64426442
:type _positional: number
6443-
:param inputs:
6444-
:type inputs: :py:class:`~jsii-calc.TopLevelStruct`\ []
6443+
:param \*inputs:
6444+
:type \*inputs: :py:class:`~jsii-calc.TopLevelStruct`\
64456445
:rtype: number
64466446

64476447

packages/jsii-python-runtime/tests/test_compliance.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -936,10 +936,10 @@ def test_passNestedScalar():
936936
assert output.second_level == 5
937937

938938
def test_passStructsInVariadic():
939-
output = StructPassing.how_many_var_args_did_i_pass(123, [
939+
output = StructPassing.how_many_var_args_did_i_pass(123,
940940
TopLevelStruct(required='hello', second_level=1),
941941
TopLevelStruct(required='bye', second_level=SecondLevelStruct(deeper_required_prop='ciao'))
942-
])
942+
)
943943
assert output == 2
944944

945945
def test_structEquality():

0 commit comments

Comments
 (0)