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 d27cfcad72..20fac29cd7 100644 --- a/packages/jsii-calc/test/assembly.jsii +++ b/packages/jsii-calc/test/assembly.jsii @@ -377,6 +377,23 @@ }, "kind": "class", "methods": [ + { + "name": "anyIn", + "parameters": [ + { + "name": "inp", + "type": { + "primitive": "any" + } + } + ] + }, + { + "name": "anyOut", + "returns": { + "primitive": "any" + } + }, { "name": "enumMethod", "parameters": [ @@ -498,7 +515,7 @@ "primitive": "number" }, { - "fqn": "jsii-calc.composition.CompositeOperation" + "fqn": "@scope/jsii-calc-lib.Value" } ] } @@ -1241,6 +1258,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": { @@ -1254,6 +1302,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" @@ -2113,11 +2180,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": { @@ -2264,7 +2349,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": { @@ -3585,6 +3686,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", @@ -4373,5 +4500,5 @@ } }, "version": "0.8.0", - "fingerprint": "kxASQYx+RdQQFUSD4FNIHmKMdV4L37gNRKJx0DAohMQ=" + "fingerprint": "WIFIhqgEwUDjCsDr7gliujhqcKZQSkDP+NstBfmdvZU=" } 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..e91c524404 --- /dev/null +++ b/packages/jsii-kernel/lib/serialization.ts @@ -0,0 +1,621 @@ + + // 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 { + 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; +} diff --git a/packages/jsii-kernel/test/test.kernel.ts b/packages/jsii-kernel/test/test.kernel.ts index f0258c1cda..9b34419021 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); }); @@ -294,7 +310,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/); @@ -307,8 +323,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) => { @@ -543,6 +559,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 }); @@ -591,7 +608,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); }); @@ -620,8 +637,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) => { @@ -735,6 +752,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' }); @@ -960,15 +979,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' ] }); @@ -996,6 +1041,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 } = { }; @@ -1085,3 +1179,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 d27cfcad72..20fac29cd7 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 @@ -377,6 +377,23 @@ }, "kind": "class", "methods": [ + { + "name": "anyIn", + "parameters": [ + { + "name": "inp", + "type": { + "primitive": "any" + } + } + ] + }, + { + "name": "anyOut", + "returns": { + "primitive": "any" + } + }, { "name": "enumMethod", "parameters": [ @@ -498,7 +515,7 @@ "primitive": "number" }, { - "fqn": "jsii-calc.composition.CompositeOperation" + "fqn": "@scope/jsii-calc-lib.Value" } ] } @@ -1241,6 +1258,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": { @@ -1254,6 +1302,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" @@ -2113,11 +2180,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": { @@ -2264,7 +2349,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": { @@ -3585,6 +3686,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", @@ -4373,5 +4500,5 @@ } }, "version": "0.8.0", - "fingerprint": "kxASQYx+RdQQFUSD4FNIHmKMdV4L37gNRKJx0DAohMQ=" + "fingerprint": "WIFIhqgEwUDjCsDr7gliujhqcKZQSkDP+NstBfmdvZU=" } 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/python/src/jsii_calc/__init__.py b/packages/jsii-pacmak/test/expected.jsii-calc/python/src/jsii_calc/__init__.py index 119f5b9e15..3db9cadad7 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/python/src/jsii_calc/__init__.py +++ b/packages/jsii-pacmak/test/expected.jsii-calc/python/src/jsii_calc/__init__.py @@ -56,6 +56,14 @@ class AllTypes(metaclass=jsii.JSIIMeta, jsii_type="jsii-calc.AllTypes"): def __init__(self) -> None: jsii.create(AllTypes, self, []) + @jsii.member(jsii_name="anyIn") + def any_in(self, inp: typing.Any) -> None: + return jsii.invoke(self, "anyIn", [inp]) + + @jsii.member(jsii_name="anyOut") + def any_out(self) -> typing.Any: + return jsii.invoke(self, "anyOut", []) + @jsii.member(jsii_name="enumMethod") def enum_method(self, value: "StringEnum") -> "StringEnum": return jsii.invoke(self, "enumMethod", [value]) @@ -166,11 +174,11 @@ def string_property(self, value: str): @property @jsii.member(jsii_name="unionArrayProperty") - def union_array_property(self) -> typing.List[typing.Union[jsii.Number, "composition.CompositeOperation"]]: + def union_array_property(self) -> typing.List[typing.Union[jsii.Number, scope.jsii_calc_lib.Value]]: return jsii.get(self, "unionArrayProperty") @union_array_property.setter - def union_array_property(self, value: typing.List[typing.Union[jsii.Number, "composition.CompositeOperation"]]): + def union_array_property(self, value: typing.List[typing.Union[jsii.Number, scope.jsii_calc_lib.Value]]): return jsii.set(self, "unionArrayProperty", value) @property @@ -361,6 +369,21 @@ class Constructors(metaclass=jsii.JSIIMeta, jsii_type="jsii-calc.Constructors"): def __init__(self) -> None: jsii.create(Constructors, self, []) + @jsii.member(jsii_name="hiddenInterface") + @classmethod + def hidden_interface(cls) -> "IPublicInterface": + return jsii.sinvoke(cls, "hiddenInterface", []) + + @jsii.member(jsii_name="hiddenInterfaces") + @classmethod + def hidden_interfaces(cls) -> typing.List["IPublicInterface"]: + return jsii.sinvoke(cls, "hiddenInterfaces", []) + + @jsii.member(jsii_name="hiddenSubInterfaces") + @classmethod + def hidden_sub_interfaces(cls) -> typing.List["IPublicInterface"]: + return jsii.sinvoke(cls, "hiddenSubInterfaces", []) + @jsii.member(jsii_name="makeClass") @classmethod def make_class(cls) -> "PublicClass": @@ -371,6 +394,16 @@ def make_class(cls) -> "PublicClass": def make_interface(cls) -> "IPublicInterface": return jsii.sinvoke(cls, "makeInterface", []) + @jsii.member(jsii_name="makeInterface2") + @classmethod + def make_interface2(cls) -> "IPublicInterface2": + return jsii.sinvoke(cls, "makeInterface2", []) + + @jsii.member(jsii_name="makeInterfaces") + @classmethod + def make_interfaces(cls) -> typing.List["IPublicInterface"]: + return jsii.sinvoke(cls, "makeInterfaces", []) + class ConsumersOfThisCrazyTypeSystem(metaclass=jsii.JSIIMeta, jsii_type="jsii-calc.ConsumersOfThisCrazyTypeSystem"): def __init__(self) -> None: @@ -966,17 +999,35 @@ def __jsii_proxy_class__(): return _IPublicInterfaceProxy @jsii.member(jsii_name="bye") - def bye(self) -> None: + def bye(self) -> str: ... class _IPublicInterfaceProxy(): __jsii_type__ = "jsii-calc.IPublicInterface" @jsii.member(jsii_name="bye") - def bye(self) -> None: + def bye(self) -> str: return jsii.invoke(self, "bye", []) +@jsii.interface(jsii_type="jsii-calc.IPublicInterface2") +class IPublicInterface2(jsii.compat.Protocol): + @staticmethod + def __jsii_proxy_class__(): + return _IPublicInterface2Proxy + + @jsii.member(jsii_name="ciao") + def ciao(self) -> str: + ... + + +class _IPublicInterface2Proxy(): + __jsii_type__ = "jsii-calc.IPublicInterface2" + @jsii.member(jsii_name="ciao") + def ciao(self) -> str: + return jsii.invoke(self, "ciao", []) + + @jsii.interface(jsii_type="jsii-calc.IRandomNumberGenerator") class IRandomNumberGenerator(jsii.compat.Protocol): @staticmethod @@ -1610,10 +1661,15 @@ def hello(self) -> None: return jsii.invoke(self, "hello", []) +@jsii.implements(IPublicInterface2) class InbetweenClass(PublicClass, metaclass=jsii.JSIIMeta, jsii_type="jsii-calc.InbetweenClass"): def __init__(self) -> None: jsii.create(InbetweenClass, self, []) + @jsii.member(jsii_name="ciao") + def ciao(self) -> str: + return jsii.invoke(self, "ciao", []) + class PythonReservedWords(metaclass=jsii.JSIIMeta, jsii_type="jsii-calc.PythonReservedWords"): def __init__(self) -> None: @@ -1797,6 +1853,19 @@ def method_with_optional_arguments(self, arg1: jsii.Number, arg2: str, arg3: typ return jsii.invoke(self, "methodWithOptionalArguments", [arg1, arg2, arg3]) +class SingleInstanceTwoTypes(metaclass=jsii.JSIIMeta, jsii_type="jsii-calc.SingleInstanceTwoTypes"): + def __init__(self) -> None: + jsii.create(SingleInstanceTwoTypes, self, []) + + @jsii.member(jsii_name="interface1") + def interface1(self) -> "InbetweenClass": + return jsii.invoke(self, "interface1", []) + + @jsii.member(jsii_name="interface2") + def interface2(self) -> "IPublicInterface": + return jsii.invoke(self, "interface2", []) + + class Statics(metaclass=jsii.JSIIMeta, jsii_type="jsii-calc.Statics"): def __init__(self, value: str) -> None: jsii.create(Statics, self, [value]) @@ -2282,6 +2351,6 @@ def parts(self, value: typing.List[scope.jsii_calc_lib.Value]): return jsii.set(self, "parts", value) -__all__ = ["AbstractClass", "AbstractClassBase", "AbstractClassReturner", "Add", "AllTypes", "AllTypesEnum", "AllowedMethodNames", "AsyncVirtualMethods", "AugmentableClass", "BinaryOperation", "Calculator", "CalculatorProps", "ClassThatImplementsTheInternalInterface", "ClassThatImplementsThePrivateInterface", "ClassWithMutableObjectLiteralProperty", "ClassWithPrivateConstructorAndAutomaticProperties", "Constructors", "ConsumersOfThisCrazyTypeSystem", "DefaultedConstructorArgument", "DerivedClassHasNoProperties", "DerivedStruct", "DoNotOverridePrivates", "DoNotRecognizeAnyAsOptional", "DontComplainAboutVariadicAfterOptional", "DoubleTrouble", "EraseUndefinedHashValues", "EraseUndefinedHashValuesOptions", "ExportedBaseClass", "ExtendsInternalInterface", "ExtendsPrivateInterface", "GiveMeStructs", "GreetingAugmenter", "IAnotherPublicInterface", "IFriendlier", "IFriendlyRandomGenerator", "IInterfaceThatShouldNotBeADataType", "IInterfaceWithInternal", "IInterfaceWithMethods", "IInterfaceWithOptionalMethodArguments", "IInterfaceWithProperties", "IInterfaceWithPropertiesExtension", "IMutableObjectLiteral", "INonInternalInterface", "IPrivatelyImplemented", "IPublicInterface", "IRandomNumberGenerator", "IReturnsNumber", "ImplementInternalInterface", "ImplementsInterfaceWithInternal", "ImplementsInterfaceWithInternalSubclass", "ImplementsPrivateInterface", "ImplictBaseOfBase", "InbetweenClass", "InterfaceImplementedByAbstractClass", "InterfaceInNamespaceIncludesClasses", "InterfaceInNamespaceOnlyInterface", "JSObjectLiteralForInterface", "JSObjectLiteralToNative", "JSObjectLiteralToNativeClass", "JavaReservedWords", "JsiiAgent", "LoadBalancedFargateServiceProps", "Multiply", "Negate", "NodeStandardLibrary", "NullShouldBeTreatedAsUndefined", "NullShouldBeTreatedAsUndefinedData", "NumberGenerator", "ObjectRefsInCollections", "OptionalConstructorArgument", "OverrideReturnsObject", "Polymorphism", "Power", "PublicClass", "PythonReservedWords", "ReferenceEnumFromScopedPackage", "ReturnsPrivateImplementationOfInterface", "RuntimeTypeChecking", "Statics", "StringEnum", "StripInternal", "Sum", "SyncVirtualMethods", "Thrower", "UnaryOperation", "UnionProperties", "UseBundledDependency", "UseCalcBase", "UsesInterfaceWithProperties", "VariadicMethod", "VirtualMethodPlayground", "__jsii_assembly__", "composition"] +__all__ = ["AbstractClass", "AbstractClassBase", "AbstractClassReturner", "Add", "AllTypes", "AllTypesEnum", "AllowedMethodNames", "AsyncVirtualMethods", "AugmentableClass", "BinaryOperation", "Calculator", "CalculatorProps", "ClassThatImplementsTheInternalInterface", "ClassThatImplementsThePrivateInterface", "ClassWithMutableObjectLiteralProperty", "ClassWithPrivateConstructorAndAutomaticProperties", "Constructors", "ConsumersOfThisCrazyTypeSystem", "DefaultedConstructorArgument", "DerivedClassHasNoProperties", "DerivedStruct", "DoNotOverridePrivates", "DoNotRecognizeAnyAsOptional", "DontComplainAboutVariadicAfterOptional", "DoubleTrouble", "EraseUndefinedHashValues", "EraseUndefinedHashValuesOptions", "ExportedBaseClass", "ExtendsInternalInterface", "ExtendsPrivateInterface", "GiveMeStructs", "GreetingAugmenter", "IAnotherPublicInterface", "IFriendlier", "IFriendlyRandomGenerator", "IInterfaceThatShouldNotBeADataType", "IInterfaceWithInternal", "IInterfaceWithMethods", "IInterfaceWithOptionalMethodArguments", "IInterfaceWithProperties", "IInterfaceWithPropertiesExtension", "IMutableObjectLiteral", "INonInternalInterface", "IPrivatelyImplemented", "IPublicInterface", "IPublicInterface2", "IRandomNumberGenerator", "IReturnsNumber", "ImplementInternalInterface", "ImplementsInterfaceWithInternal", "ImplementsInterfaceWithInternalSubclass", "ImplementsPrivateInterface", "ImplictBaseOfBase", "InbetweenClass", "InterfaceImplementedByAbstractClass", "InterfaceInNamespaceIncludesClasses", "InterfaceInNamespaceOnlyInterface", "JSObjectLiteralForInterface", "JSObjectLiteralToNative", "JSObjectLiteralToNativeClass", "JavaReservedWords", "JsiiAgent", "LoadBalancedFargateServiceProps", "Multiply", "Negate", "NodeStandardLibrary", "NullShouldBeTreatedAsUndefined", "NullShouldBeTreatedAsUndefinedData", "NumberGenerator", "ObjectRefsInCollections", "OptionalConstructorArgument", "OverrideReturnsObject", "Polymorphism", "Power", "PublicClass", "PythonReservedWords", "ReferenceEnumFromScopedPackage", "ReturnsPrivateImplementationOfInterface", "RuntimeTypeChecking", "SingleInstanceTwoTypes", "Statics", "StringEnum", "StripInternal", "Sum", "SyncVirtualMethods", "Thrower", "UnaryOperation", "UnionProperties", "UseBundledDependency", "UseCalcBase", "UsesInterfaceWithProperties", "VariadicMethod", "VirtualMethodPlayground", "__jsii_assembly__", "composition"] publication.publish() 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)