From 8fe5c419c5b7274335dbffe3055c9b49a72f7874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Wed, 20 Mar 2019 14:09:16 +0100 Subject: [PATCH 1/6] fix(runtime): Passing 'this' to a callback from constructor In Javascript, a constructor is allowed to pass references to `this` to method calls (effectively passing a partially initialized instance). When done, the kernel will assign an Object ID to `this` on the spot in order to pass the reference through the JSII boundary. However, the `create` API before this change would have attempted to re-allocate an Object ID to the object (which is illegal and causes a crash). In addition, callbacks from at least Java and .NET runtimes could not receive JSII object references, for they would not turn them into native objects before passing them to the implementation, resulting in a class conversion error. --- packages/jsii-calc/lib/compliance.ts | 17 +++++ packages/jsii-calc/test/assembly.jsii | 46 ++++++++++++- ...mazon.JSII.Runtime.IntegrationTests.csproj | 16 ++--- .../ComplianceTests.cs | 17 +++++ .../Amazon.JSII.Runtime/CallbackExtensions.cs | 21 +++++- .../Amazon.JSII.Runtime/Deputy/DeputyBase.cs | 2 +- .../Services/IReferenceMap.cs | 2 +- .../Services/ReferenceMap.cs | 4 +- .../amazon/jsii/testing/ComplianceTest.java | 17 +++++ .../java/software/amazon/jsii/JsiiEngine.java | 39 +++++++++-- packages/jsii-kernel/lib/kernel.ts | 21 +++++- packages/jsii-kernel/test/test.kernel.ts | 18 +++++ packages/jsii-pacmak/lib/generator.ts | 6 +- packages/jsii-pacmak/package-lock.json | 9 ++- .../jsii/tests/calculator/base/Base.java | 4 ++ .../jsii/tests/calculator/lib/Operation.java | 4 ++ .../jsii/tests/calculator/lib/Value.java | 4 ++ .../.jsii | 46 ++++++++++++- .../ConstructorPassesThisOut.cs | 20 ++++++ .../PartiallyInitializedThisConsumer.cs | 23 +++++++ .../PartiallyInitializedThisConsumerProxy.cs | 18 +++++ .../amazon/jsii/tests/calculator/$Module.java | 2 + .../jsii/tests/calculator/AbstractClass.java | 4 ++ .../tests/calculator/AbstractClassBase.java | 4 ++ .../tests/calculator/BinaryOperation.java | 9 +++ .../calculator/ConstructorPassesThisOut.java | 13 ++++ .../PartiallyInitializedThisConsumer.java | 29 ++++++++ .../jsii/tests/calculator/UnaryOperation.java | 4 ++ .../composition/CompositeOperation.java | 4 ++ .../expected.jsii-calc/sphinx/jsii-calc.rst | 67 +++++++++++++++++++ .../tests/test_compliance.py | 12 ++++ .../jsii-reflect/test/classes.expected.txt | 2 + .../test/jsii-tree.test.all.expected.txt | 17 +++++ .../jsii-tree.test.inheritance.expected.txt | 2 + .../test/jsii-tree.test.members.expected.txt | 7 ++ .../test/jsii-tree.test.types.expected.txt | 2 + 36 files changed, 504 insertions(+), 28 deletions(-) create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/ConstructorPassesThisOut.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/PartiallyInitializedThisConsumer.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/PartiallyInitializedThisConsumerProxy.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ConstructorPassesThisOut.java create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/PartiallyInitializedThisConsumer.java diff --git a/packages/jsii-calc/lib/compliance.ts b/packages/jsii-calc/lib/compliance.ts index edf6b8b3f4..508f8ac5e2 100644 --- a/packages/jsii-calc/lib/compliance.ts +++ b/packages/jsii-calc/lib/compliance.ts @@ -1476,3 +1476,20 @@ export class ConsumersOfThisCrazyTypeSystem { return { a: obj.a, b: obj.b, c: obj.c }; } } + + +// +// Ensure the JSII kernel can pass "this" out to JSII remotes from within the constructor (this is dirty, but possible) +/// +export abstract class PartiallyInitializedThisConsumer { + public abstract consumePartiallyInitializedThis(obj: ConstructorPassesThisOut): string; +} + +export class ConstructorPassesThisOut { + constructor(consumer: PartiallyInitializedThisConsumer) { + const result = consumer.consumePartiallyInitializedThis(this); + if (result !== 'OK') { + throw new Error(`Expected OK but received ${result}`); + } + } +} diff --git a/packages/jsii-calc/test/assembly.jsii b/packages/jsii-calc/test/assembly.jsii index 4abcda6426..ec29b7dd3d 100644 --- a/packages/jsii-calc/test/assembly.jsii +++ b/packages/jsii-calc/test/assembly.jsii @@ -1207,6 +1207,23 @@ } ] }, + "jsii-calc.ConstructorPassesThisOut": { + "assembly": "jsii-calc", + "fqn": "jsii-calc.ConstructorPassesThisOut", + "initializer": { + "initializer": true, + "parameters": [ + { + "name": "consumer", + "type": { + "fqn": "jsii-calc.PartiallyInitializedThisConsumer" + } + } + ] + }, + "kind": "class", + "name": "ConstructorPassesThisOut" + }, "jsii-calc.Constructors": { "assembly": "jsii-calc", "fqn": "jsii-calc.Constructors", @@ -3103,6 +3120,33 @@ ], "name": "OverrideReturnsObject" }, + "jsii-calc.PartiallyInitializedThisConsumer": { + "abstract": true, + "assembly": "jsii-calc", + "fqn": "jsii-calc.PartiallyInitializedThisConsumer", + "initializer": { + "initializer": true + }, + "kind": "class", + "methods": [ + { + "abstract": true, + "name": "consumePartiallyInitializedThis", + "parameters": [ + { + "name": "obj", + "type": { + "fqn": "jsii-calc.ConstructorPassesThisOut" + } + } + ], + "returns": { + "primitive": "string" + } + } + ], + "name": "PartiallyInitializedThisConsumer" + }, "jsii-calc.Polymorphism": { "assembly": "jsii-calc", "fqn": "jsii-calc.Polymorphism", @@ -4249,5 +4293,5 @@ } }, "version": "0.7.15", - "fingerprint": "y7h6OQBzbQrvU43LHTizXl12OUAEOpXhqD02OJfmhWQ=" + "fingerprint": "R1xDj9nkBHfChUdY5UBwN33gnpRmlVY2z00o3KvjHSQ=" } diff --git a/packages/jsii-dotnet-runtime-test/test/Amazon.JSII.Runtime.IntegrationTests/Amazon.JSII.Runtime.IntegrationTests.csproj b/packages/jsii-dotnet-runtime-test/test/Amazon.JSII.Runtime.IntegrationTests/Amazon.JSII.Runtime.IntegrationTests.csproj index bb130050b1..59dea45c8f 100644 --- a/packages/jsii-dotnet-runtime-test/test/Amazon.JSII.Runtime.IntegrationTests/Amazon.JSII.Runtime.IntegrationTests.csproj +++ b/packages/jsii-dotnet-runtime-test/test/Amazon.JSII.Runtime.IntegrationTests/Amazon.JSII.Runtime.IntegrationTests.csproj @@ -7,20 +7,20 @@ - + - - - - - - + + + + + + - + diff --git a/packages/jsii-dotnet-runtime-test/test/Amazon.JSII.Runtime.IntegrationTests/ComplianceTests.cs b/packages/jsii-dotnet-runtime-test/test/Amazon.JSII.Runtime.IntegrationTests/ComplianceTests.cs index 05f8f3230d..2166697399 100644 --- a/packages/jsii-dotnet-runtime-test/test/Amazon.JSII.Runtime.IntegrationTests/ComplianceTests.cs +++ b/packages/jsii-dotnet-runtime-test/test/Amazon.JSII.Runtime.IntegrationTests/ComplianceTests.cs @@ -884,6 +884,23 @@ public void ObjRefsAreLabelledUsingWithTheMostCorrectType() Assert.NotEqual(typeof(InbetweenClass), ifaceRef.GetType()); } + [Fact(DisplayName = Prefix + nameof(ObjectIdDoesNotGetReallocatedWhenTheConstructorPassesThisOut))] + public void ObjectIdDoesNotGetReallocatedWhenTheConstructorPassesThisOut() + { + var reflector = new PartiallyInitializedThisConsumerImpl(); + var obj = new ConstructorPassesThisOut(reflector); + + Assert.NotNull(obj); + } + + class PartiallyInitializedThisConsumerImpl : PartiallyInitializedThisConsumer + { + public override String ConsumePartiallyInitializedThis(ConstructorPassesThisOut obj) + { + return "OK"; + } + } + class NumberReturner : DeputyBase, IIReturnsNumber { public NumberReturner(double number) diff --git a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/CallbackExtensions.cs b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/CallbackExtensions.cs index 8fe9067aae..214aac795d 100644 --- a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/CallbackExtensions.cs +++ b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/CallbackExtensions.cs @@ -4,7 +4,9 @@ using Amazon.JSII.Runtime.Deputy; using Amazon.JSII.Runtime.Services; using Amazon.JSII.Runtime.Services.Converters; +using Newtonsoft.Json.Linq; using System; +using System.Linq; using System.Reflection; namespace Amazon.JSII.Runtime @@ -71,11 +73,11 @@ static CallbackResult InvokeMethod(InvokeRequest request, IReferenceMap referenc if (methodInfo == null) { - throw new InvalidOperationException($"Received callback for {deputy.GetType().Name}.{request.Method} getter, but this method does not exist"); + throw new InvalidOperationException($"Received callback for {deputy.GetType().Name}.{request.Method} method, but this method does not exist"); } JsiiMethodAttribute attribute = methodInfo.GetCustomAttribute(); - return new CallbackResult(attribute?.Returns, methodInfo.Invoke(deputy, request.Arguments)); + return new CallbackResult(attribute?.Returns, methodInfo.Invoke(deputy, request.Arguments.Select(arg => FromKernel(arg, referenceMap)).ToArray())); } static CallbackResult InvokeGetter(GetRequest request, IReferenceMap referenceMap) @@ -117,7 +119,20 @@ static void InvokeSetter(SetRequest request, IReferenceMap referenceMap) throw new InvalidOperationException($"Received callback for {deputy.GetType().Name}.{request.Property} setter, but this property does not have a setter"); } - methodInfo.Invoke(deputy, new object[] { request.Value }); + methodInfo.Invoke(deputy, new object[] { FromKernel(request.Value, referenceMap) }); + } + + private static object FromKernel(object obj, IReferenceMap referenceMap) + { + if (obj is JObject) + { + var prop = ((JObject)obj).Property("$jsii.byref"); + if (prop != null) + { + return referenceMap.GetOrCreateNativeReference(new ByRefValue(prop.Value.Value())); + } + } + return obj; } } diff --git a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Deputy/DeputyBase.cs b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Deputy/DeputyBase.cs index c315018125..bf04054421 100644 --- a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Deputy/DeputyBase.cs +++ b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Deputy/DeputyBase.cs @@ -54,7 +54,7 @@ protected DeputyBase(DeputyProps props = null) Reference = new ByRefValue(response["$jsii.byref"]); IReferenceMap referenceMap = serviceProvider.GetRequiredService(); - referenceMap.AddNativeReference(Reference, this); + referenceMap.AddNativeReference(Reference, this, true); Override[] GetOverrides() { diff --git a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/IReferenceMap.cs b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/IReferenceMap.cs index 3294f2de5f..48de5ea9db 100644 --- a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/IReferenceMap.cs +++ b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/IReferenceMap.cs @@ -5,7 +5,7 @@ namespace Amazon.JSII.Runtime.Services { public interface IReferenceMap { - void AddNativeReference(ByRefValue reference, DeputyBase nativeReference); + void AddNativeReference(ByRefValue reference, DeputyBase nativeReference, bool force = false); DeputyBase GetOrCreateNativeReference(ObjectReference reference); diff --git a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/ReferenceMap.cs b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/ReferenceMap.cs index 609ab784fb..4ffaef5cc6 100644 --- a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/ReferenceMap.cs +++ b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/ReferenceMap.cs @@ -16,9 +16,9 @@ public ReferenceMap(ITypeCache types) _types = types ?? throw new ArgumentNullException(nameof(types)); } - public void AddNativeReference(ByRefValue reference, DeputyBase nativeReference) + public void AddNativeReference(ByRefValue reference, DeputyBase nativeReference, bool force) { - if (_references.ContainsKey(reference.Value)) + if (_references.ContainsKey(reference.Value) && !force) { throw new ArgumentException( $"Cannot add reference for {reference.Value}: A reference with this name already exists", 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 c2dd9bb095..4a800ace18 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 @@ -31,6 +31,7 @@ import software.amazon.jsii.tests.calculator.NullShouldBeTreatedAsUndefined; import software.amazon.jsii.tests.calculator.NullShouldBeTreatedAsUndefinedData; import software.amazon.jsii.tests.calculator.NumberGenerator; +import software.amazon.jsii.tests.calculator.PartiallyInitializedThisConsumer; import software.amazon.jsii.tests.calculator.Polymorphism; import software.amazon.jsii.tests.calculator.Power; import software.amazon.jsii.tests.calculator.PublicClass; @@ -51,6 +52,7 @@ import software.amazon.jsii.tests.calculator.lib.Value; import software.amazon.jsii.tests.calculator.JavaReservedWords; import software.amazon.jsii.tests.calculator.ClassWithPrivateConstructorAndAutomaticProperties; +import software.amazon.jsii.tests.calculator.ConstructorPassesThisOut; import software.amazon.jsii.tests.calculator.Constructors; import org.junit.Test; @@ -982,6 +984,21 @@ public void objRefsAreLabelledUsingWithTheMostCorrectType() { assertTrue(ifaceRef instanceof IPublicInterface); } + @Test + public void objectIdDoesNotGetReallocatedWhenTheConstructorPassesThisOut() { + final PartiallyInitializedThisConsumer reflector = new PartiallyInitializedThisConsumerImpl(); + final ConstructorPassesThisOut object = new ConstructorPassesThisOut(reflector); + + assertTrue(object != null); + } + + static class PartiallyInitializedThisConsumerImpl extends PartiallyInitializedThisConsumer { + @Override + public String consumePartiallyInitializedThis(final ConstructorPassesThisOut obj) { + return "OK"; + } + } + static class MulTen extends Multiply { public MulTen(final int value) { super(new Number(value), new Number(10)); diff --git a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java index c500a17522..c723816635 100644 --- a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java +++ b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java @@ -6,6 +6,7 @@ import software.amazon.jsii.api.JsiiOverride; import software.amazon.jsii.api.SetRequest; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.base.Throwables; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -253,7 +254,7 @@ private JsiiObject createNative(final String fqn) { + e.getMessage(), e); } } catch (ClassNotFoundException e) { - System.err.println("WARNING: Cannot find the class: " + fqn + ". Defaulting to JsiiObject"); + this.log("WARNING: Cannot find the class: %s. Defaulting to JsiiObject", fqn); return new JsiiObject(JsiiObject.InitializationMode.Jsii); } } @@ -350,7 +351,7 @@ private JsonNode invokeCallbackSet(final SetRequest req) { throw new JsiiException("Unable to find property setter " + setterMethodName); } - return OM.valueToTree(invokeMethod(obj, setter, req.getValue())); + return OM.valueToTree(invokeMethod(obj, setter, this.fromKernel(req.getValue()))); } /** @@ -362,7 +363,28 @@ private JsonNode invokeCallbackSet(final SetRequest req) { private JsonNode invokeCallbackMethod(final InvokeRequest req, final String cookie) { Object obj = this.getObject(req.getObjref()); Method method = this.findCallbackMethod(obj.getClass(), cookie); - return OM.valueToTree(invokeMethod(obj, method, req.getArgs().toArray())); + return OM.valueToTree(invokeMethod(obj, + method, + req.getArgs() + .stream() + .map(this::fromKernel) + .toArray())); + } + + private Object fromKernel(final Object object) { + if (object == null) { + return null; + } + if (object instanceof Map) { + final Map map = (Map) object; + if (map.containsKey(JsiiObjectRef.TOKEN_REF)) { + final JsiiObjectRef objRef = JsiiObjectRef.fromObjId(map.get(JsiiObjectRef.TOKEN_REF).toString()); + final Object result = this.nativeFromObjRef(objRef); + this.log("Resolved to object of type %s", result.getClass().getCanonicalName()); + return result; + } + } + return object; } /** @@ -379,7 +401,12 @@ private Object invokeMethod(final Object obj, final Method method, final Object. method.setAccessible(true); try { - return method.invoke(obj, args); + try { + return method.invoke(obj, args); + } catch (Exception e) { + this.log("Error while invoking %s with %s: %s", method, Arrays.toString(args), Throwables.getStackTraceAsString(e)); + throw e; + } } catch (InvocationTargetException e) { throw new JsiiException(e.getTargetException()); } catch (IllegalAccessException e) { @@ -508,6 +535,10 @@ private static Collection discoverOverrides(final Class classTo return overrides.values(); } + private void log(final String format, final Object... args) { + System.err.println(String.format(format, args)); + } + /** * Attempts to find the @Jsii annotation from a type. * @param type The type. diff --git a/packages/jsii-kernel/lib/kernel.ts b/packages/jsii-kernel/lib/kernel.ts index 0ebf8bcabf..b0f96014ea 100644 --- a/packages/jsii-kernel/lib/kernel.ts +++ b/packages/jsii-kernel/lib/kernel.ts @@ -476,7 +476,10 @@ export class Kernel { const ctor = this._findCtor(fqn, requestArgs); const obj = this._wrapSandboxCode(() => new ctor(...this._toSandboxValues(requestArgs))); - const objref = this._createObjref(obj, fqn); + // Obj might have been identified already if it passes "this" to a callback! + const objref: api.ObjRef = _isIdentifiedObject(obj) + ? { [api.TOKEN_REF]: obj[OBJID_PROP] } + : this._createObjref(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". @@ -745,6 +748,10 @@ export class Kernel { private _createObjref(obj: any, fqn: string): api.ObjRef { const objid = this._mkobjid(fqn); + this._debug('assigning', OBJID_PROP, objid, 'to an instance of type', fqn); + if (obj[OBJID_PROP]) { + this._debug('... but instance has ', OBJID_PROP, obj[OBJID_PROP]); + } Object.defineProperty(obj, OBJID_PROP, { value: objid, configurable: false, @@ -786,7 +793,7 @@ export class Kernel { throw new Error(`Module '${moduleName}' not found`); } - const types = assembly.metadata.types || {}; + const types = assembly.metadata.types || {}; const fqnInfo = types[fqn]; if (!fqnInfo) { throw new Error(`Type '${fqn}' not found`); @@ -1324,3 +1331,13 @@ interface ProxyReference { objRef: api.ObjRef; handler: KernelProxyHandler; } + +interface IdentifiedObject { + [OBJID_PROP]: string; +} +function _isIdentifiedObject(obj: T): obj is T & IdentifiedObject { + if (obj == null || typeof obj !== 'object') { + return false; + } + return OBJID_PROP in obj; +} diff --git a/packages/jsii-kernel/test/test.kernel.ts b/packages/jsii-kernel/test/test.kernel.ts index e602fa1d6b..d9e180fc0d 100644 --- a/packages/jsii-kernel/test/test.kernel.ts +++ b/packages/jsii-kernel/test/test.kernel.ts @@ -973,6 +973,24 @@ defineTest('ObjRefs are labeled with the "most correct" type', async (test, sand `${ifaceRef[api.TOKEN_REF]} starts with jsii-calc.IPublicInterface`); }); +defineTest('Object ID does not get re-allocated when the constructor passes "this" out', async (test, sandbox) => { + sandbox.callbackHandler = makeSyncCallbackHandler((callback) => { + test.equal(callback.invoke && callback.invoke.method, 'consumePartiallyInitializedThis'); + test.deepEqual(callback.invoke && callback.invoke.args && callback.invoke.args, [{ + [api.TOKEN_REF]: 'jsii-calc.ConstructorPassesThisOut@10002' + }]); + return 'OK'; + }); + const reflector = sandbox.create({ + fqn: 'jsii-calc.PartiallyInitializedThisConsumer', + overrides: [{ method: 'consumePartiallyInitializedThis' }] + }); + test.equal(reflector[api.TOKEN_REF], 'jsii-calc.PartiallyInitializedThisConsumer@10000'); + + const classRef = sandbox.create({ fqn: 'jsii-calc.ConstructorPassesThisOut', args: [reflector] }); + test.equal(classRef[api.TOKEN_REF], 'jsii-calc.ConstructorPassesThisOut@10002'); +}); + // ================================================================================================= const testNames: { [name: string]: boolean } = { }; diff --git a/packages/jsii-pacmak/lib/generator.ts b/packages/jsii-pacmak/lib/generator.ts index f038dd432e..c5a8ef8df9 100644 --- a/packages/jsii-pacmak/lib/generator.ts +++ b/packages/jsii-pacmak/lib/generator.ts @@ -252,7 +252,7 @@ export abstract class Generator implements IGenerator { this.addAbstractPostfixToClassName(classSpec); } this.onBeginClass(classSpec, abstract); - this.visitClass(classSpec, abstract); + this.visitClass(classSpec); visitChildren(); this.onEndClass(classSpec); break; @@ -360,9 +360,9 @@ export abstract class Generator implements IGenerator { } } - private visitClass(cls: spec.ClassType, abstract: boolean | undefined) { + private visitClass(cls: spec.ClassType) { let initializer = cls.initializer; - if (!abstract && initializer) { + if (initializer) { this.onInitializer(cls, initializer); diff --git a/packages/jsii-pacmak/package-lock.json b/packages/jsii-pacmak/package-lock.json index b064faa34b..ae51432e23 100644 --- a/packages/jsii-pacmak/package-lock.json +++ b/packages/jsii-pacmak/package-lock.json @@ -117,6 +117,12 @@ "integrity": "sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ=", "dev": true }, + "@types/escape-string-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/escape-string-regexp/-/escape-string-regexp-1.0.0.tgz", + "integrity": "sha512-KAruqgk9/340M4MYYasdBET+lyYN8KMXUuRKWO72f4SbmIMMFp9nnJiXUkJS0HC2SFe4x0R/fLozXIzqoUfSjA==", + "dev": true + }, "@types/fs-extra": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.5.tgz", @@ -544,8 +550,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "esm": { "version": "3.2.14", diff --git a/packages/jsii-pacmak/test/expected.jsii-calc-base/java/src/main/java/software/amazon/jsii/tests/calculator/base/Base.java b/packages/jsii-pacmak/test/expected.jsii-calc-base/java/src/main/java/software/amazon/jsii/tests/calculator/base/Base.java index 7a64b8e3d7..aa712c99bf 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc-base/java/src/main/java/software/amazon/jsii/tests/calculator/base/Base.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc-base/java/src/main/java/software/amazon/jsii/tests/calculator/base/Base.java @@ -9,6 +9,10 @@ public abstract class Base extends software.amazon.jsii.JsiiObject { protected Base(final software.amazon.jsii.JsiiObject.InitializationMode mode) { super(mode); } + public Base() { + super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); + } /** * @return the name of the class (to verify native type names are created for derived classes). diff --git a/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/Operation.java b/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/Operation.java index 2ae2a27035..b689b90d26 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/Operation.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/Operation.java @@ -9,6 +9,10 @@ public abstract class Operation extends software.amazon.jsii.tests.calculator.li protected Operation(final software.amazon.jsii.JsiiObject.InitializationMode mode) { super(mode); } + public Operation() { + super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); + } /** * String representation of the value. diff --git a/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/Value.java b/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/Value.java index 16b9db138a..21ce8bdd18 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/Value.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/Value.java @@ -9,6 +9,10 @@ public abstract class Value extends software.amazon.jsii.tests.calculator.base.B protected Value(final software.amazon.jsii.JsiiObject.InitializationMode mode) { super(mode); } + public Value() { + super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); + } /** * String representation of the 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 4abcda6426..ec29b7dd3d 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 @@ -1207,6 +1207,23 @@ } ] }, + "jsii-calc.ConstructorPassesThisOut": { + "assembly": "jsii-calc", + "fqn": "jsii-calc.ConstructorPassesThisOut", + "initializer": { + "initializer": true, + "parameters": [ + { + "name": "consumer", + "type": { + "fqn": "jsii-calc.PartiallyInitializedThisConsumer" + } + } + ] + }, + "kind": "class", + "name": "ConstructorPassesThisOut" + }, "jsii-calc.Constructors": { "assembly": "jsii-calc", "fqn": "jsii-calc.Constructors", @@ -3103,6 +3120,33 @@ ], "name": "OverrideReturnsObject" }, + "jsii-calc.PartiallyInitializedThisConsumer": { + "abstract": true, + "assembly": "jsii-calc", + "fqn": "jsii-calc.PartiallyInitializedThisConsumer", + "initializer": { + "initializer": true + }, + "kind": "class", + "methods": [ + { + "abstract": true, + "name": "consumePartiallyInitializedThis", + "parameters": [ + { + "name": "obj", + "type": { + "fqn": "jsii-calc.ConstructorPassesThisOut" + } + } + ], + "returns": { + "primitive": "string" + } + } + ], + "name": "PartiallyInitializedThisConsumer" + }, "jsii-calc.Polymorphism": { "assembly": "jsii-calc", "fqn": "jsii-calc.Polymorphism", @@ -4249,5 +4293,5 @@ } }, "version": "0.7.15", - "fingerprint": "y7h6OQBzbQrvU43LHTizXl12OUAEOpXhqD02OJfmhWQ=" + "fingerprint": "R1xDj9nkBHfChUdY5UBwN33gnpRmlVY2z00o3KvjHSQ=" } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/ConstructorPassesThisOut.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/ConstructorPassesThisOut.cs new file mode 100644 index 0000000000..e31c3513f0 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/ConstructorPassesThisOut.cs @@ -0,0 +1,20 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + [JsiiClass(typeof(ConstructorPassesThisOut), "jsii-calc.ConstructorPassesThisOut", "[{\"name\":\"consumer\",\"type\":{\"fqn\":\"jsii-calc.PartiallyInitializedThisConsumer\"}}]")] + public class ConstructorPassesThisOut : DeputyBase + { + public ConstructorPassesThisOut(PartiallyInitializedThisConsumer consumer): base(new DeputyProps(new object[]{consumer})) + { + } + + protected ConstructorPassesThisOut(ByRefValue reference): base(reference) + { + } + + protected ConstructorPassesThisOut(DeputyProps props): base(props) + { + } + } +} \ 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/PartiallyInitializedThisConsumer.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/PartiallyInitializedThisConsumer.cs new file mode 100644 index 0000000000..4aec89421c --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/PartiallyInitializedThisConsumer.cs @@ -0,0 +1,23 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + [JsiiClass(typeof(PartiallyInitializedThisConsumer), "jsii-calc.PartiallyInitializedThisConsumer", "[]")] + public abstract class PartiallyInitializedThisConsumer : DeputyBase + { + protected PartiallyInitializedThisConsumer(): base(new DeputyProps(new object[]{})) + { + } + + protected PartiallyInitializedThisConsumer(ByRefValue reference): base(reference) + { + } + + protected PartiallyInitializedThisConsumer(DeputyProps props): base(props) + { + } + + [JsiiMethod("consumePartiallyInitializedThis", "{\"primitive\":\"string\"}", "[{\"name\":\"obj\",\"type\":{\"fqn\":\"jsii-calc.ConstructorPassesThisOut\"}}]")] + public abstract string ConsumePartiallyInitializedThis(ConstructorPassesThisOut obj); + } +} \ 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/PartiallyInitializedThisConsumerProxy.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/PartiallyInitializedThisConsumerProxy.cs new file mode 100644 index 0000000000..cacacb0414 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/PartiallyInitializedThisConsumerProxy.cs @@ -0,0 +1,18 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + [JsiiTypeProxy(typeof(PartiallyInitializedThisConsumer), "jsii-calc.PartiallyInitializedThisConsumer")] + internal sealed class PartiallyInitializedThisConsumerProxy : PartiallyInitializedThisConsumer + { + private PartiallyInitializedThisConsumerProxy(ByRefValue reference): base(reference) + { + } + + [JsiiMethod("consumePartiallyInitializedThis", "{\"primitive\":\"string\"}", "[{\"name\":\"obj\",\"type\":{\"fqn\":\"jsii-calc.ConstructorPassesThisOut\"}}]")] + public override string ConsumePartiallyInitializedThis(ConstructorPassesThisOut obj) + { + return InvokeInstanceMethod(new object[]{obj}); + } + } +} \ 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 0124160630..337b5c04bb 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 @@ -34,6 +34,7 @@ protected Class resolveClass(final String fqn) throws ClassNotFoundException case "jsii-calc.ClassThatImplementsThePrivateInterface": return software.amazon.jsii.tests.calculator.ClassThatImplementsThePrivateInterface.class; case "jsii-calc.ClassWithMutableObjectLiteralProperty": return software.amazon.jsii.tests.calculator.ClassWithMutableObjectLiteralProperty.class; case "jsii-calc.ClassWithPrivateConstructorAndAutomaticProperties": return software.amazon.jsii.tests.calculator.ClassWithPrivateConstructorAndAutomaticProperties.class; + case "jsii-calc.ConstructorPassesThisOut": return software.amazon.jsii.tests.calculator.ConstructorPassesThisOut.class; case "jsii-calc.Constructors": return software.amazon.jsii.tests.calculator.Constructors.class; case "jsii-calc.ConsumersOfThisCrazyTypeSystem": return software.amazon.jsii.tests.calculator.ConsumersOfThisCrazyTypeSystem.class; case "jsii-calc.DefaultedConstructorArgument": return software.amazon.jsii.tests.calculator.DefaultedConstructorArgument.class; @@ -89,6 +90,7 @@ protected Class resolveClass(final String fqn) throws ClassNotFoundException case "jsii-calc.ObjectRefsInCollections": return software.amazon.jsii.tests.calculator.ObjectRefsInCollections.class; case "jsii-calc.OptionalConstructorArgument": return software.amazon.jsii.tests.calculator.OptionalConstructorArgument.class; case "jsii-calc.OverrideReturnsObject": return software.amazon.jsii.tests.calculator.OverrideReturnsObject.class; + case "jsii-calc.PartiallyInitializedThisConsumer": return software.amazon.jsii.tests.calculator.PartiallyInitializedThisConsumer.class; case "jsii-calc.Polymorphism": return software.amazon.jsii.tests.calculator.Polymorphism.class; case "jsii-calc.Power": return software.amazon.jsii.tests.calculator.Power.class; case "jsii-calc.PublicClass": return software.amazon.jsii.tests.calculator.PublicClass.class; diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AbstractClass.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AbstractClass.java index 97e11233a7..78061b7876 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AbstractClass.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AbstractClass.java @@ -6,6 +6,10 @@ public abstract class AbstractClass extends software.amazon.jsii.tests.calculato protected AbstractClass(final software.amazon.jsii.JsiiObject.InitializationMode mode) { super(mode); } + public AbstractClass() { + super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); + } public abstract java.lang.String abstractMethod(final java.lang.String name); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AbstractClassBase.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AbstractClassBase.java index 15756d5fb6..381fb205c6 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AbstractClassBase.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AbstractClassBase.java @@ -6,6 +6,10 @@ public abstract class AbstractClassBase extends software.amazon.jsii.JsiiObject protected AbstractClassBase(final software.amazon.jsii.JsiiObject.InitializationMode mode) { super(mode); } + public AbstractClassBase() { + super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); + } public java.lang.String getAbstractProperty() { return this.jsiiGet("abstractProperty", java.lang.String.class); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/BinaryOperation.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/BinaryOperation.java index c75105c770..a2f38995bc 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/BinaryOperation.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/BinaryOperation.java @@ -9,6 +9,15 @@ public abstract class BinaryOperation extends software.amazon.jsii.tests.calcula protected BinaryOperation(final software.amazon.jsii.JsiiObject.InitializationMode mode) { super(mode); } + /** + * Creates a BinaryOperation + * @param lhs Left-hand side operand + * @param rhs Right-hand side operand + */ + public BinaryOperation(final software.amazon.jsii.tests.calculator.lib.Value lhs, final software.amazon.jsii.tests.calculator.lib.Value rhs) { + super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this, java.util.stream.Stream.concat(java.util.stream.Stream.of(java.util.Objects.requireNonNull(lhs, "lhs is required")), java.util.stream.Stream.of(java.util.Objects.requireNonNull(rhs, "rhs is required"))).toArray()); + } /** * Say hello! diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ConstructorPassesThisOut.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ConstructorPassesThisOut.java new file mode 100644 index 0000000000..0090358fca --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ConstructorPassesThisOut.java @@ -0,0 +1,13 @@ +package software.amazon.jsii.tests.calculator; + +@javax.annotation.Generated(value = "jsii-pacmak") +@software.amazon.jsii.Jsii(module = software.amazon.jsii.tests.calculator.$Module.class, fqn = "jsii-calc.ConstructorPassesThisOut") +public class ConstructorPassesThisOut extends software.amazon.jsii.JsiiObject { + protected ConstructorPassesThisOut(final software.amazon.jsii.JsiiObject.InitializationMode mode) { + super(mode); + } + public ConstructorPassesThisOut(final software.amazon.jsii.tests.calculator.PartiallyInitializedThisConsumer consumer) { + super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this, java.util.stream.Stream.of(java.util.Objects.requireNonNull(consumer, "consumer is required")).toArray()); + } +} diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/PartiallyInitializedThisConsumer.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/PartiallyInitializedThisConsumer.java new file mode 100644 index 0000000000..2841d53ea9 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/PartiallyInitializedThisConsumer.java @@ -0,0 +1,29 @@ +package software.amazon.jsii.tests.calculator; + +@javax.annotation.Generated(value = "jsii-pacmak") +@software.amazon.jsii.Jsii(module = software.amazon.jsii.tests.calculator.$Module.class, fqn = "jsii-calc.PartiallyInitializedThisConsumer") +public abstract class PartiallyInitializedThisConsumer extends software.amazon.jsii.JsiiObject { + protected PartiallyInitializedThisConsumer(final software.amazon.jsii.JsiiObject.InitializationMode mode) { + super(mode); + } + public PartiallyInitializedThisConsumer() { + super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); + } + + public abstract java.lang.String consumePartiallyInitializedThis(final software.amazon.jsii.tests.calculator.ConstructorPassesThisOut obj); + + /** + * A proxy class which represents a concrete javascript instance of this type. + */ + final static class Jsii$Proxy extends software.amazon.jsii.tests.calculator.PartiallyInitializedThisConsumer { + protected Jsii$Proxy(final software.amazon.jsii.JsiiObject.InitializationMode mode) { + super(mode); + } + + @Override + public java.lang.String consumePartiallyInitializedThis(final software.amazon.jsii.tests.calculator.ConstructorPassesThisOut obj) { + return this.jsiiCall("consumePartiallyInitializedThis", java.lang.String.class, java.util.stream.Stream.of(java.util.Objects.requireNonNull(obj, "obj is required")).toArray()); + } + } +} diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/UnaryOperation.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/UnaryOperation.java index 546cec81e7..2ffb511e1e 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/UnaryOperation.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/UnaryOperation.java @@ -9,6 +9,10 @@ public abstract class UnaryOperation extends software.amazon.jsii.tests.calculat protected UnaryOperation(final software.amazon.jsii.JsiiObject.InitializationMode mode) { super(mode); } + public UnaryOperation(final software.amazon.jsii.tests.calculator.lib.Value operand) { + super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this, java.util.stream.Stream.of(java.util.Objects.requireNonNull(operand, "operand is required")).toArray()); + } public software.amazon.jsii.tests.calculator.lib.Value getOperand() { return this.jsiiGet("operand", software.amazon.jsii.tests.calculator.lib.Value.class); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/composition/CompositeOperation.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/composition/CompositeOperation.java index 95d5a79bc7..c85dbb5b21 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/composition/CompositeOperation.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/composition/CompositeOperation.java @@ -9,6 +9,10 @@ public abstract class CompositeOperation extends software.amazon.jsii.tests.calc protected CompositeOperation(final software.amazon.jsii.JsiiObject.InitializationMode mode) { super(mode); } + public CompositeOperation() { + super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); + } /** * String representation of the value. 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 6f776c4819..94125b901e 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 @@ -1247,6 +1247,36 @@ ClassWithPrivateConstructorAndAutomaticProperties :type: string +ConstructorPassesThisOut +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. py:class:: ConstructorPassesThisOut(consumer) + + **Language-specific names:** + + .. tabs:: + + .. code-tab:: c# + + using Amazon.JSII.Tests.CalculatorNamespace; + + .. code-tab:: java + + import software.amazon.jsii.tests.calculator.ConstructorPassesThisOut; + + .. code-tab:: javascript + + const { ConstructorPassesThisOut } = require('jsii-calc'); + + .. code-tab:: typescript + + import { ConstructorPassesThisOut } from 'jsii-calc'; + + + + :param consumer: + :type consumer: :py:class:`~jsii-calc.PartiallyInitializedThisConsumer`\ + Constructors ^^^^^^^^^^^^ @@ -4073,6 +4103,43 @@ OverrideReturnsObject :rtype: number +PartiallyInitializedThisConsumer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. py:class:: PartiallyInitializedThisConsumer() + + **Language-specific names:** + + .. tabs:: + + .. code-tab:: c# + + using Amazon.JSII.Tests.CalculatorNamespace; + + .. code-tab:: java + + import software.amazon.jsii.tests.calculator.PartiallyInitializedThisConsumer; + + .. code-tab:: javascript + + const { PartiallyInitializedThisConsumer } = require('jsii-calc'); + + .. code-tab:: typescript + + import { PartiallyInitializedThisConsumer } from 'jsii-calc'; + + + + :abstract: Yes + + .. py:method:: consumePartiallyInitializedThis(obj) -> string + + :param obj: + :type obj: :py:class:`~jsii-calc.ConstructorPassesThisOut`\ + :rtype: string + :abstract: Yes + + Polymorphism ^^^^^^^^^^^^ diff --git a/packages/jsii-python-runtime/tests/test_compliance.py b/packages/jsii-python-runtime/tests/test_compliance.py index 70fe6e3d18..43b1329d1c 100644 --- a/packages/jsii-python-runtime/tests/test_compliance.py +++ b/packages/jsii-python-runtime/tests/test_compliance.py @@ -13,6 +13,7 @@ AsyncVirtualMethods, Calculator, ClassWithPrivateConstructorAndAutomaticProperties, + ConstructorPassesThisOut, DoNotOverridePrivates, DoubleTrouble, GreetingAugmenter, @@ -28,6 +29,7 @@ NodeStandardLibrary, NullShouldBeTreatedAsUndefined, NumberGenerator, + PartiallyInitializedThisConsumer, Polymorphism, Power, PythonReservedWords, @@ -870,3 +872,13 @@ def test_testJsiiAgent(): def test_receiveInstanceOfPrivateClass(): assert ReturnsPrivateImplementationOfInterface().private_implementation.success + +@pytest.mark.skip +def test_objectIdDoesNotGetReallocatedWhenTheConstructorPassesThisOut(): + class PartiallyInitializedThisConsumerImpl(PartiallyInitializedThisConsumer): + def consume_partially_initialized_this(self): + return "OK" + + reflector = PartiallyInitializedThisConsumerImpl() + obj = ConstructorPassesThisOut(reflector) + assert obj is not None diff --git a/packages/jsii-reflect/test/classes.expected.txt b/packages/jsii-reflect/test/classes.expected.txt index 0ad0a17e89..23185fb571 100644 --- a/packages/jsii-reflect/test/classes.expected.txt +++ b/packages/jsii-reflect/test/classes.expected.txt @@ -15,6 +15,7 @@ ClassThatImplementsThePrivateInterface ClassWithMutableObjectLiteralProperty ClassWithPrivateConstructorAndAutomaticProperties CompositeOperation +ConstructorPassesThisOut Constructors ConsumersOfThisCrazyTypeSystem DefaultedConstructorArgument @@ -47,6 +48,7 @@ ObjectRefsInCollections Operation OptionalConstructorArgument OverrideReturnsObject +PartiallyInitializedThisConsumer Polymorphism Power PublicClass 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 7d84d37e75..944337b827 100644 --- a/packages/jsii-reflect/test/jsii-tree.test.all.expected.txt +++ b/packages/jsii-reflect/test/jsii-tree.test.all.expected.txt @@ -271,6 +271,13 @@ assemblies │ │ │ └── type: primitive:string │ │ └─┬ readWriteString property │ │ └── type: primitive:string + │ ├─┬ class ConstructorPassesThisOut + │ │ └─┬ members + │ │ └─┬ (consumer) method + │ │ ├─┬ parameters + │ │ │ └─┬ consumer + │ │ │ └── type: class:jsii-calc.PartiallyInitializedThisConsumer + │ │ └── returns: void │ ├─┬ class Constructors │ │ └─┬ members │ │ ├─┬ () method @@ -727,6 +734,16 @@ assemblies │ │ │ └─┬ obj │ │ │ └── type: interface:jsii-calc.IReturnsNumber │ │ └── returns: primitive:number + │ ├─┬ class PartiallyInitializedThisConsumer + │ │ └─┬ members + │ │ ├─┬ () method + │ │ │ └── returns: void + │ │ └─┬ consumePartiallyInitializedThis(obj) method + │ │ ├── abstract + │ │ ├─┬ parameters + │ │ │ └─┬ obj + │ │ │ └── type: class:jsii-calc.ConstructorPassesThisOut + │ │ └── returns: primitive:string │ ├─┬ class Polymorphism │ │ └─┬ members │ │ ├─┬ () 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 7261e7edb9..b381bb9211 100644 --- a/packages/jsii-reflect/test/jsii-tree.test.inheritance.expected.txt +++ b/packages/jsii-reflect/test/jsii-tree.test.inheritance.expected.txt @@ -24,6 +24,7 @@ assemblies │ ├── class ClassWithMutableObjectLiteralProperty │ ├─┬ class ClassWithPrivateConstructorAndAutomaticProperties │ │ └── interfaces: InterfaceWithProperties + │ ├── class ConstructorPassesThisOut │ ├── class Constructors │ ├── class ConsumersOfThisCrazyTypeSystem │ ├── class DefaultedConstructorArgument @@ -64,6 +65,7 @@ assemblies │ ├── class ObjectRefsInCollections │ ├── class OptionalConstructorArgument │ ├── class OverrideReturnsObject + │ ├── class PartiallyInitializedThisConsumer │ ├── class Polymorphism │ ├─┬ class Power │ │ └── base: CompositeOperation 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 26ed7c94df..fee2b7463d 100644 --- a/packages/jsii-reflect/test/jsii-tree.test.members.expected.txt +++ b/packages/jsii-reflect/test/jsii-tree.test.members.expected.txt @@ -109,6 +109,9 @@ assemblies │ │ ├── create(readOnlyString,readWriteString) method │ │ ├── readOnlyString property │ │ └── readWriteString property + │ ├─┬ class ConstructorPassesThisOut + │ │ └─┬ members + │ │ └── (consumer) method │ ├─┬ class Constructors │ │ └─┬ members │ │ ├── () method @@ -313,6 +316,10 @@ assemblies │ │ └─┬ members │ │ ├── () method │ │ └── test(obj) method + │ ├─┬ class PartiallyInitializedThisConsumer + │ │ └─┬ members + │ │ ├── () method + │ │ └── consumePartiallyInitializedThis(obj) method │ ├─┬ class Polymorphism │ │ └─┬ members │ │ ├── () 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 7c3952fd2b..40ebf8e2e5 100644 --- a/packages/jsii-reflect/test/jsii-tree.test.types.expected.txt +++ b/packages/jsii-reflect/test/jsii-tree.test.types.expected.txt @@ -15,6 +15,7 @@ assemblies │ ├── class ClassThatImplementsThePrivateInterface │ ├── class ClassWithMutableObjectLiteralProperty │ ├── class ClassWithPrivateConstructorAndAutomaticProperties + │ ├── class ConstructorPassesThisOut │ ├── class Constructors │ ├── class ConsumersOfThisCrazyTypeSystem │ ├── class DefaultedConstructorArgument @@ -46,6 +47,7 @@ assemblies │ ├── class ObjectRefsInCollections │ ├── class OptionalConstructorArgument │ ├── class OverrideReturnsObject + │ ├── class PartiallyInitializedThisConsumer │ ├── class Polymorphism │ ├── class Power │ ├── class PublicClass From 46fa60fb8c68fee36844f814c421a173e5e32204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Wed, 20 Mar 2019 18:09:13 +0100 Subject: [PATCH 2/6] Properly mark compliance test broken due to current lack of sync callback support --- packages/jsii-python-runtime/tests/test_compliance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jsii-python-runtime/tests/test_compliance.py b/packages/jsii-python-runtime/tests/test_compliance.py index fc25aa23a0..d67e98b973 100644 --- a/packages/jsii-python-runtime/tests/test_compliance.py +++ b/packages/jsii-python-runtime/tests/test_compliance.py @@ -899,7 +899,7 @@ def test_eraseUnsetDataValues(): assert not EraseUndefinedHashValues.does_key_exist(opts, "option2") -@pytest.mark.skip +@xfail_callbacks def test_objectIdDoesNotGetReallocatedWhenTheConstructorPassesThisOut(): class PartiallyInitializedThisConsumerImpl(PartiallyInitializedThisConsumer): def consume_partially_initialized_this(self): From 4c0695cf91d8dd2d5a07d9fd36e987a960a91139 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 22 Mar 2019 17:25:46 +0100 Subject: [PATCH 3/6] fix(kernel): make type serialization explicit and recursive Serialize and deserialize types according to their declared static type, and add validation on the runtime types matching the declared types. This is in contrast to previously, when we mostly only used the runtime types to determine what to do, and harly any validation was done. The runtime types used to be able to freely disagree with the declared types, and we put a lot of burden on the JSII runtimes at the other end of the wire. Fix tests that used to exercise the API with invalid arguments. Remove Proxy objects since they only existed to prevent accidentally overwriting certain keys via the jsii interface, and Symbols serve the same purpose (but simpler). Fixes awslabs/aws-cdk#1981. --- packages/jsii-calc/lib/compliance.ts | 84 ++- packages/jsii-calc/test/assembly.jsii | 133 +++- packages/jsii-java-runtime-test/README.md | 3 + .../amazon/jsii/testing/ComplianceTest.java | 6 +- packages/jsii-kernel/lib/api.ts | 44 +- packages/jsii-kernel/lib/kernel.ts | 629 +++++------------- packages/jsii-kernel/lib/objects.ts | 135 ++++ packages/jsii-kernel/lib/serialization.ts | 622 +++++++++++++++++ packages/jsii-kernel/test/test.kernel.ts | 172 ++++- .../.jsii | 133 +++- .../Tests/CalculatorNamespace/AllTypes.cs | 15 +- .../Tests/CalculatorNamespace/Constructors.cs | 30 + .../CalculatorNamespace/IIPublicInterface.cs | 4 +- .../CalculatorNamespace/IIPublicInterface2.cs | 11 + .../IPublicInterface2Proxy.cs | 18 + .../IPublicInterfaceProxy.cs | 6 +- .../CalculatorNamespace/InbetweenClass.cs | 8 +- .../SingleInstanceTwoTypes.cs | 39 ++ .../amazon/jsii/tests/calculator/$Module.java | 2 + .../jsii/tests/calculator/AllTypes.java | 8 + .../jsii/tests/calculator/Constructors.java | 20 + .../tests/calculator/IPublicInterface.java | 6 +- .../tests/calculator/IPublicInterface2.java | 20 + .../jsii/tests/calculator/InbetweenClass.java | 7 +- .../calculator/SingleInstanceTwoTypes.java | 28 + .../expected.jsii-calc/sphinx/jsii-calc.rst | 134 +++- .../tests/test_compliance.py | 6 +- .../jsii-reflect/test/classes.expected.txt | 1 + .../test/jsii-tree.test.all.expected.txt | 50 +- .../jsii-tree.test.inheritance.expected.txt | 5 +- .../test/jsii-tree.test.members.expected.txt | 20 +- .../test/jsii-tree.test.types.expected.txt | 2 + .../project/test/jsii_runtime_test.rb | 2 +- 33 files changed, 1891 insertions(+), 512 deletions(-) create mode 100644 packages/jsii-java-runtime-test/README.md create mode 100644 packages/jsii-kernel/lib/objects.ts create mode 100644 packages/jsii-kernel/lib/serialization.ts create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface2.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterface2Proxy.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/SingleInstanceTwoTypes.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface2.java create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/SingleInstanceTwoTypes.java diff --git a/packages/jsii-calc/lib/compliance.ts b/packages/jsii-calc/lib/compliance.ts index 1d80389d71..81e7c5739d 100644 --- a/packages/jsii-calc/lib/compliance.ts +++ b/packages/jsii-calc/lib/compliance.ts @@ -13,7 +13,7 @@ import * as path from 'path'; import * as os from 'os'; import * as crypto from 'crypto'; import { promisify } from 'util'; -import { composition, IFriendlyRandomGenerator, IRandomNumberGenerator, Multiply } from './calculator'; +import { IFriendlyRandomGenerator, IRandomNumberGenerator, Multiply } from './calculator'; const bundled = require('jsii-calc-bundled'); import base = require('@scope/jsii-calc-base'); @@ -163,7 +163,7 @@ export class AllTypes { // unions unionProperty: string | number | Number | Multiply = 'foo'; - unionArrayProperty: (composition.CompositeOperation | number)[] = []; + unionArrayProperty: (Value | number)[] = []; unionMapProperty: { [key: string]: (Number | number | string) } = {}; // enum @@ -194,6 +194,21 @@ export class AllTypes { enumMethod(value: StringEnum) { return value; } + + + public anyOut(): any { + const ret = new Number(42); + Object.defineProperty(ret, 'tag', { + value: "you're it" + }); + return ret; + } + + public anyIn(inp: any) { + if (inp.tag !== "you're it") { + throw new Error(`Not the same object that I gave you, got: ${JSON.stringify(inp)}`); + } + } } // @@ -1321,18 +1336,73 @@ export class PublicClass { public hello(): void {} } export interface IPublicInterface { - bye(): void; + bye(): string; +} + +export interface IPublicInterface2 { + ciao(): string; +} +export class InbetweenClass extends PublicClass implements IPublicInterface2 { + public ciao(): string { return 'ciao'; } } -export class InbetweenClass extends PublicClass {} class PrivateClass extends InbetweenClass implements IPublicInterface { - public bye(): void {} + public bye(): string { return 'bye'; } +} + +class HiddenClass implements IPublicInterface, IPublicInterface2 { + public bye(): string { return 'bye'; } + public ciao(): string { return 'ciao'; } +} + +class HiddenSubclass extends HiddenClass { } + export class Constructors { public static makeClass(): PublicClass { - return new PrivateClass(); + return new PrivateClass(); // Wire type should be InbetweenClass } + public static makeInterface(): IPublicInterface { - return new PrivateClass(); + return new PrivateClass(); // Wire type should be IPublicInterface + } + + public static makeInterface2(): IPublicInterface2 { + return new PrivateClass(); // Wire type should be InbetweenClass + } + + public static makeInterfaces(): IPublicInterface[] { + return [new PrivateClass()]; // Wire type should be IPublicInterface[] + } + + public static hiddenInterface(): IPublicInterface { + return new HiddenClass(); // Wire type should be IPublicInterface + } + + public static hiddenInterfaces(): IPublicInterface[] { + return [new HiddenClass()]; // Wire type should be IPublicInterface[] + } + + public static hiddenSubInterfaces(): IPublicInterface[] { + return [new HiddenSubclass()]; // Wire type should be IPublicInterface[] + } +} + +/** + * Test that a single instance can be returned under two different FQNs + * + * JSII clients can instantiate 2 different strongly-typed wrappers for the same + * object. Unfortunately, this will break object equality, but if we didn't do + * this it would break runtime type checks in the JVM or CLR. + */ +export class SingleInstanceTwoTypes { + private instance = new PrivateClass(); + + public interface1(): InbetweenClass { + return this.instance; + } + + public interface2(): IPublicInterface { + return this.instance; } } diff --git a/packages/jsii-calc/test/assembly.jsii b/packages/jsii-calc/test/assembly.jsii index e9081de3c1..88d04378d2 100644 --- a/packages/jsii-calc/test/assembly.jsii +++ b/packages/jsii-calc/test/assembly.jsii @@ -353,6 +353,23 @@ }, "kind": "class", "methods": [ + { + "name": "anyIn", + "parameters": [ + { + "name": "inp", + "type": { + "primitive": "any" + } + } + ] + }, + { + "name": "anyOut", + "returns": { + "primitive": "any" + } + }, { "name": "enumMethod", "parameters": [ @@ -474,7 +491,7 @@ "primitive": "number" }, { - "fqn": "jsii-calc.composition.CompositeOperation" + "fqn": "@scope/jsii-calc-lib.Value" } ] } @@ -1217,6 +1234,37 @@ }, "kind": "class", "methods": [ + { + "name": "hiddenInterface", + "returns": { + "fqn": "jsii-calc.IPublicInterface" + }, + "static": true + }, + { + "name": "hiddenInterfaces", + "returns": { + "collection": { + "elementtype": { + "fqn": "jsii-calc.IPublicInterface" + }, + "kind": "array" + } + }, + "static": true + }, + { + "name": "hiddenSubInterfaces", + "returns": { + "collection": { + "elementtype": { + "fqn": "jsii-calc.IPublicInterface" + }, + "kind": "array" + } + }, + "static": true + }, { "name": "makeClass", "returns": { @@ -1230,6 +1278,25 @@ "fqn": "jsii-calc.IPublicInterface" }, "static": true + }, + { + "name": "makeInterface2", + "returns": { + "fqn": "jsii-calc.IPublicInterface2" + }, + "static": true + }, + { + "name": "makeInterfaces", + "returns": { + "collection": { + "elementtype": { + "fqn": "jsii-calc.IPublicInterface" + }, + "kind": "array" + } + }, + "static": true } ], "name": "Constructors" @@ -2089,11 +2156,29 @@ "methods": [ { "abstract": true, - "name": "bye" + "name": "bye", + "returns": { + "primitive": "string" + } } ], "name": "IPublicInterface" }, + "jsii-calc.IPublicInterface2": { + "assembly": "jsii-calc", + "fqn": "jsii-calc.IPublicInterface2", + "kind": "interface", + "methods": [ + { + "abstract": true, + "name": "ciao", + "returns": { + "primitive": "string" + } + } + ], + "name": "IPublicInterface2" + }, "jsii-calc.IRandomNumberGenerator": { "assembly": "jsii-calc", "docs": { @@ -2240,7 +2325,23 @@ "initializer": { "initializer": true }, + "interfaces": [ + { + "fqn": "jsii-calc.IPublicInterface2" + } + ], "kind": "class", + "methods": [ + { + "name": "ciao", + "overrides": { + "fqn": "jsii-calc.IPublicInterface2" + }, + "returns": { + "primitive": "string" + } + } + ], "name": "InbetweenClass" }, "jsii-calc.InterfaceImplementedByAbstractClass": { @@ -3561,6 +3662,32 @@ ], "name": "RuntimeTypeChecking" }, + "jsii-calc.SingleInstanceTwoTypes": { + "assembly": "jsii-calc", + "docs": { + "comment": "Test that a single instance can be returned under two different FQNs\n\nJSII clients can instantiate 2 different strongly-typed wrappers for the same\nobject. Unfortunately, this will break object equality, but if we didn't do\nthis it would break runtime type checks in the JVM or CLR." + }, + "fqn": "jsii-calc.SingleInstanceTwoTypes", + "initializer": { + "initializer": true + }, + "kind": "class", + "methods": [ + { + "name": "interface1", + "returns": { + "fqn": "jsii-calc.InbetweenClass" + } + }, + { + "name": "interface2", + "returns": { + "fqn": "jsii-calc.IPublicInterface" + } + } + ], + "name": "SingleInstanceTwoTypes" + }, "jsii-calc.Statics": { "assembly": "jsii-calc", "fqn": "jsii-calc.Statics", @@ -4349,5 +4476,5 @@ } }, "version": "0.8.0", - "fingerprint": "4LMgT0Rllw3CIJWiDiR/eUfSRPvCEeWyGWxJXMiDvcU=" + "fingerprint": "7BN7XlOFufVEMTaAbMH5GDcE4zQmJj4iiDlYoXiRvCs=" } diff --git a/packages/jsii-java-runtime-test/README.md b/packages/jsii-java-runtime-test/README.md new file mode 100644 index 0000000000..0cb17f3027 --- /dev/null +++ b/packages/jsii-java-runtime-test/README.md @@ -0,0 +1,3 @@ +Dive into a single failing test: + + JSII_DEBUG=1 mvn test -Dtest=software.amazon.jsii.testing.ComplianceTest#unionTypes diff --git a/packages/jsii-java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java b/packages/jsii-java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java index 1e27575d60..b15956ab4c 100644 --- a/packages/jsii-java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java +++ b/packages/jsii-java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java @@ -196,12 +196,12 @@ public void unionTypes() { // map Map map = new HashMap<>(); - map.put("Foo", new Multiply(new Number(2), new Number(99))); + map.put("Foo", new Number(99)); types.setUnionMapProperty(map); // array - types.setUnionArrayProperty(Arrays.asList("Hello", 123, new Number(33))); - assertEquals(33, ((Number)((List)types.getUnionArrayProperty()).get(2)).getValue()); + types.setUnionArrayProperty(Arrays.asList(123, new Number(33))); + assertEquals(33, ((Number)((List)types.getUnionArrayProperty()).get(1)).getValue()); } diff --git a/packages/jsii-kernel/lib/api.ts b/packages/jsii-kernel/lib/api.ts index 71a9c50d9c..d1be65cd9d 100644 --- a/packages/jsii-kernel/lib/api.ts +++ b/packages/jsii-kernel/lib/api.ts @@ -2,16 +2,50 @@ export const TOKEN_REF = '$jsii.byref'; export const TOKEN_DATE = '$jsii.date'; export const TOKEN_ENUM = '$jsii.enum'; -export class ObjRef { - [token: string]: string; // token = TOKEN_REF +export interface ObjRef { + [TOKEN_REF]: string; } -export interface Override { - method?: string; - property?: string; +export function isObjRef(value: any): value is ObjRef { + return typeof value === 'object' && value !== null && TOKEN_REF in value; +} + +export interface WireDate { + [TOKEN_DATE]: string; +} + +export function isWireDate(value: any): value is WireDate { + return typeof value === 'object' && value !== null && TOKEN_DATE in value; +} + +export interface WireEnum { + [TOKEN_ENUM]: string; +} + +export function isWireEnum(value: any): value is WireEnum { + return typeof value === 'object' && value !== null && TOKEN_ENUM in value; +} + +export type Override = MethodOverride | PropertyOverride; + +export interface MethodOverride { + method: string; + cookie?: string; +} + +export function isMethodOverride(value: Override): value is MethodOverride { + return (value as any).method != null; // Python passes "null" +} + +export interface PropertyOverride { + property: string; cookie?: string; } +export function isPropertyOverride(value: Override): value is PropertyOverride { + return (value as any).property != null; // Python passes "null" +} + export interface Callback { cbid: string; cookie: string | undefined; diff --git a/packages/jsii-kernel/lib/kernel.ts b/packages/jsii-kernel/lib/kernel.ts index 15e615aac4..d76fafe0ce 100644 --- a/packages/jsii-kernel/lib/kernel.ts +++ b/packages/jsii-kernel/lib/kernel.ts @@ -6,21 +6,9 @@ import { SourceMapConsumer } from 'source-map'; import * as tar from 'tar'; import * as vm from 'vm'; import * as api from './api'; -import { TOKEN_DATE, TOKEN_ENUM, TOKEN_REF } from './api'; - -/** - * Added to objects and contains the objid (the object reference). - * Used to find the object id from an object. - */ -const OBJID_PROP = '$__jsii__objid__$'; -const FQN_PROP = '$__jsii__fqn__$'; -const PROXIES_PROP = '$__jsii__proxies__$'; -const PROXY_REFERENT_PROP = '$__jsii__proxy_referent__$'; - -/** - * A special FQN that can be used to create empty javascript objects. - */ -const EMPTY_OBJECT_FQN = 'Object'; +import { TOKEN_REF } from './api'; +import { ObjectTable, tagJsiiConstructor } from './objects'; +import { CompleteTypeReference, EMPTY_OBJECT_FQN, serializationType, SerializerHost, SERIALIZERS } from './serialization'; export class Kernel { /** @@ -29,11 +17,11 @@ export class Kernel { public traceEnabled = false; private assemblies: { [name: string]: Assembly } = { }; - private objects: { [objid: string]: any } = { }; + private objects = new ObjectTable(); private cbs: { [cbid: string]: Callback } = { }; private waiting: { [cbid: string]: Callback } = { }; private promises: { [prid: string]: AsyncInvocation } = { }; - private nextid = 10000; // incrementing counter for objid, cbid, promiseid + private nextid = 20000; // incrementing counter for objid, cbid, promiseid private syncInProgress?: string; // forbids async calls (begin) while processing sync calls (get/set/invoke) private installDir?: string; @@ -151,13 +139,7 @@ export class Kernel { const { objref } = req; this._debug('del', objref); - const obj = this._findObject(objref); // make sure object exists - delete this.objects[objref[TOKEN_REF]]; - - if (obj[PROXY_REFERENT_PROP]) { - // De-register the proxy if this was a proxy... - delete obj[PROXY_REFERENT_PROP][PROXIES_PROP][obj[FQN_PROP]]; - } + this.objects.deleteObject(objref); return { }; } @@ -200,7 +182,7 @@ export class Kernel { const prototype = this._findSymbol(fqn); this._ensureSync(`property ${property}`, () => - this._wrapSandboxCode(() => prototype[property] = this._toSandbox(value))); + this._wrapSandboxCode(() => prototype[property] = this._toSandbox(value, ti.type))); return {}; } @@ -208,8 +190,7 @@ export class Kernel { public get(req: api.GetRequest): api.GetResponse { const { objref, property } = req; this._debug('get', objref, property); - const obj = this._findObject(objref); - const fqn = this._fqnForObject(obj); + const { instance, fqn } = this.objects.findObject(objref); const ti = this._typeInfoForProperty(fqn, property); // if the property is overridden by the native code and "get" is called on the object, it @@ -217,12 +198,12 @@ export class Kernel { // that, we actually keep a copy of the original property descriptor when we override, // so `findPropertyTarget` will return either the original property name ("property") or // the "super" property name (somehing like "$jsii$super$$"). - const propertyToGet = this._findPropertyTarget(obj, property); + const propertyToGet = this._findPropertyTarget(instance, property); // make the actual "get", and block any async calls that might be performed // by jsii overrides. const value = this._ensureSync(`property '${objref[TOKEN_REF]}.${propertyToGet}'`, - () => this._wrapSandboxCode(() => obj[propertyToGet])); + () => this._wrapSandboxCode(() => instance[propertyToGet])); this._debug('value:', value); const ret = this._fromSandbox(value, ti.type); this._debug('ret:', ret); @@ -232,19 +213,18 @@ export class Kernel { public set(req: api.SetRequest): api.SetResponse { const { objref, property, value } = req; this._debug('set', objref, property, value); - const obj = this._findObject(objref); + const { instance, fqn } = this.objects.findObject(objref); - const fqn = this._fqnForObject(obj); const propInfo = this._typeInfoForProperty(fqn, req.property); if (propInfo.immutable) { throw new Error(`Cannot set value of immutable property ${req.property} to ${req.value}`); } - const propertyToSet = this._findPropertyTarget(obj, property); + const propertyToSet = this._findPropertyTarget(instance, property); this._ensureSync(`property '${objref[TOKEN_REF]}.${propertyToSet}'`, - () => this._wrapSandboxCode(() => obj[propertyToSet] = this._toSandbox(value))); + () => this._wrapSandboxCode(() => instance[propertyToSet] = this._toSandbox(value, propInfo.type))); return { }; } @@ -262,10 +242,13 @@ export class Kernel { } const ret = this._ensureSync(`method '${objref[TOKEN_REF]}.${method}'`, () => { - return this._wrapSandboxCode(() => fn.apply(obj, this._toSandboxValues(args))); + return this._wrapSandboxCode(() => fn.apply(obj, this._toSandboxValues(args, ti.parameters))); }); - return { result: this._fromSandbox(ret, ti.returns) }; + const result = this._fromSandbox(ret, ti.returns || 'void'); + this._debug('invoke result', result); + + return { result }; } public sinvoke(req: api.StaticInvokeRequest): api.InvokeResponse { @@ -289,11 +272,11 @@ export class Kernel { const fn = prototype[method]; const ret = this._ensureSync(`method '${fqn}.${method}'`, () => { - return this._wrapSandboxCode(() => fn.apply(null, this._toSandboxValues(args))); + return this._wrapSandboxCode(() => fn.apply(null, this._toSandboxValues(args, ti.parameters))); }); this._debug('method returned:', ret); - return { result: this._fromSandbox(ret, ti.returns) }; + return { result: this._fromSandbox(ret, ti.returns || 'void') }; } public begin(req: api.BeginRequest): api.BeginResponse { @@ -314,7 +297,7 @@ export class Kernel { throw new Error(`Method ${method} is expected to be an async method`); } - const promise = this._wrapSandboxCode(() => fn.apply(obj, this._toSandboxValues(args))) as Promise; + const promise = this._wrapSandboxCode(() => fn.apply(obj, this._toSandboxValues(args, ti.parameters))) as Promise; // since we are planning to resolve this promise in a different scope // we need to handle rejections here [1] @@ -349,7 +332,7 @@ export class Kernel { throw mapSource(e, this.sourceMaps); } - return { result: this._fromSandbox(result, method.returns) }; + return { result: this._fromSandbox(result, method.returns || 'void') }; } public callbacks(_req?: api.CallbacksRequest): api.CallbacksResponse { @@ -362,7 +345,7 @@ export class Kernel { cookie: cb.override.cookie, invoke: { objref: cb.objref, - method: cb.override.method!, + method: cb.override.method, args: cb.args }, }; @@ -388,7 +371,7 @@ export class Kernel { this._debug('completed with error:', err); cb.fail(new Error(err)); } else { - const sandoxResult = this._toSandbox(result); + const sandoxResult = this._toSandbox(result, cb.expectedReturnType || 'void'); this._debug('completed with result:', sandoxResult); cb.succeed(sandoxResult); } @@ -418,7 +401,7 @@ export class Kernel { public stats(_req?: api.StatsRequest): api.StatsResponse { return { - objectCount: Object.keys(this.objects).length + objectCount: this.objects.count }; } @@ -435,20 +418,15 @@ export class Kernel { case spec.TypeKind.Class: case spec.TypeKind.Enum: const constructor = this._findSymbol(fqn); - Object.defineProperty(constructor, '__jsii__', { - configurable: false, - enumerable: false, - writable: false, - value: { fqn } - }); + tagJsiiConstructor(constructor, fqn); } } } // find the javascript constructor function for a jsii FQN. - private _findCtor(fqn: string, args: any[]) { + private _findCtor(fqn: string, args: any[]): { ctor: any, parameters?: spec.Parameter[] } { if (fqn === EMPTY_OBJECT_FQN) { - return Object; + return { ctor: Object }; } const typeinfo = this._typeInfoForFqn(fqn); @@ -457,7 +435,7 @@ export class Kernel { case spec.TypeKind.Class: const classType = typeinfo as spec.ClassType; this._validateMethodArguments(classType.initializer, args); - return this._findSymbol(fqn); + return { ctor: this._findSymbol(fqn), parameters: classType.initializer && classType.initializer.parameters }; case spec.TypeKind.Interface: throw new Error(`Cannot create an object with an FQN of an interface: ${fqn}`); @@ -470,13 +448,15 @@ export class Kernel { // prefixed with _ to allow calling this method internally without // getting it recorded for testing. private _create(req: api.CreateRequest): api.CreateResponse { + this._debug('create', req); const { fqn, overrides } = req; const requestArgs = req.args || []; - const ctor = this._findCtor(fqn, requestArgs); - const obj = this._wrapSandboxCode(() => new ctor(...this._toSandboxValues(requestArgs))); - const objref = this._createObjref(obj, fqn); + const ctorResult = this._findCtor(fqn, requestArgs); + const ctor = ctorResult.ctor; + const obj = this._wrapSandboxCode(() => new ctor(...this._toSandboxValues(requestArgs, ctorResult.parameters))); + const objref = this.objects.registerObject(obj, fqn); // overrides: for each one of the override method names, installs a // method on the newly created object which represents the remote "reverse proxy". @@ -489,40 +469,18 @@ export class Kernel { const properties = new Set(); for (const override of overrides) { - if (override.method) { - if (override.property) { throw new Error(overrideTypeErrorMessage); } + if (api.isMethodOverride(override)) { + if (api.isPropertyOverride(override)) { throw new Error(overrideTypeErrorMessage); } if (methods.has(override.method)) { throw new Error(`Duplicate override for method '${override.method}'`); } - methods.add(override.method); - // check that the method being overridden actually exists - let methodInfo; - if (fqn !== EMPTY_OBJECT_FQN) { - // error if we can find a property with this name - if (this._tryTypeInfoForProperty(fqn, override.method)) { - throw new Error(`Trying to override property '${override.method}' as a method`); - } - - methodInfo = this._tryTypeInfoForMethod(fqn, override.method); - } - - this._applyMethodOverride(obj, objref, override, methodInfo); - } else if (override.property) { - if (override.method) { throw new Error(overrideTypeErrorMessage); } + this._applyMethodOverride(obj, objref, fqn, override); + } else if (api.isPropertyOverride(override)) { + if (api.isMethodOverride(override)) { throw new Error(overrideTypeErrorMessage); } if (properties.has(override.property)) { throw Error(`Duplicate override for property '${override.property}'`); } properties.add(override.property); - let propInfo: spec.Property | undefined; - if (fqn !== EMPTY_OBJECT_FQN) { - // error if we can find a method with this name - if (this._tryTypeInfoForMethod(fqn, override.property)) { - throw new Error(`Trying to override method '${override.property}' as a property`); - } - - propInfo = this._tryTypeInfoForProperty(fqn, override.property); - } - - this._applyPropertyOverride(obj, objref, override, propInfo); + this._applyPropertyOverride(obj, objref, fqn, override); } else { throw new Error(overrideTypeErrorMessage); } @@ -536,16 +494,43 @@ export class Kernel { return `$jsii$super$${name}$`; } - private _applyPropertyOverride(obj: any, objref: api.ObjRef, override: api.Override, propInfo?: spec.Property) { - const self = this; - const propertyName = override.property!; + private _applyPropertyOverride(obj: any, objref: api.ObjRef, typeFqn: string, override: api.PropertyOverride) { + let propInfo; + if (typeFqn !== EMPTY_OBJECT_FQN) { + // error if we can find a method with this name + if (this._tryTypeInfoForMethod(typeFqn, override.property)) { + throw new Error(`Trying to override method '${override.property}' as a property`); + } + + propInfo = this._tryTypeInfoForProperty(typeFqn, override.property); + } // if this is a private property (i.e. doesn't have `propInfo` the object has a key) - if (!propInfo && propertyName in obj) { - this._debug(`Skipping override of private property ${propertyName}`); + if (!propInfo && override.property in obj) { + this._debug(`Skipping override of private property ${override.property}`); return; } + if (!propInfo) { + // We've overriding a property on an object we have NO type information on (probably + // because it's an anonymous object). + // Pretend it's 'prop: any'; + // + // FIXME: We could do better type checking during the conversion if JSII clients + // would tell us the intended interface type. + propInfo = { + name: override.property, + type: ANY_TYPE, + }; + } + + this._defineOverridenProperty(obj, objref, override, propInfo); + } + + private _defineOverridenProperty(obj: any, objref: api.ObjRef, override: api.PropertyOverride, propInfo: spec.Property) { + const self = this; + const propertyName = override.property!; + this._debug('apply override', propertyName); // save the old property under $jsii$super$$ so that property overrides @@ -568,49 +553,75 @@ export class Kernel { enumerable: prevEnumerable, configurable: prev.configurable, get: () => { + self._debug('virtual get', objref, propertyName, { cookie: override.cookie }); const result = self.callbackHandler({ cookie: override.cookie, cbid: self._makecbid(), get: { objref, property: propertyName } }); this._debug('callback returned', result); - return this._toSandbox(result); + return this._toSandbox(result, propInfo.type); }, set: (value: any) => { self._debug('virtual set', objref, propertyName, { cookie: override.cookie }); self.callbackHandler({ cookie: override.cookie, cbid: self._makecbid(), - set: { objref, property: propertyName, value: self._fromSandbox(value) } + set: { objref, property: propertyName, value: self._fromSandbox(value, propInfo.type) } }); } }); } - private _applyMethodOverride(obj: any, objref: api.ObjRef, override: api.Override, methodInfo?: spec.Method) { - const self = this; - const methodName = override.method!; + private _applyMethodOverride(obj: any, objref: api.ObjRef, typeFqn: string, override: api.MethodOverride) { + let methodInfo; + if (typeFqn !== EMPTY_OBJECT_FQN) { + // error if we can find a property with this name + if (this._tryTypeInfoForProperty(typeFqn, override.method)) { + throw new Error(`Trying to override property '${override.method}' as a method`); + } + + methodInfo = this._tryTypeInfoForMethod(typeFqn, override.method); + } // If this is a private method (doesn't have methodInfo, key resolves on the object), we // are going to skip the override. - if (!methodInfo && obj[methodName]) { - this._debug(`Skipping override of private method ${methodName}`); + if (!methodInfo && obj[override.method]) { + this._debug(`Skipping override of private method ${override.method}`); return; } - // note that we are applying the override even if the method doesn't exist - // on the type spec in order to allow native code to override methods from - // interfaces. + if (!methodInfo) { + // We've overriding a method on an object we have NO type information on (probably + // because it's an anonymous object). + // Pretend it's an (...args: any[]) => any + // + // FIXME: We could do better type checking during the conversion if JSII clients + // would tell us the intended interface type. + methodInfo = { + name: override.method, + returns: ANY_TYPE, + parameters: [{ name: 'args', variadic: true, type: ANY_TYPE}], + variadic: true + }; + } + + this._defineOverridenMethod(obj, objref, override, methodInfo); + } + + private _defineOverridenMethod(obj: any, objref: api.ObjRef, override: api.MethodOverride, methodInfo: spec.Method) { + const self = this; + const methodName = override.method; - if (methodInfo && methodInfo.returns && methodInfo.returns.promise) { + if (methodInfo.returns && methodInfo.returns.promise) { // async method override Object.defineProperty(obj, methodName, { enumerable: false, configurable: false, writable: false, value: (...methodArgs: any[]) => { - self._debug('invoked async override', override); - const args = self._toSandboxValues(methodArgs); + self._debug('invoke async method override', override); + const args = self._toSandboxValues(methodArgs, methodInfo.parameters); return new Promise((succeed, fail) => { const cbid = self._makecbid(); self._debug('adding callback to queue', cbid); @@ -618,6 +629,7 @@ export class Kernel { objref, override, args, + expectedReturnType: methodInfo.returns || 'void', succeed, fail }; @@ -631,24 +643,28 @@ export class Kernel { configurable: false, writable: false, value: (...methodArgs: any[]) => { + self._debug('invoke sync method override', override, 'args', methodArgs); + // We should be validating the actual arguments according to the + // declared parameters here, but let's just assume the JSII runtime on the + // other end has done its work. const result = self.callbackHandler({ cookie: override.cookie, cbid: self._makecbid(), invoke: { objref, method: methodName, - args: this._fromSandbox(methodArgs) + args: this._fromSandboxValues(methodArgs, methodInfo.parameters), } }); - return this._toSandbox(result); + self._debug('Result', result); + return this._toSandbox(result, methodInfo.returns || 'void'); } }); } } private _findInvokeTarget(objref: any, methodName: string, args: any[]) { - const obj = this._findObject(objref); - const fqn = this._fqnForObject(obj); + const { instance, fqn } = this.objects.findObject(objref); const ti = this._typeInfoForMethod(fqn, methodName); this._validateMethodArguments(ti, args); @@ -659,14 +675,14 @@ export class Kernel { // if we didn't find the method on the prototype, it could be a literal object // that implements an interface, so we look if we have the method on the object // itself. if we do, we invoke it. - let fn = obj.constructor.prototype[methodName]; + let fn = instance.constructor.prototype[methodName]; if (!fn) { - fn = obj[methodName]; + fn = instance[methodName]; if (!fn) { throw new Error(`Cannot find ${methodName} on object`); } } - return { ti, obj, fn }; + return { ti, obj: instance, fn }; } private _formatTypeRef(typeRef: spec.TypeReference): string { @@ -743,40 +759,6 @@ export class Kernel { return curr; } - private _createObjref(obj: any, fqn: string): api.ObjRef { - const objid = this._mkobjid(fqn); - Object.defineProperty(obj, OBJID_PROP, { - value: objid, - configurable: false, - enumerable: false, - writable: false - }); - - Object.defineProperty(obj, FQN_PROP, { - value: fqn, - configurable: false, - enumerable: false, - writable: false - }); - - this.objects[objid] = obj; - return { [TOKEN_REF]: objid }; - } - - private _findObject(objref: api.ObjRef) { - if (typeof(objref) !== 'object' || !(TOKEN_REF in objref)) { - throw new Error(`Malformed object reference: ${JSON.stringify(objref)}`); - } - - const objid = objref[TOKEN_REF]; - this._debug('findObject', objid); - const obj = this.objects[objid]; - if (!obj) { - throw new Error(`Object ${objid} not found`); - } - return obj; - } - private _typeInfoForFqn(fqn: string): spec.Type { const components = fqn.split('.'); const moduleName = components[0]; @@ -876,206 +858,77 @@ export class Kernel { return typeInfo; } - private _toSandbox(v: any): any { - // undefined - if (typeof v === 'undefined') { - return undefined; - } - - // null is treated as "undefined" because most languages do not have this distinction - // see awslabs/aws-cdk#157 and awslabs/jsii#282 - if (v === null) { - return undefined; - } + private _toSandbox(v: any, expectedType: CompleteTypeReference): any { + const serTypes = serializationType(expectedType, this._typeInfoForFqn.bind(this)); + this._debug('toSandbox', v, JSON.stringify(serTypes)); - // pointer - if (typeof v === 'object' && TOKEN_REF in v) { - return this._findObject(v); - } - - // date - if (typeof v === 'object' && TOKEN_DATE in v) { - this._debug('Found date:', v); - return new Date(v[TOKEN_DATE]); - } - - // enums - if (typeof v === 'object' && TOKEN_ENUM in v) { - this._debug('Enum:', v); - - const value = v[TOKEN_ENUM] as string; - const sep = value.lastIndexOf('/'); - if (sep === -1) { - throw new Error(`Malformed enum value: ${v[TOKEN_ENUM]}`); - } - - const typeName = value.substr(0, sep); - const valueName = value.substr(sep + 1); - - const enumValue = this._findSymbol(typeName)[valueName]; - if (enumValue === undefined) { - throw new Error(`No enum member named ${valueName} in ${typeName}`); - } - - this._debug('resolved enum value:', enumValue); - return enumValue; - } - - // array - if (Array.isArray(v)) { - return v.map(x => this._toSandbox(x)); - } - - // map - if (typeof v === 'object') { - const out: any = { }; - for (const k of Object.keys(v)) { - const value = this._toSandbox(v[k]); - - // javascript has a fun behavior where - // { ...{ x: 'hello' }, ...{ x: undefined } } - // will result in: - // { x: undefined } - // so omit any keys that have an `undefined` values. - // see awslabs/aws-cdk#965 and compliance test "mergeObjects" - if (value === undefined) { - continue; - } + const host: SerializerHost = { + objects: this.objects, + debug: this._debug.bind(this), + findSymbol: this._findSymbol.bind(this), + lookupType: this._typeInfoForFqn.bind(this), + recurse: this._toSandbox.bind(this), + }; - out[k] = value; + const errors = new Array(); + for (const { serializationClass, typeRef } of serTypes) { + try { + return SERIALIZERS[serializationClass].deserialize(v, typeRef, host); + } catch (e) { + // If no union (99% case), rethrow immediately to preserve stack trace + if (serTypes.length === 1) { throw e; } + errors.push(e.message); } - return out; } - // primitive - return v; + throw new Error(`Value did not match any type in union: ${errors}`); } - private _fromSandbox(v: any, targetType?: spec.TypeReference): any { - this._debug('fromSandbox', v, targetType); + private _fromSandbox(v: any, targetType: CompleteTypeReference): any { + const serTypes = serializationType(targetType, this._typeInfoForFqn.bind(this)); + this._debug('fromSandbox', v, JSON.stringify(serTypes)); - // undefined is returned as null: true - if (typeof(v) === 'undefined') { - return undefined; - } - - if (v === null) { - return undefined; - } - - // existing object - const objid = v[OBJID_PROP]; - if (objid) { - // object already has an objid, return it as a ref. - this._debug('objref exists', objid); - return { [TOKEN_REF]: objid }; - } - - // new object - if (typeof(v) === 'object' && v.constructor.__jsii__) { - // this is jsii object which was created inside the sandbox and still doesn't - // have an object id, so we need to allocate one for it. - this._debug('creating objref for', v); - const fqn = this._fqnForObject(v); - if (!targetType || !spec.isNamedTypeReference(targetType) || this._isAssignable(fqn, targetType)) { - return this._createObjref(v, fqn); - } - } + const host: SerializerHost = { + objects: this.objects, + debug: this._debug.bind(this), + findSymbol: this._findSymbol.bind(this), + lookupType: this._typeInfoForFqn.bind(this), + recurse: this._fromSandbox.bind(this), + }; - // if the method/property returns an object literal and the return type - // is a class, we create a new object based on the fqn and assign all keys. - // so the client receives a real object. - if (typeof(v) === 'object' && targetType && spec.isNamedTypeReference(targetType)) { - this._debug('coalescing to', targetType); - /* - * We "cache" proxy instances in [PROXIES_PROP] so we can return an - * identical object reference upon multiple accesses of the same - * object literal under the same exposed type. This results in a - * behavior that is more consistent with class instances. - */ - const proxies: Proxies = v[PROXIES_PROP] = v[PROXIES_PROP] || {}; - if (!proxies[targetType.fqn]) { - const handler = new KernelProxyHandler(v); - const proxy = new Proxy(v, handler); - // _createObjref will set the FQN_PROP & OBJID_PROP on the proxy. - proxies[targetType.fqn] = { objRef: this._createObjref(proxy, targetType.fqn), handler }; + const errors = new Array(); + for (const { serializationClass, typeRef } of serTypes) { + try { + return SERIALIZERS[serializationClass].serialize(v, typeRef, host); + } catch (e) { + // If no union (99% case), rethrow immediately to preserve stack trace + if (serTypes.length === 1) { throw e; } + errors.push(e.message); } - return proxies[targetType.fqn].objRef; } - // date (https://stackoverflow.com/a/643827/737957) - if (typeof(v) === 'object' && Object.prototype.toString.call(v) === '[object Date]') { - this._debug('date', v); - return { [TOKEN_DATE]: v.toISOString() }; - } - - // array - if (Array.isArray(v)) { - this._debug('array', v); - return v.map(x => this._fromSandbox(x)); - } - - if (targetType && spec.isNamedTypeReference(targetType)) { - const propType = this._typeInfoForFqn(targetType.fqn); - - // enum - if (propType.kind === spec.TypeKind.Enum) { - this._debug('enum', v); - const fqn = propType.fqn; - - const valueName = this._findSymbol(fqn)[v]; - - return { [TOKEN_ENUM]: `${propType.fqn}/${valueName}` }; - } - - } + throw new Error(`Value did not match any type in union: ${errors}`); + } - // map - if (typeof(v) === 'object') { - this._debug('map', v); - const out: any = { }; - for (const k of Object.keys(v)) { - const value = this._fromSandbox(v[k]); - if (value === undefined) { - continue; - } - out[k] = value; - } - return out; - } + private _toSandboxValues(xs: any[], parameters?: spec.Parameter[]) { + return this._boxUnboxParameters(xs, parameters, this._toSandbox.bind(this)); + } - // primitive - this._debug('primitive', v); - return v; + private _fromSandboxValues(xs: any[], parameters?: spec.Parameter[]) { + return this._boxUnboxParameters(xs, parameters, this._fromSandbox.bind(this)); } - /** - * Tests whether a given type (by it's FQN) can be assigned to a named type reference. - * - * @param actualTypeFqn the FQN of the type that is being tested. - * @param requiredType the required reference type. - * - * @returns true if ``requiredType`` is a super-type (base class or implemented interface) of the type designated by - * ``actualTypeFqn``. - */ - private _isAssignable(actualTypeFqn: string, requiredType: spec.NamedTypeReference): boolean { - if (requiredType.fqn === actualTypeFqn) { - return true; + private _boxUnboxParameters(xs: any[], parameters: spec.Parameter[] | undefined, boxUnbox: (x: any, t: CompleteTypeReference) => any) { + parameters = parameters || []; + const types = parameters.map(p => p.type); + // Repeat the last (variadic) type to match the number of actual arguments + while (types.length < xs.length && parameters.length > 0 && parameters[parameters.length - 1].variadic) { + types.push(types[types.length - 1]); } - const actualType = this._typeInfoForFqn(actualTypeFqn); - if (spec.isClassType(actualType) && actualType.base) { - if (this._isAssignable(actualType.base.fqn, requiredType)) { - return true; - } - } - if (spec.isClassOrInterfaceType(actualType) && actualType.interfaces) { - return actualType.interfaces.find(iface => this._isAssignable(iface.fqn, requiredType)) != null; + if (xs.length > types.length) { + throw new Error(`Argument list (${JSON.stringify(xs)}) not same size as expected argument list (length ${types.length})`); } - return false; - } - - private _toSandboxValues(args: any[]) { - return args.map(v => this._toSandbox(v)); + return xs.map((x, i) => boxUnbox(x, types[i])); } private _debug(...args: any[]) { @@ -1083,8 +936,7 @@ export class Kernel { // tslint:disable-next-line:no-console console.error.apply(console, [ '[jsii-kernel]', - args[0], - ...args.slice(1) + ...args ]); } } @@ -1117,22 +969,6 @@ export class Kernel { // type information // - private _fqnForObject(obj: any) { - if (FQN_PROP in obj) { - return obj[FQN_PROP]; - } - - if (!obj.constructor.__jsii__) { - throw new Error('No jsii type info for object'); - } - - return obj.constructor.__jsii__.fqn; - } - - private _mkobjid(fqn: string) { - return `${fqn}@${this.nextid++}`; - } - private _makecbid() { return `jsii::callback::${this.nextid++}`; } @@ -1171,8 +1007,9 @@ export class Kernel { interface Callback { objref: api.ObjRef; - override: api.Override; + override: api.MethodOverride; args: any[]; + expectedReturnType: CompleteTypeReference; // completion callbacks succeed: (...args: any[]) => any; @@ -1237,110 +1074,4 @@ function mapSource(err: Error, sourceMaps: { [assm: string]: SourceMapConsumer } } } -type ObjectKey = string | number | symbol; -/** - * A Proxy handler class to support mutation of the returned object literals, as - * they may "embody" several different interfaces. The handler is in particular - * responsible to make sure the ``FQN_PROP`` and ``OBJID_PROP`` do not get set - * on the ``referent`` object, for this would cause subsequent accesses to - * possibly return incorrect object references. - */ -class KernelProxyHandler implements ProxyHandler { - private readonly ownProperties: { [key: string]: any } = {}; - - /** - * @param referent the "real" value that will be returned. - */ - constructor(public readonly referent: any) { - /* - * Proxy-properties must exist as non-configurable & writable on the - * referent, otherwise the Proxy will not allow returning ``true`` in - * response to ``defineProperty``. - */ - for (const prop of [FQN_PROP, OBJID_PROP]) { - Object.defineProperty(referent, prop, { - configurable: false, - enumerable: false, - writable: true, - value: undefined - }); - } - } - - public defineProperty(target: any, property: ObjectKey, attributes: PropertyDescriptor): boolean { - switch (property) { - case FQN_PROP: - case OBJID_PROP: - return Object.defineProperty(this.ownProperties, property, attributes); - default: - return Object.defineProperty(target, property, attributes); - } - } - - public deleteProperty(target: any, property: ObjectKey): boolean { - switch (property) { - case FQN_PROP: - case OBJID_PROP: - delete this.ownProperties[property]; - break; - default: - delete target[property]; - } - return true; - } - - public getOwnPropertyDescriptor(target: any, property: ObjectKey): PropertyDescriptor | undefined { - switch (property) { - case FQN_PROP: - case OBJID_PROP: - return Object.getOwnPropertyDescriptor(this.ownProperties, property); - default: - return Object.getOwnPropertyDescriptor(target, property); - } - } - - public get(target: any, property: ObjectKey): any { - switch (property) { - // Magical property for the proxy, so we can tell it's one... - case PROXY_REFERENT_PROP: - return this.referent; - case FQN_PROP: - case OBJID_PROP: - return this.ownProperties[property]; - default: - return target[property]; - } - } - - public set(target: any, property: ObjectKey, value: any): boolean { - switch (property) { - case FQN_PROP: - case OBJID_PROP: - this.ownProperties[property] = value; - break; - default: - target[property] = value; - } - return true; - } - - public has(target: any, property: ObjectKey): boolean { - switch (property) { - case FQN_PROP: - case OBJID_PROP: - return property in this.ownProperties; - default: - return property in target; - } - } - - public ownKeys(target: any): ObjectKey[] { - return Reflect.ownKeys(target).concat(Reflect.ownKeys(this.ownProperties)); - } -} - -type Proxies = { [fqn: string]: ProxyReference }; -interface ProxyReference { - objRef: api.ObjRef; - handler: KernelProxyHandler; -} +const ANY_TYPE: spec.PrimitiveTypeReference = { primitive: spec.PrimitiveType.Any }; \ No newline at end of file diff --git a/packages/jsii-kernel/lib/objects.ts b/packages/jsii-kernel/lib/objects.ts new file mode 100644 index 0000000000..0801b1d1e6 --- /dev/null +++ b/packages/jsii-kernel/lib/objects.ts @@ -0,0 +1,135 @@ +import * as api from './api'; + +/** + * Symbol under which we store the { type -> objid } map on object instances + */ +const OBJID_SYMBOL = Symbol('$__jsii__objid__$'); + +/** + * Symbol we use to tag the constructor of a JSII class + */ +const JSII_SYMBOL = Symbol('__jsii__'); + +/** + * Get the JSII fqn for an object (if available) + * + * This will return something if the object was constructed from a JSII-enabled + * class/constructor, or if a literal object was annotated with type + * information. + */ +export function jsiiTypeFqn(obj: any): string | undefined { + const jsii = obj.constructor[JSII_SYMBOL]; + return jsii && jsii.fqn; +} + +/** + * If this object was previously serialized under a given reference, return the same reference + * + * This is to retain object identity across invocations. + */ +export function objectReference(obj: object): api.ObjRef | undefined { + // If this object as already returned + if ((obj as any)[OBJID_SYMBOL]) { + return { [api.TOKEN_REF]: (obj as any)[OBJID_SYMBOL] }; + } + + return undefined; +} + +function tagObject(obj: object, objid: string) { + (obj as any)[OBJID_SYMBOL] = objid; +} + +/** + * Ensure there's a hidden map with the given symbol name on the given object, and return it + */ +export function hiddenMap(obj: any, mapSymbol: symbol): {[key: string]: T} { + let map: any = obj[mapSymbol]; + if (!map) { + map = {}; + Object.defineProperty(obj, mapSymbol, { + value: map, + configurable: false, + enumerable: false, + writable: false + }); + } + return map; +} + +/** + * Set the JSII FQN for classes produced by a given constructor + */ +export function tagJsiiConstructor(constructor: any, fqn: string) { + Object.defineProperty(constructor, JSII_SYMBOL, { + configurable: false, + enumerable: false, + writable: false, + value: { fqn } + }); +} + +/** + * Table of JSII objects + * + * There can be multiple references to the same object, each under a different requested + * type. + */ +export class ObjectTable { + private objects: { [objid: string]: RegisteredObject } = { }; + private nextid = 10000; + + /** + * Register the given object with the given type + * + * Return the existing registration if available. + */ + public registerObject(obj: object, fqn: string): api.ObjRef { + if (fqn === undefined) { + throw new Error('FQN cannot be undefined'); + } + + const objid = this.makeId(fqn); + this.objects[objid] = { instance: obj, fqn }; + tagObject(obj, objid); + + return { [api.TOKEN_REF]: objid }; + } + + /** + * Find the object and registered type for the given ObjRef + */ + public findObject(objref: api.ObjRef): RegisteredObject { + if (typeof(objref) !== 'object' || !(api.TOKEN_REF in objref)) { + throw new Error(`Malformed object reference: ${JSON.stringify(objref)}`); + } + + const objid = objref[api.TOKEN_REF]; + const obj = this.objects[objid]; + if (!obj) { + throw new Error(`Object ${objid} not found`); + } + return obj; + } + + /** + * Delete the registration with the given objref + */ + public deleteObject(objref: api.ObjRef) { + this.findObject(objref); // make sure object exists + delete this.objects[objref[api.TOKEN_REF]]; + } + + public get count(): number { + return Object.keys(this.objects).length; + } + + private makeId(fqn: string) { + return `${fqn}@${this.nextid++}`; + } +} + +export interface RegisteredObject { + instance: any; + fqn: string; +} \ No newline at end of file diff --git a/packages/jsii-kernel/lib/serialization.ts b/packages/jsii-kernel/lib/serialization.ts new file mode 100644 index 0000000000..3d9861b204 --- /dev/null +++ b/packages/jsii-kernel/lib/serialization.ts @@ -0,0 +1,622 @@ + + // tslint:disable:max-line-length +/** + * Handling of types in JSII + * + * Types will be serialized according to the following table: + * + * ┬───────────────────────────────────────────────────────────────────────────────────────────────┐ + * │ JAVASCRIPT TYPE │ + * ┼────────────────┬───────────┬────────────┬───────────────┬───────────────────┬─────────────────┤ + * │ undefined/null │ date │ scalar (*) │ array │ JSII-class object │ literal object │ + * ├──────────┼────────────┼────────────────┼───────────┼────────────┼───────────────┼───────────────────┼─────────────────┤ + * │ DECLARED │ void │ undefined │ undefined │ undefined │ undefined │ undefined │ undefined │ + * │ TYPE │ date │ undefined(†) │ { date } │ - │ - │ - │ - │ + * │ │ scalar (*) │ undefined(†) │ - │ value │ - │ - │ - │ + * │ │ json │ undefined │ string │ value │ array/R(json) │ - │ byvalue/R(json) │ + * │ │ enum │ undefined(†) │ - │ { enum } │ - │ - │ - │ + * │ │ array of T │ undefined(†) │ - │ - │ array/R(T) │ - │ - │ + * │ │ map of T │ undefined(†) │ - │ - │ - │ - │ byvalue/R(T) │ + * │ │ interface │ undefined(†) │ - │ - │ - │ { ref } │ { ref: proxy } │ + * │ │ struct │ undefined(†) │ - │ - │ - │ - │ byvalue/R(T[k]) │ + * │ │ class │ undefined(†) │ - │ - │ - │ { ref } │ { ref: proxy } │ + * │ │ any │ undefined │ { date } │ value │ array/R(any) │ { ref } │ byvalue/R(any) │ + * └──────────┴────────────┴────────────────┴───────────┴────────────┴───────────────┴───────────────────┴─────────────────┘ + * + * - (*) scalar means 'string | number | boolean' + * - (†) throw if not nullable + * - /R(t) recurse with declared type t + */ + + // tslint:enable:max-line-length + +import * as spec from 'jsii-spec'; +import { isObjRef, isWireDate, isWireEnum, ObjRef, TOKEN_DATE, TOKEN_ENUM, WireDate, WireEnum } from './api'; +import { hiddenMap, jsiiTypeFqn, objectReference, ObjectTable } from './objects'; + +/** + * A specific singleton type to be explicit about a Void type + * + * In the spec, 'void' is represented as 'undefined'(*), but allowing the + * value 'undefined' in function calls has lead to consumers failing to pass + * type information that they had, just because they didn't "have to" (the + * parameter was optional). + * + * (*) As in, declaration of a method looks like { returns?: TypeReference } + * and the absence of a type means it returns 'void'. + */ +export type Void = 'void'; + +/** + * A type reference that includes the special type reference Void + */ +export type CompleteTypeReference = spec.TypeReference | Void; + +/** + * A special FQN that can be used to create empty javascript objects. + */ +export const EMPTY_OBJECT_FQN = 'Object'; + +/** + * The type kind, that controls how it will be serialized according to the above table + */ +export const enum SerializationClass { + Void = 'Void', + Date = 'Date', + Scalar = 'Scalar', + Json = 'Json', + Enum = 'Enum', + Array = 'Array', + Map = 'Map', + Struct = 'Struct', + ReferenceType = 'RefType', + Any = 'Any', +} + +type TypeLookup = (fqn: string) => spec.Type; +type SymbolLookup = (fqn: string) => any; + +export interface SerializerHost { + readonly objects: ObjectTable; + debug(...args: any[]): void; + lookupType(fqn: string): spec.Type; + recurse(x: any, type: CompleteTypeReference): any; + findSymbol(fqn: string): any; +} + +interface Serializer { + serialize(value: unknown, type: CompleteTypeReference, host: SerializerHost): any; + deserialize(value: unknown, type: CompleteTypeReference, host: SerializerHost): any; +} + +export const SERIALIZERS: {[k: string]: Serializer} = { + // ---------------------------------------------------------------------- + [SerializationClass.Void]: { + serialize(value, _type, host) { + if (value != null) { + host.debug('Expected void, got', value); + } + return undefined; + }, + + deserialize(value, _type, host) { + if (value != null) { + host.debug('Expected void, got', value); + } + return undefined; + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Date]: { + serialize(value, type): WireDate | undefined { + if (nullAndOk(value, type)) { return undefined; } + + if (!isDate(value)) { + throw new Error(`Expected Date, got ${JSON.stringify(value)}`); + } + return serializeDate(value); + }, + + deserialize(value, type) { + if (nullAndOk(value, type)) { return undefined; } + + if (!isWireDate(value)) { + throw new Error(`Expected Date, got ${JSON.stringify(value)}`); + } + return deserializeDate(value); + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Scalar]: { + serialize(value, type) { + if (nullAndOk(value, type)) { return undefined; } + + const primitiveType = type as spec.PrimitiveTypeReference; + + if (!isScalar(value)) { + throw new Error(`Expected Scalar, got ${JSON.stringify(value)}`); + } + if (typeof value !== primitiveType.primitive) { + throw new Error(`Expected '${primitiveType.primitive}', got ${JSON.stringify(value)} (${typeof value})`); + } + return value; + }, + + deserialize(value, type) { + if (nullAndOk(value, type)) { return undefined; } + + const primitiveType = type as spec.PrimitiveTypeReference; + + if (!isScalar(value)) { + throw new Error(`Expected Scalar, got ${JSON.stringify(value)}`); + } + if (typeof value !== primitiveType.primitive) { + throw new Error(`Expected '${primitiveType.primitive}', got ${JSON.stringify(value)} (${typeof value})`); + } + + return value; + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Json]: { + serialize(value) { + // Just whatever. Dates will automatically serialize themselves to strings. + return value; + }, + deserialize(value) { + return value; + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Enum]: { + serialize(value, type, host): WireEnum | undefined { + host.debug('hullo'); + if (nullAndOk(value, type)) { return undefined; } + + if (typeof value !== 'string' && typeof value !== 'number') { + throw new Error(`Expected enum value, got ${JSON.stringify(value)}`); + } + + host.debug('Serializing enum'); + + const enumType = type as spec.EnumType; + return { [TOKEN_ENUM]: `${enumType.fqn}/${host.findSymbol(enumType.fqn)[value]}` }; + }, + deserialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + if (!isWireEnum(value)) { + throw new Error(`Expected enum value, got ${JSON.stringify(value)}`); + } + + return deserializeEnum(value, host.findSymbol); + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Array]: { + serialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + if (!Array.isArray(value)) { + throw new Error(`Expected array type, got ${JSON.stringify(value)}`); + } + + const arrayType = type as spec.CollectionTypeReference; + + return value.map(x => host.recurse(x, arrayType.collection.elementtype)); + }, + deserialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + if (!Array.isArray(value)) { + throw new Error(`Expected array type, got ${JSON.stringify(value)}`); + } + + const arrayType = type as spec.CollectionTypeReference; + + return value.map(x => host.recurse(x, arrayType.collection.elementtype)); + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Map]: { + serialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + const mapType = type as spec.CollectionTypeReference; + return mapValues(value, v => host.recurse(v, mapType.collection.elementtype)); + }, + deserialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + const mapType = type as spec.CollectionTypeReference; + return mapValues(value, v => host.recurse(v, mapType.collection.elementtype)); + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Struct]: { + serialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + if (typeof value !== 'object' || value == null) { + throw new Error(`Expected object, got ${JSON.stringify(value)}`); + } + + // This looks odd, but if an object was originally passed in as a by-ref + // class, and it happens to conform to a datatype interface we say we're + // returning, return the actual object instead of the serialized value. + // NOTE: Not entirely sure yet whether this is a bug masquerading as a + // feature or not. + const prevRef = objectReference(value); + if (prevRef) { return prevRef; } + + /* + This is what we'd like to do, but we can't because at least the Java client + does not understand by-value serialized interface types, so we'll have to + serialize by-reference for now: + https://github.com/awslabs/jsii/issues/400 + + const props = propertiesOf(namedType); + + return mapValues(value, (v, key) => { + if (!props[key]) { return undefined; } // Don't map if unknown property + return host.recurse(v, props[key].type); + }); + */ + + host.debug('Returning value type as reference type for now (awslabs/jsii#400)'); + const wireFqn = selectWireType(value, type as spec.NamedTypeReference, host.lookupType); + return host.objects.registerObject(value, wireFqn); + }, + deserialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + if (typeof value !== 'object' || value == null) { + throw new Error(`Expected object reference, got ${JSON.stringify(value)}`); + } + + // Similarly to other end, we might be getting a reference type where we're + // expecting a value type. Accept this for now. + const prevRef = objectReference(value); + if (prevRef) { + host.debug('Expected value type but got reference type, accepting for now (awslabs/jsii#400)'); + return prevRef; + } + + const namedType = host.lookupType((type as spec.NamedTypeReference).fqn); + const props = propertiesOf(namedType); + + return mapValues(value, (v, key) => { + if (!props[key]) { return undefined; } // Don't map if unknown property + return host.recurse(v, props[key].type); + }); + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.ReferenceType]: { + serialize(value, type, host): ObjRef | undefined { + if (nullAndOk(value, type)) { return undefined; } + + if (typeof value !== 'object' || value == null) { + throw new Error(`Expected object reference, got ${JSON.stringify(value)}`); + } + + const prevRef = objectReference(value); + if (prevRef) { return prevRef; } + + const wireFqn = selectWireType(value, type as spec.NamedTypeReference, host.lookupType); + return host.objects.registerObject(value, wireFqn); + }, + deserialize(value, type, host) { + if (nullAndOk(value, type)) { return undefined; } + + // The only way to pass a by-ref object is to have created it + // previously inside JSII kernel, so it must have an objref already. + + if (!isObjRef(value)) { + throw new Error(`Expected object reference, got ${JSON.stringify(value)}`); + } + + const { instance, fqn } = host.objects.findObject(value); + + const namedTypeRef = type as spec.NamedTypeReference; + if (namedTypeRef.fqn !== EMPTY_OBJECT_FQN) { + const namedType = host.lookupType(namedTypeRef.fqn); + + // Check that the object we got is of the right type + // We only do this for classes, not interfaces, since Java might pass us objects that + // privately implement some interface and we can't prove they don't. + // https://github.com/awslabs/jsii/issues/399 + if (spec.isClassType(namedType) && !isAssignable(fqn, type as spec.NamedTypeReference, host.lookupType)) { + throw new Error(`Object of type ${fqn} is not convertible to ${(type as spec.NamedTypeReference).fqn}`); + } + } + + return instance; + }, + }, + + // ---------------------------------------------------------------------- + [SerializationClass.Any]: { + serialize(value, _type, host) { + if (value == null) { return undefined; } + + if (isDate(value)) { return serializeDate(value); } + if (isScalar(value)) { return value; } + if (Array.isArray(value)) { + return value.map(e => host.recurse(e, { primitive: spec.PrimitiveType.Any })); + } + + // Note: no case for "ENUM" here, without type declaration we can't tell the difference + // between an enum member and a scalar. + + if (typeof value !== 'object' || value == null) { + throw new Error(`JSII kernel assumption violated, ${JSON.stringify(value)} is not an object`); + } + + // To make sure people aren't going to try and return Map<> or Set<> out, test for + // those and throw a descriptive error message. We can't detect these cases any other + // way, and the by-value serialized object will be quite useless. + if (value instanceof Set || value instanceof Map) { throw new Error(`Can't return objects of type Set or Map`); } + + // Use a previous reference to maintain object identity. NOTE: this may cause us to return + // a different type than requested! This is just how it is right now. + // https://github.com/awslabs/jsii/issues/399 + const prevRef = objectReference(value); + if (prevRef) { return prevRef; } + + // If this is or should be a reference type, pass or make the reference + // (Like regular reftype serialization, but without the type derivation to an interface) + const jsiiType = jsiiTypeFqn(value); + if (jsiiType) { return host.objects.registerObject(value, jsiiType); } + + // At this point we have an object that is not of an exported type. Either an object + // literal, or an instance of a fully private class (cannot distinguish those cases). + + // We will serialize by-value, but recurse for serialization so that if + // the object contains reference objects, they will be serialized appropriately. + // (Basically, serialize anything else as a map of 'any'). + return mapValues(value, (v) => host.recurse(v, { primitive: spec.PrimitiveType.Any })); + }, + + deserialize(value, _type, host) { + if (value == null) { return undefined; } + + if (isWireDate(value)) { + host.debug('ANY is a Date'); + return deserializeDate(value); + } + if (isScalar(value)) { + host.debug('ANY is a Scalar'); + return value; + } + if (Array.isArray(value)) { + host.debug('ANY is an Array'); + return value.map(e => host.recurse(e, { primitive: spec.PrimitiveType.Any })); + } + + if (isWireEnum(value)) { + host.debug('ANY is an Enum'); + return deserializeEnum(value, host.findSymbol); + } + if (isObjRef(value)) { + host.debug('ANY is a Ref'); + return host.objects.findObject(value).instance; + } + + // At this point again, deserialize by-value. + host.debug('ANY is a Map'); + return mapValues(value, (v) => host.recurse(v, { primitive: spec.PrimitiveType.Any })); + }, + }, +}; + +function serializeDate(value: Date): WireDate { + return { [TOKEN_DATE]: value.toISOString() }; +} + +function deserializeDate(value: WireDate): Date { + return new Date(value[TOKEN_DATE]); +} + +function deserializeEnum(value: WireEnum, lookup: SymbolLookup) { + const enumLocator = value[TOKEN_ENUM] as string; + const sep = enumLocator.lastIndexOf('/'); + if (sep === -1) { + throw new Error(`Malformed enum value: ${JSON.stringify(value)}`); + } + + const typeName = enumLocator.substr(0, sep); + const valueName = enumLocator.substr(sep + 1); + + const enumValue = lookup(typeName)[valueName]; + if (enumValue === undefined) { + throw new Error(`No enum member named ${valueName} in ${typeName}`); + } + return enumValue; +} + +export interface TypeSerialization { + serializationClass: SerializationClass; + typeRef: CompleteTypeReference; +} + +/** + * From a type reference, return the possible serialization types + * + * There can be multiple, because the type can be a type union. + */ +export function serializationType(typeRef: CompleteTypeReference, lookup: TypeLookup): TypeSerialization[] { + if (typeRef == null) { throw new Error(`Kernel error: expected type information, got 'undefined'`); } + if (typeRef === 'void') { return [{ serializationClass: SerializationClass.Void, typeRef }]; } + if (spec.isPrimitiveTypeReference(typeRef)) { + switch (typeRef.primitive) { + case spec.PrimitiveType.Any: return [{ serializationClass: SerializationClass.Any, typeRef }]; + case spec.PrimitiveType.Date: return [{ serializationClass: SerializationClass.Date, typeRef }]; + case spec.PrimitiveType.Json: return [{ serializationClass: SerializationClass.Json, typeRef }]; + case spec.PrimitiveType.Boolean: + case spec.PrimitiveType.Number: + case spec.PrimitiveType.String: + return [{ serializationClass: SerializationClass.Scalar, typeRef }]; + } + + throw new Error('Unknown primitive type'); + } + if (spec.isCollectionTypeReference(typeRef)) { + return [{ + serializationClass: typeRef.collection.kind === spec.CollectionKind.Array ? SerializationClass.Array : SerializationClass.Map, + typeRef + }]; + } + if (spec.isUnionTypeReference(typeRef)) { + const compoundTypes = flatMap(typeRef.union.types, t => serializationType(t, lookup)); + // Propagate the top-level 'optional' field to each individual subtype + for (const t of compoundTypes) { + if (t.typeRef !== 'void') { + t.typeRef.optional = typeRef.optional; + } + } + return compoundTypes; + } + + // The next part of the conversion is lookup-dependent + const type = lookup(typeRef.fqn); + + if (spec.isEnumType(type)) { + return [{ serializationClass: SerializationClass.Enum, typeRef }]; + } + + if (spec.isInterfaceType(type) && type.datatype) { + return [{ serializationClass: SerializationClass.Struct, typeRef }]; + } + + return [{ serializationClass: SerializationClass.ReferenceType, typeRef }]; +} + +function nullAndOk(x: unknown, type: CompleteTypeReference): boolean { + if (x != null) { return false; } + + if (type !== 'void' && !type.optional) { + throw new Error(`Got 'undefined' for non-nullable type ${JSON.stringify(type)}`); + } + + return true; +} + +function isDate(x: unknown): x is Date { + return typeof x === 'object' && Object.prototype.toString.call(x) === '[object Date]'; +} + +function isScalar(x: unknown): x is string | number | boolean { + return typeof x === 'string' || typeof x === 'number' || typeof x === 'boolean'; +} + +function flatMap(xs: T[], fn: (x: T) => U[]): U[] { + const ret = new Array(); + for (const x of xs) { ret.push(...fn(x)); } + return ret; +} + +/** + * Map an object's values, skipping 'undefined' values' + */ +function mapValues(value: unknown, fn: (value: any, field: string) => any) { + if (typeof value !== 'object' || value == null) { + throw new Error(`Expected object type, got ${JSON.stringify(value)}`); + } + + const out: any = { }; + for (const [k, v] of Object.entries(value)) { + const wireValue = fn(v, k); + if (wireValue === undefined) { continue; } + out[k] = wireValue; + } + return out; +} + +function propertiesOf(t: spec.Type): {[name: string]: spec.Property} { + if (!spec.isClassOrInterfaceType(t)) { return {}; } + + const ret: {[name: string]: spec.Property} = {}; + for (const prop of t.properties || []) { + ret[prop.name] = prop; + } + return ret; +} + +const WIRE_TYPE_MAP = Symbol('$__jsii_wire_type__$'); + +/** + * Select the wire type for the given object and requested type + * + * Should return the most specific type that is in the JSII assembly and + * assignable to the required type. + * + * We actually don't need to search much; because of prototypal constructor + * linking, object.constructor.__jsii__ will have the FQN of the most specific + * exported JSII class this object is an instance of. + * + * Either that's assignable to the requested type, in which case we return it, + * or it's not, in which case there's a hidden class that implements the interface + * and we just return the interface so the other side can instantiate an interface + * proxy for it. + * + * Cache the analysis on the object to avoid having to do too many searches through + * the type system for repeated accesses on the same object. + */ +function selectWireType(obj: any, expectedType: spec.NamedTypeReference, lookup: TypeLookup): string { + const map = hiddenMap(obj, WIRE_TYPE_MAP); + + if (!(expectedType.fqn in map)) { + const jsiiType = jsiiTypeFqn(obj); + if (jsiiType) { + const assignable = isAssignable(jsiiType, expectedType, lookup); + + // If we're not assignable and both types are class types, this cannot be satisfied. + if (!assignable && spec.isClassType(lookup(expectedType.fqn))) { + throw new Error(`Object of type ${jsiiType} is not convertible to ${expectedType.fqn}`); + } + + map[expectedType.fqn] = assignable ? jsiiType : expectedType.fqn; + } else { + map[expectedType.fqn] = expectedType.fqn; + } + } + + return map[expectedType.fqn]; +} + +/** + * Tests whether a given type (by it's FQN) can be assigned to a named type reference. + * + * @param actualTypeFqn the FQN of the type that is being tested. + * @param requiredType the required reference type. + * + * @returns true if ``requiredType`` is a super-type (base class or implemented interface) of the type designated by + * ``actualTypeFqn``. + */ +function isAssignable(actualTypeFqn: string, requiredType: spec.NamedTypeReference, lookup: TypeLookup): boolean { + // The empty object is assignable to everything + if (actualTypeFqn === EMPTY_OBJECT_FQN) { return true; } + + if (requiredType.fqn === actualTypeFqn) { + return true; + } + const actualType = lookup(actualTypeFqn); + if (spec.isClassType(actualType)) { + if (actualType.base && isAssignable(actualType.base.fqn, requiredType, lookup)) { + return true; + } + } + if (spec.isClassOrInterfaceType(actualType) && actualType.interfaces) { + return actualType.interfaces.find(iface => isAssignable(iface.fqn, requiredType, lookup)) != null; + } + return false; +} \ No newline at end of file diff --git a/packages/jsii-kernel/test/test.kernel.ts b/packages/jsii-kernel/test/test.kernel.ts index 730911a589..fe81bb4de3 100644 --- a/packages/jsii-kernel/test/test.kernel.ts +++ b/packages/jsii-kernel/test/test.kernel.ts @@ -5,7 +5,7 @@ import { join } from 'path'; import path = require('path'); import vm = require('vm'); import { api, Kernel } from '../lib'; -import { Callback, TOKEN_REF } from '../lib/api'; +import { Callback, ObjRef, TOKEN_REF } from '../lib/api'; import { closeRecording, recordInteraction } from './recording'; // extract versions of fixtures @@ -18,6 +18,9 @@ const calcVersion = require('jsii-calc/package.json').version.replace(/\+.+$/, ' // tslint:disable:no-console // tslint:disable:max-line-length +// Do this so that regexes stringify nicely in approximate tests +(RegExp.prototype as any).toJSON = function() { return this.source; }; + process.setMaxListeners(9999); // since every kernel instance adds an `on('exit')` handler. process.on('unhandledRejection', e => { @@ -124,9 +127,14 @@ defineTest('in/out primitive types', async (test, sandbox) => { sandbox.set({ objref: alltypes, property: 'numberProperty', value: 123 }); test.deepEqual(sandbox.get({ objref: alltypes, property: 'numberProperty' }).value, 123); + // in -> out for an ANY const num = sandbox.create({ fqn: '@scope/jsii-calc-lib.Number', args: [ 444 ] }); sandbox.set({ objref: alltypes, property: 'anyProperty', value: num }); test.deepEqual(sandbox.get({ objref: alltypes, property: 'anyProperty' }).value, num); + + // out -> in for an ANY + const ret = sandbox.invoke({ objref: alltypes, method: 'anyOut' }).result; + sandbox.invoke({ objref: alltypes, method: 'anyIn', args: [ret] }); }); defineTest('in/out objects', async (test, sandbox) => { @@ -140,11 +148,19 @@ defineTest('in/out objects', async (test, sandbox) => { defineTest('in/out collections', async (test, sandbox) => { const alltypes = sandbox.create({ fqn: 'jsii-calc.AllTypes', args: [ ] }); - const array = [ 1, 2, 3, 4 ]; + const array = [ '1', '2', '3', '4' ]; sandbox.set({ objref: alltypes, property: 'arrayProperty', value: array }); test.deepEqual(sandbox.get({ objref: alltypes, property: 'arrayProperty' }).value, array); - const map = { a: 12, b: 33, c: 33, d: { e: 123 }}; + const num = create(sandbox, '@scope/jsii-calc-lib.Number'); + + const map = { + a: num(12), + b: num(33), + c: num(33), + d: num(123), + }; + sandbox.set({ objref: alltypes, property: 'mapProperty', value: map }); test.deepEqual(sandbox.get({ objref: alltypes, property: 'mapProperty' }).value, map); }); @@ -292,7 +308,7 @@ defineTest('type-checking: try to create an object from a non-class type', async defineTest('type-checking: argument count in methods and initializers', async (test, sandbox) => { // ctor has one optional argument sandbox.create({ fqn: 'jsii-calc.Calculator' }); - sandbox.create({ fqn: 'jsii-calc.Calculator', args: [ 11 ] }); + sandbox.create({ fqn: 'jsii-calc.Calculator', args: [ {} ] }); // but we expect an error if more arguments are passed test.throws(() => sandbox.create({ fqn: 'jsii-calc.Calculator', args: [ 1, 2, 3 ] }), /Too many arguments/); @@ -305,8 +321,8 @@ defineTest('type-checking: argument count in methods and initializers', async (t test.throws(() => sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [] }), /Not enough arguments/); test.throws(() => sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [ 1 ]}), /Not enough arguments/); sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [ 1, 'hello' ] }); - sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [ 1, 'hello', new Date() ] }); - test.throws(() => sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [ 1, 'hello', new Date(), 'too much' ] }), /Too many arguments/); + sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [ 1, 'hello', { [api.TOKEN_DATE]: new Date().toISOString() } ]}); + test.throws(() => sandbox.invoke({ objref: obj, method: 'methodWithOptionalArguments', args: [ 1, 'hello', { [api.TOKEN_DATE]: new Date().toISOString() }, 'too much' ] }), /Too many arguments/); }); defineTest('verify object literals are converted to real classes', async (test, sandbox) => { @@ -541,6 +557,7 @@ defineTest('sync overrides', async (test, sandbox) => { sandbox.callbackHandler = makeSyncCallbackHandler(callback => { test.equal(callback.invoke!.args![0], 999); called = true; + return callback.invoke!.args![0]; }); sandbox.set({ objref: obj, property: 'callerIsProperty', value: 999 }); @@ -589,7 +606,7 @@ defineTest('sync overrides: properties - readwrite', async (test, sandbox) => { test.deepEqual(value, { result: 'override applied' }); // make sure we can still set the property - sandbox.invoke({ objref: obj, method: 'modifyValueOfTheProperty', args: [ 1234 ] }); + sandbox.invoke({ objref: obj, method: 'modifyValueOfTheProperty', args: [ '1234' ] }); test.deepEqual(setValue, 1234); }); @@ -618,8 +635,8 @@ defineTest('sync overrides: properties - readwrite (backed by functions)', async test.deepEqual(value, { result: 'override applied for otherProperty' }); // make sure we can still set the property - sandbox.invoke({ objref: obj, method: 'modifyOtherProperty', args: [ 778877 ]}); - test.deepEqual(setValue, 778877); + sandbox.invoke({ objref: obj, method: 'modifyOtherProperty', args: [ '778877' ]}); + test.deepEqual(setValue, '778877'); }); defineTest('sync overrides: duplicate overrides for the same property', async (test, sandbox) => { @@ -733,6 +750,8 @@ defineTest('fail to begin async from sync - method', async (test, sandbox) => { const innerObj = sandbox.create({ fqn: 'jsii-calc.AsyncVirtualMethods' }); test.throws(() => sandbox.begin({ objref: innerObj, method: 'callMe' })); called++; + + return 42; // Need a valid return value }); sandbox.invoke({ objref: obj, method: 'callerIsMethod' }); @@ -958,15 +977,41 @@ defineTest('JSII_AGENT is undefined in node.js', async (test, sandbox) => { }); defineTest('ObjRefs are labeled with the "most correct" type', async (test, sandbox) => { - const classRef = sandbox.sinvoke({ fqn: 'jsii-calc.Constructors', method: 'makeClass' }).result as api.ObjRef; - const ifaceRef = sandbox.sinvoke({ fqn: 'jsii-calc.Constructors', method: 'makeInterface' }).result as api.ObjRef; + typeMatches('makeClass', { '$jsii.byref': /^jsii-calc.InbetweenClass@/ }); + typeMatches('makeInterface', { '$jsii.byref': /^jsii-calc.IPublicInterface@/ }); + typeMatches('makeInterface2', { '$jsii.byref': /^jsii-calc.InbetweenClass@/ }); + typeMatches('makeInterfaces', [ { '$jsii.byref': /^jsii-calc.IPublicInterface@/ } ]); + typeMatches('hiddenInterface', { '$jsii.byref': /^jsii-calc.IPublicInterface@/ }); + typeMatches('hiddenInterfaces', [ { '$jsii.byref': /^jsii-calc.IPublicInterface@/ } ]); + typeMatches('hiddenSubInterfaces', [ { '$jsii.byref': /^jsii-calc.IPublicInterface@/ } ]); + + function typeMatches(staticMethod: string, typeSpec: any) { + const ret = sandbox.sinvoke({ fqn: 'jsii-calc.Constructors', method: staticMethod }).result as api.ObjRef; + + test.ok(deepEqualWithRegex(ret, typeSpec), `Constructors.${staticMethod}() => ${JSON.stringify(ret)}, does not match ${JSON.stringify(typeSpec)}`); + } +}); + +/* + +Test currently disabled because we don't have the infrastructure to make it pass. +https://github.com/awslabs/jsii/issues/399 + +defineTest('A single instance can be returned under two types', async (test, sandbox) => { + const singleInstanceTwoTypes = create(sandbox, 'jsii-calc.SingleInstanceTwoTypes')(); + + typeMatches('interface1', { '$jsii.byref': /^jsii-calc.InbetweenClass@/ }); + typeMatches('interface2', { '$jsii.byref': /^jsii-calc.IPublicInterface@/ }); - test.ok(classRef[api.TOKEN_REF].startsWith('jsii-calc.InbetweenClass'), - `${classRef[api.TOKEN_REF]} starts with jsii-calc.InbetweenClass`); - test.ok(ifaceRef[api.TOKEN_REF].startsWith('jsii-calc.IPublicInterface'), - `${ifaceRef[api.TOKEN_REF]} starts with jsii-calc.IPublicInterface`); + function typeMatches(method: string, typeSpec: any) { + const ret = sandbox.invoke({ objref: singleInstanceTwoTypes, method }).result as api.ObjRef; + + test.ok(deepEqualWithRegex(ret, typeSpec), `Constructors.${method}() => ${JSON.stringify(ret)}, does not match ${JSON.stringify(typeSpec)}`); + } }); +*/ + defineTest('toSandbox: "null" in hash values send to JS should be treated as non-existing key', async (test, sandbox) => { const input = { option1: null, option2: 'hello' }; const option1Exists = sandbox.sinvoke({ fqn: 'jsii-calc.EraseUndefinedHashValues', method: 'doesKeyExist', args: [ input, 'option1' ] }); @@ -994,6 +1039,55 @@ defineTest('fromSandbox: "null" in hash values returned from JS erases the key', test.deepEqual(output, { result: { prop2: 'value2' } }); }); +defineTest('calculator can set and retrieve union properties', async (test, sandbox) => { + const calculator = create(sandbox, 'jsii-calc.Calculator')(); + + const mul = create(sandbox, 'jsii-calc.Multiply'); + const num = create(sandbox, '@scope/jsii-calc-lib.Number'); + + sandbox.set({ objref: calculator, property: 'unionProperty', value: mul(num(9), num(3)) }); + + const value = sandbox.invoke({ objref: calculator, method: 'readUnionValue' }).result; + test.equal(27, value); + + const expression = sandbox.get({ objref: calculator, property: 'unionProperty' }).value; + + console.log(expression); + + test.ok(deepEqualWithRegex(expression, { '$jsii.byref': /^jsii-calc.Multiply@/ })); +}); + +defineTest('can set and retrieve union properties', async (test, sandbox) => { + const types = create(sandbox, 'jsii-calc.AllTypes')(); + const typesSet = set(sandbox, types); + const typesGet = get(sandbox, types); + const mul = create(sandbox, 'jsii-calc.Multiply'); + const num = create(sandbox, '@scope/jsii-calc-lib.Number'); + + typesSet('unionProperty', 1234); + test.equal(typesGet('unionProperty'), 1234); + + typesSet('unionProperty', 'Hello'); + test.equal(typesGet('unionProperty'), 'Hello'); + + typesSet('unionProperty', mul(num(2), num(12))); + const mulObj = typesGet('unionProperty'); + test.equal(get(sandbox, mulObj)('value'), 24); + + // Collections + + typesSet('unionMapProperty', { + Foo: num(99), + }); + + typesSet('unionArrayProperty', [ + 123, + num(33), + ]); + const unionArray = typesGet('unionArrayProperty'); + test.equal(get(sandbox, unionArray[1])('value'), 33); +}); + // ================================================================================================= const testNames: { [name: string]: boolean } = { }; @@ -1083,3 +1177,51 @@ function makeSyncCallbackHandler(logic: (callback: Callback) => any) { return result; }; } + +export function deepEqualWithRegex(lvalue: any, rvalue: any): boolean { + if (lvalue === rvalue) { return true; } + if (typeof lvalue === 'string' && rvalue instanceof RegExp) { return rvalue.test(lvalue); } + if (typeof lvalue !== typeof rvalue) { return false; } + if (Array.isArray(lvalue) !== Array.isArray(rvalue)) { return false; } + if (Array.isArray(lvalue) /* && Array.isArray(rvalue) */) { + if (lvalue.length !== rvalue.length) { return false; } + for (let i = 0 ; i < lvalue.length ; i++) { + if (!deepEqualWithRegex(lvalue[i], rvalue[i])) { return false; } + } + return true; + } + if (typeof lvalue === 'object' /* && typeof rvalue === 'object' */) { + if (lvalue === null || rvalue === null) { + // If both were null, they'd have been === + return false; + } + const keys = Object.keys(lvalue); + if (keys.length !== Object.keys(rvalue).length) { return false; } + for (const key of keys) { + if (!rvalue.hasOwnProperty(key)) { return false; } + if (!deepEqualWithRegex(lvalue[key], rvalue[key])) { return false; } + } + return true; + } + // Neither object, nor array: I deduce this is primitive type + // Primitive type and not ===, so I deduce not deepEqual + return false; +} + +function create(kernel: Kernel, fqn: string) { + return (...args: any[]) => { + return kernel.create({ fqn, args }); + }; +} + +function set(kernel: Kernel, objref: ObjRef) { + return (property: string, value: any) => { + return kernel.set({ objref, property, value }); + }; +} + +function get(kernel: Kernel, objref: ObjRef) { + return (property: string) => { + return kernel.get({ objref, property }).value; + }; +} diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii index e9081de3c1..88d04378d2 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii @@ -353,6 +353,23 @@ }, "kind": "class", "methods": [ + { + "name": "anyIn", + "parameters": [ + { + "name": "inp", + "type": { + "primitive": "any" + } + } + ] + }, + { + "name": "anyOut", + "returns": { + "primitive": "any" + } + }, { "name": "enumMethod", "parameters": [ @@ -474,7 +491,7 @@ "primitive": "number" }, { - "fqn": "jsii-calc.composition.CompositeOperation" + "fqn": "@scope/jsii-calc-lib.Value" } ] } @@ -1217,6 +1234,37 @@ }, "kind": "class", "methods": [ + { + "name": "hiddenInterface", + "returns": { + "fqn": "jsii-calc.IPublicInterface" + }, + "static": true + }, + { + "name": "hiddenInterfaces", + "returns": { + "collection": { + "elementtype": { + "fqn": "jsii-calc.IPublicInterface" + }, + "kind": "array" + } + }, + "static": true + }, + { + "name": "hiddenSubInterfaces", + "returns": { + "collection": { + "elementtype": { + "fqn": "jsii-calc.IPublicInterface" + }, + "kind": "array" + } + }, + "static": true + }, { "name": "makeClass", "returns": { @@ -1230,6 +1278,25 @@ "fqn": "jsii-calc.IPublicInterface" }, "static": true + }, + { + "name": "makeInterface2", + "returns": { + "fqn": "jsii-calc.IPublicInterface2" + }, + "static": true + }, + { + "name": "makeInterfaces", + "returns": { + "collection": { + "elementtype": { + "fqn": "jsii-calc.IPublicInterface" + }, + "kind": "array" + } + }, + "static": true } ], "name": "Constructors" @@ -2089,11 +2156,29 @@ "methods": [ { "abstract": true, - "name": "bye" + "name": "bye", + "returns": { + "primitive": "string" + } } ], "name": "IPublicInterface" }, + "jsii-calc.IPublicInterface2": { + "assembly": "jsii-calc", + "fqn": "jsii-calc.IPublicInterface2", + "kind": "interface", + "methods": [ + { + "abstract": true, + "name": "ciao", + "returns": { + "primitive": "string" + } + } + ], + "name": "IPublicInterface2" + }, "jsii-calc.IRandomNumberGenerator": { "assembly": "jsii-calc", "docs": { @@ -2240,7 +2325,23 @@ "initializer": { "initializer": true }, + "interfaces": [ + { + "fqn": "jsii-calc.IPublicInterface2" + } + ], "kind": "class", + "methods": [ + { + "name": "ciao", + "overrides": { + "fqn": "jsii-calc.IPublicInterface2" + }, + "returns": { + "primitive": "string" + } + } + ], "name": "InbetweenClass" }, "jsii-calc.InterfaceImplementedByAbstractClass": { @@ -3561,6 +3662,32 @@ ], "name": "RuntimeTypeChecking" }, + "jsii-calc.SingleInstanceTwoTypes": { + "assembly": "jsii-calc", + "docs": { + "comment": "Test that a single instance can be returned under two different FQNs\n\nJSII clients can instantiate 2 different strongly-typed wrappers for the same\nobject. Unfortunately, this will break object equality, but if we didn't do\nthis it would break runtime type checks in the JVM or CLR." + }, + "fqn": "jsii-calc.SingleInstanceTwoTypes", + "initializer": { + "initializer": true + }, + "kind": "class", + "methods": [ + { + "name": "interface1", + "returns": { + "fqn": "jsii-calc.InbetweenClass" + } + }, + { + "name": "interface2", + "returns": { + "fqn": "jsii-calc.IPublicInterface" + } + } + ], + "name": "SingleInstanceTwoTypes" + }, "jsii-calc.Statics": { "assembly": "jsii-calc", "fqn": "jsii-calc.Statics", @@ -4349,5 +4476,5 @@ } }, "version": "0.8.0", - "fingerprint": "4LMgT0Rllw3CIJWiDiR/eUfSRPvCEeWyGWxJXMiDvcU=" + "fingerprint": "7BN7XlOFufVEMTaAbMH5GDcE4zQmJj4iiDlYoXiRvCs=" } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/AllTypes.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/AllTypes.cs index 2e6f227a3f..7cba7ed717 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/AllTypes.cs +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/AllTypes.cs @@ -1,5 +1,4 @@ using Amazon.JSII.Runtime.Deputy; -using Amazon.JSII.Tests.CalculatorNamespace.composition; using Amazon.JSII.Tests.CalculatorNamespace.LibNamespace; using Newtonsoft.Json.Linq; using System; @@ -109,7 +108,7 @@ public virtual string StringProperty set => SetInstanceProperty(value); } - [JsiiProperty("unionArrayProperty", "{\"collection\":{\"kind\":\"array\",\"elementtype\":{\"union\":{\"types\":[{\"primitive\":\"number\"},{\"fqn\":\"jsii-calc.composition.CompositeOperation\"}]}}}}")] + [JsiiProperty("unionArrayProperty", "{\"collection\":{\"kind\":\"array\",\"elementtype\":{\"union\":{\"types\":[{\"primitive\":\"number\"},{\"fqn\":\"@scope/jsii-calc-lib.Value\"}]}}}}")] public virtual object[] UnionArrayProperty { get => GetInstanceProperty(); @@ -158,6 +157,18 @@ public virtual StringEnum OptionalEnumValue set => SetInstanceProperty(value); } + [JsiiMethod("anyIn", null, "[{\"name\":\"inp\",\"type\":{\"primitive\":\"any\"}}]")] + public virtual void AnyIn(object inp) + { + InvokeInstanceVoidMethod(new object[]{inp}); + } + + [JsiiMethod("anyOut", "{\"primitive\":\"any\"}", "[]")] + public virtual object AnyOut() + { + return InvokeInstanceMethod(new object[]{}); + } + [JsiiMethod("enumMethod", "{\"fqn\":\"jsii-calc.StringEnum\"}", "[{\"name\":\"value\",\"type\":{\"fqn\":\"jsii-calc.StringEnum\"}}]")] public virtual StringEnum EnumMethod(StringEnum value) { diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/Constructors.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/Constructors.cs index 6b93bdf0f2..b747def337 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/Constructors.cs +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/Constructors.cs @@ -17,6 +17,24 @@ protected Constructors(DeputyProps props): base(props) { } + [JsiiMethod("hiddenInterface", "{\"fqn\":\"jsii-calc.IPublicInterface\"}", "[]")] + public static IIPublicInterface HiddenInterface() + { + return InvokeStaticMethod(typeof(Constructors), new object[]{}); + } + + [JsiiMethod("hiddenInterfaces", "{\"collection\":{\"kind\":\"array\",\"elementtype\":{\"fqn\":\"jsii-calc.IPublicInterface\"}}}", "[]")] + public static IIPublicInterface[] HiddenInterfaces() + { + return InvokeStaticMethod(typeof(Constructors), new object[]{}); + } + + [JsiiMethod("hiddenSubInterfaces", "{\"collection\":{\"kind\":\"array\",\"elementtype\":{\"fqn\":\"jsii-calc.IPublicInterface\"}}}", "[]")] + public static IIPublicInterface[] HiddenSubInterfaces() + { + return InvokeStaticMethod(typeof(Constructors), new object[]{}); + } + [JsiiMethod("makeClass", "{\"fqn\":\"jsii-calc.PublicClass\"}", "[]")] public static PublicClass MakeClass() { @@ -28,5 +46,17 @@ public static IIPublicInterface MakeInterface() { return InvokeStaticMethod(typeof(Constructors), new object[]{}); } + + [JsiiMethod("makeInterface2", "{\"fqn\":\"jsii-calc.IPublicInterface2\"}", "[]")] + public static IIPublicInterface2 MakeInterface2() + { + return InvokeStaticMethod(typeof(Constructors), new object[]{}); + } + + [JsiiMethod("makeInterfaces", "{\"collection\":{\"kind\":\"array\",\"elementtype\":{\"fqn\":\"jsii-calc.IPublicInterface\"}}}", "[]")] + public static IIPublicInterface[] MakeInterfaces() + { + return InvokeStaticMethod(typeof(Constructors), new object[]{}); + } } } \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface.cs index 0c7b567757..32345c04cb 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface.cs +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface.cs @@ -5,7 +5,7 @@ namespace Amazon.JSII.Tests.CalculatorNamespace [JsiiInterface(typeof(IIPublicInterface), "jsii-calc.IPublicInterface")] public interface IIPublicInterface { - [JsiiMethod("bye", null, "[]")] - void Bye(); + [JsiiMethod("bye", "{\"primitive\":\"string\"}", "[]")] + string Bye(); } } \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface2.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface2.cs new file mode 100644 index 0000000000..ba10fc43b8 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIPublicInterface2.cs @@ -0,0 +1,11 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + [JsiiInterface(typeof(IIPublicInterface2), "jsii-calc.IPublicInterface2")] + public interface IIPublicInterface2 + { + [JsiiMethod("ciao", "{\"primitive\":\"string\"}", "[]")] + string Ciao(); + } +} \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterface2Proxy.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterface2Proxy.cs new file mode 100644 index 0000000000..50a68d9a3c --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterface2Proxy.cs @@ -0,0 +1,18 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + [JsiiTypeProxy(typeof(IIPublicInterface2), "jsii-calc.IPublicInterface2")] + internal sealed class IPublicInterface2Proxy : DeputyBase, IIPublicInterface2 + { + private IPublicInterface2Proxy(ByRefValue reference): base(reference) + { + } + + [JsiiMethod("ciao", "{\"primitive\":\"string\"}", "[]")] + public string Ciao() + { + return InvokeInstanceMethod(new object[]{}); + } + } +} \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterfaceProxy.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterfaceProxy.cs index c06f7c1929..90bdf4b6d7 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterfaceProxy.cs +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IPublicInterfaceProxy.cs @@ -9,10 +9,10 @@ private IPublicInterfaceProxy(ByRefValue reference): base(reference) { } - [JsiiMethod("bye", null, "[]")] - public void Bye() + [JsiiMethod("bye", "{\"primitive\":\"string\"}", "[]")] + public string Bye() { - InvokeInstanceVoidMethod(new object[]{}); + return InvokeInstanceMethod(new object[]{}); } } } \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/InbetweenClass.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/InbetweenClass.cs index 6a678944a1..dfd8f93294 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/InbetweenClass.cs +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/InbetweenClass.cs @@ -3,7 +3,7 @@ namespace Amazon.JSII.Tests.CalculatorNamespace { [JsiiClass(typeof(InbetweenClass), "jsii-calc.InbetweenClass", "[]")] - public class InbetweenClass : PublicClass + public class InbetweenClass : PublicClass, IIPublicInterface2 { public InbetweenClass(): base(new DeputyProps(new object[]{})) { @@ -16,5 +16,11 @@ protected InbetweenClass(ByRefValue reference): base(reference) protected InbetweenClass(DeputyProps props): base(props) { } + + [JsiiMethod("ciao", "{\"primitive\":\"string\"}", "[]")] + public virtual string Ciao() + { + return InvokeInstanceMethod(new object[]{}); + } } } \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/SingleInstanceTwoTypes.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/SingleInstanceTwoTypes.cs new file mode 100644 index 0000000000..2c986ec994 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/SingleInstanceTwoTypes.cs @@ -0,0 +1,39 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + /// + /// Test that a single instance can be returned under two different FQNs + /// + /// JSII clients can instantiate 2 different strongly-typed wrappers for the same + /// object. Unfortunately, this will break object equality, but if we didn't do + /// this it would break runtime type checks in the JVM or CLR. + /// + [JsiiClass(typeof(SingleInstanceTwoTypes), "jsii-calc.SingleInstanceTwoTypes", "[]")] + public class SingleInstanceTwoTypes : DeputyBase + { + public SingleInstanceTwoTypes(): base(new DeputyProps(new object[]{})) + { + } + + protected SingleInstanceTwoTypes(ByRefValue reference): base(reference) + { + } + + protected SingleInstanceTwoTypes(DeputyProps props): base(props) + { + } + + [JsiiMethod("interface1", "{\"fqn\":\"jsii-calc.InbetweenClass\"}", "[]")] + public virtual InbetweenClass Interface1() + { + return InvokeInstanceMethod(new object[]{}); + } + + [JsiiMethod("interface2", "{\"fqn\":\"jsii-calc.IPublicInterface\"}", "[]")] + public virtual IIPublicInterface Interface2() + { + return InvokeInstanceMethod(new object[]{}); + } + } +} \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java index ac0be89bb5..770fbe051a 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java @@ -64,6 +64,7 @@ protected Class resolveClass(final String fqn) throws ClassNotFoundException case "jsii-calc.INonInternalInterface": return software.amazon.jsii.tests.calculator.INonInternalInterface.class; case "jsii-calc.IPrivatelyImplemented": return software.amazon.jsii.tests.calculator.IPrivatelyImplemented.class; case "jsii-calc.IPublicInterface": return software.amazon.jsii.tests.calculator.IPublicInterface.class; + case "jsii-calc.IPublicInterface2": return software.amazon.jsii.tests.calculator.IPublicInterface2.class; case "jsii-calc.IRandomNumberGenerator": return software.amazon.jsii.tests.calculator.IRandomNumberGenerator.class; case "jsii-calc.IReturnsNumber": return software.amazon.jsii.tests.calculator.IReturnsNumber.class; case "jsii-calc.ImplementInternalInterface": return software.amazon.jsii.tests.calculator.ImplementInternalInterface.class; @@ -98,6 +99,7 @@ protected Class resolveClass(final String fqn) throws ClassNotFoundException case "jsii-calc.ReferenceEnumFromScopedPackage": return software.amazon.jsii.tests.calculator.ReferenceEnumFromScopedPackage.class; case "jsii-calc.ReturnsPrivateImplementationOfInterface": return software.amazon.jsii.tests.calculator.ReturnsPrivateImplementationOfInterface.class; case "jsii-calc.RuntimeTypeChecking": return software.amazon.jsii.tests.calculator.RuntimeTypeChecking.class; + case "jsii-calc.SingleInstanceTwoTypes": return software.amazon.jsii.tests.calculator.SingleInstanceTwoTypes.class; case "jsii-calc.Statics": return software.amazon.jsii.tests.calculator.Statics.class; case "jsii-calc.StringEnum": return software.amazon.jsii.tests.calculator.StringEnum.class; case "jsii-calc.StripInternal": return software.amazon.jsii.tests.calculator.StripInternal.class; diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AllTypes.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AllTypes.java index 2b1d0fec93..26ac0bda4c 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AllTypes.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/AllTypes.java @@ -15,6 +15,14 @@ public AllTypes() { software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); } + public void anyIn(final java.lang.Object inp) { + this.jsiiCall("anyIn", Void.class, java.util.stream.Stream.of(java.util.Objects.requireNonNull(inp, "inp is required")).toArray()); + } + + public java.lang.Object anyOut() { + return this.jsiiCall("anyOut", java.lang.Object.class); + } + public software.amazon.jsii.tests.calculator.StringEnum enumMethod(final software.amazon.jsii.tests.calculator.StringEnum value) { return this.jsiiCall("enumMethod", software.amazon.jsii.tests.calculator.StringEnum.class, java.util.stream.Stream.of(java.util.Objects.requireNonNull(value, "value is required")).toArray()); } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/Constructors.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/Constructors.java index 0e16462262..51423d4d30 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/Constructors.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/Constructors.java @@ -11,6 +11,18 @@ public Constructors() { software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); } + public static software.amazon.jsii.tests.calculator.IPublicInterface hiddenInterface() { + return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "hiddenInterface", software.amazon.jsii.tests.calculator.IPublicInterface.class); + } + + public static java.util.List hiddenInterfaces() { + return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "hiddenInterfaces", java.util.List.class); + } + + public static java.util.List hiddenSubInterfaces() { + return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "hiddenSubInterfaces", java.util.List.class); + } + public static software.amazon.jsii.tests.calculator.PublicClass makeClass() { return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "makeClass", software.amazon.jsii.tests.calculator.PublicClass.class); } @@ -18,4 +30,12 @@ public static software.amazon.jsii.tests.calculator.PublicClass makeClass() { public static software.amazon.jsii.tests.calculator.IPublicInterface makeInterface() { return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "makeInterface", software.amazon.jsii.tests.calculator.IPublicInterface.class); } + + public static software.amazon.jsii.tests.calculator.IPublicInterface2 makeInterface2() { + return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "makeInterface2", software.amazon.jsii.tests.calculator.IPublicInterface2.class); + } + + public static java.util.List makeInterfaces() { + return software.amazon.jsii.JsiiObject.jsiiStaticCall(software.amazon.jsii.tests.calculator.Constructors.class, "makeInterfaces", java.util.List.class); + } } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface.java index 1f18b22395..236d299497 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface.java @@ -2,7 +2,7 @@ @javax.annotation.Generated(value = "jsii-pacmak") public interface IPublicInterface extends software.amazon.jsii.JsiiSerializable { - void bye(); + java.lang.String bye(); /** * A proxy class which represents a concrete javascript instance of this type. @@ -13,8 +13,8 @@ final static class Jsii$Proxy extends software.amazon.jsii.JsiiObject implements } @Override - public void bye() { - this.jsiiCall("bye", Void.class); + public java.lang.String bye() { + return this.jsiiCall("bye", java.lang.String.class); } } } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface2.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface2.java new file mode 100644 index 0000000000..80092269e8 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IPublicInterface2.java @@ -0,0 +1,20 @@ +package software.amazon.jsii.tests.calculator; + +@javax.annotation.Generated(value = "jsii-pacmak") +public interface IPublicInterface2 extends software.amazon.jsii.JsiiSerializable { + java.lang.String ciao(); + + /** + * A proxy class which represents a concrete javascript instance of this type. + */ + final static class Jsii$Proxy extends software.amazon.jsii.JsiiObject implements software.amazon.jsii.tests.calculator.IPublicInterface2 { + protected Jsii$Proxy(final software.amazon.jsii.JsiiObject.InitializationMode mode) { + super(mode); + } + + @Override + public java.lang.String ciao() { + return this.jsiiCall("ciao", java.lang.String.class); + } + } +} diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InbetweenClass.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InbetweenClass.java index c2ca0d34d8..b1371c8f9d 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InbetweenClass.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InbetweenClass.java @@ -2,7 +2,7 @@ @javax.annotation.Generated(value = "jsii-pacmak") @software.amazon.jsii.Jsii(module = software.amazon.jsii.tests.calculator.$Module.class, fqn = "jsii-calc.InbetweenClass") -public class InbetweenClass extends software.amazon.jsii.tests.calculator.PublicClass { +public class InbetweenClass extends software.amazon.jsii.tests.calculator.PublicClass implements software.amazon.jsii.tests.calculator.IPublicInterface2 { protected InbetweenClass(final software.amazon.jsii.JsiiObject.InitializationMode mode) { super(mode); } @@ -10,4 +10,9 @@ public InbetweenClass() { super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); } + + @Override + public java.lang.String ciao() { + return this.jsiiCall("ciao", java.lang.String.class); + } } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/SingleInstanceTwoTypes.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/SingleInstanceTwoTypes.java new file mode 100644 index 0000000000..fcd143f136 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/SingleInstanceTwoTypes.java @@ -0,0 +1,28 @@ +package software.amazon.jsii.tests.calculator; + +/** + * Test that a single instance can be returned under two different FQNs + * + * JSII clients can instantiate 2 different strongly-typed wrappers for the same + * object. Unfortunately, this will break object equality, but if we didn't do + * this it would break runtime type checks in the JVM or CLR. + */ +@javax.annotation.Generated(value = "jsii-pacmak") +@software.amazon.jsii.Jsii(module = software.amazon.jsii.tests.calculator.$Module.class, fqn = "jsii-calc.SingleInstanceTwoTypes") +public class SingleInstanceTwoTypes extends software.amazon.jsii.JsiiObject { + protected SingleInstanceTwoTypes(final software.amazon.jsii.JsiiObject.InitializationMode mode) { + super(mode); + } + public SingleInstanceTwoTypes() { + super(software.amazon.jsii.JsiiObject.InitializationMode.Jsii); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); + } + + public software.amazon.jsii.tests.calculator.InbetweenClass interface1() { + return this.jsiiCall("interface1", software.amazon.jsii.tests.calculator.InbetweenClass.class); + } + + public software.amazon.jsii.tests.calculator.IPublicInterface interface2() { + return this.jsiiCall("interface2", software.amazon.jsii.tests.calculator.IPublicInterface.class); + } +} diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst b/packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst index 6a1e23ed8c..a88becce56 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst +++ b/packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst @@ -394,6 +394,17 @@ AllTypes + .. py:method:: anyIn(inp) + + :param inp: + :type inp: any + + + .. py:method:: anyOut() -> any + + :rtype: any + + .. py:method:: enumMethod(value) -> jsii-calc.StringEnum :param value: @@ -463,7 +474,7 @@ AllTypes .. py:attribute:: unionArrayProperty - :type: (number or :py:class:`~jsii-calc.composition.CompositeOperation`\ )[] + :type: (number or :py:class:`@scope/jsii-calc-lib.Value`\ )[] .. py:attribute:: unionMapProperty @@ -1275,6 +1286,21 @@ Constructors + .. py:staticmethod:: hiddenInterface() -> jsii-calc.IPublicInterface + + :rtype: :py:class:`~jsii-calc.IPublicInterface`\ + + + .. py:staticmethod:: hiddenInterfaces() -> jsii-calc.IPublicInterface[] + + :rtype: :py:class:`~jsii-calc.IPublicInterface`\ [] + + + .. py:staticmethod:: hiddenSubInterfaces() -> jsii-calc.IPublicInterface[] + + :rtype: :py:class:`~jsii-calc.IPublicInterface`\ [] + + .. py:staticmethod:: makeClass() -> jsii-calc.PublicClass :rtype: :py:class:`~jsii-calc.PublicClass`\ @@ -1285,6 +1311,16 @@ Constructors :rtype: :py:class:`~jsii-calc.IPublicInterface`\ + .. py:staticmethod:: makeInterface2() -> jsii-calc.IPublicInterface2 + + :rtype: :py:class:`~jsii-calc.IPublicInterface2`\ + + + .. py:staticmethod:: makeInterfaces() -> jsii-calc.IPublicInterface[] + + :rtype: :py:class:`~jsii-calc.IPublicInterface`\ [] + + ConsumersOfThisCrazyTypeSystem ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -2588,8 +2624,44 @@ IPublicInterface (interface) - .. py:method:: bye() + .. py:method:: bye() -> string + + :rtype: string + :abstract: Yes + + +IPublicInterface2 (interface) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. py:class:: IPublicInterface2 + + **Language-specific names:** + + .. tabs:: + + .. code-tab:: c# + + using Amazon.JSII.Tests.CalculatorNamespace; + + .. code-tab:: java + + import software.amazon.jsii.tests.calculator.IPublicInterface2; + + .. code-tab:: javascript + + // IPublicInterface2 is an interface + + .. code-tab:: typescript + + import { IPublicInterface2 } from 'jsii-calc'; + + + + + + .. py:method:: ciao() -> string + :rtype: string :abstract: Yes @@ -2890,6 +2962,14 @@ InbetweenClass :extends: :py:class:`~jsii-calc.PublicClass`\ + :implements: :py:class:`~jsii-calc.IPublicInterface2`\ + + .. py:method:: ciao() -> string + + *Implements* :py:meth:`jsii-calc.IPublicInterface2.ciao` + + :rtype: string + .. py:method:: hello() @@ -4672,6 +4752,56 @@ RuntimeTypeChecking :type arg3: date *(optional)* +SingleInstanceTwoTypes +^^^^^^^^^^^^^^^^^^^^^^ + +.. py:class:: SingleInstanceTwoTypes() + + **Language-specific names:** + + .. tabs:: + + .. code-tab:: c# + + using Amazon.JSII.Tests.CalculatorNamespace; + + .. code-tab:: java + + import software.amazon.jsii.tests.calculator.SingleInstanceTwoTypes; + + .. code-tab:: javascript + + const { SingleInstanceTwoTypes } = require('jsii-calc'); + + .. code-tab:: typescript + + import { SingleInstanceTwoTypes } from 'jsii-calc'; + + + + Test that a single instance can be returned under two different FQNs + + + + JSII clients can instantiate 2 different strongly-typed wrappers for the same + + object. Unfortunately, this will break object equality, but if we didn't do + + this it would break runtime type checks in the JVM or CLR. + + + + + .. py:method:: interface1() -> jsii-calc.InbetweenClass + + :rtype: :py:class:`~jsii-calc.InbetweenClass`\ + + + .. py:method:: interface2() -> jsii-calc.IPublicInterface + + :rtype: :py:class:`~jsii-calc.IPublicInterface`\ + + Statics ^^^^^^^ diff --git a/packages/jsii-python-runtime/tests/test_compliance.py b/packages/jsii-python-runtime/tests/test_compliance.py index 63bcbfce21..e1ca7ab010 100644 --- a/packages/jsii-python-runtime/tests/test_compliance.py +++ b/packages/jsii-python-runtime/tests/test_compliance.py @@ -277,13 +277,13 @@ def test_unionTypes(): # map map_ = {} - map_["Foo"] = Multiply(Number(2), Number(99)) + map_["Foo"] = Number(99) types.union_map_property = map_ # TODO: No Assertion? # array - types.union_array_property = ["Hello", 123, Number(33)] - assert types.union_array_property[2].value == 33 + types.union_array_property = [123, Number(33)] + assert types.union_array_property[1].value == 33 def test_createObjectAndCtorOverloads(): diff --git a/packages/jsii-reflect/test/classes.expected.txt b/packages/jsii-reflect/test/classes.expected.txt index 1ce4f8ea77..e29d7390be 100644 --- a/packages/jsii-reflect/test/classes.expected.txt +++ b/packages/jsii-reflect/test/classes.expected.txt @@ -55,6 +55,7 @@ PythonReservedWords ReferenceEnumFromScopedPackage ReturnsPrivateImplementationOfInterface RuntimeTypeChecking +SingleInstanceTwoTypes Statics StripInternal Sum diff --git a/packages/jsii-reflect/test/jsii-tree.test.all.expected.txt b/packages/jsii-reflect/test/jsii-tree.test.all.expected.txt index 31df64bd14..fc41e4bedc 100644 --- a/packages/jsii-reflect/test/jsii-tree.test.all.expected.txt +++ b/packages/jsii-reflect/test/jsii-tree.test.all.expected.txt @@ -59,6 +59,13 @@ assemblies │ │ └─┬ members │ │ ├─┬ () method │ │ │ └── returns: void + │ │ ├─┬ anyIn(inp) method + │ │ │ ├─┬ parameters + │ │ │ │ └─┬ inp + │ │ │ │ └── type: primitive:any + │ │ │ └── returns: void + │ │ ├─┬ anyOut() method + │ │ │ └── returns: primitive:any │ │ ├─┬ enumMethod(value) method │ │ │ ├─┬ parameters │ │ │ │ └─┬ value @@ -90,7 +97,7 @@ assemblies │ │ ├─┬ stringProperty property │ │ │ └── type: primitive:string │ │ ├─┬ unionArrayProperty property - │ │ │ └── type: Array + │ │ │ └── type: Array │ │ ├─┬ unionMapProperty property │ │ │ └── type: Map primitive:string | primitive:number | class:@scope/jsii-calc-lib.Number> │ │ ├─┬ unionProperty property @@ -275,12 +282,27 @@ assemblies │ │ └─┬ members │ │ ├─┬ () method │ │ │ └── returns: void + │ │ ├─┬ hiddenInterface() method + │ │ │ ├── static + │ │ │ └── returns: interface:jsii-calc.IPublicInterface + │ │ ├─┬ hiddenInterfaces() method + │ │ │ ├── static + │ │ │ └── returns: Array + │ │ ├─┬ hiddenSubInterfaces() method + │ │ │ ├── static + │ │ │ └── returns: Array │ │ ├─┬ makeClass() method │ │ │ ├── static │ │ │ └── returns: class:jsii-calc.PublicClass - │ │ └─┬ makeInterface() method + │ │ ├─┬ makeInterface() method + │ │ │ ├── static + │ │ │ └── returns: interface:jsii-calc.IPublicInterface + │ │ ├─┬ makeInterface2() method + │ │ │ ├── static + │ │ │ └── returns: interface:jsii-calc.IPublicInterface2 + │ │ └─┬ makeInterfaces() method │ │ ├── static - │ │ └── returns: interface:jsii-calc.IPublicInterface + │ │ └── returns: Array │ ├─┬ class ConsumersOfThisCrazyTypeSystem │ │ └─┬ members │ │ ├─┬ () method @@ -459,9 +481,12 @@ assemblies │ │ └── type: primitive:string │ ├─┬ class InbetweenClass │ │ ├── base: PublicClass + │ │ ├── interfaces: IPublicInterface2 │ │ └─┬ members - │ │ └─┬ () method - │ │ └── returns: void + │ │ ├─┬ () method + │ │ │ └── returns: void + │ │ └─┬ ciao() method + │ │ └── returns: primitive:string │ ├─┬ class Foo │ │ └─┬ members │ │ ├─┬ () method @@ -894,6 +919,14 @@ assemblies │ │ │ └─┬ arg3 │ │ │ └── type: primitive:date (optional) │ │ └── returns: void + │ ├─┬ class SingleInstanceTwoTypes + │ │ └─┬ members + │ │ ├─┬ () method + │ │ │ └── returns: void + │ │ ├─┬ interface1() method + │ │ │ └── returns: class:jsii-calc.InbetweenClass + │ │ └─┬ interface2() method + │ │ └── returns: interface:jsii-calc.IPublicInterface │ ├─┬ class Statics │ │ └─┬ members │ │ ├─┬ (value) method @@ -1271,7 +1304,12 @@ assemblies │ │ └─┬ members │ │ └─┬ bye() method │ │ ├── abstract - │ │ └── returns: void + │ │ └── returns: primitive:string + │ ├─┬ interface IPublicInterface2 + │ │ └─┬ members + │ │ └─┬ ciao() method + │ │ ├── abstract + │ │ └── returns: primitive:string │ ├─┬ interface IRandomNumberGenerator │ │ └─┬ members │ │ └─┬ next() method diff --git a/packages/jsii-reflect/test/jsii-tree.test.inheritance.expected.txt b/packages/jsii-reflect/test/jsii-tree.test.inheritance.expected.txt index 1590147053..f0bda4dddb 100644 --- a/packages/jsii-reflect/test/jsii-tree.test.inheritance.expected.txt +++ b/packages/jsii-reflect/test/jsii-tree.test.inheritance.expected.txt @@ -46,7 +46,8 @@ assemblies │ │ └── base: ImplementsInterfaceWithInternal │ ├── class ImplementsPrivateInterface │ ├─┬ class InbetweenClass - │ │ └── base: PublicClass + │ │ ├── base: PublicClass + │ │ └── interfaces: IPublicInterface2 │ ├── class Foo │ ├── class JSObjectLiteralForInterface │ ├── class JSObjectLiteralToNative @@ -73,6 +74,7 @@ assemblies │ ├── class ReferenceEnumFromScopedPackage │ ├── class ReturnsPrivateImplementationOfInterface │ ├── class RuntimeTypeChecking + │ ├── class SingleInstanceTwoTypes │ ├── class Statics │ ├── class StripInternal │ ├─┬ class Sum @@ -119,6 +121,7 @@ assemblies │ │ └── IAnotherPublicInterface │ ├── interface IPrivatelyImplemented │ ├── interface IPublicInterface + │ ├── interface IPublicInterface2 │ ├── interface IRandomNumberGenerator │ ├── interface IReturnsNumber │ ├─┬ interface ImplictBaseOfBase diff --git a/packages/jsii-reflect/test/jsii-tree.test.members.expected.txt b/packages/jsii-reflect/test/jsii-tree.test.members.expected.txt index e85417eb37..23a8d2c2de 100644 --- a/packages/jsii-reflect/test/jsii-tree.test.members.expected.txt +++ b/packages/jsii-reflect/test/jsii-tree.test.members.expected.txt @@ -25,6 +25,8 @@ assemblies │ ├─┬ class AllTypes │ │ └─┬ members │ │ ├── () method + │ │ ├── anyIn(inp) method + │ │ ├── anyOut() method │ │ ├── enumMethod(value) method │ │ ├── enumPropertyValue property │ │ ├── anyArrayProperty property @@ -112,8 +114,13 @@ assemblies │ ├─┬ class Constructors │ │ └─┬ members │ │ ├── () method + │ │ ├── hiddenInterface() method + │ │ ├── hiddenInterfaces() method + │ │ ├── hiddenSubInterfaces() method │ │ ├── makeClass() method - │ │ └── makeInterface() method + │ │ ├── makeInterface() method + │ │ ├── makeInterface2() method + │ │ └── makeInterfaces() method │ ├─┬ class ConsumersOfThisCrazyTypeSystem │ │ └─┬ members │ │ ├── () method @@ -189,7 +196,8 @@ assemblies │ │ └── private property │ ├─┬ class InbetweenClass │ │ └─┬ members - │ │ └── () method + │ │ ├── () method + │ │ └── ciao() method │ ├─┬ class Foo │ │ └─┬ members │ │ ├── () method @@ -384,6 +392,11 @@ assemblies │ │ ├── methodWithDefaultedArguments(arg1,arg2,arg3) method │ │ ├── methodWithOptionalAnyArgument(arg) method │ │ └── methodWithOptionalArguments(arg1,arg2,arg3) method + │ ├─┬ class SingleInstanceTwoTypes + │ │ └─┬ members + │ │ ├── () method + │ │ ├── interface1() method + │ │ └── interface2() method │ ├─┬ class Statics │ │ └─┬ members │ │ ├── (value) method @@ -531,6 +544,9 @@ assemblies │ ├─┬ interface IPublicInterface │ │ └─┬ members │ │ └── bye() method + │ ├─┬ interface IPublicInterface2 + │ │ └─┬ members + │ │ └── ciao() method │ ├─┬ interface IRandomNumberGenerator │ │ └─┬ members │ │ └── next() method diff --git a/packages/jsii-reflect/test/jsii-tree.test.types.expected.txt b/packages/jsii-reflect/test/jsii-tree.test.types.expected.txt index 7914e96da2..da05887ea3 100644 --- a/packages/jsii-reflect/test/jsii-tree.test.types.expected.txt +++ b/packages/jsii-reflect/test/jsii-tree.test.types.expected.txt @@ -54,6 +54,7 @@ assemblies │ ├── class ReferenceEnumFromScopedPackage │ ├── class ReturnsPrivateImplementationOfInterface │ ├── class RuntimeTypeChecking + │ ├── class SingleInstanceTwoTypes │ ├── class Statics │ ├── class StripInternal │ ├── class Sum @@ -84,6 +85,7 @@ assemblies │ ├── interface INonInternalInterface │ ├── interface IPrivatelyImplemented │ ├── interface IPublicInterface + │ ├── interface IPublicInterface2 │ ├── interface IRandomNumberGenerator │ ├── interface IReturnsNumber │ ├── interface ImplictBaseOfBase diff --git a/packages/jsii-ruby-runtime/project/test/jsii_runtime_test.rb b/packages/jsii-ruby-runtime/project/test/jsii_runtime_test.rb index 3a30f20c51..b452324ae8 100644 --- a/packages/jsii-ruby-runtime/project/test/jsii_runtime_test.rb +++ b/packages/jsii-ruby-runtime/project/test/jsii_runtime_test.rb @@ -48,7 +48,7 @@ def test_async_callbacks ) promise = @client.begin(objref: objref, method: 'callMe') - assert_equal({ 'promiseid' => 'jsii::promise::10002' }, promise) + assert_equal({ 'promiseid' => 'jsii::promise::20001' }, promise) callbacks = @client.callbacks['callbacks'] assert_equal(1, callbacks.length) From 8f66fe4c2f388f19badc59adfd2a6f27ef8914bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Wed, 27 Mar 2019 15:49:22 +0100 Subject: [PATCH 4/6] Improve Java handling by leveraging jackson standard ways --- .../amazon/jsii/testing/ComplianceTest.java | 5 - .../amazon/jsii/testing/JsiiClientTest.java | 9 +- packages/jsii-java-runtime/package.json | 2 +- packages/jsii-java-runtime/pom.xml.t.js | 34 ++ .../java/software/amazon/jsii/JsiiClient.java | 19 +- .../java/software/amazon/jsii/JsiiEngine.java | 38 +- .../java/software/amazon/jsii/JsiiObject.java | 64 +--- .../amazon/jsii/JsiiObjectMapper.java | 348 +++++++++++------- .../software/amazon/jsii/JsiiObjectRef.java | 17 + .../software/amazon/jsii/JsiiRuntime.java | 17 +- .../amazon/jsii/api/InvokeRequest.java | 7 +- .../software/amazon/jsii/api/SetRequest.java | 6 +- .../amazon/jsii/JsiiObjectMapperTest.java | 131 +++++++ .../org.mockito.plugins.MockMaker | 1 + .../amazon/jsii/complex-callback.json | 12 + packages/jsii-pacmak/lib/targets/java.ts | 2 +- .../jsii/tests/calculator/base/BaseProps.java | 2 +- .../tests/calculator/lib/MyFirstStruct.java | 2 +- .../lib/StructWithOnlyOptionals.java | 2 +- .../tests/calculator/CalculatorProps.java | 2 +- .../jsii/tests/calculator/DerivedStruct.java | 2 +- .../EraseUndefinedHashValuesOptions.java | 2 +- .../calculator/ExtendsInternalInterface.java | 2 +- .../calculator/ExtendsPrivateInterface.java | 2 +- .../tests/calculator/ImplictBaseOfBase.java | 2 +- .../InterfaceImplementedByAbstractClass.java | 2 +- .../Hello.java | 2 +- .../Hello.java | 2 +- .../LoadBalancedFargateServiceProps.java | 2 +- .../NullShouldBeTreatedAsUndefinedData.java | 2 +- .../tests/calculator/UnionProperties.java | 2 +- 31 files changed, 462 insertions(+), 280 deletions(-) create mode 100644 packages/jsii-java-runtime/project/src/test/java/software/amazon/jsii/JsiiObjectMapperTest.java create mode 100644 packages/jsii-java-runtime/project/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 packages/jsii-java-runtime/project/src/test/resources/software/amazon/jsii/complex-callback.json 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 444a8054d0..e579c24d7c 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 @@ -56,12 +56,7 @@ import software.amazon.jsii.tests.calculator.lib.Number; import software.amazon.jsii.tests.calculator.lib.StructWithOnlyOptionals; import software.amazon.jsii.tests.calculator.lib.Value; -import software.amazon.jsii.tests.calculator.JavaReservedWords; -import software.amazon.jsii.tests.calculator.ClassWithPrivateConstructorAndAutomaticProperties; import software.amazon.jsii.tests.calculator.ConstructorPassesThisOut; -import software.amazon.jsii.tests.calculator.Constructors; - -import org.junit.Test; import java.io.IOException; import java.time.Instant; diff --git a/packages/jsii-java-runtime-test/project/src/test/java/software/amazon/jsii/testing/JsiiClientTest.java b/packages/jsii-java-runtime-test/project/src/test/java/software/amazon/jsii/testing/JsiiClientTest.java index 3c9da690e4..480383adbb 100644 --- a/packages/jsii-java-runtime-test/project/src/test/java/software/amazon/jsii/testing/JsiiClientTest.java +++ b/packages/jsii-java-runtime-test/project/src/test/java/software/amazon/jsii/testing/JsiiClientTest.java @@ -116,7 +116,7 @@ public void asyncMethodOverrides() { assertEquals("overrideMe", first.getInvoke().getMethod()); assertEquals("myCookie", first.getCookie()); assertEquals(1, first.getInvoke().getArgs().size()); - assertEquals(10, first.getInvoke().getArgs().get(0)); + assertEquals(JsiiObjectMapper.valueToTree(10), first.getInvoke().getArgs().get(0)); assertEquals(obj.getObjId(), JsiiObjectRef.parse(first.getInvoke().getObjref()).getObjId()); // now complete the callback with some override value @@ -146,7 +146,7 @@ public void asyncMethodOverridesThrow() { assertEquals("overrideMe", first.getInvoke().getMethod()); assertEquals("myCookie", first.getCookie()); assertEquals(1, first.getInvoke().getArgs().size()); - assertEquals(10, first.getInvoke().getArgs().get(0)); + assertEquals(JsiiObjectMapper.valueToTree(10), first.getInvoke().getArgs().get(0)); assertEquals(obj.getObjId(), JsiiObjectRef.parse(first.getInvoke().getObjref()).getObjId()); // now complete the callback with an error @@ -171,7 +171,7 @@ public void syncVirtualMethods() { jsiiRuntime.setCallbackHandler(callback -> { assertEquals(obj.getObjId(), JsiiObjectRef.parse(callback.getInvoke().getObjref()).getObjId()); assertEquals("virtualMethod", callback.getInvoke().getMethod()); - assertEquals(10, callback.getInvoke().getArgs().get(0)); + assertEquals(JsiiObjectMapper.valueToTree(10), callback.getInvoke().getArgs().get(0)); assertEquals("myCookie", callback.getCookie()); // interact with jsii from inside the callback @@ -226,8 +226,7 @@ public void staticMethods() { */ @Test public void serializeViaJsiiToJsonIfExists() { - JsiiObjectMapper om = JsiiObjectMapper.instance; - JsonNode result = om.valueToTree(new JsiiSerializable() { + JsonNode result = JsiiObjectMapper.INSTANCE.valueToTree(new JsiiSerializable() { public JsonNode $jsii$toJson() { ObjectNode node = JSON.objectNode(); node.set("foo", OM.valueToTree("bar")); diff --git a/packages/jsii-java-runtime/package.json b/packages/jsii-java-runtime/package.json index 5f6ddfcfbb..6c5509e8dd 100644 --- a/packages/jsii-java-runtime/package.json +++ b/packages/jsii-java-runtime/package.json @@ -8,7 +8,7 @@ "scripts": { "gen": "/bin/bash ./generate.sh", "build": "tsc && npm run gen && cd project && mvn deploy -D altDeploymentRepository=local::default::file://${PWD}/../maven-repo", - "test": "echo 'Tests are performed by jsii-java-runtime-test'", + "test": "echo 'Tests are run as part of the build target'", "package": "package-java" }, "devDependencies": { diff --git a/packages/jsii-java-runtime/pom.xml.t.js b/packages/jsii-java-runtime/pom.xml.t.js index 12eb2aed90..d77310c4d0 100644 --- a/packages/jsii-java-runtime/pom.xml.t.js +++ b/packages/jsii-java-runtime/pom.xml.t.js @@ -83,6 +83,30 @@ process.stdout.write(` [1.3.2,) provided + + + + org.junit.jupiter + junit-jupiter-api + 5.4.1 + test + + + + + org.junit.jupiter + junit-jupiter-engine + 5.4.1 + test + + + + + org.mockito + mockito-core + 2.25.1 + test + @@ -128,6 +152,16 @@ process.stdout.write(` protected + + + maven-surefire-plugin + 2.22.0 + + + + maven-failsafe-plugin + 2.22.0 + diff --git a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiClient.java b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiClient.java index 7f5b84d3db..bdd86f20cb 100644 --- a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiClient.java +++ b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiClient.java @@ -5,7 +5,6 @@ import software.amazon.jsii.api.JsiiOverride; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -27,16 +26,6 @@ public final class JsiiClient { */ private static final JsonNodeFactory JSON = JsonNodeFactory.instance; - /** - * JSON object mapper. - */ - private static final ObjectMapper STD_OM = new ObjectMapper(); - - /** - * Jsii custom object mapper. - */ - private static final JsiiObjectMapper JSII_OM = JsiiObjectMapper.instance; - /** * TCP port to connect to (always "localhost"). */ @@ -95,7 +84,7 @@ public JsiiObjectRef createObject(final String fqn, request.setArgs(initializerArgs); request.setOverrides(overrides); - ObjectNode req = JSII_OM.valueToTree(request); + ObjectNode req = JsiiObjectMapper.valueToTree(request); req.put("api", "create"); JsonNode resp = this.runtime.requestResponse(req); @@ -245,11 +234,7 @@ public List pendingCallbacks() { List result = new ArrayList<>(); callbacksArray.forEach(node -> { - try { - result.add(STD_OM.treeToValue(node, Callback.class)); - } catch (JsonProcessingException e) { - throw new JsiiException(e); - } + result.add(JsiiObjectMapper.treeToValue(node, Callback.class)); }); return result; diff --git a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java index a20f8ea80f..c957574a31 100644 --- a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java +++ b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java @@ -5,6 +5,8 @@ import software.amazon.jsii.api.InvokeRequest; import software.amazon.jsii.api.JsiiOverride; import software.amazon.jsii.api.SetRequest; + +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Throwables; @@ -46,11 +48,6 @@ public final class JsiiEngine implements JsiiCallbackHandler { */ private final JsiiRuntime runtime = new JsiiRuntime(); - /** - * JSON object mapper. - */ - private static final JsiiObjectMapper OM = JsiiObjectMapper.instance; - /** * The set of modules we already loaded into the VM. */ @@ -324,7 +321,7 @@ private JsonNode invokeCallbackGet(final GetRequest req) { String methodName = javaScriptPropertyToJavaPropertyName("get", req.getProperty()); try { Method getter = obj.getClass().getMethod(methodName); - return OM.valueToTree(invokeMethod(obj, getter)); + return JsiiObjectMapper.valueToTree(invokeMethod(obj, getter)); } catch (NoSuchMethodException e) { throw new JsiiException(e); } @@ -351,7 +348,8 @@ private JsonNode invokeCallbackSet(final SetRequest req) { throw new JsiiException("Unable to find property setter " + setterMethodName); } - return OM.valueToTree(invokeMethod(obj, setter, this.fromKernel(req.getValue()))); + final Object arg = JsiiObjectMapper.treeToValue(req.getValue(), setter.getParameterTypes()[0]); + return JsiiObjectMapper.valueToTree(invokeMethod(obj, setter, arg)); } /** @@ -363,28 +361,14 @@ private JsonNode invokeCallbackSet(final SetRequest req) { private JsonNode invokeCallbackMethod(final InvokeRequest req, final String cookie) { Object obj = this.getObject(req.getObjref()); Method method = this.findCallbackMethod(obj.getClass(), cookie); - return OM.valueToTree(invokeMethod(obj, - method, - req.getArgs() - .stream() - .map(this::fromKernel) - .toArray())); - } - private Object fromKernel(final Object object) { - if (object == null) { - return null; - } - if (object instanceof Map) { - final Map map = (Map) object; - if (map.containsKey(JsiiObjectRef.TOKEN_REF)) { - final JsiiObjectRef objRef = JsiiObjectRef.fromObjId(map.get(JsiiObjectRef.TOKEN_REF).toString()); - final Object result = this.nativeFromObjRef(objRef); - this.log("Resolved to object of type %s", result.getClass().getCanonicalName()); - return result; - } + final Class[] argTypes = method.getParameterTypes(); + final Object[] args = new Object[argTypes.length]; + for (int i = 0; i < argTypes.length; i++) { + args[i] = JsiiObjectMapper.treeToValue(req.getArgs().get(i), argTypes[i]); } - return object; + + return JsiiObjectMapper.valueToTree(invokeMethod(obj, method, args)); } /** diff --git a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiObject.java b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiObject.java index d3f1afc2b8..d6cc888cf6 100644 --- a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiObject.java +++ b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiObject.java @@ -11,11 +11,6 @@ */ public class JsiiObject implements JsiiSerializable { - /** - * JSON object mapper. - */ - private static final JsiiObjectMapper OM = JsiiObjectMapper.instance; - /** * The jsii engine used by this object. */ @@ -58,16 +53,11 @@ public enum InitializationMode { */ @Nullable protected final T jsiiCall(final String method, final Class returnType, @Nullable final Object... args) { - try { - return OM.treeToValue(JsiiObject.engine.getClient().callMethod( - this.objRef, - method, - OM.valueToTree(args)), - returnType); - - } catch (JsonProcessingException e) { - throw new JsiiException(e); - } + return JsiiObjectMapper.treeToValue(JsiiObject.engine.getClient() + .callMethod(this.objRef, + method, + JsiiObjectMapper.valueToTree(args)), + returnType); } /** @@ -82,16 +72,9 @@ protected final T jsiiCall(final String method, final Class returnType, @ @Nullable protected static T jsiiStaticCall(final Class nativeClass, final String method, final Class returnType, @Nullable final Object... args) { String fqn = engine.loadModuleForClass(nativeClass); - try { - return OM.treeToValue(engine.getClient().callStaticMethod( - fqn, - method, - OM.valueToTree(args)), - returnType); - - } catch (JsonProcessingException e) { - throw new JsiiException(e); - } + return JsiiObjectMapper.treeToValue(engine.getClient() + .callStaticMethod(fqn, method, JsiiObjectMapper.valueToTree(args)), + returnType); } /** @@ -104,17 +87,12 @@ protected static T jsiiStaticCall(final Class nativeClass, final String m */ @Nullable protected final T jsiiAsyncCall(final String method, final Class returnType, @Nullable final Object... args) { - try { - JsiiClient client = engine.getClient(); - JsiiPromise promise = client.beginAsyncMethod(this.objRef, method, OM.valueToTree(args)); + JsiiClient client = engine.getClient(); + JsiiPromise promise = client.beginAsyncMethod(this.objRef, method, JsiiObjectMapper.valueToTree(args)); - engine.processAllPendingCallbacks(); + engine.processAllPendingCallbacks(); - JsonNode ret = client.endAsyncMethod(promise); - return OM.treeToValue(ret, returnType); - } catch (JsonProcessingException e) { - throw new JsiiException(e); - } + return JsiiObjectMapper.treeToValue(client.endAsyncMethod(promise), returnType); } /** @@ -126,11 +104,7 @@ protected final T jsiiAsyncCall(final String method, final Class returnTy */ @Nullable protected final T jsiiGet(final String property, final Class type) { - try { - return OM.treeToValue(engine.getClient().getPropertyValue(this.objRef, property), type); - } catch (JsonProcessingException e) { - throw new JsiiException(e); - } + return JsiiObjectMapper.treeToValue(engine.getClient().getPropertyValue(this.objRef, property), type); } /** @@ -143,12 +117,8 @@ protected final T jsiiGet(final String property, final Class type) { */ @Nullable protected static T jsiiStaticGet(final Class nativeClass, final String property, final Class type) { - try { - String fqn = engine.loadModuleForClass(nativeClass); - return OM.treeToValue(engine.getClient().getStaticPropertyValue(fqn, property), type); - } catch (JsonProcessingException e) { - throw new JsiiException(e); - } + String fqn = engine.loadModuleForClass(nativeClass); + return JsiiObjectMapper.treeToValue(engine.getClient().getStaticPropertyValue(fqn, property), type); } /** @@ -157,7 +127,7 @@ protected static T jsiiStaticGet(final Class nativeClass, final String pr * @param value The property value. */ protected final void jsiiSet(final String property, @Nullable final Object value) { - engine.getClient().setPropertyValue(this.objRef, property, OM.valueToTree(value)); + engine.getClient().setPropertyValue(this.objRef, property, JsiiObjectMapper.valueToTree(value)); } /** @@ -168,7 +138,7 @@ protected final void jsiiSet(final String property, @Nullable final Object value */ protected static void jsiiStaticSet(final Class nativeClass, final String property, @Nullable final Object value) { String fqn = engine.loadModuleForClass(nativeClass); - engine.getClient().setStaticPropertyValue(fqn, property, OM.valueToTree(value)); + engine.getClient().setStaticPropertyValue(fqn, property, JsiiObjectMapper.valueToTree(value)); } /** diff --git a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectMapper.java b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectMapper.java index 15bfcf4dfc..f562de697d 100644 --- a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectMapper.java +++ b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectMapper.java @@ -1,186 +1,250 @@ package software.amazon.jsii; -import com.fasterxml.jackson.annotation.JsonInclude; +import java.io.IOException; +import java.time.Instant; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import java.io.IOException; -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -import static software.amazon.jsii.JsiiEngine.tryGetJsiiAnnotation; +import com.fasterxml.jackson.databind.type.MapLikeType; +import com.fasterxml.jackson.databind.type.MapType; +import com.google.common.annotations.VisibleForTesting; /** - * Implements serialization/deserialization of jsii data. + * Provides a correctly configured JSON processor for handling JSII requests and responses. */ public final class JsiiObjectMapper { - /** - * Singleton instance of the object mapper. - */ - public static JsiiObjectMapper instance = new JsiiObjectMapper(); + public static final long serialVersionUID = 1L; + + /** + * An ObjectMapper that can be used to serialize and deserialize JSII requests and responses. + */ + public static final ObjectMapper INSTANCE = new JsiiObjectMapper().getObjectMapper(); + + /** + * Similar to calling JsiiObjectMapper.INSTANCE.treeToValue, but handles a null JsonNode argument + * well, and throws JsiiException instead of JsonProcessingException. + */ + public static T treeToValue(final JsonNode tree, final Class valueType) { + if (tree == null) { + return null; + } + try { + return INSTANCE.treeToValue(tree, valueType); + } catch (final JsonProcessingException jpe) { + throw new JsiiException(jpe); + } + } + + /** + * Similar to calling JsiiObjectMapper.INSTANCE.valueToTree, but handles a null argument well by + * returning null. + */ + public static T valueToTree(final Object value) { + if (value == null) { + return null; + } + return INSTANCE.valueToTree(value); + } - /** - * JSON token that represents an object reference. - */ - private static final String TOKEN_REF = JsiiObjectRef.TOKEN_REF; + private static final String TOKEN_REF = JsiiObjectRef.TOKEN_REF; - /** - * JSON token to represent a date. - */ - private static final String TOKEN_DATE = "$jsii.date"; + private static final String TOKEN_DATE = "$jsii.date"; - /** - * JSON token to represent an enum. - */ - private static final String TOKEN_ENUM = "$jsii.enum"; + private static final String TOKEN_ENUM = "$jsii.enum"; - /** - * The standard JSON mapper. - */ - private static ObjectMapper standardMapper = new ObjectMapper(); + private final ObjectMapper objectMapper; - /** - * Object mapper configured for jsii serialization. - */ - private ObjectMapper serializer; + private final JsiiEngine jsiiEngine; - /** - * Creates an object mapper. - */ - private JsiiObjectMapper() { - this.serializer = new ObjectMapper(); - this.serializer.setSerializationInclusion(JsonInclude.Include.NON_NULL); + private JsiiObjectMapper() { + this(JsiiEngine.getInstance()); + } - SimpleModule module = new SimpleModule(); - module.addSerializer(JsiiSerializable.class, new JsiiSerializer()); - module.addSerializer(Instant.class, new DateSerializer()); - module.addSerializer(Enum.class, new EnumSerializer()); + @VisibleForTesting + JsiiObjectMapper(final JsiiEngine jsiiEngine) { + this.jsiiEngine = jsiiEngine; + this.objectMapper = new ObjectMapper(); + this.objectMapper.setSerializationInclusion(Include.NON_NULL); - this.serializer.registerModule(module); - } + final SimpleModule module = new SimpleModule("JSII", Version.unknownVersion()); + module.setDeserializerModifier(new JsiiDeserializerModifier()); + module.addSerializer(Enum.class, new EnumSerializer()); + module.addSerializer(Instant.class, new Instanterializer()); + module.addSerializer(JsiiSerializable.class, new JsiiSerializer()); - /** - * Converts a local value to a jsii JSON representation. - * - * @param value The local java value. - * @param The return type. - * @return A JSON tree. - */ - public T valueToTree(final Object value) { - if (value == null) { - return null; - } + this.objectMapper.findAndRegisterModules(); + this.objectMapper.registerModule(module); + } - return this.serializer.valueToTree(value); - } + @VisibleForTesting + ObjectMapper getObjectMapper() { + return this.objectMapper; + } + + /** + * A JsonDeserializer designed to correctly handle JSII "magic objects" that are used to remodel "pass-by-reference" + * values, dates, and enum constants. + */ + private final class JsiiDeserializer extends StdDeserializer implements ContextualDeserializer, ResolvableDeserializer{ + public static final long serialVersionUID = 1L; + + private final JsonDeserializer standardDeserializer; /** - * Converts a jsii JSON tree to a local Java type. - * @param tree The JSON tree. - * @param type The expected return type. - * @param The return type. - * @return The local Java value. - * @throws JsonProcessingException If there was a problem reading the JSON tree. + * @param standardDeserializer a standard Jackson deserialize that can be delegated to in case the object is not a + * JSII "magic object". */ - @SuppressWarnings("unchecked") - public T treeToValue(final JsonNode tree, final Class type) throws JsonProcessingException { - if (tree == null) { - return null; - } + public JsiiDeserializer(final JsonDeserializer standardDeserializer) { + super(Object.class); + this.standardDeserializer = standardDeserializer; + } - if (tree.isArray()) { - ArrayNode array = (ArrayNode) tree; - ArrayList out = new ArrayList<>(); - for (JsonNode x : array) { - out.add(treeToValue(x, Object.class)); - } - return (T) out; - } + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + final JsonNode node = p.readValueAsTree(); - if (tree.isObject() && tree.has(TOKEN_DATE)) { - return (T) Instant.parse(tree.get(TOKEN_DATE).textValue()); + if (node.isObject()) { + if (node.has(TOKEN_DATE)) { + return Instant.parse(node.get(TOKEN_DATE).textValue()); } - - if (tree.isObject() && tree.has(TOKEN_REF)) { - return (T) JsiiEngine.getInstance().nativeFromObjRef(JsiiObjectRef.parse(tree)); + if (node.has(TOKEN_ENUM)) { + return jsiiEngine.findEnumValue(node.get(TOKEN_ENUM).textValue()); } - - if (tree.isObject() && tree.has(TOKEN_ENUM)) { - return (T) JsiiEngine.getInstance().findEnumValue(tree.get(TOKEN_ENUM).textValue()); + if (node.has(TOKEN_REF)) { + return jsiiEngine.nativeFromObjRef(JsiiObjectRef.parse(node)); } + } - // json type - if (ObjectNode.class.isAssignableFrom(type)) { - return (T) tree; - } + final JsonParser nodeParser = node.traverse(p.getCodec()); + nodeParser.nextToken(); + return standardDeserializer.deserialize(nodeParser, ctxt); + } - if (tree.isObject()) { - ObjectNode objectNode = (ObjectNode) tree; - Map map = new HashMap<>(); - Iterable fields = objectNode::fieldNames; - for (String field : fields) { - Object value = treeToValue(objectNode.get(field), Object.class); - map.put(field, value); - } - return (T) map; - } + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { + if (this.standardDeserializer instanceof ContextualDeserializer) { + return new JsiiDeserializer(((ContextualDeserializer)this.standardDeserializer).createContextual(ctxt, property)); + } + return this; + } - return standardMapper.treeToValue(tree, type); + @Override + public void resolve(DeserializationContext ctxt) throws JsonMappingException { + if (this.standardDeserializer instanceof ResolvableDeserializer) { + ((ResolvableDeserializer)this.standardDeserializer).resolve(ctxt); + } + } + } + + public final class JsiiDeserializerModifier extends BeanDeserializerModifier { + @Override + public JsonDeserializer modifyDeserializer(DeserializationConfig config, + BeanDescription beanDesc, + JsonDeserializer deserializer) { + return new JsiiDeserializer(deserializer); } - /** - * Serializer for enum values. - */ - @SuppressWarnings("rawtypes") - private static class EnumSerializer extends JsonSerializer { - @Override - public void serialize(final Enum value, final JsonGenerator gen, final SerializerProvider serializers) - throws IOException { - Jsii jsii = tryGetJsiiAnnotation(value.getClass(), false); - if (jsii == null) { - throw new JsiiException("Cannot serialize non-jsii enums"); - } else { - gen.writeStartObject(); - gen.writeStringField(TOKEN_ENUM, jsii.fqn() + "/" + value.toString()); - gen.writeEndObject(); - } - } + @Override + public JsonDeserializer modifyEnumDeserializer(DeserializationConfig config, + JavaType type, + BeanDescription beanDesc, + JsonDeserializer deserializer) { + return new JsiiDeserializer(deserializer); } - /** - * Serializer for dates. - */ - private static class DateSerializer extends JsonSerializer { - @Override - public void serialize(final Instant value, final JsonGenerator gen, final SerializerProvider serializers) - throws IOException { - gen.writeStartObject(); - gen.writeStringField(TOKEN_DATE, value.toString()); - gen.writeEndObject(); - } + @Override + public JsonDeserializer modifyMapDeserializer(DeserializationConfig config, + MapType type, + BeanDescription beanDesc, + JsonDeserializer deserializer) { + return new JsiiDeserializer(deserializer); } - /** - * Serializer for classes that extend JsiiObject and any other class that implements a jsii interface. - * We use the JsiiSerializable interface as a way to identify "anything jsii-able". - */ - private static class JsiiSerializer extends JsonSerializer { - @Override - public void serialize(final JsiiSerializable o, - final JsonGenerator jsonGenerator, - final SerializerProvider serializerProvider) throws IOException { + @Override + public JsonDeserializer modifyMapLikeDeserializer(DeserializationConfig config, + MapLikeType type, + BeanDescription beanDesc, + JsonDeserializer deserializer) { + return new JsiiDeserializer(deserializer); + } + } + + /** + * Serializer for classes that extend JsiiObject and any other class that implements a jsii interface. + * We use the JsiiSerializable interface as a way to identify "anything jsii-able". + */ + private static final class JsiiSerializer extends JsonSerializer { + @Override + public void serialize(final JsiiSerializable o, + final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeTree(o.$jsii$toJson()); + } + } + + /** + * Serializer for enum values. + */ + @SuppressWarnings("rawtypes") + private static final class EnumSerializer extends JsonSerializer { + @Override + public void serialize(final Enum value, final JsonGenerator gen, final SerializerProvider serializers) throws IOException { + Jsii jsii = this.tryGetJsiiAnnotation(value.getClass(), false); + if (jsii == null) { + throw new JsiiException("Cannot serialize non-jsii enums"); + } else { + gen.writeStartObject(); + gen.writeStringField(TOKEN_ENUM, jsii.fqn() + "/" + value.toString()); + gen.writeEndObject(); + } + } - jsonGenerator.writeTree(o.$jsii$toJson()); - } + private Jsii tryGetJsiiAnnotation(final Class type, final boolean inherited) { + Jsii[] ann; + + if (inherited) { + ann = (Jsii[]) type.getAnnotationsByType(Jsii.class); + } else { + ann = (Jsii[]) type.getDeclaredAnnotationsByType(Jsii.class); + } + + if (ann.length == 0) { + return null; + } + + return ann[0]; + } + } + + /** + * Serializer for Instants. + */ + private static final class Instanterializer extends JsonSerializer { + @Override + public void serialize(final Instant value, final JsonGenerator gen, final SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField(TOKEN_DATE, value.toString()); + gen.writeEndObject(); } + } } diff --git a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectRef.java b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectRef.java index 76d372211b..3fe7826e3c 100644 --- a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectRef.java +++ b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectRef.java @@ -91,4 +91,21 @@ public String getObjId() { public String toString() { return objId; } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || !(obj instanceof JsiiObjectRef)) { + return false; + } + final JsiiObjectRef other = (JsiiObjectRef) obj; + return this.objId.equals(other.objId); + } + + @Override + public int hashCode() { + return this.objId.hashCode(); + } } diff --git a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiRuntime.java b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiRuntime.java index f27d2d6121..9cd0755092 100644 --- a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiRuntime.java +++ b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/JsiiRuntime.java @@ -3,7 +3,6 @@ import software.amazon.jsii.api.Callback; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -28,11 +27,6 @@ public final class JsiiRuntime { */ private static final String VERSION_BUILD_PART_REGEX = "\\+[a-z0-9]+$"; - /** - * JSON object mapper. - */ - private static final ObjectMapper OM = new ObjectMapper(); - /** * True to print server traces to STDERR. */ @@ -129,12 +123,7 @@ private JsonNode processCallbackResponse(final JsonNode resp) { throw new JsiiException("Cannot process callback since callbackHandler was not set"); } - Callback callback; - try { - callback = OM.treeToValue(resp.get("callback"), Callback.class); - } catch (JsonProcessingException e) { - throw new JsiiException(e); - } + Callback callback = JsiiObjectMapper.treeToValue(resp.get("callback"), Callback.class); JsonNode result = null; String error = null; @@ -222,7 +211,7 @@ private void startRuntimeIfNeeded() { if (traceEnabled) { pb.environment().put("JSII_DEBUG", "1"); } - + pb.environment().put("JSII_AGENT", "Java/" + System.getProperty("java.version")); try { @@ -282,7 +271,7 @@ JsonNode readNextResponse() { String error = this.stderr.lines().collect(Collectors.joining("\n\t")); throw new JsiiException("Child process exited unexpectedly: " + error); } - return OM.readTree(responseLine); + return JsiiObjectMapper.INSTANCE.readTree(responseLine); } catch (IOException e) { throw new JsiiException("Unable to read reply from jsii-runtime: " + e.toString(), e); } diff --git a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/api/InvokeRequest.java b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/api/InvokeRequest.java index 6381844459..3fd51a9c59 100644 --- a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/api/InvokeRequest.java +++ b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/api/InvokeRequest.java @@ -1,5 +1,6 @@ package software.amazon.jsii.api; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.List; @@ -21,7 +22,7 @@ public class InvokeRequest { /** * Method arguments. */ - private List args; + private List args; /** * @return The object reference. @@ -54,14 +55,14 @@ public void setMethod(final String method) { /** * @return Method arguments. */ - public List getArgs() { + public List getArgs() { return args; } /** * @param args Method arguments. */ - public void setArgs(final List args) { + public void setArgs(final List args) { this.args = args; } } diff --git a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/api/SetRequest.java b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/api/SetRequest.java index 8849a2caa3..13dd37eafd 100644 --- a/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/api/SetRequest.java +++ b/packages/jsii-java-runtime/project/src/main/java/software/amazon/jsii/api/SetRequest.java @@ -19,7 +19,7 @@ public class SetRequest { /** * The new value. */ - private Object value; + private JsonNode value; /** * @return The jsii object reference. @@ -52,14 +52,14 @@ public void setProperty(final String property) { /** * @return The new value. */ - public Object getValue() { + public JsonNode getValue() { return value; } /** * @param value The new value. */ - public void setValue(final Object value) { + public void setValue(final JsonNode value) { this.value = value; } } diff --git a/packages/jsii-java-runtime/project/src/test/java/software/amazon/jsii/JsiiObjectMapperTest.java b/packages/jsii-java-runtime/project/src/test/java/software/amazon/jsii/JsiiObjectMapperTest.java new file mode 100644 index 0000000000..c12f129e6d --- /dev/null +++ b/packages/jsii-java-runtime/project/src/test/java/software/amazon/jsii/JsiiObjectMapperTest.java @@ -0,0 +1,131 @@ +package software.amazon.jsii; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Charsets; +import com.google.common.io.Resources; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public final class JsiiObjectMapperTest { + private JsiiEngine jsiiEngine; + private ObjectMapper subject; + + @BeforeEach + public void setUp() { + this.jsiiEngine = mock(JsiiEngine.class); + this.subject = new JsiiObjectMapper(jsiiEngine).getObjectMapper(); + } + + @AfterEach + public void cleanUp() { + this.jsiiEngine = null; + this.subject = null; + } + + @Test + public void testDeserialization() throws Exception { + final Object mockObject1 = new Object(); + when(jsiiEngine.nativeFromObjRef(JsiiObjectRef.fromObjId("module.Type@1"))).thenReturn(mockObject1); + + when((TestEnum) jsiiEngine.findEnumValue("module.Enum#value")).thenReturn(TestEnum.DUMMY); + + final String json = + Resources.toString(Resources.getResource(this.getClass(), "complex-callback.json"), Charsets.UTF_8); + final TestCallback callback = subject.treeToValue(subject.readTree(json), TestCallback.class); + + assertEquals("CallbackID", callback.getCbid()); + assertNotNull(callback.getInvoke()); + if (callback.getCbid() != null) { + assertEquals("methodName", callback.getInvoke().getMethod()); + assertEquals(1337, callback.getInvoke().getArgs().get(0)); + assertEquals(mockObject1, callback.getInvoke().getArgs().get(1)); + assertEquals(Instant.ofEpochMilli(1553624863569L), callback.getInvoke().getArgs().get(2)); + assertEquals(TestEnum.DUMMY, callback.getInvoke().getArgs().get(3)); + } + } + + @Test + public void testSerializationOfJsiiSerializables() throws Exception { + final String dummyValue = "{\"foo\":\"Bar\"}"; + final JsiiSerializable object = mock(JsiiSerializable.class); + when(object.$jsii$toJson()).thenReturn(new ObjectMapper().readTree(dummyValue)); + + final String json = subject.writeValueAsString(object); + + assertEquals(dummyValue, json); + } + + @Test + public void testSerializationOfInstant() throws Exception { + final Instant object = Instant.ofEpochMilli(1553691207095L); + + final String json = subject.writeValueAsString(object); + + assertEquals("{\"$jsii.date\":\"2019-03-27T12:53:27.095Z\"}", json); + } + + private static enum TestEnum { + DUMMY + } + + /** + * Kinda like the Callback object expcet it does not model stuff as JsonNodes, + * so we can verify the deserialization behaviour. + */ + public static final class TestCallback { + private String cbid; + + private TestInvokeRequest invoke; + + public String getCbid() { + return cbid; + } + + public void setCbid(String cbid) { + this.cbid = cbid; + } + + public TestInvokeRequest getInvoke() { + return invoke; + } + + public void setInvoke(TestInvokeRequest invoke) { + this.invoke = invoke; + } + } + + /** + * Kinda like the InvokeRequest, except it does not model anything as JsonNode, + * so we can test the deserialization behavior properly. + */ + public static final class TestInvokeRequest { + private String method; + + private List args; + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public List getArgs() { + return args; + } + + public void setArgs(List args) { + this.args = args; + } + } +} diff --git a/packages/jsii-java-runtime/project/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/packages/jsii-java-runtime/project/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/packages/jsii-java-runtime/project/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/packages/jsii-java-runtime/project/src/test/resources/software/amazon/jsii/complex-callback.json b/packages/jsii-java-runtime/project/src/test/resources/software/amazon/jsii/complex-callback.json new file mode 100644 index 0000000000..8216ba356c --- /dev/null +++ b/packages/jsii-java-runtime/project/src/test/resources/software/amazon/jsii/complex-callback.json @@ -0,0 +1,12 @@ +{ + "cbid": "CallbackID", + "invoke": { + "method": "methodName", + "args": [ + 1337, + { "$jsii.byref": "module.Type@1" }, + { "$jsii.date": "2019-03-26T18:27:43.569Z" }, + { "$jsii.enum": "module.Enum#value" } + ] + } +} diff --git a/packages/jsii-pacmak/lib/targets/java.ts b/packages/jsii-pacmak/lib/targets/java.ts index 6c2fdc17db..8422cfdd53 100644 --- a/packages/jsii-pacmak/lib/targets/java.ts +++ b/packages/jsii-pacmak/lib/targets/java.ts @@ -804,7 +804,7 @@ class JavaGenerator extends Generator { // emit $jsii$toJson which will be called to serialize this object when sent to JS this.code.line(); this.code.openBlock(`public com.fasterxml.jackson.databind.JsonNode $jsii$toJson()`); - this.code.line(`software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance;`); + this.code.line(`com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE;`); // tslint:disable-next-line:max-line-length this.code.line(`com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode();`); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc-base/java/src/main/java/software/amazon/jsii/tests/calculator/base/BaseProps.java b/packages/jsii-pacmak/test/expected.jsii-calc-base/java/src/main/java/software/amazon/jsii/tests/calculator/base/BaseProps.java index 3a7e75c7c1..ada23728e0 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc-base/java/src/main/java/software/amazon/jsii/tests/calculator/base/BaseProps.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc-base/java/src/main/java/software/amazon/jsii/tests/calculator/base/BaseProps.java @@ -58,7 +58,7 @@ public software.amazon.jsii.tests.calculator.baseofbase.Very getFoo() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("bar", om.valueToTree(this.getBar())); obj.set("foo", om.valueToTree(this.getFoo())); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/MyFirstStruct.java b/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/MyFirstStruct.java index a78ea35ce8..0fd8d95830 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/MyFirstStruct.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/MyFirstStruct.java @@ -87,7 +87,7 @@ public java.util.List getFirstOptional() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("anumber", om.valueToTree(this.getAnumber())); obj.set("astring", om.valueToTree(this.getAstring())); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/StructWithOnlyOptionals.java b/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/StructWithOnlyOptionals.java index 7eecb3e01a..87ac4d7100 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/StructWithOnlyOptionals.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc-lib/java/src/main/java/software/amazon/jsii/tests/calculator/lib/StructWithOnlyOptionals.java @@ -88,7 +88,7 @@ public java.lang.Boolean getOptional3() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("optional1", om.valueToTree(this.getOptional1())); obj.set("optional2", om.valueToTree(this.getOptional2())); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/CalculatorProps.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/CalculatorProps.java index b338d7a347..b965bc1b48 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/CalculatorProps.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/CalculatorProps.java @@ -66,7 +66,7 @@ public java.lang.Number getMaximumValue() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("initialValue", om.valueToTree(this.getInitialValue())); obj.set("maximumValue", om.valueToTree(this.getMaximumValue())); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/DerivedStruct.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/DerivedStruct.java index 3521a946d2..9e0a7ef4a8 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/DerivedStruct.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/DerivedStruct.java @@ -192,7 +192,7 @@ public java.util.List getFirstOptional() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("anotherRequired", om.valueToTree(this.getAnotherRequired())); obj.set("bool", om.valueToTree(this.getBool())); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/EraseUndefinedHashValuesOptions.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/EraseUndefinedHashValuesOptions.java index 5679345465..6c3d314d83 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/EraseUndefinedHashValuesOptions.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/EraseUndefinedHashValuesOptions.java @@ -63,7 +63,7 @@ public java.lang.String getOption2() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("option1", om.valueToTree(this.getOption1())); obj.set("option2", om.valueToTree(this.getOption2())); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ExtendsInternalInterface.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ExtendsInternalInterface.java index 2dd56d0f9e..2147ac9850 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ExtendsInternalInterface.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ExtendsInternalInterface.java @@ -42,7 +42,7 @@ public java.lang.Boolean getBoom() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("boom", om.valueToTree(this.getBoom())); return obj; diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ExtendsPrivateInterface.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ExtendsPrivateInterface.java index ddddccb47e..a54e9c9b96 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ExtendsPrivateInterface.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ExtendsPrivateInterface.java @@ -42,7 +42,7 @@ public java.util.List getMoreThings() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("moreThings", om.valueToTree(this.getMoreThings())); return obj; diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ImplictBaseOfBase.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ImplictBaseOfBase.java index 81e1bd0256..7467b51816 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ImplictBaseOfBase.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/ImplictBaseOfBase.java @@ -74,7 +74,7 @@ public software.amazon.jsii.tests.calculator.baseofbase.Very getFoo() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("goo", om.valueToTree(this.getGoo())); obj.set("bar", om.valueToTree(this.getBar())); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InterfaceImplementedByAbstractClass.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InterfaceImplementedByAbstractClass.java index 7a86e32bdf..2d82ab4fda 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InterfaceImplementedByAbstractClass.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InterfaceImplementedByAbstractClass.java @@ -46,7 +46,7 @@ public java.lang.String getPropFromInterface() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("propFromInterface", om.valueToTree(this.getPropFromInterface())); return obj; diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InterfaceInNamespaceIncludesClasses/Hello.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InterfaceInNamespaceIncludesClasses/Hello.java index 5be456c296..df4d2c11a9 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InterfaceInNamespaceIncludesClasses/Hello.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InterfaceInNamespaceIncludesClasses/Hello.java @@ -42,7 +42,7 @@ public java.lang.Number getFoo() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("foo", om.valueToTree(this.getFoo())); return obj; diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InterfaceInNamespaceOnlyInterface/Hello.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InterfaceInNamespaceOnlyInterface/Hello.java index 16c645adbf..cd5d24e776 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InterfaceInNamespaceOnlyInterface/Hello.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/InterfaceInNamespaceOnlyInterface/Hello.java @@ -42,7 +42,7 @@ public java.lang.Number getFoo() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("foo", om.valueToTree(this.getFoo())); return obj; diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/LoadBalancedFargateServiceProps.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/LoadBalancedFargateServiceProps.java index dd8791035c..39ce1d3ac4 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/LoadBalancedFargateServiceProps.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/LoadBalancedFargateServiceProps.java @@ -189,7 +189,7 @@ public java.lang.Boolean getPublicTasks() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("containerPort", om.valueToTree(this.getContainerPort())); obj.set("cpu", om.valueToTree(this.getCpu())); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/NullShouldBeTreatedAsUndefinedData.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/NullShouldBeTreatedAsUndefinedData.java index bdef5fc26a..ec17387884 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/NullShouldBeTreatedAsUndefinedData.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/NullShouldBeTreatedAsUndefinedData.java @@ -61,7 +61,7 @@ public java.lang.Object getThisShouldBeUndefined() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("arrayWithThreeElementsAndUndefinedAsSecondArgument", om.valueToTree(this.getArrayWithThreeElementsAndUndefinedAsSecondArgument())); obj.set("thisShouldBeUndefined", om.valueToTree(this.getThisShouldBeUndefined())); diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/UnionProperties.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/UnionProperties.java index ff141b54da..03d9389fc2 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/UnionProperties.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/UnionProperties.java @@ -88,7 +88,7 @@ public java.lang.Object getFoo() { } public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { - software.amazon.jsii.JsiiObjectMapper om = software.amazon.jsii.JsiiObjectMapper.instance; + com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); obj.set("bar", om.valueToTree(this.getBar())); obj.set("foo", om.valueToTree(this.getFoo())); From 1700a10081cb6195616b5b5722eff6ec87926e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Wed, 27 Mar 2019 16:28:17 +0100 Subject: [PATCH 5/6] WIP --- packages/jsii-calc/lib/compliance.ts | 4 +-- packages/jsii-calc/package.json | 2 ++ packages/jsii-calc/test/assembly.jsii | 38 +++++++++++++++++++++++- packages/jsii-kernel/test/test.kernel.ts | 4 +++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/jsii-calc/lib/compliance.ts b/packages/jsii-calc/lib/compliance.ts index 514b2ff8da..d9b9d253b5 100644 --- a/packages/jsii-calc/lib/compliance.ts +++ b/packages/jsii-calc/lib/compliance.ts @@ -1519,12 +1519,12 @@ export class ConsumersOfThisCrazyTypeSystem { // Ensure the JSII kernel can pass "this" out to JSII remotes from within the constructor (this is dirty, but possible) /// export abstract class PartiallyInitializedThisConsumer { - public abstract consumePartiallyInitializedThis(obj: ConstructorPassesThisOut): string; + public abstract consumePartiallyInitializedThis(obj: ConstructorPassesThisOut, dt: Date, ev: AllTypesEnum): string; } export class ConstructorPassesThisOut { constructor(consumer: PartiallyInitializedThisConsumer) { - const result = consumer.consumePartiallyInitializedThis(this); + const result = consumer.consumePartiallyInitializedThis(this, new Date(0), AllTypesEnum.ThisIsGreat); if (result !== 'OK') { throw new Error(`Expected OK but received ${result}`); } diff --git a/packages/jsii-calc/package.json b/packages/jsii-calc/package.json index 0a9caf90ce..6c1019c133 100644 --- a/packages/jsii-calc/package.json +++ b/packages/jsii-calc/package.json @@ -36,11 +36,13 @@ ], "dependencies": { "@scope/jsii-calc-base": "^0.8.0", + "@scope/jsii-calc-base-of-base": "^0.8.0", "@scope/jsii-calc-lib": "^0.8.0", "jsii-calc-bundled": "^0.8.0" }, "peerDependencies": { "@scope/jsii-calc-base": "^0.8.0", + "@scope/jsii-calc-base-of-base": "^0.8.0", "@scope/jsii-calc-lib": "^0.8.0" }, "devDependencies": { diff --git a/packages/jsii-calc/test/assembly.jsii b/packages/jsii-calc/test/assembly.jsii index d0052c0864..c34cd76a6f 100644 --- a/packages/jsii-calc/test/assembly.jsii +++ b/packages/jsii-calc/test/assembly.jsii @@ -84,6 +84,30 @@ }, "version": "0.8.0" }, + "@scope/jsii-calc-base-of-base": { + "peer": true, + "targets": { + "dotnet": { + "namespace": "Amazon.JSII.Tests.CalculatorNamespace.BaseOfBaseNamespace", + "packageId": "Amazon.JSII.Tests.CalculatorPackageId.BaseOfBasePackageId" + }, + "java": { + "maven": { + "artifactId": "calculator-base-of-base", + "groupId": "software.amazon.jsii.tests" + }, + "package": "software.amazon.jsii.tests.calculator.baseofbase" + }, + "js": { + "npm": "@scope/jsii-calc-base-of-base" + }, + "python": { + "distName": "scope.jsii-calc-base-of-base", + "module": "scope.jsii_calc_base_of_base" + } + }, + "version": "0.8.0" + }, "@scope/jsii-calc-lib": { "dependencies": { "@scope/jsii-calc-base": { @@ -3261,6 +3285,18 @@ "type": { "fqn": "jsii-calc.ConstructorPassesThisOut" } + }, + { + "name": "dt", + "type": { + "primitive": "date" + } + }, + { + "name": "ev", + "type": { + "fqn": "jsii-calc.AllTypesEnum" + } } ], "returns": { @@ -4417,5 +4453,5 @@ } }, "version": "0.8.0", - "fingerprint": "aqcu48O8v6/9d2dj1e13X/Bhw/epeUBSh3OnlTIM2Vw=" + "fingerprint": "NhkOemLaqdaG4tjpWfH8KpE8H6KXusJi3YGtjqwnwf8=" } diff --git a/packages/jsii-kernel/test/test.kernel.ts b/packages/jsii-kernel/test/test.kernel.ts index 4378ae6d45..f6fcee2faa 100644 --- a/packages/jsii-kernel/test/test.kernel.ts +++ b/packages/jsii-kernel/test/test.kernel.ts @@ -974,6 +974,10 @@ defineTest('Object ID does not get re-allocated when the constructor passes "thi test.equal(callback.invoke && callback.invoke.method, 'consumePartiallyInitializedThis'); test.deepEqual(callback.invoke && callback.invoke.args && callback.invoke.args, [{ [api.TOKEN_REF]: 'jsii-calc.ConstructorPassesThisOut@10002' + }, { + [api.TOKEN_DATE]: '1970-01-01T00:00:00.000Z' + }, { + [api.TOKEN_ENUM]: '' }]); return 'OK'; }); From 42a50fb891d12ea08844aec6301f145f2a956440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Thu, 28 Mar 2019 11:50:09 +0100 Subject: [PATCH 6/6] Add a comment --- .../src/Amazon.JSII.Runtime/CallbackExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/CallbackExtensions.cs b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/CallbackExtensions.cs index 214aac795d..fef3002435 100644 --- a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/CallbackExtensions.cs +++ b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/CallbackExtensions.cs @@ -122,6 +122,10 @@ static void InvokeSetter(SetRequest request, IReferenceMap referenceMap) methodInfo.Invoke(deputy, new object[] { FromKernel(request.Value, referenceMap) }); } + /* + * This is a temporary workaround / hack to solve an immediate problem, but does not completely solve the + * problem to it's full extent. See https://github.com/awslabs/jsii/issues/404 for more information. + */ private static object FromKernel(object obj, IReferenceMap referenceMap) { if (obj is JObject)