From 4c0695cf91d8dd2d5a07d9fd36e987a960a91139 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 22 Mar 2019 17:25:46 +0100 Subject: [PATCH] fix(kernel): make type serialization explicit and recursive Serialize and deserialize types according to their declared static type, and add validation on the runtime types matching the declared types. This is in contrast to previously, when we mostly only used the runtime types to determine what to do, and harly any validation was done. The runtime types used to be able to freely disagree with the declared types, and we put a lot of burden on the JSII runtimes at the other end of the wire. Fix tests that used to exercise the API with invalid arguments. Remove Proxy objects since they only existed to prevent accidentally overwriting certain keys via the jsii interface, and Symbols serve the same purpose (but simpler). Fixes awslabs/aws-cdk#1981. --- packages/jsii-calc/lib/compliance.ts | 84 ++- packages/jsii-calc/test/assembly.jsii | 133 +++- packages/jsii-java-runtime-test/README.md | 3 + .../amazon/jsii/testing/ComplianceTest.java | 6 +- packages/jsii-kernel/lib/api.ts | 44 +- packages/jsii-kernel/lib/kernel.ts | 629 +++++------------- packages/jsii-kernel/lib/objects.ts | 135 ++++ packages/jsii-kernel/lib/serialization.ts | 622 +++++++++++++++++ packages/jsii-kernel/test/test.kernel.ts | 172 ++++- .../.jsii | 133 +++- .../Tests/CalculatorNamespace/AllTypes.cs | 15 +- .../Tests/CalculatorNamespace/Constructors.cs | 30 + .../CalculatorNamespace/IIPublicInterface.cs | 4 +- .../CalculatorNamespace/IIPublicInterface2.cs | 11 + .../IPublicInterface2Proxy.cs | 18 + .../IPublicInterfaceProxy.cs | 6 +- .../CalculatorNamespace/InbetweenClass.cs | 8 +- .../SingleInstanceTwoTypes.cs | 39 ++ .../amazon/jsii/tests/calculator/$Module.java | 2 + .../jsii/tests/calculator/AllTypes.java | 8 + .../jsii/tests/calculator/Constructors.java | 20 + .../tests/calculator/IPublicInterface.java | 6 +- .../tests/calculator/IPublicInterface2.java | 20 + .../jsii/tests/calculator/InbetweenClass.java | 7 +- .../calculator/SingleInstanceTwoTypes.java | 28 + .../expected.jsii-calc/sphinx/jsii-calc.rst | 134 +++- .../tests/test_compliance.py | 6 +- .../jsii-reflect/test/classes.expected.txt | 1 + .../test/jsii-tree.test.all.expected.txt | 50 +- .../jsii-tree.test.inheritance.expected.txt | 5 +- .../test/jsii-tree.test.members.expected.txt | 20 +- .../test/jsii-tree.test.types.expected.txt | 2 + .../project/test/jsii_runtime_test.rb | 2 +- 33 files changed, 1891 insertions(+), 512 deletions(-) create mode 100644 packages/jsii-java-runtime-test/README.md create mode 100644 packages/jsii-kernel/lib/objects.ts create mode 100644 packages/jsii-kernel/lib/serialization.ts create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface2.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterface2Proxy.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/SingleInstanceTwoTypes.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface2.java create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/SingleInstanceTwoTypes.java diff --git a/packages/jsii-calc/lib/compliance.ts b/packages/jsii-calc/lib/compliance.ts index 1d80389d71..81e7c5739d 100644 --- a/packages/jsii-calc/lib/compliance.ts +++ b/packages/jsii-calc/lib/compliance.ts @@ -13,7 +13,7 @@ import * as path from 'path'; import * as os from 'os'; import * as crypto from 'crypto'; import { promisify } from 'util'; -import { composition, IFriendlyRandomGenerator, IRandomNumberGenerator, Multiply } from './calculator'; +import { IFriendlyRandomGenerator, IRandomNumberGenerator, Multiply } from './calculator'; const bundled = require('jsii-calc-bundled'); import base = require('@scope/jsii-calc-base'); @@ -163,7 +163,7 @@ export class AllTypes { // unions unionProperty: string | number | Number | Multiply = 'foo'; - unionArrayProperty: (composition.CompositeOperation | number)[] = []; + unionArrayProperty: (Value | number)[] = []; unionMapProperty: { [key: string]: (Number | number | string) } = {}; // enum @@ -194,6 +194,21 @@ export class AllTypes { enumMethod(value: StringEnum) { return value; } + + + public anyOut(): any { + const ret = new Number(42); + Object.defineProperty(ret, 'tag', { + value: "you're it" + }); + return ret; + } + + public anyIn(inp: any) { + if (inp.tag !== "you're it") { + throw new Error(`Not the same object that I gave you, got: ${JSON.stringify(inp)}`); + } + } } // @@ -1321,18 +1336,73 @@ export class PublicClass { public hello(): void {} } export interface IPublicInterface { - bye(): void; + bye(): string; +} + +export interface IPublicInterface2 { + ciao(): string; +} +export class InbetweenClass extends PublicClass implements IPublicInterface2 { + public ciao(): string { return 'ciao'; } } -export class InbetweenClass extends PublicClass {} class PrivateClass extends InbetweenClass implements IPublicInterface { - public bye(): void {} + public bye(): string { return 'bye'; } +} + +class HiddenClass implements IPublicInterface, IPublicInterface2 { + public bye(): string { return 'bye'; } + public ciao(): string { return 'ciao'; } +} + +class HiddenSubclass extends HiddenClass { } + export class Constructors { public static makeClass(): PublicClass { - return new PrivateClass(); + return new PrivateClass(); // Wire type should be InbetweenClass } + public static makeInterface(): IPublicInterface { - return new PrivateClass(); + return new PrivateClass(); // Wire type should be IPublicInterface + } + + public static makeInterface2(): IPublicInterface2 { + return new PrivateClass(); // Wire type should be InbetweenClass + } + + public static makeInterfaces(): IPublicInterface[] { + return [new PrivateClass()]; // Wire type should be IPublicInterface[] + } + + public static hiddenInterface(): IPublicInterface { + return new HiddenClass(); // Wire type should be IPublicInterface + } + + public static hiddenInterfaces(): IPublicInterface[] { + return [new HiddenClass()]; // Wire type should be IPublicInterface[] + } + + public static hiddenSubInterfaces(): IPublicInterface[] { + return [new HiddenSubclass()]; // Wire type should be IPublicInterface[] + } +} + +/** + * Test that a single instance can be returned under two different FQNs + * + * JSII clients can instantiate 2 different strongly-typed wrappers for the same + * object. Unfortunately, this will break object equality, but if we didn't do + * this it would break runtime type checks in the JVM or CLR. + */ +export class SingleInstanceTwoTypes { + private instance = new PrivateClass(); + + public interface1(): InbetweenClass { + return this.instance; + } + + public interface2(): IPublicInterface { + return this.instance; } } diff --git a/packages/jsii-calc/test/assembly.jsii b/packages/jsii-calc/test/assembly.jsii index e9081de3c1..88d04378d2 100644 --- a/packages/jsii-calc/test/assembly.jsii +++ b/packages/jsii-calc/test/assembly.jsii @@ -353,6 +353,23 @@ }, "kind": "class", "methods": [ + { + "name": "anyIn", + "parameters": [ + { + "name": "inp", + "type": { + "primitive": "any" + } + } + ] + }, + { + "name": "anyOut", + "returns": { + "primitive": "any" + } + }, { "name": "enumMethod", "parameters": [ @@ -474,7 +491,7 @@ "primitive": "number" }, { - "fqn": "jsii-calc.composition.CompositeOperation" + "fqn": "@scope/jsii-calc-lib.Value" } ] } @@ -1217,6 +1234,37 @@ }, "kind": "class", "methods": [ + { + "name": "hiddenInterface", + "returns": { + "fqn": "jsii-calc.IPublicInterface" + }, + "static": true + }, + { + "name": "hiddenInterfaces", + "returns": { + "collection": { + "elementtype": { + "fqn": "jsii-calc.IPublicInterface" + }, + "kind": "array" + } + }, + "static": true + }, + { + "name": "hiddenSubInterfaces", + "returns": { + "collection": { + "elementtype": { + "fqn": "jsii-calc.IPublicInterface" + }, + "kind": "array" + } + }, + "static": true + }, { "name": "makeClass", "returns": { @@ -1230,6 +1278,25 @@ "fqn": "jsii-calc.IPublicInterface" }, "static": true + }, + { + "name": "makeInterface2", + "returns": { + "fqn": "jsii-calc.IPublicInterface2" + }, + "static": true + }, + { + "name": "makeInterfaces", + "returns": { + "collection": { + "elementtype": { + "fqn": "jsii-calc.IPublicInterface" + }, + "kind": "array" + } + }, + "static": true } ], "name": "Constructors" @@ -2089,11 +2156,29 @@ "methods": [ { "abstract": true, - "name": "bye" + "name": "bye", + "returns": { + "primitive": "string" + } } ], "name": "IPublicInterface" }, + "jsii-calc.IPublicInterface2": { + "assembly": "jsii-calc", + "fqn": "jsii-calc.IPublicInterface2", + "kind": "interface", + "methods": [ + { + "abstract": true, + "name": "ciao", + "returns": { + "primitive": "string" + } + } + ], + "name": "IPublicInterface2" + }, "jsii-calc.IRandomNumberGenerator": { "assembly": "jsii-calc", "docs": { @@ -2240,7 +2325,23 @@ "initializer": { "initializer": true }, + "interfaces": [ + { + "fqn": "jsii-calc.IPublicInterface2" + } + ], "kind": "class", + "methods": [ + { + "name": "ciao", + "overrides": { + "fqn": "jsii-calc.IPublicInterface2" + }, + "returns": { + "primitive": "string" + } + } + ], "name": "InbetweenClass" }, "jsii-calc.InterfaceImplementedByAbstractClass": { @@ -3561,6 +3662,32 @@ ], "name": "RuntimeTypeChecking" }, + "jsii-calc.SingleInstanceTwoTypes": { + "assembly": "jsii-calc", + "docs": { + "comment": "Test that a single instance can be returned under two different FQNs\n\nJSII clients can instantiate 2 different strongly-typed wrappers for the same\nobject. Unfortunately, this will break object equality, but if we didn't do\nthis it would break runtime type checks in the JVM or CLR." + }, + "fqn": "jsii-calc.SingleInstanceTwoTypes", + "initializer": { + "initializer": true + }, + "kind": "class", + "methods": [ + { + "name": "interface1", + "returns": { + "fqn": "jsii-calc.InbetweenClass" + } + }, + { + "name": "interface2", + "returns": { + "fqn": "jsii-calc.IPublicInterface" + } + } + ], + "name": "SingleInstanceTwoTypes" + }, "jsii-calc.Statics": { "assembly": "jsii-calc", "fqn": "jsii-calc.Statics", @@ -4349,5 +4476,5 @@ } }, "version": "0.8.0", - "fingerprint": "4LMgT0Rllw3CIJWiDiR/eUfSRPvCEeWyGWxJXMiDvcU=" + "fingerprint": "7BN7XlOFufVEMTaAbMH5GDcE4zQmJj4iiDlYoXiRvCs=" } diff --git a/packages/jsii-java-runtime-test/README.md b/packages/jsii-java-runtime-test/README.md new file mode 100644 index 0000000000..0cb17f3027 --- /dev/null +++ b/packages/jsii-java-runtime-test/README.md @@ -0,0 +1,3 @@ +Dive into a single failing test: + + JSII_DEBUG=1 mvn test -Dtest=software.amazon.jsii.testing.ComplianceTest#unionTypes diff --git a/packages/jsii-java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java b/packages/jsii-java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java index 1e27575d60..b15956ab4c 100644 --- a/packages/jsii-java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java +++ b/packages/jsii-java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java @@ -196,12 +196,12 @@ public void unionTypes() { // map Map map = new HashMap<>(); - map.put("Foo", new Multiply(new Number(2), new Number(99))); + map.put("Foo", new Number(99)); types.setUnionMapProperty(map); // array - types.setUnionArrayProperty(Arrays.asList("Hello", 123, new Number(33))); - assertEquals(33, ((Number)((List)types.getUnionArrayProperty()).get(2)).getValue()); + types.setUnionArrayProperty(Arrays.asList(123, new Number(33))); + assertEquals(33, ((Number)((List)types.getUnionArrayProperty()).get(1)).getValue()); } diff --git a/packages/jsii-kernel/lib/api.ts b/packages/jsii-kernel/lib/api.ts index 71a9c50d9c..d1be65cd9d 100644 --- a/packages/jsii-kernel/lib/api.ts +++ b/packages/jsii-kernel/lib/api.ts @@ -2,16 +2,50 @@ export const TOKEN_REF = '$jsii.byref'; export const TOKEN_DATE = '$jsii.date'; export const TOKEN_ENUM = '$jsii.enum'; -export class ObjRef { - [token: string]: string; // token = TOKEN_REF +export interface ObjRef { + [TOKEN_REF]: string; } -export interface Override { - method?: string; - property?: string; +export function isObjRef(value: any): value is ObjRef { + return typeof value === 'object' && value !== null && TOKEN_REF in value; +} + +export interface WireDate { + [TOKEN_DATE]: string; +} + +export function isWireDate(value: any): value is WireDate { + return typeof value === 'object' && value !== null && TOKEN_DATE in value; +} + +export interface WireEnum { + [TOKEN_ENUM]: string; +} + +export function isWireEnum(value: any): value is WireEnum { + return typeof value === 'object' && value !== null && TOKEN_ENUM in value; +} + +export type Override = MethodOverride | PropertyOverride; + +export interface MethodOverride { + method: string; + cookie?: string; +} + +export function isMethodOverride(value: Override): value is MethodOverride { + return (value as any).method != null; // Python passes "null" +} + +export interface PropertyOverride { + property: string; cookie?: string; } +export function isPropertyOverride(value: Override): value is PropertyOverride { + return (value as any).property != null; // Python passes "null" +} + export interface Callback { cbid: string; cookie: string | undefined; diff --git a/packages/jsii-kernel/lib/kernel.ts b/packages/jsii-kernel/lib/kernel.ts index 15e615aac4..d76fafe0ce 100644 --- a/packages/jsii-kernel/lib/kernel.ts +++ b/packages/jsii-kernel/lib/kernel.ts @@ -6,21 +6,9 @@ import { SourceMapConsumer } from 'source-map'; import * as tar from 'tar'; import * as vm from 'vm'; import * as api from './api'; -import { TOKEN_DATE, TOKEN_ENUM, TOKEN_REF } from './api'; - -/** - * Added to objects and contains the objid (the object reference). - * Used to find the object id from an object. - */ -const OBJID_PROP = '$__jsii__objid__$'; -const FQN_PROP = '$__jsii__fqn__$'; -const PROXIES_PROP = '$__jsii__proxies__$'; -const PROXY_REFERENT_PROP = '$__jsii__proxy_referent__$'; - -/** - * A special FQN that can be used to create empty javascript objects. - */ -const EMPTY_OBJECT_FQN = 'Object'; +import { TOKEN_REF } from './api'; +import { ObjectTable, tagJsiiConstructor } from './objects'; +import { CompleteTypeReference, EMPTY_OBJECT_FQN, serializationType, SerializerHost, SERIALIZERS } from './serialization'; export class Kernel { /** @@ -29,11 +17,11 @@ export class Kernel { public traceEnabled = false; private assemblies: { [name: string]: Assembly } = { }; - private objects: { [objid: string]: any } = { }; + private objects = new ObjectTable(); private cbs: { [cbid: string]: Callback } = { }; private waiting: { [cbid: string]: Callback } = { }; private promises: { [prid: string]: AsyncInvocation } = { }; - private nextid = 10000; // incrementing counter for objid, cbid, promiseid + private nextid = 20000; // incrementing counter for objid, cbid, promiseid private syncInProgress?: string; // forbids async calls (begin) while processing sync calls (get/set/invoke) private installDir?: string; @@ -151,13 +139,7 @@ export class Kernel { const { objref } = req; this._debug('del', objref); - const obj = this._findObject(objref); // make sure object exists - delete this.objects[objref[TOKEN_REF]]; - - if (obj[PROXY_REFERENT_PROP]) { - // De-register the proxy if this was a proxy... - delete obj[PROXY_REFERENT_PROP][PROXIES_PROP][obj[FQN_PROP]]; - } + this.objects.deleteObject(objref); return { }; } @@ -200,7 +182,7 @@ export class Kernel { const prototype = this._findSymbol(fqn); this._ensureSync(`property ${property}`, () => - this._wrapSandboxCode(() => prototype[property] = this._toSandbox(value))); + this._wrapSandboxCode(() => prototype[property] = this._toSandbox(value, ti.type))); return {}; } @@ -208,8 +190,7 @@ export class Kernel { public get(req: api.GetRequest): api.GetResponse { const { objref, property } = req; this._debug('get', objref, property); - const obj = this._findObject(objref); - const fqn = this._fqnForObject(obj); + const { instance, fqn } = this.objects.findObject(objref); const ti = this._typeInfoForProperty(fqn, property); // if the property is overridden by the native code and "get" is called on the object, it @@ -217,12 +198,12 @@ export class Kernel { // that, we actually keep a copy of the original property descriptor when we override, // so `findPropertyTarget` will return either the original property name ("property") or // the "super" property name (somehing like "$jsii$super$$"). - const propertyToGet = this._findPropertyTarget(obj, property); + const propertyToGet = this._findPropertyTarget(instance, property); // make the actual "get", and block any async calls that might be performed // by jsii overrides. const value = this._ensureSync(`property '${objref[TOKEN_REF]}.${propertyToGet}'`, - () => this._wrapSandboxCode(() => obj[propertyToGet])); + () => this._wrapSandboxCode(() => instance[propertyToGet])); this._debug('value:', value); const ret = this._fromSandbox(value, ti.type); this._debug('ret:', ret); @@ -232,19 +213,18 @@ export class Kernel { public set(req: api.SetRequest): api.SetResponse { const { objref, property, value } = req; this._debug('set', objref, property, value); - const obj = this._findObject(objref); + const { instance, fqn } = this.objects.findObject(objref); - const fqn = this._fqnForObject(obj); const propInfo = this._typeInfoForProperty(fqn, req.property); if (propInfo.immutable) { throw new Error(`Cannot set value of immutable property ${req.property} to ${req.value}`); } - const propertyToSet = this._findPropertyTarget(obj, property); + const propertyToSet = this._findPropertyTarget(instance, property); this._ensureSync(`property '${objref[TOKEN_REF]}.${propertyToSet}'`, - () => this._wrapSandboxCode(() => obj[propertyToSet] = this._toSandbox(value))); + () => this._wrapSandboxCode(() => instance[propertyToSet] = this._toSandbox(value, propInfo.type))); return { }; } @@ -262,10 +242,13 @@ export class Kernel { } const ret = this._ensureSync(`method '${objref[TOKEN_REF]}.${method}'`, () => { - return this._wrapSandboxCode(() => fn.apply(obj, this._toSandboxValues(args))); + return this._wrapSandboxCode(() => fn.apply(obj, this._toSandboxValues(args, ti.parameters))); }); - return { result: this._fromSandbox(ret, ti.returns) }; + const result = this._fromSandbox(ret, ti.returns || 'void'); + this._debug('invoke result', result); + + return { result }; } public sinvoke(req: api.StaticInvokeRequest): api.InvokeResponse { @@ -289,11 +272,11 @@ export class Kernel { const fn = prototype[method]; const ret = this._ensureSync(`method '${fqn}.${method}'`, () => { - return this._wrapSandboxCode(() => fn.apply(null, this._toSandboxValues(args))); + return this._wrapSandboxCode(() => fn.apply(null, this._toSandboxValues(args, ti.parameters))); }); this._debug('method returned:', ret); - return { result: this._fromSandbox(ret, ti.returns) }; + return { result: this._fromSandbox(ret, ti.returns || 'void') }; } public begin(req: api.BeginRequest): api.BeginResponse { @@ -314,7 +297,7 @@ export class Kernel { throw new Error(`Method ${method} is expected to be an async method`); } - const promise = this._wrapSandboxCode(() => fn.apply(obj, this._toSandboxValues(args))) as Promise; + const promise = this._wrapSandboxCode(() => fn.apply(obj, this._toSandboxValues(args, ti.parameters))) as Promise; // since we are planning to resolve this promise in a different scope // we need to handle rejections here [1] @@ -349,7 +332,7 @@ export class Kernel { throw mapSource(e, this.sourceMaps); } - return { result: this._fromSandbox(result, method.returns) }; + return { result: this._fromSandbox(result, method.returns || 'void') }; } public callbacks(_req?: api.CallbacksRequest): api.CallbacksResponse { @@ -362,7 +345,7 @@ export class Kernel { cookie: cb.override.cookie, invoke: { objref: cb.objref, - method: cb.override.method!, + method: cb.override.method, args: cb.args }, }; @@ -388,7 +371,7 @@ export class Kernel { this._debug('completed with error:', err); cb.fail(new Error(err)); } else { - const sandoxResult = this._toSandbox(result); + const sandoxResult = this._toSandbox(result, cb.expectedReturnType || 'void'); this._debug('completed with result:', sandoxResult); cb.succeed(sandoxResult); } @@ -418,7 +401,7 @@ export class Kernel { public stats(_req?: api.StatsRequest): api.StatsResponse { return { - objectCount: Object.keys(this.objects).length + objectCount: this.objects.count }; } @@ -435,20 +418,15 @@ export class Kernel { case spec.TypeKind.Class: case spec.TypeKind.Enum: const constructor = this._findSymbol(fqn); - Object.defineProperty(constructor, '__jsii__', { - configurable: false, - enumerable: false, - writable: false, - value: { fqn } - }); + tagJsiiConstructor(constructor, fqn); } } } // find the javascript constructor function for a jsii FQN. - private _findCtor(fqn: string, args: any[]) { + private _findCtor(fqn: string, args: any[]): { ctor: any, parameters?: spec.Parameter[] } { if (fqn === EMPTY_OBJECT_FQN) { - return Object; + return { ctor: Object }; } const typeinfo = this._typeInfoForFqn(fqn); @@ -457,7 +435,7 @@ export class Kernel { case spec.TypeKind.Class: const classType = typeinfo as spec.ClassType; this._validateMethodArguments(classType.initializer, args); - return this._findSymbol(fqn); + return { ctor: this._findSymbol(fqn), parameters: classType.initializer && classType.initializer.parameters }; case spec.TypeKind.Interface: throw new Error(`Cannot create an object with an FQN of an interface: ${fqn}`); @@ -470,13 +448,15 @@ export class Kernel { // prefixed with _ to allow calling this method internally without // getting it recorded for testing. private _create(req: api.CreateRequest): api.CreateResponse { + this._debug('create', req); const { fqn, overrides } = req; const requestArgs = req.args || []; - const ctor = this._findCtor(fqn, requestArgs); - const obj = this._wrapSandboxCode(() => new ctor(...this._toSandboxValues(requestArgs))); - const objref = this._createObjref(obj, fqn); + const ctorResult = this._findCtor(fqn, requestArgs); + const ctor = ctorResult.ctor; + const obj = this._wrapSandboxCode(() => new ctor(...this._toSandboxValues(requestArgs, ctorResult.parameters))); + const objref = this.objects.registerObject(obj, fqn); // overrides: for each one of the override method names, installs a // method on the newly created object which represents the remote "reverse proxy". @@ -489,40 +469,18 @@ export class Kernel { const properties = new Set(); for (const override of overrides) { - if (override.method) { - if (override.property) { throw new Error(overrideTypeErrorMessage); } + if (api.isMethodOverride(override)) { + if (api.isPropertyOverride(override)) { throw new Error(overrideTypeErrorMessage); } if (methods.has(override.method)) { throw new Error(`Duplicate override for method '${override.method}'`); } - methods.add(override.method); - // check that the method being overridden actually exists - let methodInfo; - if (fqn !== EMPTY_OBJECT_FQN) { - // error if we can find a property with this name - if (this._tryTypeInfoForProperty(fqn, override.method)) { - throw new Error(`Trying to override property '${override.method}' as a method`); - } - - methodInfo = this._tryTypeInfoForMethod(fqn, override.method); - } - - this._applyMethodOverride(obj, objref, override, methodInfo); - } else if (override.property) { - if (override.method) { throw new Error(overrideTypeErrorMessage); } + this._applyMethodOverride(obj, objref, fqn, override); + } else if (api.isPropertyOverride(override)) { + if (api.isMethodOverride(override)) { throw new Error(overrideTypeErrorMessage); } if (properties.has(override.property)) { throw Error(`Duplicate override for property '${override.property}'`); } properties.add(override.property); - let propInfo: spec.Property | undefined; - if (fqn !== EMPTY_OBJECT_FQN) { - // error if we can find a method with this name - if (this._tryTypeInfoForMethod(fqn, override.property)) { - throw new Error(`Trying to override method '${override.property}' as a property`); - } - - propInfo = this._tryTypeInfoForProperty(fqn, override.property); - } - - this._applyPropertyOverride(obj, objref, override, propInfo); + this._applyPropertyOverride(obj, objref, fqn, override); } else { throw new Error(overrideTypeErrorMessage); } @@ -536,16 +494,43 @@ export class Kernel { return `$jsii$super$${name}$`; } - private _applyPropertyOverride(obj: any, objref: api.ObjRef, override: api.Override, propInfo?: spec.Property) { - const self = this; - const propertyName = override.property!; + private _applyPropertyOverride(obj: any, objref: api.ObjRef, typeFqn: string, override: api.PropertyOverride) { + let propInfo; + if (typeFqn !== EMPTY_OBJECT_FQN) { + // error if we can find a method with this name + if (this._tryTypeInfoForMethod(typeFqn, override.property)) { + throw new Error(`Trying to override method '${override.property}' as a property`); + } + + propInfo = this._tryTypeInfoForProperty(typeFqn, override.property); + } // if this is a private property (i.e. doesn't have `propInfo` the object has a key) - if (!propInfo && propertyName in obj) { - this._debug(`Skipping override of private property ${propertyName}`); + if (!propInfo && override.property in obj) { + this._debug(`Skipping override of private property ${override.property}`); return; } + if (!propInfo) { + // We've overriding a property on an object we have NO type information on (probably + // because it's an anonymous object). + // Pretend it's 'prop: any'; + // + // FIXME: We could do better type checking during the conversion if JSII clients + // would tell us the intended interface type. + propInfo = { + name: override.property, + type: ANY_TYPE, + }; + } + + this._defineOverridenProperty(obj, objref, override, propInfo); + } + + private _defineOverridenProperty(obj: any, objref: api.ObjRef, override: api.PropertyOverride, propInfo: spec.Property) { + const self = this; + const propertyName = override.property!; + this._debug('apply override', propertyName); // save the old property under $jsii$super$$ so that property overrides @@ -568,49 +553,75 @@ export class Kernel { enumerable: prevEnumerable, configurable: prev.configurable, get: () => { + self._debug('virtual get', objref, propertyName, { cookie: override.cookie }); const result = self.callbackHandler({ cookie: override.cookie, cbid: self._makecbid(), get: { objref, property: propertyName } }); this._debug('callback returned', result); - return this._toSandbox(result); + return this._toSandbox(result, propInfo.type); }, set: (value: any) => { self._debug('virtual set', objref, propertyName, { cookie: override.cookie }); self.callbackHandler({ cookie: override.cookie, cbid: self._makecbid(), - set: { objref, property: propertyName, value: self._fromSandbox(value) } + set: { objref, property: propertyName, value: self._fromSandbox(value, propInfo.type) } }); } }); } - private _applyMethodOverride(obj: any, objref: api.ObjRef, override: api.Override, methodInfo?: spec.Method) { - const self = this; - const methodName = override.method!; + private _applyMethodOverride(obj: any, objref: api.ObjRef, typeFqn: string, override: api.MethodOverride) { + let methodInfo; + if (typeFqn !== EMPTY_OBJECT_FQN) { + // error if we can find a property with this name + if (this._tryTypeInfoForProperty(typeFqn, override.method)) { + throw new Error(`Trying to override property '${override.method}' as a method`); + } + + methodInfo = this._tryTypeInfoForMethod(typeFqn, override.method); + } // If this is a private method (doesn't have methodInfo, key resolves on the object), we // are going to skip the override. - if (!methodInfo && obj[methodName]) { - this._debug(`Skipping override of private method ${methodName}`); + if (!methodInfo && obj[override.method]) { + this._debug(`Skipping override of private method ${override.method}`); return; } - // note that we are applying the override even if the method doesn't exist - // on the type spec in order to allow native code to override methods from - // interfaces. + if (!methodInfo) { + // We've overriding a method on an object we have NO type information on (probably + // because it's an anonymous object). + // Pretend it's an (...args: any[]) => any + // + // FIXME: We could do better type checking during the conversion if JSII clients + // would tell us the intended interface type. + methodInfo = { + name: override.method, + returns: ANY_TYPE, + parameters: [{ name: 'args', variadic: true, type: ANY_TYPE}], + variadic: true + }; + } + + this._defineOverridenMethod(obj, objref, override, methodInfo); + } + + private _defineOverridenMethod(obj: any, objref: api.ObjRef, override: api.MethodOverride, methodInfo: spec.Method) { + const self = this; + const methodName = override.method; - if (methodInfo && methodInfo.returns && methodInfo.returns.promise) { + if (methodInfo.returns && methodInfo.returns.promise) { // async method override Object.defineProperty(obj, methodName, { enumerable: false, configurable: false, writable: false, value: (...methodArgs: any[]) => { - self._debug('invoked async override', override); - const args = self._toSandboxValues(methodArgs); + self._debug('invoke async method override', override); + const args = self._toSandboxValues(methodArgs, methodInfo.parameters); return new Promise((succeed, fail) => { const cbid = self._makecbid(); self._debug('adding callback to queue', cbid); @@ -618,6 +629,7 @@ export class Kernel { objref, override, args, + expectedReturnType: methodInfo.returns || 'void', succeed, fail }; @@ -631,24 +643,28 @@ export class Kernel { configurable: false, writable: false, value: (...methodArgs: any[]) => { + self._debug('invoke sync method override', override, 'args', methodArgs); + // We should be validating the actual arguments according to the + // declared parameters here, but let's just assume the JSII runtime on the + // other end has done its work. const result = self.callbackHandler({ cookie: override.cookie, cbid: self._makecbid(), invoke: { objref, method: methodName, - args: this._fromSandbox(methodArgs) + args: this._fromSandboxValues(methodArgs, methodInfo.parameters), } }); - return this._toSandbox(result); + self._debug('Result', result); + return this._toSandbox(result, methodInfo.returns || 'void'); } }); } } private _findInvokeTarget(objref: any, methodName: string, args: any[]) { - const obj = this._findObject(objref); - const fqn = this._fqnForObject(obj); + const { instance, fqn } = this.objects.findObject(objref); const ti = this._typeInfoForMethod(fqn, methodName); this._validateMethodArguments(ti, args); @@ -659,14 +675,14 @@ export class Kernel { // if we didn't find the method on the prototype, it could be a literal object // that implements an interface, so we look if we have the method on the object // itself. if we do, we invoke it. - let fn = obj.constructor.prototype[methodName]; + let fn = instance.constructor.prototype[methodName]; if (!fn) { - fn = obj[methodName]; + fn = instance[methodName]; if (!fn) { throw new Error(`Cannot find ${methodName} on object`); } } - return { ti, obj, fn }; + return { ti, obj: instance, fn }; } private _formatTypeRef(typeRef: spec.TypeReference): string { @@ -743,40 +759,6 @@ export class Kernel { return curr; } - private _createObjref(obj: any, fqn: string): api.ObjRef { - const objid = this._mkobjid(fqn); - Object.defineProperty(obj, OBJID_PROP, { - value: objid, - configurable: false, - enumerable: false, - writable: false - }); - - Object.defineProperty(obj, FQN_PROP, { - value: fqn, - configurable: false, - enumerable: false, - writable: false - }); - - this.objects[objid] = obj; - return { [TOKEN_REF]: objid }; - } - - private _findObject(objref: api.ObjRef) { - if (typeof(objref) !== 'object' || !(TOKEN_REF in objref)) { - throw new Error(`Malformed object reference: ${JSON.stringify(objref)}`); - } - - const objid = objref[TOKEN_REF]; - this._debug('findObject', objid); - const obj = this.objects[objid]; - if (!obj) { - throw new Error(`Object ${objid} not found`); - } - return obj; - } - private _typeInfoForFqn(fqn: string): spec.Type { const components = fqn.split('.'); const moduleName = components[0]; @@ -876,206 +858,77 @@ export class Kernel { return typeInfo; } - private _toSandbox(v: any): any { - // undefined - if (typeof v === 'undefined') { - return undefined; - } - - // null is treated as "undefined" because most languages do not have this distinction - // see awslabs/aws-cdk#157 and awslabs/jsii#282 - if (v === null) { - return undefined; - } + private _toSandbox(v: any, expectedType: CompleteTypeReference): any { + const serTypes = serializationType(expectedType, this._typeInfoForFqn.bind(this)); + this._debug('toSandbox', v, JSON.stringify(serTypes)); - // pointer - if (typeof v === 'object' && TOKEN_REF in v) { - return this._findObject(v); - } - - // date - if (typeof v === 'object' && TOKEN_DATE in v) { - this._debug('Found date:', v); - return new Date(v[TOKEN_DATE]); - } - - // enums - if (typeof v === 'object' && TOKEN_ENUM in v) { - this._debug('Enum:', v); - - const value = v[TOKEN_ENUM] as string; - const sep = value.lastIndexOf('/'); - if (sep === -1) { - throw new Error(`Malformed enum value: ${v[TOKEN_ENUM]}`); - } - - const typeName = value.substr(0, sep); - const valueName = value.substr(sep + 1); - - const enumValue = this._findSymbol(typeName)[valueName]; - if (enumValue === undefined) { - throw new Error(`No enum member named ${valueName} in ${typeName}`); - } - - this._debug('resolved enum value:', enumValue); - return enumValue; - } - - // array - if (Array.isArray(v)) { - return v.map(x => this._toSandbox(x)); - } - - // map - if (typeof v === 'object') { - const out: any = { }; - for (const k of Object.keys(v)) { - const value = this._toSandbox(v[k]); - - // javascript has a fun behavior where - // { ...{ x: 'hello' }, ...{ x: undefined } } - // will result in: - // { x: undefined } - // so omit any keys that have an `undefined` values. - // see awslabs/aws-cdk#965 and compliance test "mergeObjects" - if (value === undefined) { - continue; - } + const host: SerializerHost = { + objects: this.objects, + debug: this._debug.bind(this), + findSymbol: this._findSymbol.bind(this), + lookupType: this._typeInfoForFqn.bind(this), + recurse: this._toSandbox.bind(this), + }; - out[k] = value; + const errors = new Array(); + for (const { serializationClass, typeRef } of serTypes) { + try { + return SERIALIZERS[serializationClass].deserialize(v, typeRef, host); + } catch (e) { + // If no union (99% case), rethrow immediately to preserve stack trace + if (serTypes.length === 1) { throw e; } + errors.push(e.message); } - return out; } - // primitive - return v; + throw new Error(`Value did not match any type in union: ${errors}`); } - private _fromSandbox(v: any, targetType?: spec.TypeReference): any { - this._debug('fromSandbox', v, targetType); + private _fromSandbox(v: any, targetType: CompleteTypeReference): any { + const serTypes = serializationType(targetType, this._typeInfoForFqn.bind(this)); + this._debug('fromSandbox', v, JSON.stringify(serTypes)); - // undefined is returned as null: true - if (typeof(v) === 'undefined') { - return undefined; - } - - if (v === null) { - return undefined; - } - - // existing object - const objid = v[OBJID_PROP]; - if (objid) { - // object already has an objid, return it as a ref. - this._debug('objref exists', objid); - return { [TOKEN_REF]: objid }; - } - - // new object - if (typeof(v) === 'object' && v.constructor.__jsii__) { - // this is jsii object which was created inside the sandbox and still doesn't - // have an object id, so we need to allocate one for it. - this._debug('creating objref for', v); - const fqn = this._fqnForObject(v); - if (!targetType || !spec.isNamedTypeReference(targetType) || this._isAssignable(fqn, targetType)) { - return this._createObjref(v, fqn); - } - } + const host: SerializerHost = { + objects: this.objects, + debug: this._debug.bind(this), + findSymbol: this._findSymbol.bind(this), + lookupType: this._typeInfoForFqn.bind(this), + recurse: this._fromSandbox.bind(this), + }; - // if the method/property returns an object literal and the return type - // is a class, we create a new object based on the fqn and assign all keys. - // so the client receives a real object. - if (typeof(v) === 'object' && targetType && spec.isNamedTypeReference(targetType)) { - this._debug('coalescing to', targetType); - /* - * We "cache" proxy instances in [PROXIES_PROP] so we can return an - * identical object reference upon multiple accesses of the same - * object literal under the same exposed type. This results in a - * behavior that is more consistent with class instances. - */ - const proxies: Proxies = v[PROXIES_PROP] = v[PROXIES_PROP] || {}; - if (!proxies[targetType.fqn]) { - const handler = new KernelProxyHandler(v); - const proxy = new Proxy(v, handler); - // _createObjref will set the FQN_PROP & OBJID_PROP on the proxy. - proxies[targetType.fqn] = { objRef: this._createObjref(proxy, targetType.fqn), handler }; + const errors = new Array(); + for (const { serializationClass, typeRef } of serTypes) { + try { + return SERIALIZERS[serializationClass].serialize(v, typeRef, host); + } catch (e) { + // If no union (99% case), rethrow immediately to preserve stack trace + if (serTypes.length === 1) { throw e; } + errors.push(e.message); } - return proxies[targetType.fqn].objRef; } - // date (https://stackoverflow.com/a/643827/737957) - if (typeof(v) === 'object' && Object.prototype.toString.call(v) === '[object Date]') { - this._debug('date', v); - return { [TOKEN_DATE]: v.toISOString() }; - } - - // array - if (Array.isArray(v)) { - this._debug('array', v); - return v.map(x => this._fromSandbox(x)); - } - - if (targetType && spec.isNamedTypeReference(targetType)) { - const propType = this._typeInfoForFqn(targetType.fqn); - - // enum - if (propType.kind === spec.TypeKind.Enum) { - this._debug('enum', v); - const fqn = propType.fqn; - - const valueName = this._findSymbol(fqn)[v]; - - return { [TOKEN_ENUM]: `${propType.fqn}/${valueName}` }; - } - - } + throw new Error(`Value did not match any type in union: ${errors}`); + } - // map - if (typeof(v) === 'object') { - this._debug('map', v); - const out: any = { }; - for (const k of Object.keys(v)) { - const value = this._fromSandbox(v[k]); - if (value === undefined) { - continue; - } - out[k] = value; - } - return out; - } + private _toSandboxValues(xs: any[], parameters?: spec.Parameter[]) { + return this._boxUnboxParameters(xs, parameters, this._toSandbox.bind(this)); + } - // primitive - this._debug('primitive', v); - return v; + private _fromSandboxValues(xs: any[], parameters?: spec.Parameter[]) { + return this._boxUnboxParameters(xs, parameters, this._fromSandbox.bind(this)); } - /** - * Tests whether a given type (by it's FQN) can be assigned to a named type reference. - * - * @param actualTypeFqn the FQN of the type that is being tested. - * @param requiredType the required reference type. - * - * @returns true if ``requiredType`` is a super-type (base class or implemented interface) of the type designated by - * ``actualTypeFqn``. - */ - private _isAssignable(actualTypeFqn: string, requiredType: spec.NamedTypeReference): boolean { - if (requiredType.fqn === actualTypeFqn) { - return true; + private _boxUnboxParameters(xs: any[], parameters: spec.Parameter[] | undefined, boxUnbox: (x: any, t: CompleteTypeReference) => any) { + parameters = parameters || []; + const types = parameters.map(p => p.type); + // Repeat the last (variadic) type to match the number of actual arguments + while (types.length < xs.length && parameters.length > 0 && parameters[parameters.length - 1].variadic) { + types.push(types[types.length - 1]); } - const actualType = this._typeInfoForFqn(actualTypeFqn); - if (spec.isClassType(actualType) && actualType.base) { - if (this._isAssignable(actualType.base.fqn, requiredType)) { - return true; - } - } - if (spec.isClassOrInterfaceType(actualType) && actualType.interfaces) { - return actualType.interfaces.find(iface => this._isAssignable(iface.fqn, requiredType)) != null; + if (xs.length > types.length) { + throw new Error(`Argument list (${JSON.stringify(xs)}) not same size as expected argument list (length ${types.length})`); } - return false; - } - - private _toSandboxValues(args: any[]) { - return args.map(v => this._toSandbox(v)); + return xs.map((x, i) => boxUnbox(x, types[i])); } private _debug(...args: any[]) { @@ -1083,8 +936,7 @@ export class Kernel { // tslint:disable-next-line:no-console console.error.apply(console, [ '[jsii-kernel]', - args[0], - ...args.slice(1) + ...args ]); } } @@ -1117,22 +969,6 @@ export class Kernel { // type information // - private _fqnForObject(obj: any) { - if (FQN_PROP in obj) { - return obj[FQN_PROP]; - } - - if (!obj.constructor.__jsii__) { - throw new Error('No jsii type info for object'); - } - - return obj.constructor.__jsii__.fqn; - } - - private _mkobjid(fqn: string) { - return `${fqn}@${this.nextid++}`; - } - private _makecbid() { return `jsii::callback::${this.nextid++}`; } @@ -1171,8 +1007,9 @@ export class Kernel { interface Callback { objref: api.ObjRef; - override: api.Override; + override: api.MethodOverride; args: any[]; + expectedReturnType: CompleteTypeReference; // completion callbacks succeed: (...args: any[]) => any; @@ -1237,110 +1074,4 @@ function mapSource(err: Error, sourceMaps: { [assm: string]: SourceMapConsumer } } } -type ObjectKey = string | number | symbol; -/** - * A Proxy handler class to support mutation of the returned object literals, as - * they may "embody" several different interfaces. The handler is in particular - * responsible to make sure the ``FQN_PROP`` and ``OBJID_PROP`` do not get set - * on the ``referent`` object, for this would cause subsequent accesses to - * possibly return incorrect object references. - */ -class KernelProxyHandler implements ProxyHandler { - private readonly ownProperties: { [key: string]: any } = {}; - - /** - * @param referent the "real" value that will be returned. - */ - constructor(public readonly referent: any) { - /* - * Proxy-properties must exist as non-configurable & writable on the - * referent, otherwise the Proxy will not allow returning ``true`` in - * response to ``defineProperty``. - */ - for (const prop of [FQN_PROP, OBJID_PROP]) { - Object.defineProperty(referent, prop, { - configurable: false, - enumerable: false, - writable: true, - value: undefined - }); - } - } - - public defineProperty(target: any, property: ObjectKey, attributes: PropertyDescriptor): boolean { - switch (property) { - case FQN_PROP: - case OBJID_PROP: - return Object.defineProperty(this.ownProperties, property, attributes); - default: - return Object.defineProperty(target, property, attributes); - } - } - - public deleteProperty(target: any, property: ObjectKey): boolean { - switch (property) { - case FQN_PROP: - case OBJID_PROP: - delete this.ownProperties[property]; - break; - default: - delete target[property]; - } - return true; - } - - public getOwnPropertyDescriptor(target: any, property: ObjectKey): PropertyDescriptor | undefined { - switch (property) { - case FQN_PROP: - case OBJID_PROP: - return Object.getOwnPropertyDescriptor(this.ownProperties, property); - default: - return Object.getOwnPropertyDescriptor(target, property); - } - } - - public get(target: any, property: ObjectKey): any { - switch (property) { - // Magical property for the proxy, so we can tell it's one... - case PROXY_REFERENT_PROP: - return this.referent; - case FQN_PROP: - case OBJID_PROP: - return this.ownProperties[property]; - default: - return target[property]; - } - } - - public set(target: any, property: ObjectKey, value: any): boolean { - switch (property) { - case FQN_PROP: - case OBJID_PROP: - this.ownProperties[property] = value; - break; - default: - target[property] = value; - } - return true; - } - - public has(target: any, property: ObjectKey): boolean { - switch (property) { - case FQN_PROP: - case OBJID_PROP: - return property in this.ownProperties; - default: - return property in target; - } - } - - public ownKeys(target: any): ObjectKey[] { - return Reflect.ownKeys(target).concat(Reflect.ownKeys(this.ownProperties)); - } -} - -type Proxies = { [fqn: string]: ProxyReference }; -interface ProxyReference { - objRef: api.ObjRef; - handler: KernelProxyHandler; -} +const ANY_TYPE: spec.PrimitiveTypeReference = { primitive: spec.PrimitiveType.Any }; \ No newline at end of file diff --git a/packages/jsii-kernel/lib/objects.ts b/packages/jsii-kernel/lib/objects.ts new file mode 100644 index 0000000000..0801b1d1e6 --- /dev/null +++ b/packages/jsii-kernel/lib/objects.ts @@ -0,0 +1,135 @@ +import * as api from './api'; + +/** + * Symbol under which we store the { type -> objid } map on object instances + */ +const OBJID_SYMBOL = Symbol('$__jsii__objid__$'); + +/** + * Symbol we use to tag the constructor of a JSII class + */ +const JSII_SYMBOL = Symbol('__jsii__'); + +/** + * Get the JSII fqn for an object (if available) + * + * This will return something if the object was constructed from a JSII-enabled + * class/constructor, or if a literal object was annotated with type + * information. + */ +export function jsiiTypeFqn(obj: any): string | undefined { + const jsii = obj.constructor[JSII_SYMBOL]; + return jsii && jsii.fqn; +} + +/** + * If this object was previously serialized under a given reference, return the same reference + * + * This is to retain object identity across invocations. + */ +export function objectReference(obj: object): api.ObjRef | undefined { + // If this object as already returned + if ((obj as any)[OBJID_SYMBOL]) { + return { [api.TOKEN_REF]: (obj as any)[OBJID_SYMBOL] }; + } + + return undefined; +} + +function tagObject(obj: object, objid: string) { + (obj as any)[OBJID_SYMBOL] = objid; +} + +/** + * Ensure there's a hidden map with the given symbol name on the given object, and return it + */ +export function hiddenMap(obj: any, mapSymbol: symbol): {[key: string]: T} { + let map: any = obj[mapSymbol]; + if (!map) { + map = {}; + Object.defineProperty(obj, mapSymbol, { + value: map, + configurable: false, + enumerable: false, + writable: false + }); + } + return map; +} + +/** + * Set the JSII FQN for classes produced by a given constructor + */ +export function tagJsiiConstructor(constructor: any, fqn: string) { + Object.defineProperty(constructor, JSII_SYMBOL, { + configurable: false, + enumerable: false, + writable: false, + value: { fqn } + }); +} + +/** + * Table of JSII objects + * + * There can be multiple references to the same object, each under a different requested + * type. + */ +export class ObjectTable { + private objects: { [objid: string]: RegisteredObject } = { }; + private nextid = 10000; + + /** + * Register the given object with the given type + * + * Return the existing registration if available. + */ + public registerObject(obj: object, fqn: string): api.ObjRef { + if (fqn === undefined) { + throw new Error('FQN cannot be undefined'); + } + + const objid = this.makeId(fqn); + this.objects[objid] = { instance: obj, fqn }; + tagObject(obj, objid); + + return { [api.TOKEN_REF]: objid }; + } + + /** + * Find the object and registered type for the given ObjRef + */ + public findObject(objref: api.ObjRef): RegisteredObject { + if (typeof(objref) !== 'object' || !(api.TOKEN_REF in objref)) { + throw new Error(`Malformed object reference: ${JSON.stringify(objref)}`); + } + + const objid = objref[api.TOKEN_REF]; + const obj = this.objects[objid]; + if (!obj) { + throw new Error(`Object ${objid} not found`); + } + return obj; + } + + /** + * Delete the registration with the given objref + */ + public deleteObject(objref: api.ObjRef) { + this.findObject(objref); // make sure object exists + delete this.objects[objref[api.TOKEN_REF]]; + } + + public get count(): number { + return Object.keys(this.objects).length; + } + + private makeId(fqn: string) { + return `${fqn}@${this.nextid++}`; + } +} + +export interface RegisteredObject { + instance: any; + fqn: string; +} \ No newline at end of file diff --git a/packages/jsii-kernel/lib/serialization.ts b/packages/jsii-kernel/lib/serialization.ts new file mode 100644 index 0000000000..3d9861b204 --- /dev/null +++ b/packages/jsii-kernel/lib/serialization.ts @@ -0,0 +1,622 @@ + + // tslint:disable:max-line-length +/** + * Handling of types in JSII + * + * Types will be serialized according to the following table: + * + * ┬───────────────────────────────────────────────────────────────────────────────────────────────┐ + * │ JAVASCRIPT TYPE │ + * ┼────────────────┬───────────┬────────────┬───────────────┬───────────────────┬─────────────────┤ + * │ undefined/null │ date │ scalar (*) │ array │ JSII-class object │ literal object │ + * ├──────────┼────────────┼────────────────┼───────────┼────────────┼───────────────┼───────────────────┼─────────────────┤ + * │ DECLARED │ void │ undefined │ undefined │ undefined │ undefined │ undefined │ undefined │ + * │ TYPE │ date │ undefined(†) │ { date } │ - │ - │ - │ - │ + * │ │ scalar (*) │ undefined(†) │ - │ value │ - │ - │ - │ + * │ │ json │ undefined │ string │ value │ array/R(json) │ - │ byvalue/R(json) │ + * │ │ enum │ undefined(†) │ - │ { enum } │ - │ - │ - │ + * │ │ array of T │ undefined(†) │ - │ - │ array/R(T) │ - │ - │ + * │ │ map of T │ undefined(†) │ - │ - │ - │ - │ byvalue/R(T) │ + * │ │ interface │ undefined(†) │ - │ - │ - │ { ref } │ { ref: proxy } │ + * │ │ struct │ undefined(†) │ - │ - │ - │ - │ byvalue/R(T[k]) │ + * │ │ class │ undefined(†) │ - │ - │ - │ { ref } │ { ref: proxy } │ + * │ │ any │ undefined │ { date } │ value │ array/R(any) │ { ref } │ byvalue/R(any) │ + * └──────────┴────────────┴────────────────┴───────────┴────────────┴───────────────┴───────────────────┴─────────────────┘ + * + * - (*) scalar means 'string | number | boolean' + * - (†) throw if not nullable + * - /R(t) recurse with declared type t + */ + + // tslint:enable:max-line-length + +import * as spec from 'jsii-spec'; +import { isObjRef, isWireDate, isWireEnum, ObjRef, TOKEN_DATE, TOKEN_ENUM, WireDate, WireEnum } from './api'; +import { hiddenMap, jsiiTypeFqn, objectReference, ObjectTable } from './objects'; + +/** + * A specific singleton type to be explicit about a Void type + * + * In the spec, 'void' is represented as 'undefined'(*), but allowing the + * value 'undefined' in function calls has lead to consumers failing to pass + * type information that they had, just because they didn't "have to" (the + * parameter was optional). + * + * (*) As in, declaration of a method looks like { returns?: TypeReference } + * and the absence of a type means it returns 'void'. + */ +export type Void = 'void'; + +/** + * A type reference that includes the special type reference Void + */ +export type CompleteTypeReference = spec.TypeReference | Void; + +/** + * A special FQN that can be used to create empty javascript objects. + */ +export const EMPTY_OBJECT_FQN = 'Object'; + +/** + * The type kind, that controls how it will be serialized according to the above table + */ +export const enum SerializationClass { + Void = 'Void', + Date = 'Date', + Scalar = 'Scalar', + Json = 'Json', + Enum = 'Enum', + Array = 'Array', + Map = 'Map', + Struct = 'Struct', + ReferenceType = 'RefType', + Any = 'Any', +} + +type TypeLookup = (fqn: string) => spec.Type; +type SymbolLookup = (fqn: string) => any; + +export interface SerializerHost { + readonly objects: ObjectTable; + debug(...args: any[]): void; + lookupType(fqn: string): spec.Type; + recurse(x: any, type: CompleteTypeReference): any; + findSymbol(fqn: string): any; +} + +interface Serializer { + serialize(value: unknown, type: CompleteTypeReference, host: SerializerHost): any; + deserialize(value: unknown, type: CompleteTypeReference, host: SerializerHost): any; +} + +export const SERIALIZERS: {[k: string]: Serializer} = { + // ---------------------------------------------------------------------- + [SerializationClass.Void]: { + serialize(value, _type, host) { + if (value != null) { + host.debug('Expected void, got', value); + } + return undefined; + }, + + deserialize(value, _type, host) { + if (value != null) { + host.debug('Expected void, got', value); + } + return undefined; + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Date]: { + serialize(value, type): WireDate | undefined { + if (nullAndOk(value, type)) { return undefined; } + + if (!isDate(value)) { + throw new Error(`Expected Date, got ${JSON.stringify(value)}`); + } + return serializeDate(value); + }, + + deserialize(value, type) { + if (nullAndOk(value, type)) { return undefined; } + + if (!isWireDate(value)) { + throw new Error(`Expected Date, got ${JSON.stringify(value)}`); + } + return deserializeDate(value); + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Scalar]: { + serialize(value, type) { + if (nullAndOk(value, type)) { return undefined; } + + const primitiveType = type as spec.PrimitiveTypeReference; + + if (!isScalar(value)) { + throw new Error(`Expected Scalar, got ${JSON.stringify(value)}`); + } + if (typeof value !== primitiveType.primitive) { + throw new Error(`Expected '${primitiveType.primitive}', got ${JSON.stringify(value)} (${typeof value})`); + } + return value; + }, + + deserialize(value, type) { + if (nullAndOk(value, type)) { return undefined; } + + const primitiveType = type as spec.PrimitiveTypeReference; + + if (!isScalar(value)) { + throw new Error(`Expected Scalar, got ${JSON.stringify(value)}`); + } + if (typeof value !== primitiveType.primitive) { + throw new Error(`Expected '${primitiveType.primitive}', got ${JSON.stringify(value)} (${typeof value})`); + } + + return value; + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Json]: { + serialize(value) { + // Just whatever. Dates will automatically serialize themselves to strings. + return value; + }, + deserialize(value) { + return value; + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Enum]: { + serialize(value, type, host): WireEnum | undefined { + host.debug('hullo'); + if (nullAndOk(value, type)) { return undefined; } + + if (typeof value !== 'string' && typeof value !== 'number') { + throw new Error(`Expected enum value, got ${JSON.stringify(value)}`); + } + + host.debug('Serializing enum'); + + const enumType = type as spec.EnumType; + return { [TOKEN_ENUM]: `${enumType.fqn}/${host.findSymbol(enumType.fqn)[value]}` }; + }, + deserialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + if (!isWireEnum(value)) { + throw new Error(`Expected enum value, got ${JSON.stringify(value)}`); + } + + return deserializeEnum(value, host.findSymbol); + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Array]: { + serialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + if (!Array.isArray(value)) { + throw new Error(`Expected array type, got ${JSON.stringify(value)}`); + } + + const arrayType = type as spec.CollectionTypeReference; + + return value.map(x => host.recurse(x, arrayType.collection.elementtype)); + }, + deserialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + if (!Array.isArray(value)) { + throw new Error(`Expected array type, got ${JSON.stringify(value)}`); + } + + const arrayType = type as spec.CollectionTypeReference; + + return value.map(x => host.recurse(x, arrayType.collection.elementtype)); + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Map]: { + serialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + const mapType = type as spec.CollectionTypeReference; + return mapValues(value, v => host.recurse(v, mapType.collection.elementtype)); + }, + deserialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + const mapType = type as spec.CollectionTypeReference; + return mapValues(value, v => host.recurse(v, mapType.collection.elementtype)); + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Struct]: { + serialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + if (typeof value !== 'object' || value == null) { + throw new Error(`Expected object, got ${JSON.stringify(value)}`); + } + + // This looks odd, but if an object was originally passed in as a by-ref + // class, and it happens to conform to a datatype interface we say we're + // returning, return the actual object instead of the serialized value. + // NOTE: Not entirely sure yet whether this is a bug masquerading as a + // feature or not. + const prevRef = objectReference(value); + if (prevRef) { return prevRef; } + + /* + This is what we'd like to do, but we can't because at least the Java client + does not understand by-value serialized interface types, so we'll have to + serialize by-reference for now: + https://github.com/awslabs/jsii/issues/400 + + const props = propertiesOf(namedType); + + return mapValues(value, (v, key) => { + if (!props[key]) { return undefined; } // Don't map if unknown property + return host.recurse(v, props[key].type); + }); + */ + + host.debug('Returning value type as reference type for now (awslabs/jsii#400)'); + const wireFqn = selectWireType(value, type as spec.NamedTypeReference, host.lookupType); + return host.objects.registerObject(value, wireFqn); + }, + deserialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + if (typeof value !== 'object' || value == null) { + throw new Error(`Expected object reference, got ${JSON.stringify(value)}`); + } + + // Similarly to other end, we might be getting a reference type where we're + // expecting a value type. Accept this for now. + const prevRef = objectReference(value); + if (prevRef) { + host.debug('Expected value type but got reference type, accepting for now (awslabs/jsii#400)'); + return prevRef; + } + + const namedType = host.lookupType((type as spec.NamedTypeReference).fqn); + const props = propertiesOf(namedType); + + return mapValues(value, (v, key) => { + if (!props[key]) { return undefined; } // Don't map if unknown property + return host.recurse(v, props[key].type); + }); + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.ReferenceType]: { + serialize(value, type, host): ObjRef | undefined { + if (nullAndOk(value, type)) { return undefined; } + + if (typeof value !== 'object' || value == null) { + throw new Error(`Expected object reference, got ${JSON.stringify(value)}`); + } + + const prevRef = objectReference(value); + if (prevRef) { return prevRef; } + + const wireFqn = selectWireType(value, type as spec.NamedTypeReference, host.lookupType); + return host.objects.registerObject(value, wireFqn); + }, + deserialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + // The only way to pass a by-ref object is to have created it + // previously inside JSII kernel, so it must have an objref already. + + if (!isObjRef(value)) { + throw new Error(`Expected object reference, got ${JSON.stringify(value)}`); + } + + const { instance, fqn } = host.objects.findObject(value); + + const namedTypeRef = type as spec.NamedTypeReference; + if (namedTypeRef.fqn !== EMPTY_OBJECT_FQN) { + const namedType = host.lookupType(namedTypeRef.fqn); + + // Check that the object we got is of the right type + // We only do this for classes, not interfaces, since Java might pass us objects that + // privately implement some interface and we can't prove they don't. + // https://github.com/awslabs/jsii/issues/399 + if (spec.isClassType(namedType) && !isAssignable(fqn, type as spec.NamedTypeReference, host.lookupType)) { + throw new Error(`Object of type ${fqn} is not convertible to ${(type as spec.NamedTypeReference).fqn}`); + } + } + + return instance; + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Any]: { + serialize(value, _type, host) { + if (value == null) { return undefined; } + + if (isDate(value)) { return serializeDate(value); } + if (isScalar(value)) { return value; } + if (Array.isArray(value)) { + return value.map(e => host.recurse(e, { primitive: spec.PrimitiveType.Any })); + } + + // Note: no case for "ENUM" here, without type declaration we can't tell the difference + // between an enum member and a scalar. + + if (typeof value !== 'object' || value == null) { + throw new Error(`JSII kernel assumption violated, ${JSON.stringify(value)} is not an object`); + } + + // To make sure people aren't going to try and return Map<> or Set<> out, test for + // those and throw a descriptive error message. We can't detect these cases any other + // way, and the by-value serialized object will be quite useless. + if (value instanceof Set || value instanceof Map) { throw new Error(`Can't return objects of type Set or Map`); } + + // Use a previous reference to maintain object identity. NOTE: this may cause us to return + // a different type than requested! This is just how it is right now. + // https://github.com/awslabs/jsii/issues/399 + const prevRef = objectReference(value); + if (prevRef) { return prevRef; } + + // If this is or should be a reference type, pass or make the reference + // (Like regular reftype serialization, but without the type derivation to an interface) + const jsiiType = jsiiTypeFqn(value); + if (jsiiType) { return host.objects.registerObject(value, jsiiType); } + + // At this point we have an object that is not of an exported type. Either an object + // literal, or an instance of a fully private class (cannot distinguish those cases). + + // We will serialize by-value, but recurse for serialization so that if + // the object contains reference objects, they will be serialized appropriately. + // (Basically, serialize anything else as a map of 'any'). + return mapValues(value, (v) => host.recurse(v, { primitive: spec.PrimitiveType.Any })); + }, + + deserialize(value, _type, host) { + if (value == null) { return undefined; } + + if (isWireDate(value)) { + host.debug('ANY is a Date'); + return deserializeDate(value); + } + if (isScalar(value)) { + host.debug('ANY is a Scalar'); + return value; + } + if (Array.isArray(value)) { + host.debug('ANY is an Array'); + return value.map(e => host.recurse(e, { primitive: spec.PrimitiveType.Any })); + } + + if (isWireEnum(value)) { + host.debug('ANY is an Enum'); + return deserializeEnum(value, host.findSymbol); + } + if (isObjRef(value)) { + host.debug('ANY is a Ref'); + return host.objects.findObject(value).instance; + } + + // At this point again, deserialize by-value. + host.debug('ANY is a Map'); + return mapValues(value, (v) => host.recurse(v, { primitive: spec.PrimitiveType.Any })); + }, + }, +}; + +function serializeDate(value: Date): WireDate { + return { [TOKEN_DATE]: value.toISOString() }; +} + +function deserializeDate(value: WireDate): Date { + return new Date(value[TOKEN_DATE]); +} + +function deserializeEnum(value: WireEnum, lookup: SymbolLookup) { + const enumLocator = value[TOKEN_ENUM] as string; + const sep = enumLocator.lastIndexOf('/'); + if (sep === -1) { + throw new Error(`Malformed enum value: ${JSON.stringify(value)}`); + } + + const typeName = enumLocator.substr(0, sep); + const valueName = enumLocator.substr(sep + 1); + + const enumValue = lookup(typeName)[valueName]; + if (enumValue === undefined) { + throw new Error(`No enum member named ${valueName} in ${typeName}`); + } + return enumValue; +} + +export interface TypeSerialization { + serializationClass: SerializationClass; + typeRef: CompleteTypeReference; +} + +/** + * From a type reference, return the possible serialization types + * + * There can be multiple, because the type can be a type union. + */ +export function serializationType(typeRef: CompleteTypeReference, lookup: TypeLookup): TypeSerialization[] { + if (typeRef == null) { throw new Error(`Kernel error: expected type information, got 'undefined'`); } + if (typeRef === 'void') { return [{ serializationClass: SerializationClass.Void, typeRef }]; } + if (spec.isPrimitiveTypeReference(typeRef)) { + switch (typeRef.primitive) { + case spec.PrimitiveType.Any: return [{ serializationClass: SerializationClass.Any, typeRef }]; + case spec.PrimitiveType.Date: return [{ serializationClass: SerializationClass.Date, typeRef }]; + case spec.PrimitiveType.Json: return [{ serializationClass: SerializationClass.Json, typeRef }]; + case spec.PrimitiveType.Boolean: + case spec.PrimitiveType.Number: + case spec.PrimitiveType.String: + return [{ serializationClass: SerializationClass.Scalar, typeRef }]; + } + + throw new Error('Unknown primitive type'); + } + if (spec.isCollectionTypeReference(typeRef)) { + return [{ + serializationClass: typeRef.collection.kind === spec.CollectionKind.Array ? SerializationClass.Array : SerializationClass.Map, + typeRef + }]; + } + if (spec.isUnionTypeReference(typeRef)) { + const compoundTypes = flatMap(typeRef.union.types, t => serializationType(t, lookup)); + // Propagate the top-level 'optional' field to each individual subtype + for (const t of compoundTypes) { + if (t.typeRef !== 'void') { + t.typeRef.optional = typeRef.optional; + } + } + return compoundTypes; + } + + // The next part of the conversion is lookup-dependent + const type = lookup(typeRef.fqn); + + if (spec.isEnumType(type)) { + return [{ serializationClass: SerializationClass.Enum, typeRef }]; + } + + if (spec.isInterfaceType(type) && type.datatype) { + return [{ serializationClass: SerializationClass.Struct, typeRef }]; + } + + return [{ serializationClass: SerializationClass.ReferenceType, typeRef }]; +} + +function nullAndOk(x: unknown, type: CompleteTypeReference): boolean { + if (x != null) { return false; } + + if (type !== 'void' && !type.optional) { + throw new Error(`Got 'undefined' for non-nullable type ${JSON.stringify(type)}`); + } + + return true; +} + +function isDate(x: unknown): x is Date { + return typeof x === 'object' && Object.prototype.toString.call(x) === '[object Date]'; +} + +function isScalar(x: unknown): x is string | number | boolean { + return typeof x === 'string' || typeof x === 'number' || typeof x === 'boolean'; +} + +function flatMap(xs: T[], fn: (x: T) => U[]): U[] { + const ret = new Array(); + for (const x of xs) { ret.push(...fn(x)); } + return ret; +} + +/** + * Map an object's values, skipping 'undefined' values' + */ +function mapValues(value: unknown, fn: (value: any, field: string) => any) { + if (typeof value !== 'object' || value == null) { + throw new Error(`Expected object type, got ${JSON.stringify(value)}`); + } + + const out: any = { }; + for (const [k, v] of Object.entries(value)) { + const wireValue = fn(v, k); + if (wireValue === undefined) { continue; } + out[k] = wireValue; + } + return out; +} + +function propertiesOf(t: spec.Type): {[name: string]: spec.Property} { + if (!spec.isClassOrInterfaceType(t)) { return {}; } + + const ret: {[name: string]: spec.Property} = {}; + for (const prop of t.properties || []) { + ret[prop.name] = prop; + } + return ret; +} + +const WIRE_TYPE_MAP = Symbol('$__jsii_wire_type__$'); + +/** + * Select the wire type for the given object and requested type + * + * Should return the most specific type that is in the JSII assembly and + * assignable to the required type. + * + * We actually don't need to search much; because of prototypal constructor + * linking, object.constructor.__jsii__ will have the FQN of the most specific + * exported JSII class this object is an instance of. + * + * Either that's assignable to the requested type, in which case we return it, + * or it's not, in which case there's a hidden class that implements the interface + * and we just return the interface so the other side can instantiate an interface + * proxy for it. + * + * Cache the analysis on the object to avoid having to do too many searches through + * the type system for repeated accesses on the same object. + */ +function selectWireType(obj: any, expectedType: spec.NamedTypeReference, lookup: TypeLookup): string { + const map = hiddenMap(obj, WIRE_TYPE_MAP); + + if (!(expectedType.fqn in map)) { + const jsiiType = jsiiTypeFqn(obj); + if (jsiiType) { + const assignable = isAssignable(jsiiType, expectedType, lookup); + + // If we're not assignable and both types are class types, this cannot be satisfied. + if (!assignable && spec.isClassType(lookup(expectedType.fqn))) { + throw new Error(`Object of type ${jsiiType} is not convertible to ${expectedType.fqn}`); + } + + map[expectedType.fqn] = assignable ? jsiiType : expectedType.fqn; + } else { + map[expectedType.fqn] = expectedType.fqn; + } + } + + return map[expectedType.fqn]; +} + +/** + * Tests whether a given type (by it's FQN) can be assigned to a named type reference. + * + * @param actualTypeFqn the FQN of the type that is being tested. + * @param requiredType the required reference type. + * + * @returns true if ``requiredType`` is a super-type (base class or implemented interface) of the type designated by + * ``actualTypeFqn``. + */ +function isAssignable(actualTypeFqn: string, requiredType: spec.NamedTypeReference, lookup: TypeLookup): boolean { + // The empty object is assignable to everything + if (actualTypeFqn === EMPTY_OBJECT_FQN) { return true; } + + if (requiredType.fqn === actualTypeFqn) { + return true; + } + const actualType = lookup(actualTypeFqn); + if (spec.isClassType(actualType)) { + if (actualType.base && isAssignable(actualType.base.fqn, requiredType, lookup)) { + return true; + } + } + if (spec.isClassOrInterfaceType(actualType) && actualType.interfaces) { + return actualType.interfaces.find(iface => isAssignable(iface.fqn, requiredType, lookup)) != null; + } + return false; +} \ No newline at end of file diff --git a/packages/jsii-kernel/test/test.kernel.ts b/packages/jsii-kernel/test/test.kernel.ts index 730911a589..fe81bb4de3 100644 --- a/packages/jsii-kernel/test/test.kernel.ts +++ b/packages/jsii-kernel/test/test.kernel.ts @@ -5,7 +5,7 @@ import { join } from 'path'; import path = require('path'); import vm = require('vm'); import { api, Kernel } from '../lib'; -import { Callback, TOKEN_REF } from '../lib/api'; +import { Callback, ObjRef, TOKEN_REF } from '../lib/api'; import { closeRecording, recordInteraction } from './recording'; // extract versions of fixtures @@ -18,6 +18,9 @@ const calcVersion = require('jsii-calc/package.json').version.replace(/\+.+$/, ' // tslint:disable:no-console // tslint:disable:max-line-length +// Do this so that regexes stringify nicely in approximate tests +(RegExp.prototype as any).toJSON = function() { return this.source; }; + process.setMaxListeners(9999); // since every kernel instance adds an `on('exit')` handler. process.on('unhandledRejection', e => { @@ -124,9 +127,14 @@ defineTest('in/out primitive types', async (test, sandbox) => { sandbox.set({ objref: alltypes, property: 'numberProperty', value: 123 }); test.deepEqual(sandbox.get({ objref: alltypes, property: 'numberProperty' }).value, 123); + // in -> out for an ANY const num = sandbox.create({ fqn: '@scope/jsii-calc-lib.Number', args: [ 444 ] }); sandbox.set({ objref: alltypes, property: 'anyProperty', value: num }); test.deepEqual(sandbox.get({ objref: alltypes, property: 'anyProperty' }).value, num); + + // out -> in for an ANY + const ret = sandbox.invoke({ objref: alltypes, method: 'anyOut' }).result; + sandbox.invoke({ objref: alltypes, method: 'anyIn', args: [ret] }); }); defineTest('in/out objects', async (test, sandbox) => { @@ -140,11 +148,19 @@ defineTest('in/out objects', async (test, sandbox) => { defineTest('in/out collections', async (test, sandbox) => { const alltypes = sandbox.create({ fqn: 'jsii-calc.AllTypes', args: [ ] }); - const array = [ 1, 2, 3, 4 ]; + const array = [ '1', '2', '3', '4' ]; sandbox.set({ objref: alltypes, property: 'arrayProperty', value: array }); test.deepEqual(sandbox.get({ objref: alltypes, property: 'arrayProperty' }).value, array); - const map = { a: 12, b: 33, c: 33, d: { e: 123 }}; + const num = create(sandbox, '@scope/jsii-calc-lib.Number'); + + const map = { + a: num(12), + b: num(33), + c: num(33), + d: num(123), + }; + sandbox.set({ objref: alltypes, property: 'mapProperty', value: map }); test.deepEqual(sandbox.get({ objref: alltypes, property: 'mapProperty' }).value, map); }); @@ -292,7 +308,7 @@ defineTest('type-checking: try to create an object from a non-class type', async defineTest('type-checking: argument count in methods and initializers', async (test, sandbox) => { // ctor has one optional argument sandbox.create({ fqn: 'jsii-calc.Calculator' }); - sandbox.create({ fqn: 'jsii-calc.Calculator', args: [ 11 ] }); + sandbox.create({ fqn: 'jsii-calc.Calculator', args: [ {} ] }); // but we expect an error if more arguments are passed test.throws(() => sandbox.create({ fqn: 'jsii-calc.Calculator', args: [ 1, 2, 3 ] }), /Too many arguments/); @@ -305,8 +321,8 @@ defineTest('type-checking: argument count in methods and initializers', async (t test.throws(() => sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [] }), /Not enough arguments/); test.throws(() => sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [ 1 ]}), /Not enough arguments/); sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [ 1, 'hello' ] }); - sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [ 1, 'hello', new Date() ] }); - test.throws(() => sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [ 1, 'hello', new Date(), 'too much' ] }), /Too many arguments/); + sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [ 1, 'hello', { [api.TOKEN_DATE]: new Date().toISOString() } ]}); + test.throws(() => sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [ 1, 'hello', { [api.TOKEN_DATE]: new Date().toISOString() }, 'too much' ] }), /Too many arguments/); }); defineTest('verify object literals are converted to real classes', async (test, sandbox) => { @@ -541,6 +557,7 @@ defineTest('sync overrides', async (test, sandbox) => { sandbox.callbackHandler = makeSyncCallbackHandler(callback => { test.equal(callback.invoke!.args![0], 999); called = true; + return callback.invoke!.args![0]; }); sandbox.set({ objref: obj, property: 'callerIsProperty', value: 999 }); @@ -589,7 +606,7 @@ defineTest('sync overrides: properties - readwrite', async (test, sandbox) => { test.deepEqual(value, { result: 'override applied' }); // make sure we can still set the property - sandbox.invoke({ objref: obj, method: 'modifyValueOfTheProperty', args: [ 1234 ] }); + sandbox.invoke({ objref: obj, method: 'modifyValueOfTheProperty', args: [ '1234' ] }); test.deepEqual(setValue, 1234); }); @@ -618,8 +635,8 @@ defineTest('sync overrides: properties - readwrite (backed by functions)', async test.deepEqual(value, { result: 'override applied for otherProperty' }); // make sure we can still set the property - sandbox.invoke({ objref: obj, method: 'modifyOtherProperty', args: [ 778877 ]}); - test.deepEqual(setValue, 778877); + sandbox.invoke({ objref: obj, method: 'modifyOtherProperty', args: [ '778877' ]}); + test.deepEqual(setValue, '778877'); }); defineTest('sync overrides: duplicate overrides for the same property', async (test, sandbox) => { @@ -733,6 +750,8 @@ defineTest('fail to begin async from sync - method', async (test, sandbox) => { const innerObj = sandbox.create({ fqn: 'jsii-calc.AsyncVirtualMethods' }); test.throws(() => sandbox.begin({ objref: innerObj, method: 'callMe' })); called++; + + return 42; // Need a valid return value }); sandbox.invoke({ objref: obj, method: 'callerIsMethod' }); @@ -958,15 +977,41 @@ defineTest('JSII_AGENT is undefined in node.js', async (test, sandbox) => { }); defineTest('ObjRefs are labeled with the "most correct" type', async (test, sandbox) => { - const classRef = sandbox.sinvoke({ fqn: 'jsii-calc.Constructors', method: 'makeClass' }).result as api.ObjRef; - const ifaceRef = sandbox.sinvoke({ fqn: 'jsii-calc.Constructors', method: 'makeInterface' }).result as api.ObjRef; + typeMatches('makeClass', { '$jsii.byref': /^jsii-calc.InbetweenClass@/ }); + typeMatches('makeInterface', { '$jsii.byref': /^jsii-calc.IPublicInterface@/ }); + typeMatches('makeInterface2', { '$jsii.byref': /^jsii-calc.InbetweenClass@/ }); + typeMatches('makeInterfaces', [ { '$jsii.byref': /^jsii-calc.IPublicInterface@/ } ]); + typeMatches('hiddenInterface', { '$jsii.byref': /^jsii-calc.IPublicInterface@/ }); + typeMatches('hiddenInterfaces', [ { '$jsii.byref': /^jsii-calc.IPublicInterface@/ } ]); + typeMatches('hiddenSubInterfaces', [ { '$jsii.byref': /^jsii-calc.IPublicInterface@/ } ]); + + function typeMatches(staticMethod: string, typeSpec: any) { + const ret = sandbox.sinvoke({ fqn: 'jsii-calc.Constructors', method: staticMethod }).result as api.ObjRef; + + test.ok(deepEqualWithRegex(ret, typeSpec), `Constructors.${staticMethod}() => ${JSON.stringify(ret)}, does not match ${JSON.stringify(typeSpec)}`); + } +}); + +/* + +Test currently disabled because we don't have the infrastructure to make it pass. +https://github.com/awslabs/jsii/issues/399 + +defineTest('A single instance can be returned under two types', async (test, sandbox) => { + const singleInstanceTwoTypes = create(sandbox, 'jsii-calc.SingleInstanceTwoTypes')(); + + typeMatches('interface1', { '$jsii.byref': /^jsii-calc.InbetweenClass@/ }); + typeMatches('interface2', { '$jsii.byref': /^jsii-calc.IPublicInterface@/ }); - test.ok(classRef[api.TOKEN_REF].startsWith('jsii-calc.InbetweenClass'), - `${classRef[api.TOKEN_REF]} starts with jsii-calc.InbetweenClass`); - test.ok(ifaceRef[api.TOKEN_REF].startsWith('jsii-calc.IPublicInterface'), - `${ifaceRef[api.TOKEN_REF]} starts with jsii-calc.IPublicInterface`); + function typeMatches(method: string, typeSpec: any) { + const ret = sandbox.invoke({ objref: singleInstanceTwoTypes, method }).result as api.ObjRef; + + test.ok(deepEqualWithRegex(ret, typeSpec), `Constructors.${method}() => ${JSON.stringify(ret)}, does not match ${JSON.stringify(typeSpec)}`); + } }); +*/ + defineTest('toSandbox: "null" in hash values send to JS should be treated as non-existing key', async (test, sandbox) => { const input = { option1: null, option2: 'hello' }; const option1Exists = sandbox.sinvoke({ fqn: 'jsii-calc.EraseUndefinedHashValues', method: 'doesKeyExist', args: [ input, 'option1' ] }); @@ -994,6 +1039,55 @@ defineTest('fromSandbox: "null" in hash values returned from JS erases the key', test.deepEqual(output, { result: { prop2: 'value2' } }); }); +defineTest('calculator can set and retrieve union properties', async (test, sandbox) => { + const calculator = create(sandbox, 'jsii-calc.Calculator')(); + + const mul = create(sandbox, 'jsii-calc.Multiply'); + const num = create(sandbox, '@scope/jsii-calc-lib.Number'); + + sandbox.set({ objref: calculator, property: 'unionProperty', value: mul(num(9), num(3)) }); + + const value = sandbox.invoke({ objref: calculator, method: 'readUnionValue' }).result; + test.equal(27, value); + + const expression = sandbox.get({ objref: calculator, property: 'unionProperty' }).value; + + console.log(expression); + + test.ok(deepEqualWithRegex(expression, { '$jsii.byref': /^jsii-calc.Multiply@/ })); +}); + +defineTest('can set and retrieve union properties', async (test, sandbox) => { + const types = create(sandbox, 'jsii-calc.AllTypes')(); + const typesSet = set(sandbox, types); + const typesGet = get(sandbox, types); + const mul = create(sandbox, 'jsii-calc.Multiply'); + const num = create(sandbox, '@scope/jsii-calc-lib.Number'); + + typesSet('unionProperty', 1234); + test.equal(typesGet('unionProperty'), 1234); + + typesSet('unionProperty', 'Hello'); + test.equal(typesGet('unionProperty'), 'Hello'); + + typesSet('unionProperty', mul(num(2), num(12))); + const mulObj = typesGet('unionProperty'); + test.equal(get(sandbox, mulObj)('value'), 24); + + // Collections + + typesSet('unionMapProperty', { + Foo: num(99), + }); + + typesSet('unionArrayProperty', [ + 123, + num(33), + ]); + const unionArray = typesGet('unionArrayProperty'); + test.equal(get(sandbox, unionArray[1])('value'), 33); +}); + // ================================================================================================= const testNames: { [name: string]: boolean } = { }; @@ -1083,3 +1177,51 @@ function makeSyncCallbackHandler(logic: (callback: Callback) => any) { return result; }; } + +export function deepEqualWithRegex(lvalue: any, rvalue: any): boolean { + if (lvalue === rvalue) { return true; } + if (typeof lvalue === 'string' && rvalue instanceof RegExp) { return rvalue.test(lvalue); } + if (typeof lvalue !== typeof rvalue) { return false; } + if (Array.isArray(lvalue) !== Array.isArray(rvalue)) { return false; } + if (Array.isArray(lvalue) /* && Array.isArray(rvalue) */) { + if (lvalue.length !== rvalue.length) { return false; } + for (let i = 0 ; i < lvalue.length ; i++) { + if (!deepEqualWithRegex(lvalue[i], rvalue[i])) { return false; } + } + return true; + } + if (typeof lvalue === 'object' /* && typeof rvalue === 'object' */) { + if (lvalue === null || rvalue === null) { + // If both were null, they'd have been === + return false; + } + const keys = Object.keys(lvalue); + if (keys.length !== Object.keys(rvalue).length) { return false; } + for (const key of keys) { + if (!rvalue.hasOwnProperty(key)) { return false; } + if (!deepEqualWithRegex(lvalue[key], rvalue[key])) { return false; } + } + return true; + } + // Neither object, nor array: I deduce this is primitive type + // Primitive type and not ===, so I deduce not deepEqual + return false; +} + +function create(kernel: Kernel, fqn: string) { + return (...args: any[]) => { + return kernel.create({ fqn, args }); + }; +} + +function set(kernel: Kernel, objref: ObjRef) { + return (property: string, value: any) => { + return kernel.set({ objref, property, value }); + }; +} + +function get(kernel: Kernel, objref: ObjRef) { + return (property: string) => { + return kernel.get({ objref, property }).value; + }; +} diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii index e9081de3c1..88d04378d2 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii @@ -353,6 +353,23 @@ }, "kind": "class", "methods": [ + { + "name": "anyIn", + "parameters": [ + { + "name": "inp", + "type": { + "primitive": "any" + } + } + ] + }, + { + "name": "anyOut", + "returns": { + "primitive": "any" + } + }, { "name": "enumMethod", "parameters": [ @@ -474,7 +491,7 @@ "primitive": "number" }, { - "fqn": "jsii-calc.composition.CompositeOperation" + "fqn": "@scope/jsii-calc-lib.Value" } ] } @@ -1217,6 +1234,37 @@ }, "kind": "class", "methods": [ + { + "name": "hiddenInterface", + "returns": { + "fqn": "jsii-calc.IPublicInterface" + }, + "static": true + }, + { + "name": "hiddenInterfaces", + "returns": { + "collection": { + "elementtype": { + "fqn": "jsii-calc.IPublicInterface" + }, + "kind": "array" + } + }, + "static": true + }, + { + "name": "hiddenSubInterfaces", + "returns": { + "collection": { + "elementtype": { + "fqn": "jsii-calc.IPublicInterface" + }, + "kind": "array" + } + }, + "static": true + }, { "name": "makeClass", "returns": { @@ -1230,6 +1278,25 @@ "fqn": "jsii-calc.IPublicInterface" }, "static": true + }, + { + "name": "makeInterface2", + "returns": { + "fqn": "jsii-calc.IPublicInterface2" + }, + "static": true + }, + { + "name": "makeInterfaces", + "returns": { + "collection": { + "elementtype": { + "fqn": "jsii-calc.IPublicInterface" + }, + "kind": "array" + } + }, + "static": true } ], "name": "Constructors" @@ -2089,11 +2156,29 @@ "methods": [ { "abstract": true, - "name": "bye" + "name": "bye", + "returns": { + "primitive": "string" + } } ], "name": "IPublicInterface" }, + "jsii-calc.IPublicInterface2": { + "assembly": "jsii-calc", + "fqn": "jsii-calc.IPublicInterface2", + "kind": "interface", + "methods": [ + { + "abstract": true, + "name": "ciao", + "returns": { + "primitive": "string" + } + } + ], + "name": "IPublicInterface2" + }, "jsii-calc.IRandomNumberGenerator": { "assembly": "jsii-calc", "docs": { @@ -2240,7 +2325,23 @@ "initializer": { "initializer": true }, + "interfaces": [ + { + "fqn": "jsii-calc.IPublicInterface2" + } + ], "kind": "class", + "methods": [ + { + "name": "ciao", + "overrides": { + "fqn": "jsii-calc.IPublicInterface2" + }, + "returns": { + "primitive": "string" + } + } + ], "name": "InbetweenClass" }, "jsii-calc.InterfaceImplementedByAbstractClass": { @@ -3561,6 +3662,32 @@ ], "name": "RuntimeTypeChecking" }, + "jsii-calc.SingleInstanceTwoTypes": { + "assembly": "jsii-calc", + "docs": { + "comment": "Test that a single instance can be returned under two different FQNs\n\nJSII clients can instantiate 2 different strongly-typed wrappers for the same\nobject. Unfortunately, this will break object equality, but if we didn't do\nthis it would break runtime type checks in the JVM or CLR." + }, + "fqn": "jsii-calc.SingleInstanceTwoTypes", + "initializer": { + "initializer": true + }, + "kind": "class", + "methods": [ + { + "name": "interface1", + "returns": { + "fqn": "jsii-calc.InbetweenClass" + } + }, + { + "name": "interface2", + "returns": { + "fqn": "jsii-calc.IPublicInterface" + } + } + ], + "name": "SingleInstanceTwoTypes" + }, "jsii-calc.Statics": { "assembly": "jsii-calc", "fqn": "jsii-calc.Statics", @@ -4349,5 +4476,5 @@ } }, "version": "0.8.0", - "fingerprint": "4LMgT0Rllw3CIJWiDiR/eUfSRPvCEeWyGWxJXMiDvcU=" + "fingerprint": "7BN7XlOFufVEMTaAbMH5GDcE4zQmJj4iiDlYoXiRvCs=" } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/AllTypes.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/AllTypes.cs index 2e6f227a3f..7cba7ed717 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/AllTypes.cs +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/AllTypes.cs @@ -1,5 +1,4 @@ using Amazon.JSII.Runtime.Deputy; -using Amazon.JSII.Tests.CalculatorNamespace.composition; using Amazon.JSII.Tests.CalculatorNamespace.LibNamespace; using Newtonsoft.Json.Linq; using System; @@ -109,7 +108,7 @@ public virtual string StringProperty set => SetInstanceProperty(value); } - [JsiiProperty("unionArrayProperty", "{\"collection\":{\"kind\":\"array\",\"elementtype\":{\"union\":{\"types\":[{\"primitive\":\"number\"},{\"fqn\":\"jsii-calc.composition.CompositeOperation\"}]}}}}")] + [JsiiProperty("unionArrayProperty", "{\"collection\":{\"kind\":\"array\",\"elementtype\":{\"union\":{\"types\":[{\"primitive\":\"number\"},{\"fqn\":\"@scope/jsii-calc-lib.Value\"}]}}}}")] public virtual object[] UnionArrayProperty { get => GetInstanceProperty(); @@ -158,6 +157,18 @@ public virtual StringEnum OptionalEnumValue set => SetInstanceProperty(value); } + [JsiiMethod("anyIn", null, "[{\"name\":\"inp\",\"type\":{\"primitive\":\"any\"}}]")] + public virtual void AnyIn(object inp) + { + InvokeInstanceVoidMethod(new object[]{inp}); + } + + [JsiiMethod("anyOut", "{\"primitive\":\"any\"}", "[]")] + public virtual object AnyOut() + { + return InvokeInstanceMethod(new object[]{}); + } + [JsiiMethod("enumMethod", "{\"fqn\":\"jsii-calc.StringEnum\"}", "[{\"name\":\"value\",\"type\":{\"fqn\":\"jsii-calc.StringEnum\"}}]")] public virtual StringEnum EnumMethod(StringEnum value) { diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/Constructors.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/Constructors.cs index 6b93bdf0f2..b747def337 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/Constructors.cs +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/Constructors.cs @@ -17,6 +17,24 @@ protected Constructors(DeputyProps props): base(props) { } + [JsiiMethod("hiddenInterface", "{\"fqn\":\"jsii-calc.IPublicInterface\"}", "[]")] + public static IIPublicInterface HiddenInterface() + { + return InvokeStaticMethod(typeof(Constructors), new object[]{}); + } + + [JsiiMethod("hiddenInterfaces", "{\"collection\":{\"kind\":\"array\",\"elementtype\":{\"fqn\":\"jsii-calc.IPublicInterface\"}}}", "[]")] + public static IIPublicInterface[] HiddenInterfaces() + { + return InvokeStaticMethod(typeof(Constructors), new object[]{}); + } + + [JsiiMethod("hiddenSubInterfaces", "{\"collection\":{\"kind\":\"array\",\"elementtype\":{\"fqn\":\"jsii-calc.IPublicInterface\"}}}", "[]")] + public static IIPublicInterface[] HiddenSubInterfaces() + { + return InvokeStaticMethod(typeof(Constructors), new object[]{}); + } + [JsiiMethod("makeClass", "{\"fqn\":\"jsii-calc.PublicClass\"}", "[]")] public static PublicClass MakeClass() { @@ -28,5 +46,17 @@ public static IIPublicInterface MakeInterface() { return InvokeStaticMethod(typeof(Constructors), new object[]{}); } + + [JsiiMethod("makeInterface2", "{\"fqn\":\"jsii-calc.IPublicInterface2\"}", "[]")] + public static IIPublicInterface2 MakeInterface2() + { + return InvokeStaticMethod(typeof(Constructors), new object[]{}); + } + + [JsiiMethod("makeInterfaces", "{\"collection\":{\"kind\":\"array\",\"elementtype\":{\"fqn\":\"jsii-calc.IPublicInterface\"}}}", "[]")] + public static IIPublicInterface[] MakeInterfaces() + { + return InvokeStaticMethod(typeof(Constructors), new object[]{}); + } } } \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface.cs index 0c7b567757..32345c04cb 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface.cs +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface.cs @@ -5,7 +5,7 @@ namespace Amazon.JSII.Tests.CalculatorNamespace [JsiiInterface(typeof(IIPublicInterface), "jsii-calc.IPublicInterface")] public interface IIPublicInterface { - [JsiiMethod("bye", null, "[]")] - void Bye(); + [JsiiMethod("bye", "{\"primitive\":\"string\"}", "[]")] + string Bye(); } } \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface2.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface2.cs new file mode 100644 index 0000000000..ba10fc43b8 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface2.cs @@ -0,0 +1,11 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + [JsiiInterface(typeof(IIPublicInterface2), "jsii-calc.IPublicInterface2")] + public interface IIPublicInterface2 + { + [JsiiMethod("ciao", "{\"primitive\":\"string\"}", "[]")] + string Ciao(); + } +} \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterface2Proxy.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterface2Proxy.cs new file mode 100644 index 0000000000..50a68d9a3c --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterface2Proxy.cs @@ -0,0 +1,18 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + [JsiiTypeProxy(typeof(IIPublicInterface2), "jsii-calc.IPublicInterface2")] + internal sealed class IPublicInterface2Proxy : DeputyBase, IIPublicInterface2 + { + private IPublicInterface2Proxy(ByRefValue reference): base(reference) + { + } + + [JsiiMethod("ciao", "{\"primitive\":\"string\"}", "[]")] + public string Ciao() + { + return InvokeInstanceMethod(new object[]{}); + } + } +} \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterfaceProxy.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterfaceProxy.cs index c06f7c1929..90bdf4b6d7 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterfaceProxy.cs +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterfaceProxy.cs @@ -9,10 +9,10 @@ private IPublicInterfaceProxy(ByRefValue reference): base(reference) { } - [JsiiMethod("bye", null, "[]")] - public void Bye() + [JsiiMethod("bye", "{\"primitive\":\"string\"}", "[]")] + public string Bye() { - InvokeInstanceVoidMethod(new object[]{}); + return InvokeInstanceMethod(new object[]{}); } } } \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/InbetweenClass.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/InbetweenClass.cs index 6a678944a1..dfd8f93294 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/InbetweenClass.cs +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/InbetweenClass.cs @@ -3,7 +3,7 @@ namespace Amazon.JSII.Tests.CalculatorNamespace { [JsiiClass(typeof(InbetweenClass), "jsii-calc.InbetweenClass", "[]")] - public class InbetweenClass : PublicClass + public class InbetweenClass : PublicClass, IIPublicInterface2 { public InbetweenClass(): base(new DeputyProps(new object[]{})) { @@ -16,5 +16,11 @@ protected InbetweenClass(ByRefValue reference): base(reference) protected InbetweenClass(DeputyProps props): base(props) { } + + [JsiiMethod("ciao", "{\"primitive\":\"string\"}", "[]")] + public virtual string Ciao() + { + return InvokeInstanceMethod(new object[]{}); + } } } \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/SingleInstanceTwoTypes.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/SingleInstanceTwoTypes.cs new file mode 100644 index 0000000000..2c986ec994 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/SingleInstanceTwoTypes.cs @@ -0,0 +1,39 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + /// + /// Test that a single instance can be returned under two different FQNs + /// + /// JSII clients can instantiate 2 different strongly-typed wrappers for the same + /// object. Unfortunately, this will break object equality, but if we didn't do + /// this it would break runtime type checks in the JVM or CLR. + /// + [JsiiClass(typeof(SingleInstanceTwoTypes), "jsii-calc.SingleInstanceTwoTypes", "[]")] + public class SingleInstanceTwoTypes : DeputyBase + { + public SingleInstanceTwoTypes(): base(new DeputyProps(new object[]{})) + { + } + + protected SingleInstanceTwoTypes(ByRefValue reference): base(reference) + { + } + + protected SingleInstanceTwoTypes(DeputyProps props): base(props) + { + } + + [JsiiMethod("interface1", "{\"fqn\":\"jsii-calc.InbetweenClass\"}", "[]")] + public virtual InbetweenClass Interface1() + { + return InvokeInstanceMethod(new object[]{}); + } + + [JsiiMethod("interface2", "{\"fqn\":\"jsii-calc.IPublicInterface\"}", "[]")] + public virtual IIPublicInterface Interface2() + { + return InvokeInstanceMethod(new object[]{}); + } + } +} \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java index ac0be89bb5..770fbe051a 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java @@ -64,6 +64,7 @@ protected Class resolveClass(final String fqn) throws ClassNotFoundException case "jsii-calc.INonInternalInterface": return software.amazon.jsii.tests.calculator.INonInternalInterface.class; case "jsii-calc.IPrivatelyImplemented": return software.amazon.jsii.tests.calculator.IPrivatelyImplemented.class; case "jsii-calc.IPublicInterface": return software.amazon.jsii.tests.calculator.IPublicInterface.class; + case "jsii-calc.IPublicInterface2": return software.amazon.jsii.tests.calculator.IPublicInterface2.class; case "jsii-calc.IRandomNumberGenerator": return software.amazon.jsii.tests.calculator.IRandomNumberGenerator.class; case "jsii-calc.IReturnsNumber": return software.amazon.jsii.tests.calculator.IReturnsNumber.class; case "jsii-calc.ImplementInternalInterface": return software.amazon.jsii.tests.calculator.ImplementInternalInterface.class; @@ -98,6 +99,7 @@ protected Class resolveClass(final String fqn) throws ClassNotFoundException case "jsii-calc.ReferenceEnumFromScopedPackage": return software.amazon.jsii.tests.calculator.ReferenceEnumFromScopedPackage.class; case "jsii-calc.ReturnsPrivateImplementationOfInterface": return software.amazon.jsii.tests.calculator.ReturnsPrivateImplementationOfInterface.class; case "jsii-calc.RuntimeTypeChecking": return software.amazon.jsii.tests.calculator.RuntimeTypeChecking.class; + case "jsii-calc.SingleInstanceTwoTypes": return software.amazon.jsii.tests.calculator.SingleInstanceTwoTypes.class; case "jsii-calc.Statics": return software.amazon.jsii.tests.calculator.Statics.class; case "jsii-calc.StringEnum": return software.amazon.jsii.tests.calculator.StringEnum.class; case "jsii-calc.StripInternal": return software.amazon.jsii.tests.calculator.StripInternal.class; diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AllTypes.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AllTypes.java index 2b1d0fec93..26ac0bda4c 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AllTypes.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AllTypes.java @@ -15,6 +15,14 @@ public AllTypes() { software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); } + public void anyIn(final java.lang.Object inp) { + this.jsiiCall("anyIn", Void.class, java.util.stream.Stream.of(java.util.Objects.requireNonNull(inp, "inp is required")).toArray()); + } + + public java.lang.Object anyOut() { + return this.jsiiCall("anyOut", java.lang.Object.class); + } + public software.amazon.jsii.tests.calculator.StringEnum enumMethod(final software.amazon.jsii.tests.calculator.StringEnum value) { return this.jsiiCall("enumMethod", software.amazon.jsii.tests.calculator.StringEnum.class, java.util.stream.Stream.of(java.util.Objects.requireNonNull(value, "value is required")).toArray()); } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/Constructors.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/Constructors.java index 0e16462262..51423d4d30 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/Constructors.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/Constructors.java @@ -11,6 +11,18 @@ public Constructors() { software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); } + public static software.amazon.jsii.tests.calculator.IPublicInterface hiddenInterface() { + return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "hiddenInterface", software.amazon.jsii.tests.calculator.IPublicInterface.class); + } + + public static java.util.List hiddenInterfaces() { + return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "hiddenInterfaces", java.util.List.class); + } + + public static java.util.List hiddenSubInterfaces() { + return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "hiddenSubInterfaces", java.util.List.class); + } + public static software.amazon.jsii.tests.calculator.PublicClass makeClass() { return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "makeClass", software.amazon.jsii.tests.calculator.PublicClass.class); } @@ -18,4 +30,12 @@ public static software.amazon.jsii.tests.calculator.PublicClass makeClass() { public static software.amazon.jsii.tests.calculator.IPublicInterface makeInterface() { return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "makeInterface", software.amazon.jsii.tests.calculator.IPublicInterface.class); } + + public static software.amazon.jsii.tests.calculator.IPublicInterface2 makeInterface2() { + return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "makeInterface2", software.amazon.jsii.tests.calculator.IPublicInterface2.class); + } + + public static java.util.List makeInterfaces() { + return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "makeInterfaces", java.util.List.class); + } } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface.java index 1f18b22395..236d299497 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface.java @@ -2,7 +2,7 @@ @javax.annotation.Generated(value = "jsii-pacmak") public interface IPublicInterface extends software.amazon.jsii.JsiiSerializable { - void bye(); + java.lang.String bye(); /** * A proxy class which represents a concrete javascript instance of this type. @@ -13,8 +13,8 @@ final static class Jsii$Proxy extends software.amazon.jsii.JsiiObject implements } @Override - public void bye() { - this.jsiiCall("bye", Void.class); + public java.lang.String bye() { + return this.jsiiCall("bye", java.lang.String.class); } } } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface2.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface2.java new file mode 100644 index 0000000000..80092269e8 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface2.java @@ -0,0 +1,20 @@ +package software.amazon.jsii.tests.calculator; + +@javax.annotation.Generated(value = "jsii-pacmak") +public interface IPublicInterface2 extends software.amazon.jsii.JsiiSerializable { + java.lang.String ciao(); + + /** + * 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.IPublicInterface2 { + protected Jsii$Proxy(final software.amazon.jsii.JsiiObject.InitializationMode mode) { + super(mode); + } + + @Override + public java.lang.String ciao() { + return this.jsiiCall("ciao", java.lang.String.class); + } + } +} diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InbetweenClass.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InbetweenClass.java index c2ca0d34d8..b1371c8f9d 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InbetweenClass.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InbetweenClass.java @@ -2,7 +2,7 @@ @javax.annotation.Generated(value = "jsii-pacmak") @software.amazon.jsii.Jsii(module = software.amazon.jsii.tests.calculator.$Module.class, fqn = "jsii-calc.InbetweenClass") -public class InbetweenClass extends software.amazon.jsii.tests.calculator.PublicClass { +public class InbetweenClass extends software.amazon.jsii.tests.calculator.PublicClass implements software.amazon.jsii.tests.calculator.IPublicInterface2 { protected InbetweenClass(final software.amazon.jsii.JsiiObject.InitializationMode mode) { super(mode); } @@ -10,4 +10,9 @@ public InbetweenClass() { super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); } + + @Override + public java.lang.String ciao() { + return this.jsiiCall("ciao", java.lang.String.class); + } } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/SingleInstanceTwoTypes.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/SingleInstanceTwoTypes.java new file mode 100644 index 0000000000..fcd143f136 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/SingleInstanceTwoTypes.java @@ -0,0 +1,28 @@ +package software.amazon.jsii.tests.calculator; + +/** + * Test that a single instance can be returned under two different FQNs + * + * JSII clients can instantiate 2 different strongly-typed wrappers for the same + * object. Unfortunately, this will break object equality, but if we didn't do + * this it would break runtime type checks in the JVM or CLR. + */ +@javax.annotation.Generated(value = "jsii-pacmak") +@software.amazon.jsii.Jsii(module = software.amazon.jsii.tests.calculator.$Module.class, fqn = "jsii-calc.SingleInstanceTwoTypes") +public class SingleInstanceTwoTypes extends software.amazon.jsii.JsiiObject { + protected SingleInstanceTwoTypes(final software.amazon.jsii.JsiiObject.InitializationMode mode) { + super(mode); + } + public SingleInstanceTwoTypes() { + super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); + } + + public software.amazon.jsii.tests.calculator.InbetweenClass interface1() { + return this.jsiiCall("interface1", software.amazon.jsii.tests.calculator.InbetweenClass.class); + } + + public software.amazon.jsii.tests.calculator.IPublicInterface interface2() { + return this.jsiiCall("interface2", software.amazon.jsii.tests.calculator.IPublicInterface.class); + } +} diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst b/packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst index 6a1e23ed8c..a88becce56 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst +++ b/packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst @@ -394,6 +394,17 @@ AllTypes + .. py:method:: anyIn(inp) + + :param inp: + :type inp: any + + + .. py:method:: anyOut() -> any + + :rtype: any + + .. py:method:: enumMethod(value) -> jsii-calc.StringEnum :param value: @@ -463,7 +474,7 @@ AllTypes .. py:attribute:: unionArrayProperty - :type: (number or :py:class:`~jsii-calc.composition.CompositeOperation`\ )[] + :type: (number or :py:class:`@scope/jsii-calc-lib.Value`\ )[] .. py:attribute:: unionMapProperty @@ -1275,6 +1286,21 @@ Constructors + .. py:staticmethod:: hiddenInterface() -> jsii-calc.IPublicInterface + + :rtype: :py:class:`~jsii-calc.IPublicInterface`\ + + + .. py:staticmethod:: hiddenInterfaces() -> jsii-calc.IPublicInterface[] + + :rtype: :py:class:`~jsii-calc.IPublicInterface`\ [] + + + .. py:staticmethod:: hiddenSubInterfaces() -> jsii-calc.IPublicInterface[] + + :rtype: :py:class:`~jsii-calc.IPublicInterface`\ [] + + .. py:staticmethod:: makeClass() -> jsii-calc.PublicClass :rtype: :py:class:`~jsii-calc.PublicClass`\ @@ -1285,6 +1311,16 @@ Constructors :rtype: :py:class:`~jsii-calc.IPublicInterface`\ + .. py:staticmethod:: makeInterface2() -> jsii-calc.IPublicInterface2 + + :rtype: :py:class:`~jsii-calc.IPublicInterface2`\ + + + .. py:staticmethod:: makeInterfaces() -> jsii-calc.IPublicInterface[] + + :rtype: :py:class:`~jsii-calc.IPublicInterface`\ [] + + ConsumersOfThisCrazyTypeSystem ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -2588,8 +2624,44 @@ IPublicInterface (interface) - .. py:method:: bye() + .. py:method:: bye() -> string + + :rtype: string + :abstract: Yes + + +IPublicInterface2 (interface) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. py:class:: IPublicInterface2 + + **Language-specific names:** + + .. tabs:: + + .. code-tab:: c# + + using Amazon.JSII.Tests.CalculatorNamespace; + + .. code-tab:: java + + import software.amazon.jsii.tests.calculator.IPublicInterface2; + + .. code-tab:: javascript + + // IPublicInterface2 is an interface + + .. code-tab:: typescript + + import { IPublicInterface2 } from 'jsii-calc'; + + + + + + .. py:method:: ciao() -> string + :rtype: string :abstract: Yes @@ -2890,6 +2962,14 @@ InbetweenClass :extends: :py:class:`~jsii-calc.PublicClass`\ + :implements: :py:class:`~jsii-calc.IPublicInterface2`\ + + .. py:method:: ciao() -> string + + *Implements* :py:meth:`jsii-calc.IPublicInterface2.ciao` + + :rtype: string + .. py:method:: hello() @@ -4672,6 +4752,56 @@ RuntimeTypeChecking :type arg3: date *(optional)* +SingleInstanceTwoTypes +^^^^^^^^^^^^^^^^^^^^^^ + +.. py:class:: SingleInstanceTwoTypes() + + **Language-specific names:** + + .. tabs:: + + .. code-tab:: c# + + using Amazon.JSII.Tests.CalculatorNamespace; + + .. code-tab:: java + + import software.amazon.jsii.tests.calculator.SingleInstanceTwoTypes; + + .. code-tab:: javascript + + const { SingleInstanceTwoTypes } = require('jsii-calc'); + + .. code-tab:: typescript + + import { SingleInstanceTwoTypes } from 'jsii-calc'; + + + + Test that a single instance can be returned under two different FQNs + + + + JSII clients can instantiate 2 different strongly-typed wrappers for the same + + object. Unfortunately, this will break object equality, but if we didn't do + + this it would break runtime type checks in the JVM or CLR. + + + + + .. py:method:: interface1() -> jsii-calc.InbetweenClass + + :rtype: :py:class:`~jsii-calc.InbetweenClass`\ + + + .. py:method:: interface2() -> jsii-calc.IPublicInterface + + :rtype: :py:class:`~jsii-calc.IPublicInterface`\ + + Statics ^^^^^^^ diff --git a/packages/jsii-python-runtime/tests/test_compliance.py b/packages/jsii-python-runtime/tests/test_compliance.py index 63bcbfce21..e1ca7ab010 100644 --- a/packages/jsii-python-runtime/tests/test_compliance.py +++ b/packages/jsii-python-runtime/tests/test_compliance.py @@ -277,13 +277,13 @@ def test_unionTypes(): # map map_ = {} - map_["Foo"] = Multiply(Number(2), Number(99)) + map_["Foo"] = Number(99) types.union_map_property = map_ # TODO: No Assertion? # array - types.union_array_property = ["Hello", 123, Number(33)] - assert types.union_array_property[2].value == 33 + types.union_array_property = [123, Number(33)] + assert types.union_array_property[1].value == 33 def test_createObjectAndCtorOverloads(): diff --git a/packages/jsii-reflect/test/classes.expected.txt b/packages/jsii-reflect/test/classes.expected.txt index 1ce4f8ea77..e29d7390be 100644 --- a/packages/jsii-reflect/test/classes.expected.txt +++ b/packages/jsii-reflect/test/classes.expected.txt @@ -55,6 +55,7 @@ PythonReservedWords ReferenceEnumFromScopedPackage ReturnsPrivateImplementationOfInterface RuntimeTypeChecking +SingleInstanceTwoTypes Statics StripInternal Sum diff --git a/packages/jsii-reflect/test/jsii-tree.test.all.expected.txt b/packages/jsii-reflect/test/jsii-tree.test.all.expected.txt index 31df64bd14..fc41e4bedc 100644 --- a/packages/jsii-reflect/test/jsii-tree.test.all.expected.txt +++ b/packages/jsii-reflect/test/jsii-tree.test.all.expected.txt @@ -59,6 +59,13 @@ assemblies │ │ └─┬ members │ │ ├─┬ () method │ │ │ └── returns: void + │ │ ├─┬ anyIn(inp) method + │ │ │ ├─┬ parameters + │ │ │ │ └─┬ inp + │ │ │ │ └── type: primitive:any + │ │ │ └── returns: void + │ │ ├─┬ anyOut() method + │ │ │ └── returns: primitive:any │ │ ├─┬ enumMethod(value) method │ │ │ ├─┬ parameters │ │ │ │ └─┬ value @@ -90,7 +97,7 @@ assemblies │ │ ├─┬ stringProperty property │ │ │ └── type: primitive:string │ │ ├─┬ unionArrayProperty property - │ │ │ └── type: Array + │ │ │ └── type: Array │ │ ├─┬ unionMapProperty property │ │ │ └── type: Map primitive:string | primitive:number | class:@scope/jsii-calc-lib.Number> │ │ ├─┬ unionProperty property @@ -275,12 +282,27 @@ assemblies │ │ └─┬ members │ │ ├─┬ () method │ │ │ └── returns: void + │ │ ├─┬ hiddenInterface() method + │ │ │ ├── static + │ │ │ └── returns: interface:jsii-calc.IPublicInterface + │ │ ├─┬ hiddenInterfaces() method + │ │ │ ├── static + │ │ │ └── returns: Array + │ │ ├─┬ hiddenSubInterfaces() method + │ │ │ ├── static + │ │ │ └── returns: Array │ │ ├─┬ makeClass() method │ │ │ ├── static │ │ │ └── returns: class:jsii-calc.PublicClass - │ │ └─┬ makeInterface() method + │ │ ├─┬ makeInterface() method + │ │ │ ├── static + │ │ │ └── returns: interface:jsii-calc.IPublicInterface + │ │ ├─┬ makeInterface2() method + │ │ │ ├── static + │ │ │ └── returns: interface:jsii-calc.IPublicInterface2 + │ │ └─┬ makeInterfaces() method │ │ ├── static - │ │ └── returns: interface:jsii-calc.IPublicInterface + │ │ └── returns: Array │ ├─┬ class ConsumersOfThisCrazyTypeSystem │ │ └─┬ members │ │ ├─┬ () method @@ -459,9 +481,12 @@ assemblies │ │ └── type: primitive:string │ ├─┬ class InbetweenClass │ │ ├── base: PublicClass + │ │ ├── interfaces: IPublicInterface2 │ │ └─┬ members - │ │ └─┬ () method - │ │ └── returns: void + │ │ ├─┬ () method + │ │ │ └── returns: void + │ │ └─┬ ciao() method + │ │ └── returns: primitive:string │ ├─┬ class Foo │ │ └─┬ members │ │ ├─┬ () method @@ -894,6 +919,14 @@ assemblies │ │ │ └─┬ arg3 │ │ │ └── type: primitive:date (optional) │ │ └── returns: void + │ ├─┬ class SingleInstanceTwoTypes + │ │ └─┬ members + │ │ ├─┬ () method + │ │ │ └── returns: void + │ │ ├─┬ interface1() method + │ │ │ └── returns: class:jsii-calc.InbetweenClass + │ │ └─┬ interface2() method + │ │ └── returns: interface:jsii-calc.IPublicInterface │ ├─┬ class Statics │ │ └─┬ members │ │ ├─┬ (value) method @@ -1271,7 +1304,12 @@ assemblies │ │ └─┬ members │ │ └─┬ bye() method │ │ ├── abstract - │ │ └── returns: void + │ │ └── returns: primitive:string + │ ├─┬ interface IPublicInterface2 + │ │ └─┬ members + │ │ └─┬ ciao() method + │ │ ├── abstract + │ │ └── returns: primitive:string │ ├─┬ interface IRandomNumberGenerator │ │ └─┬ members │ │ └─┬ next() method diff --git a/packages/jsii-reflect/test/jsii-tree.test.inheritance.expected.txt b/packages/jsii-reflect/test/jsii-tree.test.inheritance.expected.txt index 1590147053..f0bda4dddb 100644 --- a/packages/jsii-reflect/test/jsii-tree.test.inheritance.expected.txt +++ b/packages/jsii-reflect/test/jsii-tree.test.inheritance.expected.txt @@ -46,7 +46,8 @@ assemblies │ │ └── base: ImplementsInterfaceWithInternal │ ├── class ImplementsPrivateInterface │ ├─┬ class InbetweenClass - │ │ └── base: PublicClass + │ │ ├── base: PublicClass + │ │ └── interfaces: IPublicInterface2 │ ├── class Foo │ ├── class JSObjectLiteralForInterface │ ├── class JSObjectLiteralToNative @@ -73,6 +74,7 @@ assemblies │ ├── class ReferenceEnumFromScopedPackage │ ├── class ReturnsPrivateImplementationOfInterface │ ├── class RuntimeTypeChecking + │ ├── class SingleInstanceTwoTypes │ ├── class Statics │ ├── class StripInternal │ ├─┬ class Sum @@ -119,6 +121,7 @@ assemblies │ │ └── IAnotherPublicInterface │ ├── interface IPrivatelyImplemented │ ├── interface IPublicInterface + │ ├── interface IPublicInterface2 │ ├── interface IRandomNumberGenerator │ ├── interface IReturnsNumber │ ├─┬ interface ImplictBaseOfBase diff --git a/packages/jsii-reflect/test/jsii-tree.test.members.expected.txt b/packages/jsii-reflect/test/jsii-tree.test.members.expected.txt index e85417eb37..23a8d2c2de 100644 --- a/packages/jsii-reflect/test/jsii-tree.test.members.expected.txt +++ b/packages/jsii-reflect/test/jsii-tree.test.members.expected.txt @@ -25,6 +25,8 @@ assemblies │ ├─┬ class AllTypes │ │ └─┬ members │ │ ├── () method + │ │ ├── anyIn(inp) method + │ │ ├── anyOut() method │ │ ├── enumMethod(value) method │ │ ├── enumPropertyValue property │ │ ├── anyArrayProperty property @@ -112,8 +114,13 @@ assemblies │ ├─┬ class Constructors │ │ └─┬ members │ │ ├── () method + │ │ ├── hiddenInterface() method + │ │ ├── hiddenInterfaces() method + │ │ ├── hiddenSubInterfaces() method │ │ ├── makeClass() method - │ │ └── makeInterface() method + │ │ ├── makeInterface() method + │ │ ├── makeInterface2() method + │ │ └── makeInterfaces() method │ ├─┬ class ConsumersOfThisCrazyTypeSystem │ │ └─┬ members │ │ ├── () method @@ -189,7 +196,8 @@ assemblies │ │ └── private property │ ├─┬ class InbetweenClass │ │ └─┬ members - │ │ └── () method + │ │ ├── () method + │ │ └── ciao() method │ ├─┬ class Foo │ │ └─┬ members │ │ ├── () method @@ -384,6 +392,11 @@ assemblies │ │ ├── methodWithDefaultedArguments(arg1,arg2,arg3) method │ │ ├── methodWithOptionalAnyArgument(arg) method │ │ └── methodWithOptionalArguments(arg1,arg2,arg3) method + │ ├─┬ class SingleInstanceTwoTypes + │ │ └─┬ members + │ │ ├── () method + │ │ ├── interface1() method + │ │ └── interface2() method │ ├─┬ class Statics │ │ └─┬ members │ │ ├── (value) method @@ -531,6 +544,9 @@ assemblies │ ├─┬ interface IPublicInterface │ │ └─┬ members │ │ └── bye() method + │ ├─┬ interface IPublicInterface2 + │ │ └─┬ members + │ │ └── ciao() method │ ├─┬ interface IRandomNumberGenerator │ │ └─┬ members │ │ └── next() method diff --git a/packages/jsii-reflect/test/jsii-tree.test.types.expected.txt b/packages/jsii-reflect/test/jsii-tree.test.types.expected.txt index 7914e96da2..da05887ea3 100644 --- a/packages/jsii-reflect/test/jsii-tree.test.types.expected.txt +++ b/packages/jsii-reflect/test/jsii-tree.test.types.expected.txt @@ -54,6 +54,7 @@ assemblies │ ├── class ReferenceEnumFromScopedPackage │ ├── class ReturnsPrivateImplementationOfInterface │ ├── class RuntimeTypeChecking + │ ├── class SingleInstanceTwoTypes │ ├── class Statics │ ├── class StripInternal │ ├── class Sum @@ -84,6 +85,7 @@ assemblies │ ├── interface INonInternalInterface │ ├── interface IPrivatelyImplemented │ ├── interface IPublicInterface + │ ├── interface IPublicInterface2 │ ├── interface IRandomNumberGenerator │ ├── interface IReturnsNumber │ ├── interface ImplictBaseOfBase diff --git a/packages/jsii-ruby-runtime/project/test/jsii_runtime_test.rb b/packages/jsii-ruby-runtime/project/test/jsii_runtime_test.rb index 3a30f20c51..b452324ae8 100644 --- a/packages/jsii-ruby-runtime/project/test/jsii_runtime_test.rb +++ b/packages/jsii-ruby-runtime/project/test/jsii_runtime_test.rb @@ -48,7 +48,7 @@ def test_async_callbacks ) promise = @client.begin(objref: objref, method: 'callMe') - assert_equal({ 'promiseid' => 'jsii::promise::10002' }, promise) + assert_equal({ 'promiseid' => 'jsii::promise::20001' }, promise) callbacks = @client.callbacks['callbacks'] assert_equal(1, callbacks.length)