Classes & Object References
Tests in this category ensure classes can be instantiated, their methods and properties used, and that object references returned by the kernel are represented by the correct host type and identity.
Values of an abstract declared type are received as references
Test: abstractTypedValueReceivedAsReference
When the host reads a property or return value whose declared type is an abstract class, the kernel returns an object reference. The host MUST represent that value using the declared abstract type and MUST be able to invoke the abstract type's members on it, even though the host cannot know the concrete runtime type of the object.
Reference Implementation
// GIVEN
export abstract class NumericValue {
public abstract readonly value: number;
}
export class Number extends NumericValue {
public constructor(public readonly value: number) {
super();
}
}
export class Calculator {
public curr: NumericValue = new Number(0);
public add(value: number) {
this.curr = new Number(this.curr.value + value);
}
}
// WHEN
const calc = new Calculator();
calc.add(120);
const value: NumericValue = calc.curr;
// THEN
expect(value.value).toBe(120);
References to classes with union-typed properties can be obtained
Test: classWithUnionPropertyCanBeReceived
The host MUST be able to obtain, through a static method, an object reference to a class that declares a settable property whose type is a union (including a union whose members are arrays). Returning such a reference MUST NOT require the kernel or the host to resolve the union, and the host MUST receive a usable reference.
Reference Implementation
// GIVEN
export interface IFriendly {
hello(): string;
}
export abstract class AbstractClass {
public abstract abstractMethod(name: string): string;
}
export class ConfusingToJackson {
public static makeInstance(): ConfusingToJackson {
return new ConfusingToJackson();
}
public unionProperty?: Array<IFriendly | AbstractClass> | IFriendly;
private constructor() {}
}
// WHEN
const instance = ConfusingToJackson.makeInstance();
// THEN
expect(instance).toBeInstanceOf(ConfusingToJackson);
Classes can reference other classes during initialization
Test: classesCanReferenceEachOtherDuringInitialization
The host MUST be able to instantiate a class whose constructor creates and references other jsii classes, and whose referenced types perform their own static initialization during that process. The resulting object reference MUST expose the nested object produced during initialization.
Reference Implementation
// GIVEN
export enum SomeEnum {
SOME = 'SOME',
}
export interface SomeStruct {
readonly prop: SomeEnum;
}
export class InnerClass {
public static readonly staticProp: SomeStruct = { prop: SomeEnum.SOME };
}
export class OuterClass {
public readonly innerClass: InnerClass;
public constructor() {
this.innerClass = new InnerClass();
}
}
// WHEN
const outer = new OuterClass();
// THEN
expect(outer.innerClass).toBeDefined();
A constructor can pass this out to the host before returning
Test: constructorCanPassThisToTheHost
When a jsii constructor passes this to a host-implemented callback before the constructor has finished running, the
host MUST receive a valid object reference for the still-initializing object and MUST be able to use it. The kernel MUST
assign that object a stable object id and MUST NOT reallocate it: the reference delivered to the callback MUST be the
same reference that the create request returns once the constructor completes. Any other arguments passed to the
callback MUST be delivered with their declared types.
Reference Implementation
// GIVEN
export enum AllTypesEnum {
MY_ENUM_VALUE,
YOUR_ENUM_VALUE = 100,
THIS_IS_GREAT,
}
export abstract class PartiallyInitializedThisConsumer {
public abstract consumePartiallyInitializedThis(obj: ConstructorPassesThisOut, dt: Date, ev: AllTypesEnum): string;
}
export class ConstructorPassesThisOut {
public constructor(consumer: PartiallyInitializedThisConsumer) {
const result = consumer.consumePartiallyInitializedThis(this, new Date(0), AllTypesEnum.THIS_IS_GREAT);
if (result !== 'OK') {
throw new Error(`Expected OK but received ${result}`);
}
}
}
// WHEN
class Consumer extends PartiallyInitializedThisConsumer {
public seen?: ConstructorPassesThisOut;
public consumePartiallyInitializedThis(obj: ConstructorPassesThisOut, dt: Date, ev: AllTypesEnum): string {
this.seen = obj;
expect(dt).toEqual(new Date(0));
expect(ev).toBe(AllTypesEnum.THIS_IS_GREAT);
return 'OK';
}
}
const consumer = new Consumer();
const object = new ConstructorPassesThisOut(consumer);
// THEN
expect(consumer.seen).toBe(object);
Host implementations of abstract members are invoked by the kernel
Test: hostImplementsAbstractMembers
When the host subclasses an abstract class and implements its abstract method and abstract property, invoking a concrete method of the base class on that instance MUST cause the kernel to call back into the host's implementations. Property reads and writes performed by the kernel MUST be routed to the host's getter and setter, and method calls to the host's method, so the result reflects the host-provided behavior.
Reference Implementation
// GIVEN
export abstract class AbstractSuite {
protected abstract property: string;
protected abstract someMethod(str: string): string;
/** Sets `property` to `seed`, then returns `someMethod(this.property)`. */
public workItAll(seed: string) {
this.property = seed;
return this.someMethod(this.property);
}
}
// WHEN
class Suite extends AbstractSuite {
private value = '';
protected someMethod(str: string): string {
return `Wrapped<${str}>`;
}
protected get property(): string {
return this.value;
}
protected set property(value: string) {
this.value = `String<${value}>`;
}
}
const suite = new Suite();
// THEN
expect(suite.workItAll('Oomf!')).toBe('Wrapped<String<Oomf!>>');
Inherited properties are usable on host subclasses
Test: inheritedPropertiesUsableOnHostSubclass
When the host declares a subclass of a jsii class, the host MUST be able to assign and read the properties the subclass
inherits from its base class. Assigning an inherited property MUST send a set request against the subclass instance,
and reading it MUST return the value most recently assigned.
Reference Implementation
// GIVEN
export class AllTypes {
private stringValue = 'first value';
private numberValue = 0;
public get stringProperty() {
return this.stringValue;
}
public set stringProperty(value: string) {
this.stringValue = value;
}
public get numberProperty() {
return this.numberValue;
}
public set numberProperty(value: number) {
this.numberValue = value;
}
}
// WHEN
class DerivedFromAllTypes extends AllTypes {}
const obj = new DerivedFromAllTypes();
obj.stringProperty = 'Hello';
obj.numberProperty = 12;
// THEN
expect(obj.stringProperty).toBe('Hello');
expect(obj.numberProperty).toBe(12);
Instance methods can be invoked and mutate kernel state
Test: instanceMethodsCanBeCalled
The host MUST be able to invoke an instance method on an object reference by sending an invoke request to the kernel,
forwarding the arguments it was given. When a method mutates the object's state, that effect MUST be observable through
subsequent property reads on the same reference. Each call MUST operate on the state left by the previous call.
Reference Implementation
// GIVEN
export abstract class NumericValue {
public abstract readonly value: number;
}
export class Number extends NumericValue {
public constructor(public readonly value: number) {
super();
}
}
export class Add extends NumericValue {
public constructor(public readonly lhs: NumericValue, public readonly rhs: NumericValue) {
super();
}
public get value() {
return this.lhs.value + this.rhs.value;
}
}
export class Multiply extends NumericValue {
public constructor(public readonly lhs: NumericValue, public readonly rhs: NumericValue) {
super();
}
public get value() {
return this.lhs.value * this.rhs.value;
}
}
export class Calculator {
public curr: NumericValue = new Number(0);
public add(value: number) {
this.curr = new Add(this.curr, new Number(value));
}
public mul(value: number) {
this.curr = new Multiply(this.curr, new Number(value));
}
public get value() {
return this.curr.value;
}
}
// WHEN
const calc = new Calculator();
calc.add(10);
const afterAdd = calc.value;
calc.mul(2);
const afterMul = calc.value;
// THEN
expect(afterAdd).toBe(10);
expect(afterMul).toBe(20);
Instances of non-exported classes are received as their interface
Test: nonExportedClassReceivedAsInterface
When the kernel returns an instance of a class that is not exported from the library, declared as an interface type, the host MUST receive a usable object reference and MUST be able to read the interface's properties on it. The host MUST NOT require the concrete (private) type to be known in order to use the value.
Reference Implementation
// GIVEN
export interface IPrivatelyImplemented {
readonly success: boolean;
}
export class ExportedBaseClass {
public constructor(public readonly success: boolean) {}
}
class PrivateImplementation extends ExportedBaseClass implements IPrivatelyImplemented {
public constructor() {
super(true);
}
}
export class ReturnsPrivateImplementationOfInterface {
public get privateImplementation(): IPrivatelyImplemented {
return new PrivateImplementation();
}
}
// WHEN
const impl = new ReturnsPrivateImplementationOfInterface().privateImplementation;
// THEN
expect(impl.success).toBe(true);
A plain object literal returned as a class is usable
Test: objectLiteralReturnedAsClassIsUsable
When a kernel method returns a plain object literal whose declared return type is a class, the host MUST receive a usable object reference and MUST be able to read the class's declared properties, returning the values present in the literal.
Reference Implementation
// GIVEN
export class JSObjectLiteralToNative {
public returnLiteral(): JSObjectLiteralToNativeClass {
return {
propA: 'Hello',
propB: 102,
};
}
}
export class JSObjectLiteralToNativeClass {
public propA = 'A';
public propB = 0;
}
// WHEN
const obj = new JSObjectLiteralToNative().returnLiteral();
// THEN
expect(obj.propA).toBe('Hello');
expect(obj.propB).toBe(102);
Object-valued properties can be read and assigned
Test: objectPropertiesCanBeReadAndAssigned
The host MUST be able to read a property whose declared type is a class, receiving an object reference from the kernel, and MUST be able to assign such a property by sending an object reference back to the kernel. A reference that the host obtained from a previous read MUST remain usable as an argument in a later request, and the kernel MUST resolve it to the same underlying object. The effect of the assignment MUST be observable through later reads.
Reference Implementation
// GIVEN
export abstract class NumericValue {
public abstract readonly value: number;
}
export class Number extends NumericValue {
public constructor(public readonly value: number) {
super();
}
}
export class Multiply extends NumericValue {
public constructor(public readonly lhs: NumericValue, public readonly rhs: NumericValue) {
super();
}
public get value() {
return this.lhs.value * this.rhs.value;
}
}
export class Calculator {
public curr: NumericValue = new Number(0);
public add(value: number) {
this.curr = new Number(this.curr.value + value);
}
public neg() {
this.curr = new Number(-this.curr.value);
}
public get value() {
return this.curr.value;
}
}
// WHEN
const calc = new Calculator();
calc.add(3200000);
calc.neg();
const previous = calc.curr; // an object reference read from the kernel
calc.curr = new Multiply(new Number(2), previous); // sent back as an argument
// THEN
expect(calc.value).toBe(-6400000);
Object references round-trip through an any-typed property
Test: objectReferencesRoundTripThroughAny
When the host assigns an object reference to a property of type any and then reads it back, it MUST receive a reference
to the same underlying object. For an object created in the kernel, the returned reference MUST carry that object's
kernel type. For an object the host created (an instance of a host subclass), the kernel MUST return the same object
reference, and the host MUST resolve it back to the very instance it created, so object identity is preserved across the
boundary.
Reference Implementation
// GIVEN
export abstract class NumericValue {
public abstract readonly value: number;
}
export class Number extends NumericValue {
public constructor(public readonly value: number) {
super();
}
}
export class Add extends NumericValue {
public constructor(public readonly lhs: NumericValue, public readonly rhs: NumericValue) {
super();
}
public get value() {
return this.lhs.value + this.rhs.value;
}
}
export class AllTypes {
public anyProperty: any;
}
// WHEN
const types = new AllTypes();
// An object created in the kernel keeps its kernel type.
const kernelObject = new Number(44);
types.anyProperty = kernelObject;
const roundTrippedKernelObject = types.anyProperty;
// An object created by the host comes back as the same host instance.
class AddTen extends Add {
public constructor(value: number) {
super(new Number(value), new Number(10));
}
}
const hostObject = new AddTen(10);
types.anyProperty = hostObject;
const roundTrippedHostObject = types.anyProperty;
// THEN
expect(roundTrippedKernelObject).toBeInstanceOf(Number);
expect(roundTrippedKernelObject).toBe(kernelObject);
expect(roundTrippedHostObject).toBe(hostObject);
Object references are labelled with the most derived public type
Test: objectsReceivedAsMostDerivedPublicType
When the kernel returns an object whose concrete class is not public, it MUST label the reference with the most derived public type in that object's ancestry. The host MUST represent the reference using that public type: a reference whose private class extends a public class MUST be usable as that public class, not merely as its root ancestor. When the value is declared as an interface, the host MUST receive it typed as that interface.
Reference Implementation
// GIVEN
export class PublicClass {
public hello(): void {
return;
}
}
export interface IPublicInterface {
bye(): string;
}
export interface IPublicInterface2 {
ciao(): string;
}
export class InbetweenClass extends PublicClass implements IPublicInterface2 {
public ciao(): string {
return 'ciao';
}
}
class PrivateClass extends InbetweenClass implements IPublicInterface {
public bye(): string {
return 'bye';
}
}
export class Constructors {
public static makeClass(): PublicClass {
return new PrivateClass(); // Wire type should be InbetweenClass
}
public static makeInterface(): IPublicInterface {
return new PrivateClass(); // Wire type should be IPublicInterface
}
}
// WHEN
const classRef = Constructors.makeClass();
const ifaceRef = Constructors.makeInterface();
// THEN
expect(classRef).toBeInstanceOf(InbetweenClass);
expect(ifaceRef).toBeDefined();
Instances returned as an abstract type are fully usable
Test: objectsReturnedAsAbstractTypeAreUsable
The host MUST be able to receive an object reference whose declared type is an abstract class or an interface, and MUST be able to invoke its abstract methods, invoke its concrete (non-abstract) methods, and read properties it inherits from an interface. A property declared to return an abstract type MUST also yield a usable reference, even when the kernel backs it with a plain object rather than a class instance.
Reference Implementation
// GIVEN
export interface IInterfaceImplementedByAbstractClass {
readonly propFromInterface: string;
}
export abstract class AbstractClassBase {
public abstract readonly abstractProperty: string;
}
export abstract class AbstractClass extends AbstractClassBase implements IInterfaceImplementedByAbstractClass {
public nonAbstractMethod() {
return 42;
}
public abstract abstractMethod(name: string): string;
public get propFromInterface() {
return 'propFromInterfaceValue';
}
}
class ConcreteClass extends AbstractClass {
public abstractMethod(name: string) {
return `Hello, ${name}!!`;
}
public get abstractProperty() {
return 'Hello, dude!';
}
}
export class AbstractClassReturner {
public giveMeAbstract(): AbstractClass {
return new ConcreteClass();
}
public giveMeInterface(): IInterfaceImplementedByAbstractClass {
return new ConcreteClass();
}
public get returnAbstractFromProperty(): AbstractClassBase {
return { abstractProperty: 'hello-abstract-property' };
}
}
// WHEN
const obj = new AbstractClassReturner();
const abstractInstance = obj.giveMeAbstract();
const iface = obj.giveMeInterface();
// THEN
expect(abstractInstance.abstractMethod('John')).toBe('Hello, John!!');
expect(abstractInstance.propFromInterface).toBe('propFromInterfaceValue');
expect(abstractInstance.nonAbstractMethod()).toBe(42);
expect(iface.propFromInterface).toBe('propFromInterfaceValue');
expect(obj.returnAbstractFromProperty.abstractProperty).toBe('hello-abstract-property');
An object is usable through the interface it implements
Test: objectsUsableThroughImplementedInterface
When the kernel returns an object reference, the host MUST be able to use the object through any interface the object
implements, by reading the interface's members with the corresponding get or invoke request, and the values returned
MUST be the ones computed by the object. This MUST hold when the concrete class is defined privately inside the kernel
and is never exported, and when the declared return type is the interface itself as well as when it is any.
Reference Implementation
// GIVEN
export interface IReturnJsii976 {
readonly foo: number;
}
export class BaseJsii976 {}
export class SomeTypeJsii976 {
public static returnReturn(): IReturnJsii976 {
class Derived extends BaseJsii976 implements IReturnJsii976 {
public readonly foo = 333;
}
return new Derived();
}
public static returnAnonymous(): any {
class Derived implements IReturnJsii976 {
public readonly foo = 1337;
}
return new Derived();
}
}
// WHEN
const declaredAsInterface = SomeTypeJsii976.returnReturn();
const declaredAsAny: IReturnJsii976 = SomeTypeJsii976.returnAnonymous();
// THEN
expect(declaredAsInterface.foo).toBe(333);
expect(declaredAsAny.foo).toBe(1337);
Optional constructor parameters can be omitted
Test: optionalConstructorParametersCanBeOmitted
The host MUST be able to instantiate a jsii class by sending a create request to the kernel. When the trailing
constructor parameter is optional, the host MAY omit it and send no argument for it, in which case the kernel MUST apply
the parameter's default. The host MAY instead provide the optional argument, which the kernel MUST use in place of the
default. Both forms MUST yield a usable object reference.
Reference Implementation
// GIVEN
export interface CalculatorProps {
readonly initialValue?: number;
readonly maximumValue?: number;
}
export class Calculator {
public maxValue?: number;
public constructor(props?: CalculatorProps) {
this.maxValue = props?.maximumValue;
}
}
// WHEN
const withoutProps = new Calculator();
const withProps = new Calculator({ maximumValue: 10 });
// THEN
expect(withoutProps).toBeInstanceOf(Calculator);
expect(withProps.maxValue).toBe(10);
Classes with a private constructor are created via a static factory
Test: privateConstructorClassFromStaticFactory
When a class declares a private constructor, the host MUST NOT expose a way to construct it directly and MUST instead obtain instances through the class's static factory method, invoked as a static method on the type. On the returned reference, the host MUST be able to read a read-only property and MUST be able to both read and assign a read-write property.
Reference Implementation
// GIVEN
export class ClassWithPrivateConstructorAndAutomaticProperties {
public static create(readOnlyString: string, readWriteString: string) {
return new ClassWithPrivateConstructorAndAutomaticProperties(readOnlyString, readWriteString);
}
private constructor(
public readonly readOnlyString: string,
public readWriteString: string,
) {}
}
// WHEN
const obj = ClassWithPrivateConstructorAndAutomaticProperties.create('Hello', 'Bye');
const initialReadWrite = obj.readWriteString;
obj.readWriteString = 'Hello';
// THEN
expect(initialReadWrite).toBe('Bye');
expect(obj.readOnlyString).toBe('Hello');
expect(obj.readWriteString).toBe('Hello');
Instances of stripped deprecated types can be received
Test: strippedDeprecatedTypeCanBeReceived
When the kernel returns a value declared as an interface, but whose concrete class has been removed from the host's type information (for example because deprecated members were stripped from the type metadata), the host MUST still receive a usable object reference typed as the declared interface. The host MUST NOT fail merely because the concrete type is not present in its loaded type information.
Reference Implementation
// GIVEN
export interface IInterface {
method(): void;
}
export class VisibleBaseClass {
public readonly propertyPresent = true;
}
/** @deprecated do not use me! */
export class DeprecatedImplementation extends VisibleBaseClass implements IInterface {
public method(): void {
/* NOOP */
}
}
export class InterfaceFactory {
public static create(): IInterface {
return new DeprecatedImplementation();
}
private constructor() {}
}
// WHEN
const instance = InterfaceFactory.create();
// THEN
expect(instance).toBeDefined();
Types not explicitly loaded by the host can be received and used
Test: typesNotLoadedByTheHostCanBeReceived
When the kernel returns an object reference whose type the host application never explicitly loaded or named, the host MUST still receive a usable reference and MUST be able to invoke the declared members on it. This includes the case where such a reference is delivered to a host callback as an argument during an override, with its type belonging to a module the host never imported.
Reference Implementation
// GIVEN
export interface IRandomNumberGenerator {
next(): number;
}
/** `UnimportedType` lives in a submodule the host never explicitly loads. */
class UnimportedType implements IRandomNumberGenerator {
public constructor(private readonly n: number) {}
public next() {
return this.n;
}
}
export abstract class Cdk16625 {
protected abstract unwrap(gen: IRandomNumberGenerator): number;
public test(): void {
const value = 1337;
const rng = new UnimportedType(value);
if (this.unwrap(rng) !== value) {
throw new Error('unexpected value');
}
}
}
// WHEN
class Subject extends Cdk16625 {
protected unwrap(gen: IRandomNumberGenerator): number {
return gen.next();
}
}
// THEN
expect(() => new Subject().test()).not.toThrow();