Structs & Keyword Arguments
Tests in this category ensure that structs (data types) cross the boundary by value as plain, undecorated data, that unset optional properties are omitted, that inherited and union-typed properties behave correctly, and that struct values compare (and, where the host supports it, hash) by content. They also cover how a struct is passed alongside a positional argument of the same name, and the conventions a host offers for constructing structs idiomatically.
Diamond-inherited struct properties are exposed exactly once
Test: diamondInheritedStructPropertiesAppearOnce
A struct may inherit from several parent structs that share a common ancestor, so that a property is reachable through more than one inheritance path (diamond inheritance). The host MUST expose each such property exactly once, with no duplication or ambiguity. When the host constructs the struct and passes it to the kernel, each property MUST be sent a single time with the assigned value, and reading the properties back MUST return those values.
Reference Implementation
// GIVEN
export interface DiamondInheritanceBaseLevelStruct {
readonly baseLevelProperty: string;
}
export interface DiamondInheritanceFirstMidLevelStruct extends DiamondInheritanceBaseLevelStruct {
readonly firstMidLevelProperty: string;
}
export interface DiamondInheritanceSecondMidLevelStruct extends DiamondInheritanceBaseLevelStruct {
readonly secondMidLevelProperty: string;
}
export interface DiamondInheritanceTopLevelStruct
extends DiamondInheritanceFirstMidLevelStruct, DiamondInheritanceSecondMidLevelStruct {
readonly topLevelProperty: string;
}
// WHEN
const struct: DiamondInheritanceTopLevelStruct = {
baseLevelProperty: 'base', // declared once, reached via both mid-level parents
firstMidLevelProperty: 'mid1',
secondMidLevelProperty: 'mid2',
topLevelProperty: 'top',
};
// THEN
expect(struct.baseLevelProperty).toBe('base');
expect(struct.firstMidLevelProperty).toBe('mid1');
expect(struct.secondMidLevelProperty).toBe('mid2');
expect(struct.topLevelProperty).toBe('top');
Constructing a struct without a required property is rejected
Test: incompleteStructIsRejected
When a struct declares a required (non-optional) property, the host MUST NOT allow a struct value that omits that property to be constructed and passed to the kernel. Attempting to construct such an incomplete struct MUST result in an error on the host, and the host MUST NOT send an incomplete struct across the boundary. In the kernel messages, this is observable as the absence of any message carrying the incomplete struct.
Reference Implementation
// GIVEN
export interface MyFirstStruct {
readonly astring: string; // required
readonly anumber: number; // required
readonly firstOptional?: string[];
}
// WHEN / THEN
// Omitting the required `astring` and `anumber` properties must be rejected.
expect(() => {
const incomplete = {} as MyFirstStruct;
acceptStruct(incomplete);
}).toThrow();
Overlapping struct types in a union are correctly disambiguated
Test: overlappingStructUnionsAreDisambiguated
When a kernel method accepts a union of two or more struct types whose shapes can structurally overlap, a statically
typed host must still be able to recover the intended declared type. The host MUST pass the struct value as plain data,
and the kernel MUST determine which member of the union the value represents based on the properties present. For a
value built as one member of the union, a test for that member MUST report true and a test for any other member MUST
report false, even when the members share one or more properties.
Reference Implementation
// GIVEN
export interface StructA {
readonly requiredString: string;
readonly optionalString?: string;
readonly optionalNumber?: number;
}
// Intentionally overlaps with StructA (when only `requiredString` is provided) to test that the
// kernel properly disambiguates them.
export interface StructB {
readonly requiredString: string;
readonly optionalBoolean?: boolean;
readonly optionalStructA?: StructA;
}
export class StructUnionConsumer {
public static isStructA(struct: StructA | StructB): struct is StructA {
const keys = new Set(Object.keys(struct));
switch (keys.size) {
case 1:
return keys.has('requiredString');
case 2:
return keys.has('requiredString') && (keys.has('optionalNumber') || keys.has('optionalString'));
case 3:
return keys.has('requiredString') && keys.has('optionalNumber') && keys.has('optionalString');
default:
return false;
}
}
public static isStructB(struct: StructA | StructB): struct is StructB {
const keys = new Set(Object.keys(struct));
switch (keys.size) {
case 1:
return keys.has('requiredString');
case 2:
return keys.has('requiredString') && (keys.has('optionalBoolean') || keys.has('optionalStructA'));
default:
return false;
}
}
private constructor() {}
}
// WHEN
const a0: StructA = { requiredString: 'Present!', optionalString: 'Bazinga!' };
const a1: StructA = { requiredString: 'Present!', optionalNumber: 1337 };
const b0: StructB = { requiredString: 'Present!', optionalBoolean: true };
const b1: StructB = { requiredString: 'Present!', optionalStructA: a1 };
// THEN
expect(StructUnionConsumer.isStructA(a0)).toBe(true);
expect(StructUnionConsumer.isStructA(a1)).toBe(true);
expect(StructUnionConsumer.isStructA(b0)).toBe(false);
expect(StructUnionConsumer.isStructA(b1)).toBe(false);
expect(StructUnionConsumer.isStructB(a0)).toBe(false);
expect(StructUnionConsumer.isStructB(a1)).toBe(false);
expect(StructUnionConsumer.isStructB(b0)).toBe(true);
expect(StructUnionConsumer.isStructB(b1)).toBe(true);
A positional argument and a struct property of the same name stay distinct
Test: positionalArgumentAndStructPropertyWithSameName
A method may declare a positional parameter and a trailing struct parameter that has a property with the same name as that positional parameter. In hosts that lift struct properties into named arguments at the call site, this creates a potential collision. The host MUST keep the two values distinct: the positional argument value MUST be delivered to the positional parameter, and the struct property value MUST be delivered inside the struct. The kernel MUST therefore receive the positional value in the positional slot and the struct, as plain data, in the trailing slot.
Reference Implementation
// GIVEN
export class Bell {
public rung = false;
public ring() {
this.rung = true;
}
}
export interface StructParameterType {
readonly scope: string; // same name as the positional parameter below
readonly props?: boolean;
}
export class AmbiguousParameters {
public constructor(public readonly scope: Bell, public readonly props: StructParameterType) {}
}
// WHEN
const bell = new Bell();
const amb = new AmbiguousParameters(bell, { scope: 'Driiiing!' });
// THEN
expect(amb.scope).toBe(bell); // positional value, delivered by reference
expect(amb.props).toEqual({ scope: 'Driiiing!' }); // struct property value
A struct received from the kernel is indistinguishable from one built by the host
Test: receivedStructEqualsHostBuiltStruct
A struct value received from the kernel MUST be indistinguishable from a struct value constructed by the host with the same content. Reading corresponding properties MUST yield equal values, including optional properties that are unset on both sides. The received struct and the host-built struct MUST compare equal under the host's idiomatic value comparison, in both directions.
Reference Implementation
// GIVEN
export interface StructWithOnlyOptionals {
readonly optional1?: string;
readonly optional2?: number;
readonly optional3?: boolean;
}
export class GiveMeStructs {
public get structLiteral(): StructWithOnlyOptionals {
return { optional1: 'optional1FromStructLiteral', optional3: false };
}
}
// WHEN
const gms = new GiveMeStructs();
const returnedLiteral = gms.structLiteral;
const nativeBuilt: StructWithOnlyOptionals = { optional1: 'optional1FromStructLiteral', optional3: false };
// THEN
expect(returnedLiteral.optional1).toBe(nativeBuilt.optional1);
expect(returnedLiteral.optional2).toBe(nativeBuilt.optional2); // both unset
expect(returnedLiteral.optional3).toBe(nativeBuilt.optional3);
expect(returnedLiteral).toEqual(nativeBuilt); // value comparison, both directions
expect(nativeBuilt).toEqual(returnedLiteral);
A returned struct can be received as a parent struct type
Test: structReceivedAsParentStructType
The kernel may return the same struct value through methods that declare different return types, where one struct type extends another. The host MUST accept the value under each declared struct type, including a parent type that declares only a subset of the properties. Receiving the value MUST succeed in both cases, whether the declared type is the full (child) struct or the narrower (parent) struct.
Reference Implementation
// GIVEN
export interface ParentStruct982 {
readonly foo: string;
}
export interface ChildStruct982 extends ParentStruct982 {
readonly bar: number;
}
export class Demonstrate982 {
// The same underlying value is handed out as a child and as a parent struct.
private static readonly value = { foo: 'foo', bar: 1337 };
public static takeThis(): ChildStruct982 {
return this.value;
}
public static takeThisToo(): ParentStruct982 {
return this.value;
}
}
// WHEN
const asChild = Demonstrate982.takeThis();
const asParent = Demonstrate982.takeThisToo();
// THEN
expect(asChild).toBeDefined();
expect(asParent).toBeDefined();
A struct is passed to the kernel by value and its properties are readable
Test: structsArePassedByValue
When the host passes a struct to the kernel, it MUST serialize the struct by value, sending its properties as a plain data object rather than an object reference. The kernel MUST then be able to read each individual property. A struct that extends another struct MUST include the inherited properties when serialized. A property whose value is an object reference MUST be passed by reference, so that the same instance is observed on both sides (identity is preserved). A struct value returned from the kernel MUST expose the property values that were set in JavaScript.
Reference Implementation
// GIVEN
export interface MyFirstStruct {
readonly astring: string;
readonly anumber: number;
readonly firstOptional?: string[];
}
export interface DerivedStruct extends MyFirstStruct {
readonly nonPrimitive: DoubleTrouble;
readonly bool: boolean;
readonly anotherRequired: Date;
}
export interface StructWithOnlyOptionals {
readonly optional1?: string;
readonly optional2?: number;
readonly optional3?: boolean;
}
export class DoubleTrouble {
/* a jsii class, used here only to observe reference identity */
}
export class GiveMeStructs {
/** Returns the `anumber` from a MyFirstStruct struct. */
public readFirstNumber(first: MyFirstStruct) {
return first.anumber;
}
/** Returns the non-primitive member from a DerivedStruct struct. */
public readDerivedNonPrimitive(derived: DerivedStruct) {
return derived.nonPrimitive;
}
public get structLiteral(): StructWithOnlyOptionals {
return { optional1: 'optional1FromStructLiteral', optional3: false };
}
}
// WHEN
const firstStruct: MyFirstStruct = { astring: 'FirstString', anumber: 999, firstOptional: ['First', 'Optional'] };
const doubleTrouble = new DoubleTrouble();
const derivedStruct: DerivedStruct = {
nonPrimitive: doubleTrouble,
bool: false,
anotherRequired: new Date(),
astring: 'String',
anumber: 1234,
firstOptional: ['one', 'two'],
};
const gms = new GiveMeStructs();
// THEN
expect(gms.readFirstNumber(firstStruct)).toBe(999);
expect(gms.readFirstNumber(derivedStruct)).toBe(1234); // inherited property is present
expect(gms.readDerivedNonPrimitive(derivedStruct)).toBe(doubleTrouble); // passed by reference (identity)
const literal = gms.structLiteral;
expect(literal.optional1).toBe('optional1FromStructLiteral');
expect(literal.optional3).toBe(false);
expect(literal.optional2).toBeUndefined();
Structs cross the boundary as plain data without type decoration
Test: structsAreSentAsPlainData
When the host passes a struct to the kernel, it MUST serialize the struct as a plain data object whose keys are exactly the struct's set properties and whose values are the serialized property values. The host MUST NOT add any type tag, wrapper, or other decoration identifying the struct type: a struct crosses the boundary as anonymous data, and the kernel infers the type from the receiving parameter.
Reference Implementation
// GIVEN
export interface StructA {
readonly requiredString: string;
readonly optionalString?: string;
readonly optionalNumber?: number;
}
export interface StructB {
readonly requiredString: string;
readonly optionalBoolean?: boolean;
readonly optionalStructA?: StructA;
}
// WHEN
const value: StructB = { requiredString: 'Bazinga!', optionalBoolean: false };
// THEN
// The data the host sends to the kernel for `value` is exactly its set properties,
// carrying no type decoration.
expect(value).toEqual({ requiredString: 'Bazinga!', optionalBoolean: false });
expect(Object.keys(value).sort()).toEqual(['optionalBoolean', 'requiredString']);
A struct declared in a submodule can be constructed and passed
Test: submoduleStructCanBePassed
A struct type may be declared inside a submodule (namespace) of an assembly, rather than at the top level. The host MUST be able to construct a value of such a struct and pass it across the boundary to a kernel method, with the struct serialized by value exactly like a top-level struct.
Reference Implementation
// GIVEN
// Declared inside a submodule of a dependency assembly.
export namespace submodule {
export interface NestedStruct {
readonly name: string;
}
}
export class StaticConsumer {
public static consume(...args: any[]) {
// Accepts any arguments, including structs, and ignores them.
}
}
// WHEN / THEN
const nested: submodule.NestedStruct = { name: 'Bond, James Bond' };
expect(() => StaticConsumer.consume(nested)).not.toThrow();
A struct property typed as a union of a list and an object keeps its value
Test: unionOfListAndObjectStructPropertyRoundTrips
A struct property may be declared as a union of a list of object references and a single object reference. When the host passes such a struct to the kernel and receives it back, the property MUST hold a value of the shape the host assigned: a single object reference MUST be received as an object reference, and a list MUST be received as a list with the same elements. Object references MUST preserve their identity. A property the host did not set MUST be received as unset.
Reference Implementation
// GIVEN
export interface IFriendly {
hello(): string;
}
export class Add extends BinaryOperation implements IFriendly {
/* ... */
}
export interface ConfusingToJacksonStruct {
readonly unionProperty?: Array<IFriendly | AbstractClass> | IFriendly;
}
export class ConfusingToJackson {
public static roundTripStruct(input: ConfusingToJacksonStruct): ConfusingToJacksonStruct {
return { unionProperty: input.unionProperty };
}
}
// WHEN
const friendly = new Add(new Number(1), new Number(2));
const single = ConfusingToJackson.roundTripStruct({ unionProperty: friendly });
const list = ConfusingToJackson.roundTripStruct({ unionProperty: [friendly] });
const unset = ConfusingToJackson.roundTripStruct({});
// THEN
expect(single.unionProperty).toBe(friendly);
expect(list.unionProperty).toEqual([friendly]);
expect((list.unionProperty as IFriendly[])[0]).toBe(friendly);
expect(unset.unionProperty).toBeUndefined();
A union-typed struct property keeps its concrete type across the boundary
Test: unionStructPropertyKeepsConcreteType
A struct property may be declared as a union of several types, for example a struct or a number. When the host passes such a struct to the kernel and receives it back, the property MUST hold a value of the same concrete type the host assigned: a struct value MUST be received as that struct type, with its properties, and a primitive value MUST be received as that primitive. Optional properties that the host did not set MUST be received as unset.
Reference Implementation
// GIVEN
export interface SecondLevelStruct {
readonly deeperRequiredProp: string;
readonly deeperOptionalProp?: string;
}
export interface TopLevelStruct {
readonly required: string;
readonly optional?: string;
readonly secondLevel: SecondLevelStruct | number;
}
export class StructPassing {
public static roundTrip(_positional: number, input: TopLevelStruct): TopLevelStruct {
return {
required: input.required,
optional: input.optional,
secondLevel: input.secondLevel,
};
}
}
// WHEN
const withStruct = StructPassing.roundTrip(123, {
required: 'hello',
secondLevel: { deeperRequiredProp: 'exists' },
});
const withNumber = StructPassing.roundTrip(123, { required: 'hello', secondLevel: 5 });
// THEN
expect(withStruct.required).toBe('hello');
expect(withStruct.optional).toBeUndefined();
expect((withStruct.secondLevel as SecondLevelStruct).deeperRequiredProp).toBe('exists');
expect(withNumber.required).toBe('hello');
expect(withNumber.optional).toBeUndefined();
expect(withNumber.secondLevel).toBe(5);
Unset optional properties are omitted, not sent as empty values
Test: unsetStructPropertiesAreOmitted
When the host passes a struct or map to the kernel, any optional property or key that has no value MUST be omitted entirely from the data the kernel receives; the host MUST NOT send it with an explicit empty value. Consequently, a membership test for an unset key MUST report that the key is absent. The same erasure MUST apply in the other direction: a map returned from the kernel MUST NOT contain keys whose value is unset.
Reference Implementation
// GIVEN
export interface EraseUndefinedHashValuesOptions {
readonly option1?: string;
readonly option2?: string;
}
export class EraseUndefinedHashValues {
/** Returns `true` if `key` is defined in `opts`. */
public static doesKeyExist(opts: EraseUndefinedHashValuesOptions, key: string): boolean {
return key in opts;
}
/** `prop1` holds no value and is expected to be erased. */
public static prop1IsNull(): { [key: string]: any } {
return { prop1: undefined, prop2: 'value2' };
}
/** `prop2` holds no value and is expected to be erased. */
public static prop2IsUndefined(): { [key: string]: any } {
return { prop1: 'value1', prop2: undefined };
}
}
// WHEN
const opts: EraseUndefinedHashValuesOptions = { option1: 'option1' };
// THEN
expect(EraseUndefinedHashValues.doesKeyExist(opts, 'option1')).toBe(true);
expect(EraseUndefinedHashValues.doesKeyExist(opts, 'option2')).toBe(false); // unset key is absent
expect(EraseUndefinedHashValues.prop1IsNull()).toEqual({ prop2: 'value2' });
expect(EraseUndefinedHashValues.prop2IsUndefined()).toEqual({ prop1: 'value1' });