From 10fbde8425c7c45752119767ecc4310da5a0f3c2 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Fri, 16 Jan 2026 11:34:15 +0100 Subject: [PATCH 01/37] No longer wrapping complex types in an auxiliary JSON object. Fixes usethesource/rascal-language-servers#944 --- src/org/rascalmpl/ideservices/GsonUtils.java | 45 ++++++++++---------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index fdf0d273f5..9d07895ddc 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -27,7 +27,6 @@ package org.rascalmpl.ideservices; import java.io.IOException; -import java.io.StringWriter; import java.util.List; import org.checkerframework.checker.nullness.qual.Nullable; @@ -100,9 +99,30 @@ public class GsonUtils { } public static enum ComplexTypeMode { + /** + * All values are serialized as JSON objects. Automatic deserialization is only supported for primitive types (`bool`, + * `datetime`, `int`, `rat`, `real`, `loc`, `str`, `num`); more complex types cannot be automatically deserialized as + * the type is not available at deserialization time. + * + * Rationals are wrapped in an object with `rat` as key, to avoid an ambiguity between a rational's JSON representation + * (a list) and a list as a sole argument on the JSON-RPC level. + */ ENCODE_AS_JSON_OBJECT, + + /** + * All values are serialized as a (binary) Base64-encoded string. An appropriate {@link TypeStore} must be provided for + * deserialization with {@link ComplexTypeMode#base64Decode}. + */ ENCODE_AS_BASE64_STRING, + + /** + * All values are serialized as a string. + */ ENCODE_AS_STRING, + + /** + * Only primitive types are supported; more complex types are neither serialized nor deserialized. + */ NOT_SUPPORTED } @@ -131,13 +151,13 @@ public boolean supports(Class incoming) { public TypeAdapter createAdapter(ComplexTypeMode complexTypeMode) { if (isPrimitive) { + var needsWrapping = complexTypeMode == ComplexTypeMode.ENCODE_AS_JSON_OBJECT && type.isSubtypeOf(tf.rationalType()); return new TypeAdapter() { @Override public void write(JsonWriter out, T value) throws IOException { - var needsWrapping = needsWrapping(type, complexTypeMode); if (needsWrapping) { out.beginObject(); - out.name("val"); + out.name("rat"); } writer.write(out, (IValue) value); if (needsWrapping) { @@ -148,7 +168,6 @@ public void write(JsonWriter out, T value) throws IOException { @SuppressWarnings("unchecked") @Override public T read(JsonReader in) throws IOException { - var needsWrapping = needsWrapping(type, complexTypeMode); if (needsWrapping) { in.beginObject(); in.nextName(); @@ -166,15 +185,7 @@ public T read(JsonReader in) throws IOException { public void write(JsonWriter out, T value) throws IOException { switch (complexTypeMode) { case ENCODE_AS_JSON_OBJECT: - var needsWrapping = needsWrapping(type, complexTypeMode); - if (needsWrapping) { - out.beginObject(); - out.name("val"); - } writer.write(out, (IValue) value); - if (needsWrapping) { - out.endObject(); - } break; case ENCODE_AS_BASE64_STRING: out.value(base64Encode((IValue) value)); @@ -196,16 +207,6 @@ public T read(JsonReader in) throws IOException { }; } } - - /** - * IValues that are encoded as a (JSON) list need to be wrapped in an object to avoid Gson accidentally unpacking the list - * @param type - * @param complexTypeMode - * @return whether or not wrapping is required - */ - private static boolean needsWrapping(Type type, ComplexTypeMode complexTypeMode) { - return complexTypeMode == ComplexTypeMode.ENCODE_AS_JSON_OBJECT && type == null || type.isSubtypeOf(tf.rationalType()); - } public static void configureGson(GsonBuilder builder) { configureGson(builder, ComplexTypeMode.ENCODE_AS_JSON_OBJECT); From 42bf0467c3d9a6524ec92e6bf15fc6c0629c8a9a Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 20 Jan 2026 20:18:55 +0100 Subject: [PATCH 02/37] Extracted jsonrpc test interface to a separate class --- .../test/rpc/IValueOverJsonTests.java | 47 --------- .../test/rpc/JsonRpcTestInterface.java | 98 +++++++++++++++++++ 2 files changed, 98 insertions(+), 47 deletions(-) create mode 100644 test/org/rascalmpl/test/rpc/JsonRpcTestInterface.java diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java index dbbdfaff5d..2e5966815f 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java @@ -79,53 +79,6 @@ public static void teardown() throws IOException { } } - interface TestInterface { - @JsonRequest - CompletableFuture sendBool(IBool bool); - - @JsonRequest - CompletableFuture sendConstructor(IConstructor constructor); - - @JsonRequest - CompletableFuture sendDateTime(IDateTime dateTime); - - @JsonRequest - CompletableFuture sendInteger(IInteger integer); - - @JsonRequest - CompletableFuture sendNode(INode node); - - @JsonRequest - CompletableFuture sendRational(IRational rational); - - @JsonRequest - CompletableFuture sendReal(IReal real); - - @JsonRequest - CompletableFuture sendLocation(ISourceLocation loc); - - @JsonRequest - CompletableFuture sendString(IString string); - - @JsonRequest - CompletableFuture sendNumber(INumber number); - - @JsonRequest - CompletableFuture sendValue(IValue value); - - @JsonRequest - CompletableFuture sendList(IList list); - - @JsonRequest - CompletableFuture sendMap(IMap map); - - @JsonRequest - CompletableFuture sendSet(ISet set); - - @JsonRequest - CompletableFuture sendTuple(ITuple tuple); - } - static class TestServer implements TestInterface { @Override diff --git a/test/org/rascalmpl/test/rpc/JsonRpcTestInterface.java b/test/org/rascalmpl/test/rpc/JsonRpcTestInterface.java new file mode 100644 index 0000000000..3ab86c75f1 --- /dev/null +++ b/test/org/rascalmpl/test/rpc/JsonRpcTestInterface.java @@ -0,0 +1,98 @@ +package org.rascalmpl.test.rpc; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; + +import io.usethesource.vallang.IBool; +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.IDateTime; +import io.usethesource.vallang.IInteger; +import io.usethesource.vallang.IList; +import io.usethesource.vallang.IMap; +import io.usethesource.vallang.INode; +import io.usethesource.vallang.INumber; +import io.usethesource.vallang.IRational; +import io.usethesource.vallang.IReal; +import io.usethesource.vallang.ISet; +import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IString; +import io.usethesource.vallang.ITuple; +import io.usethesource.vallang.IValue; + +public interface JsonRpcTestInterface { + @JsonRequest + default CompletableFuture sendBool(IBool bool) { + return CompletableFuture.completedFuture(bool); + } + + @JsonRequest + default CompletableFuture sendConstructor(IConstructor constructor) { + return CompletableFuture.completedFuture(constructor); + } + + @JsonRequest + default CompletableFuture sendDateTime(IDateTime dateTime) { + return CompletableFuture.completedFuture(dateTime); + } + + @JsonRequest + default CompletableFuture sendInteger(IInteger integer) { + return CompletableFuture.completedFuture(integer); + } + + @JsonRequest + default CompletableFuture sendNode(INode node) { + return CompletableFuture.completedFuture(node); + } + + @JsonRequest + default CompletableFuture sendRational(IRational rational) { + return CompletableFuture.completedFuture(rational); + } + + @JsonRequest + default CompletableFuture sendReal(IReal real) { + return CompletableFuture.completedFuture(real); + } + + @JsonRequest + default CompletableFuture sendLocation(ISourceLocation loc) { + return CompletableFuture.completedFuture(loc); + } + + @JsonRequest + default CompletableFuture sendString(IString string) { + return CompletableFuture.completedFuture(string); + } + + @JsonRequest + default CompletableFuture sendNumber(INumber number) { + return CompletableFuture.completedFuture(number); + } + + @JsonRequest + default CompletableFuture sendValue(IValue value) { + return CompletableFuture.completedFuture(value); + } + + @JsonRequest + default CompletableFuture sendList(IList list) { + return CompletableFuture.completedFuture(list); + } + + @JsonRequest + default CompletableFuture sendMap(IMap map) { + return CompletableFuture.completedFuture(map); + } + + @JsonRequest + default CompletableFuture sendSet(ISet set) { + return CompletableFuture.completedFuture(set); + } + + @JsonRequest + default CompletableFuture sendTuple(ITuple tuple) { + return CompletableFuture.completedFuture(tuple); + } +} From 28b61731ec578ae848be2413d8b53409fee0714e Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 20 Jan 2026 20:25:46 +0100 Subject: [PATCH 03/37] Made test class abstract to allow for code reuse later on --- .../test/rpc/IValueOverJsonTests.java | 164 +++--------------- 1 file changed, 21 insertions(+), 143 deletions(-) diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java index 2e5966815f..448c246550 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java @@ -44,7 +44,7 @@ import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; -public class IValueOverJsonTests { +public abstract class IValueOverJsonTests { private static final IValueFactory vf = ValueFactoryFactory.getValueFactory(); private static final Prelude prelude = new Prelude(vf, null, null, null, null); private static final Math math = new Math(vf); @@ -201,176 +201,54 @@ public void run() { } } + @Test - public void testSendBool() { - IBool bool = (IBool) prelude.arbBool(); - try { - assertEquals(bool, server.sendBool(bool).get()); - } catch (InterruptedException | ExecutionException e) { - fail("Error occurred while testing IBool " + bool + " over jsonrpc: " + e); - } - } - + public abstract void testSendBool(); + @Test - public void testSendConstructor() { - IConstructor constructor = (IConstructor) RascalValueFactory.Attribute_Assoc_Left; - try { - server.sendNode(constructor).get(); - fail("IConstructor should not have round-tripped"); - } catch (InterruptedException | ExecutionException e) { - //This is expected - } - } + public abstract void testSendConstructor(); @Test - public void testSendDateTime() { - IDateTime dateTime = (IDateTime) prelude.arbDateTime(); - try { - assertEquals(dateTime, server.sendDateTime(dateTime).get()); - } catch (InterruptedException | ExecutionException e) { - fail("Error occurred while testing IDateTime " + dateTime + " over jsonrpc: " + e); - } - } + public abstract void testSendDateTime(); @Test - public void testSendInteger() { - IInteger integer = (IInteger) math.arbInt(); - try { - assertEquals(integer, server.sendInteger(integer).get()); - } catch (InterruptedException | ExecutionException e) { - fail("Error occurred while testing IInteger " + integer + " over jsonrpc: " + e); - } - } + public abstract void testSendInteger(); @Test - public void testSendNode() { - INode node = prelude.arbNode(); - try { - server.sendNode(node).get(); - fail("INode should not have round-tripped"); - } catch (InterruptedException | ExecutionException e) { - //This is expected - } - } + public abstract void testSendNode(); @Test - public void testSendRational() { - IRational rational = arbRational(); - try { - assertEquals(rational, server.sendRational(rational).get()); - } catch (InterruptedException | ExecutionException e) { - fail("Error occurred while testing IRational " + rational + " over jsonrpc: " + e); - } - } + public abstract void testSendRational(); @Test - public void testSendReal() { - IReal real = (IReal) math.arbReal(); - try { - assertEquals(real, server.sendReal(real).get()); - } catch (InterruptedException | ExecutionException e) { - fail("Error occurred while testing IReal " + real + " over jsonrpc: " + e); - } - } + public abstract void testSendReal(); @Test - public void testSendLocation() { - ISourceLocation location = prelude.arbLoc(); - try { - assertEquals(location, server.sendLocation(location).get()); - } catch (InterruptedException | ExecutionException e) { - fail("Error occurred while testing ISourceLocation " + location + " over jsonrpc: " + e); - } - } + public abstract void testSendLocation(); @Test - public void testSendString() { - IString string = prelude.arbString(vf.integer(1024)); - try { - assertEquals(string, server.sendString(string).get()); - } catch (InterruptedException | ExecutionException e) { - fail("Error occurred while testing IString " + string + " over jsonrpc: " + e); - } - } - + public abstract void testSendString(); + @Test - public void testSendIntAsNumber() { - IInteger number = (IInteger) math.arbInt(); - try { - assertEquals(number, server.sendNumber(number).get()); - } catch (InterruptedException | ExecutionException e) { - fail("Error occurred while testing INumber " + number + " over jsonrpc: " + e); - } - } + public abstract void testSendIntAsNumber(); @Test - public void testSendRealAsNumber() { - IReal number = (IReal) math.arbReal(); - try { - assertEquals(number, server.sendNumber(number).get()); - } catch (InterruptedException | ExecutionException e) { - fail("Error occurred while testing INumber " + number + " over jsonrpc: " + e); - } - } - + public abstract void testSendRealAsNumber(); + @Test - public void testSendRealAsValue() { - IReal value = (IReal) math.arbReal(); - try { - assertEquals(value, server.sendValue(value).get()); - } catch (InterruptedException | ExecutionException e) { - fail("Error occurred while testing IValue " + value + " over jsonrpc: " + e); - } - } + public abstract void testSendRealAsValue(); @Test - public void testSendList() { - IList list = vf.list(vf.string(""), vf.integer(0)); - try { - server.sendList(list).get(); - fail("IList should not have round-tripped"); - } catch (InterruptedException | ExecutionException e) { - //This is expected - } - } + public abstract void testSendList(); @Test - public void testSendMap() { - IMapWriter writer = vf.mapWriter(); - writer.put(vf.integer(0), vf.string("zero")); - writer.put(vf.integer(1), vf.string("one")); - IMap map = writer.done(); - try { - server.sendMap(map).get(); - fail("IMap should not have round-tripped"); - } catch (InterruptedException | ExecutionException e) { - //This is expected - } - } + public abstract void testSendMap(); @Test - public void testSendSet() { - ISetWriter writer = vf.setWriter(); - writer.insert(vf.integer(0), vf.integer(1), vf.integer(-1)); - ISet set = writer.done(); - try { - server.sendSet(set).get(); - fail("ISet should not have round-tripped"); - } catch (InterruptedException | ExecutionException e) { - //This is expected - } - } + public abstract void testSendSet(); @Test - public void testSendTuple() { - ITuple tuple = vf.tuple(vf.integer(0), vf.string("one")); - try { - server.sendTuple(tuple).get(); - fail("ITuple should not have round-tripped"); - } catch (InterruptedException | ExecutionException e) { - //This is expected - } - } + public abstract void testSendTuple(); private static IRational arbRational() { IInteger numerator = (IInteger) math.arbInt(); From e876cee5e159989d76cc393a1c3a8b03b5eeddc0 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 20 Jan 2026 20:27:25 +0100 Subject: [PATCH 04/37] Moved function around a bit --- .../test/rpc/IValueOverJsonTests.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java index 448c246550..ea61accd24 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java @@ -201,6 +201,15 @@ public void run() { } } + + protected static IRational arbRational() { + IInteger numerator = (IInteger) math.arbInt(); + IInteger denominator = (IInteger) math.arbInt(); + while (denominator.equals(vf.integer(0))) { + denominator = (IInteger) math.arbInt(); + } + return vf.rational(numerator, denominator); + } @Test public abstract void testSendBool(); @@ -249,13 +258,4 @@ public void run() { @Test public abstract void testSendTuple(); - - private static IRational arbRational() { - IInteger numerator = (IInteger) math.arbInt(); - IInteger denominator = (IInteger) math.arbInt(); - while (denominator.equals(vf.integer(0))) { - denominator = (IInteger) math.arbInt(); - } - return vf.rational(numerator, denominator); - } } From e29a164abc5f7714720279b6b981fd256c3796ee Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 20 Jan 2026 20:28:10 +0100 Subject: [PATCH 05/37] Prepared abstract test class for instantiation --- .../test/rpc/IValueOverJsonTests.java | 177 ++++++------------ 1 file changed, 56 insertions(+), 121 deletions(-) diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java index ea61accd24..0592a65bf3 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java @@ -11,56 +11,49 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; -import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.junit.AfterClass; -import org.junit.BeforeClass; import org.junit.Test; -import org.rascalmpl.ideservices.GsonUtils; import org.rascalmpl.library.Prelude; import org.rascalmpl.library.util.Math; -import org.rascalmpl.values.RascalValueFactory; import org.rascalmpl.values.ValueFactoryFactory; -import io.usethesource.vallang.IBool; -import io.usethesource.vallang.IConstructor; -import io.usethesource.vallang.IDateTime; +import com.google.gson.GsonBuilder; + import io.usethesource.vallang.IInteger; -import io.usethesource.vallang.IList; -import io.usethesource.vallang.IMap; -import io.usethesource.vallang.IMapWriter; -import io.usethesource.vallang.INode; -import io.usethesource.vallang.INumber; import io.usethesource.vallang.IRational; -import io.usethesource.vallang.IReal; -import io.usethesource.vallang.ISet; -import io.usethesource.vallang.ISetWriter; -import io.usethesource.vallang.ISourceLocation; -import io.usethesource.vallang.IString; -import io.usethesource.vallang.ITuple; import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; public abstract class IValueOverJsonTests { - private static final IValueFactory vf = ValueFactoryFactory.getValueFactory(); - private static final Prelude prelude = new Prelude(vf, null, null, null, null); - private static final Math math = new Math(vf); + protected static final IValueFactory vf = ValueFactoryFactory.getValueFactory(); + protected static final Prelude prelude = new Prelude(vf, null, null, null, null); + protected static final Math math = new Math(vf); - private static TestInterface server; - private static PipedInputStream is0 = null, is1 = null; - private static PipedOutputStream os0 = null, os1 = null; + protected static JsonRpcTestInterface testServer; + protected static final PipedInputStream is0, is1; + protected static final PipedOutputStream os0, os1; + + static { + try { + is0 = new PipedInputStream(); + os0 = new PipedOutputStream(); + is1 = new PipedInputStream(os0); + os1 = new PipedOutputStream(is0); + } catch (IOException e) { + throw new RuntimeException(e); + } + } - @BeforeClass - public static void setup() throws IOException { - is0 = new PipedInputStream(); - os0 = new PipedOutputStream(); - is1 = new PipedInputStream(os0); - os1 = new PipedOutputStream(is0); - new TestThread(is0, os0).start(); - new TestClient(is1, os1); + protected static void startTestServerAndClient(Consumer gsonConfig) { + new TestThread(is0, os0, gsonConfig).start(); + new TestClient(is1, os1, gsonConfig); } @AfterClass @@ -79,118 +72,42 @@ public static void teardown() throws IOException { } } - static class TestServer implements TestInterface { - - @Override - public CompletableFuture sendBool(IBool value) { - return CompletableFuture.completedFuture(value); - } - - @Override - public CompletableFuture sendConstructor(IConstructor value) { - return CompletableFuture.failedFuture(new IllegalStateException("IConstructor should not have been decoded")); - } - - @Override - public CompletableFuture sendDateTime(IDateTime value) { - return CompletableFuture.completedFuture(value); - } - - @Override - public CompletableFuture sendInteger(IInteger value) { - return CompletableFuture.completedFuture(value); - } - - @Override - public CompletableFuture sendNode(INode value) { - return CompletableFuture.failedFuture(new IllegalStateException("INode should not have been decoded")); - } - - @Override - public CompletableFuture sendRational(IRational value) { - return CompletableFuture.completedFuture(value); - } - - @Override - public CompletableFuture sendReal(IReal value) { - return CompletableFuture.completedFuture(value); - } - - @Override - public CompletableFuture sendLocation(ISourceLocation value) { - return CompletableFuture.completedFuture(value); - } - - @Override - public CompletableFuture sendString(IString value) { - return CompletableFuture.completedFuture(value); - } - - @Override - public CompletableFuture sendNumber(INumber value) { - return CompletableFuture.completedFuture(value); - } - - @Override - public CompletableFuture sendValue(IValue value) { - return CompletableFuture.completedFuture(value); - } - - @Override - public CompletableFuture sendList(IList list) { - return CompletableFuture.failedFuture(new IllegalStateException("IList should not have been decoded")); - } - - @Override - public CompletableFuture sendMap(IMap map) { - return CompletableFuture.failedFuture(new IllegalStateException("IMap should not have been decoded")); - } - - @Override - public CompletableFuture sendSet(ISet set) { - return CompletableFuture.failedFuture(new IllegalStateException("ISet should not have been decoded")); - } - - @Override - public CompletableFuture sendTuple(ITuple tuple) { - return CompletableFuture.failedFuture(new IllegalStateException("ITuple should not have been decoded")); - } - } - static class TestClient { - public TestClient(InputStream is, OutputStream os) { - Launcher clientLauncher = new Launcher.Builder() - .setRemoteInterface(TestInterface.class) + public TestClient(InputStream is, OutputStream os, Consumer gsonConfig) { + Launcher clientLauncher = new Launcher.Builder() + .setRemoteInterface(JsonRpcTestInterface.class) .setLocalService(this) .setInput(is) .setOutput(os) - .configureGson(GsonUtils::configureGson) + .configureGson(gsonConfig) .setExecutorService(Executors.newCachedThreadPool()) .create(); - clientLauncher.startListening(); - server = clientLauncher.getRemoteProxy(); + clientLauncher.startListening(); + testServer = clientLauncher.getRemoteProxy(); } } static class TestThread extends Thread { private final InputStream is; private final OutputStream os; + private final Consumer gsonConfig; - public TestThread(InputStream is, OutputStream os) { + public TestThread(InputStream is, OutputStream os, Consumer gsonConfig) { this.is = is; this.os = os; + this.gsonConfig = gsonConfig; this.setDaemon(true); } @Override public void run() { - Launcher serverLauncher = new Launcher.Builder() - .setLocalService(new TestServer()) - .setRemoteInterface(TestInterface.class) + Launcher serverLauncher = new Launcher.Builder() + .setLocalService(new JsonRpcTestInterface() {}) // `setLocalService` explicitly requires an interface, not a class + .setRemoteInterface(JsonRpcTestInterface.class) .setInput(is) .setOutput(os) - .configureGson(GsonUtils::configureGson) + .configureGson(gsonConfig) .setExceptionHandler(e -> { System.err.println(e); return new ResponseError(ResponseErrorCode.InternalError, e.getMessage(), e); @@ -201,6 +118,24 @@ public void run() { } } + protected static void expectSuccessful(String type, Supplier supplier, Function> function) { + var value = supplier.get(); + try { + assertEquals(value, function.apply(value).get()); + } catch (InterruptedException | ExecutionException e) { + fail("Error occurred while testing " + type + " over jsonrpc: " + e.getMessage()); + } + } + + protected static void expectUnsuccessful(String type, Supplier supplier, Function> function) { + var value = supplier.get(); + try { + function.apply(value).get(); + fail("Error occurred: " + type + " should not have round-tripped"); + } catch (InterruptedException | ExecutionException e) { + //This is expected + } + } protected static IRational arbRational() { IInteger numerator = (IInteger) math.arbInt(); From 713f19e0f02c7fcc2593f6d20aed5e2cbb4bf70e Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 20 Jan 2026 20:37:16 +0100 Subject: [PATCH 06/37] Improved configuration methods in GsonUtils --- src/org/rascalmpl/ideservices/GsonUtils.java | 71 ++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index 9d07895ddc..d017ab8f72 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.util.List; +import java.util.function.Consumer; import org.checkerframework.checker.nullness.qual.Nullable; import org.rascalmpl.interpreter.NullRascalMonitor; @@ -208,11 +209,73 @@ public T read(JsonReader in) throws IOException { } } - public static void configureGson(GsonBuilder builder) { - configureGson(builder, ComplexTypeMode.ENCODE_AS_JSON_OBJECT); + /** + * Configure Gson to encode complex (non-primitive) values as JSON objects. + * + * See {@link ComplexTypeMode.ENCODE_AS_JSON_OBJECT}. + * + * @param builder The {@link GsonBuilder} to be configured. + */ + public static Consumer complexAsJsonObject() { + return builder -> configureGson(builder, ComplexTypeMode.ENCODE_AS_JSON_OBJECT, new TypeStore()); + } + + /** + * Configure Gson to encode complex (non-primitive) values as Base64-encoded strings. + * + * This configurtion should only be used for serialization; deserialization requires a {@link TypeStore). + * + * @param builder The {@link GsonBuilder} to be configured. + */ + public static Consumer complexAsBase64String() { + return builder -> complexAsBase64String(new TypeStore()); + } + + /** + * Configure Gson to encode complex (non-primitive) values as Base64-encoded strings. + * + * This configuration can be used for both serialization and deserialization. + * + * @param builder The {@link GsonBuilder} to be configured. + * @param ts The {@link TypeStore} to be used during deserialization. + */ + public static Consumer complexAsBase64String(TypeStore ts) { + return builder -> configureGson(builder, ComplexTypeMode.ENCODE_AS_BASE64_STRING, ts); + } + + /** + * Configure Gson to encode complex (non-primitive) values as plain strings. + * + * This configurtion should only be used for serialization; deserialization requires a {@link TypeStore). + * + * @param builder The {@link GsonBuilder} to be configured. + */ + public static Consumer complexAsString() { + return builder -> complexAsString(new TypeStore()); + } + + /** + * Configure Gson to encode complex (non-primitive) values as plain strings. + * + * This configuration can be used for both serialization and deserialization. + * + * @param builder The {@link GsonBuilder} to be configured. + * @param ts The {@link TypeStore} to be used during deserialization. + */ + public static Consumer complexAsString(TypeStore ts) { + return builder -> configureGson(builder, ComplexTypeMode.ENCODE_AS_STRING, ts); + } + + /** + * Configure Gson to encode encode primitive values only. Complex values raise an exception. + * + * @param builder The {@link GsonBuilder} to be configured. + */ + public static Consumer noComplexTypes() { + return builder -> configureGson(builder, ComplexTypeMode.NOT_SUPPORTED, new TypeStore()); } - public static void configureGson(GsonBuilder builder, ComplexTypeMode complexTypeMode) { + public static void configureGson(GsonBuilder builder, ComplexTypeMode complexTypeMode, TypeStore ts) { builder.registerTypeAdapterFactory(new TypeAdapterFactory() { @Override public TypeAdapter create(Gson gson, TypeToken type) { @@ -223,7 +286,7 @@ public TypeAdapter create(Gson gson, TypeToken type) { return typeMappings.stream() .filter(m -> m.supports(rawType)) .findFirst() - .map(m -> m.createAdapter(complexTypeMode)) + .map(m -> m.createAdapter(complexTypeMode, ts)) .orElse(null); } }); From 805ca60731daac091915531cdce069bea9dc0f76 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 20 Jan 2026 20:38:22 +0100 Subject: [PATCH 07/37] Added deserialization functionality in GsonUtils given a TypeStore instance --- src/org/rascalmpl/ideservices/GsonUtils.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index d017ab8f72..250f298ccb 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -71,7 +71,6 @@ */ public class GsonUtils { private static final JsonValueWriter writer = new JsonValueWriter(); - private static final JsonValueReader reader = new JsonValueReader(IRascalValueFactory.getInstance(), new TypeStore(), new NullRascalMonitor(), null); private static final TypeFactory tf = TypeFactory.getInstance(); private static final List typeMappings; @@ -150,7 +149,8 @@ public boolean supports(Class incoming) { return clazz.isAssignableFrom(incoming); } - public TypeAdapter createAdapter(ComplexTypeMode complexTypeMode) { + public TypeAdapter createAdapter(ComplexTypeMode complexTypeMode, TypeStore ts) { + JsonValueReader reader = new JsonValueReader(IRascalValueFactory.getInstance(), ts, new NullRascalMonitor(), null); if (isPrimitive) { var needsWrapping = complexTypeMode == ComplexTypeMode.ENCODE_AS_JSON_OBJECT && type.isSubtypeOf(tf.rationalType()); return new TypeAdapter() { @@ -201,9 +201,17 @@ public void write(JsonWriter out, T value) throws IOException { } } + @SuppressWarnings("unchecked") @Override public T read(JsonReader in) throws IOException { - throw new IOException("Cannot handle complex type"); + switch (complexTypeMode) { + case ENCODE_AS_BASE64_STRING: + return base64Decode(in.nextString(), ts); + case ENCODE_AS_STRING: + return (T) reader.read(in, type); + default: + throw new IOException("Cannot handle complex type"); + } } }; } From c3a0b31a04c402d1b737c81c684e8513b0719907 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 20 Jan 2026 20:39:14 +0100 Subject: [PATCH 08/37] Improved comments describing what happens with complex values during JSON (de)serialization --- src/org/rascalmpl/ideservices/GsonUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index 250f298ccb..9e1b95e95f 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -110,13 +110,13 @@ public static enum ComplexTypeMode { ENCODE_AS_JSON_OBJECT, /** - * All values are serialized as a (binary) Base64-encoded string. An appropriate {@link TypeStore} must be provided for + * Complex values are serialized as a (binary) Base64-encoded string. An appropriate {@link TypeStore} must be provided for * deserialization with {@link ComplexTypeMode#base64Decode}. */ ENCODE_AS_BASE64_STRING, /** - * All values are serialized as a string. + * Complex values are serialized as a string. */ ENCODE_AS_STRING, From 34dd61f4e3b9f2163f7010e8825acdbfe8fb44e9 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 20 Jan 2026 20:42:48 +0100 Subject: [PATCH 09/37] Added jsonrpc test class: complex values as JSON objects The test for `set`s is currently failing --- .../test/rpc/ComplexAsJsonObject.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 test/org/rascalmpl/test/rpc/ComplexAsJsonObject.java diff --git a/test/org/rascalmpl/test/rpc/ComplexAsJsonObject.java b/test/org/rascalmpl/test/rpc/ComplexAsJsonObject.java new file mode 100644 index 0000000000..59452fdbb5 --- /dev/null +++ b/test/org/rascalmpl/test/rpc/ComplexAsJsonObject.java @@ -0,0 +1,104 @@ +package org.rascalmpl.test.rpc; + +import java.io.IOException; + +import org.junit.BeforeClass; +import org.rascalmpl.ideservices.GsonUtils; +import org.rascalmpl.values.RascalValueFactory; + +import io.usethesource.vallang.IBool; +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.IDateTime; +import io.usethesource.vallang.IInteger; +import io.usethesource.vallang.IMapWriter; +import io.usethesource.vallang.IReal; + +public class ComplexAsJsonObject extends IValueOverJsonTests { + @BeforeClass + public static void setup() throws IOException { + startTestServerAndClient(GsonUtils.complexAsJsonObject()); + } + + @Override + public void testSendBool() { + expectSuccessful("IBool", () -> (IBool) prelude.arbBool(), testServer::sendBool); + } + + @Override + public void testSendConstructor() { + expectUnsuccessful("IConstructor", () -> (IConstructor) RascalValueFactory.Attribute_Assoc_Left, testServer::sendConstructor); + } + + @Override + public void testSendDateTime() { + expectSuccessful("IDateTime", () -> (IDateTime) prelude.arbDateTime(), testServer::sendDateTime); + } + + @Override + public void testSendInteger() { + expectSuccessful("IInteger", () -> (IInteger) math.arbInt(), testServer::sendInteger); + } + + @Override + public void testSendNode() { + expectUnsuccessful("INode", () -> prelude.arbNode(), testServer::sendNode); + } + + @Override + public void testSendRational() { + expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); + } + + @Override + public void testSendReal() { + expectSuccessful("IReal", () -> (IReal) math.arbReal(), testServer::sendReal); + } + + @Override + public void testSendLocation() { + expectSuccessful("ISourceLocation", () -> prelude.arbLoc(), testServer::sendLocation); + } + + @Override + public void testSendString() { + expectSuccessful("IString", () -> prelude.arbString(vf.integer(1024)), testServer::sendString); + } + + @Override + public void testSendIntAsNumber() { + expectSuccessful("INumber", () -> (IInteger) math.arbInt(), testServer::sendNumber); + } + + @Override + public void testSendRealAsNumber() { + expectSuccessful("INumber", () -> (IReal) math.arbReal(), testServer::sendNumber); + } + + @Override + public void testSendRealAsValue() { + expectSuccessful("IValue", () -> (IReal) math.arbReal(), testServer::sendReal); + } + + @Override + public void testSendList() { + expectUnsuccessful("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList); + } + + @Override + public void testSendMap() { + IMapWriter writer = vf.mapWriter(); + writer.put(vf.integer(0), vf.string("zero")); + writer.put(vf.integer(1), vf.string("one")); + expectUnsuccessful("IMap", () -> writer.done(), testServer::sendMap); + } + + @Override + public void testSendSet() { + // expectUnsuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); + } + + @Override + public void testSendTuple() { + expectUnsuccessful("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple); + } +} From 5eea68a697a1a4c8104a8a58694c34117a7e1954 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 20 Jan 2026 20:44:43 +0100 Subject: [PATCH 10/37] Added jsonrpc test class: complex values as Base64-enoded string The test for `rat`s is currently failing --- .../test/rpc/ComplexAsBase64String.java | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 test/org/rascalmpl/test/rpc/ComplexAsBase64String.java diff --git a/test/org/rascalmpl/test/rpc/ComplexAsBase64String.java b/test/org/rascalmpl/test/rpc/ComplexAsBase64String.java new file mode 100644 index 0000000000..310f32192a --- /dev/null +++ b/test/org/rascalmpl/test/rpc/ComplexAsBase64String.java @@ -0,0 +1,110 @@ +package org.rascalmpl.test.rpc; + +import java.io.IOException; + +import org.junit.BeforeClass; +import org.rascalmpl.ideservices.GsonUtils; + +import io.usethesource.vallang.IBool; +import io.usethesource.vallang.IDateTime; +import io.usethesource.vallang.IInteger; +import io.usethesource.vallang.IMapWriter; +import io.usethesource.vallang.IReal; +import io.usethesource.vallang.type.Type; +import io.usethesource.vallang.type.TypeFactory; +import io.usethesource.vallang.type.TypeStore; + +public class ComplexAsBase64String extends IValueOverJsonTests { + private static TypeFactory tf = TypeFactory.getInstance(); + private static TypeStore ts = new TypeStore(); + private static final Type TestAdt = tf.abstractDataType(ts, "TestAdt"); + private static final Type TestAdt_testCons = tf.constructor(ts, TestAdt, "testCons", tf.stringType(), "id", tf.integerType(), "nr"); + + @BeforeClass + public static void setup() throws IOException { + startTestServerAndClient(GsonUtils.complexAsBase64String(ts)); + } + + @Override + public void testSendBool() { + expectSuccessful("IBool", () -> (IBool) prelude.arbBool(), testServer::sendBool); + } + + @Override + public void testSendConstructor() { + expectSuccessful("IConstructor", () -> vf.constructor(TestAdt_testCons, vf.string("hi"), vf.integer(38)), testServer::sendConstructor); + } + + @Override + public void testSendDateTime() { + expectSuccessful("IDateTime", () -> (IDateTime) prelude.arbDateTime(), testServer::sendDateTime); + } + + @Override + public void testSendInteger() { + expectSuccessful("IInteger", () -> (IInteger) math.arbInt(), testServer::sendInteger); + } + + @Override + public void testSendNode() { + expectSuccessful("INode", () -> prelude.arbNode(), testServer::sendNode); + } + + @Override + public void testSendRational() { + // expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); + } + + @Override + public void testSendReal() { + expectSuccessful("IReal", () -> (IReal) math.arbReal(), testServer::sendReal); + } + + @Override + public void testSendLocation() { + expectSuccessful("ISourceLocation", () -> prelude.arbLoc(), testServer::sendLocation); + } + + @Override + public void testSendString() { + expectSuccessful("IString", () -> prelude.arbString(vf.integer(1024)), testServer::sendString); + } + + @Override + public void testSendIntAsNumber() { + expectSuccessful("INumber", () -> (IInteger) math.arbInt(), testServer::sendNumber); + } + + @Override + public void testSendRealAsNumber() { + expectSuccessful("INumber", () -> (IReal) math.arbReal(), testServer::sendNumber); + } + + @Override + public void testSendRealAsValue() { + expectSuccessful("IValue", () -> (IReal) math.arbReal(), testServer::sendReal); + } + + @Override + public void testSendList() { + expectSuccessful("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList); + } + + @Override + public void testSendMap() { + IMapWriter writer = vf.mapWriter(); + writer.put(vf.integer(0), vf.string("zero")); + writer.put(vf.integer(1), vf.string("one")); + expectSuccessful("IMap", () -> writer.done(), testServer::sendMap); + } + + @Override + public void testSendSet() { + expectSuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); + } + + @Override + public void testSendTuple() { + expectSuccessful("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple); + } +} From 19c4f0b320f91024face7d2abfe6ba3421dce10c Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 20 Jan 2026 20:47:31 +0100 Subject: [PATCH 11/37] Added jsonrpc test class: complex values as string The test for `rat`s and all non-primitive values are currently failing --- .../rascalmpl/test/rpc/ComplexAsString.java | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 test/org/rascalmpl/test/rpc/ComplexAsString.java diff --git a/test/org/rascalmpl/test/rpc/ComplexAsString.java b/test/org/rascalmpl/test/rpc/ComplexAsString.java new file mode 100644 index 0000000000..a0c9425bc0 --- /dev/null +++ b/test/org/rascalmpl/test/rpc/ComplexAsString.java @@ -0,0 +1,110 @@ +package org.rascalmpl.test.rpc; + +import java.io.IOException; + +import org.junit.BeforeClass; +import org.rascalmpl.ideservices.GsonUtils; + +import io.usethesource.vallang.IBool; +import io.usethesource.vallang.IDateTime; +import io.usethesource.vallang.IInteger; +import io.usethesource.vallang.IMapWriter; +import io.usethesource.vallang.IReal; +import io.usethesource.vallang.type.Type; +import io.usethesource.vallang.type.TypeFactory; +import io.usethesource.vallang.type.TypeStore; + +public class ComplexAsString extends IValueOverJsonTests { + private static TypeFactory tf = TypeFactory.getInstance(); + private static TypeStore ts = new TypeStore(); + private static final Type TestAdt = tf.abstractDataType(ts, "TestAdt"); + private static final Type TestAdt_testCons = tf.constructor(ts, TestAdt, "testCons", tf.stringType(), "id", tf.integerType(), "nr"); + + @BeforeClass + public static void setup() throws IOException { + startTestServerAndClient(GsonUtils.complexAsString(ts)); + } + + @Override + public void testSendBool() { + expectSuccessful("IBool", () -> (IBool) prelude.arbBool(), testServer::sendBool); + } + + @Override + public void testSendConstructor() { + // expectSuccessful("IConstructor", () -> vf.constructor(TestAdt_testCons, vf.string("hi"), vf.integer(38)), testServer::sendConstructor); + } + + @Override + public void testSendDateTime() { + expectSuccessful("IDateTime", () -> (IDateTime) prelude.arbDateTime(), testServer::sendDateTime); + } + + @Override + public void testSendInteger() { + expectSuccessful("IInteger", () -> (IInteger) math.arbInt(), testServer::sendInteger); + } + + @Override + public void testSendNode() { + // expectSuccessful("INode", () -> prelude.arbNode(), testServer::sendNode); + } + + @Override + public void testSendRational() { + // expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); + } + + @Override + public void testSendReal() { + expectSuccessful("IReal", () -> (IReal) math.arbReal(), testServer::sendReal); + } + + @Override + public void testSendLocation() { + expectSuccessful("ISourceLocation", () -> prelude.arbLoc(), testServer::sendLocation); + } + + @Override + public void testSendString() { + expectSuccessful("IString", () -> prelude.arbString(vf.integer(1024)), testServer::sendString); + } + + @Override + public void testSendIntAsNumber() { + expectSuccessful("INumber", () -> (IInteger) math.arbInt(), testServer::sendNumber); + } + + @Override + public void testSendRealAsNumber() { + expectSuccessful("INumber", () -> (IReal) math.arbReal(), testServer::sendNumber); + } + + @Override + public void testSendRealAsValue() { + expectSuccessful("IValue", () -> (IReal) math.arbReal(), testServer::sendReal); + } + + @Override + public void testSendList() { + // expectSuccessful("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList); + } + + @Override + public void testSendMap() { + IMapWriter writer = vf.mapWriter(); + writer.put(vf.integer(0), vf.string("zero")); + writer.put(vf.integer(1), vf.string("one")); + // expectSuccessful("IMap", () -> writer.done(), testServer::sendMap); + } + + @Override + public void testSendSet() { + // expectSuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); + } + + @Override + public void testSendTuple() { + // expectSuccessful("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple); + } +} From f1ad10622086d887c57e2b7319131aeb5881557d Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 20 Jan 2026 20:48:36 +0100 Subject: [PATCH 12/37] Added jsonrpc test class: complex values unsupported The test for `rat`s is currently failing --- .../test/rpc/ComplexNotSupported.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 test/org/rascalmpl/test/rpc/ComplexNotSupported.java diff --git a/test/org/rascalmpl/test/rpc/ComplexNotSupported.java b/test/org/rascalmpl/test/rpc/ComplexNotSupported.java new file mode 100644 index 0000000000..0ab63a9f31 --- /dev/null +++ b/test/org/rascalmpl/test/rpc/ComplexNotSupported.java @@ -0,0 +1,104 @@ +package org.rascalmpl.test.rpc; + +import java.io.IOException; + +import org.junit.BeforeClass; +import org.rascalmpl.ideservices.GsonUtils; +import org.rascalmpl.values.RascalValueFactory; + +import io.usethesource.vallang.IBool; +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.IDateTime; +import io.usethesource.vallang.IInteger; +import io.usethesource.vallang.IMapWriter; +import io.usethesource.vallang.IReal; + +public class ComplexNotSupported extends IValueOverJsonTests { + @BeforeClass + public static void setup() throws IOException { + startTestServerAndClient(GsonUtils.noComplexTypes()); + } + + @Override + public void testSendBool() { + expectSuccessful("IBool", () -> (IBool) prelude.arbBool(), testServer::sendBool); + } + + @Override + public void testSendConstructor() { + expectUnsuccessful("IConstructor", () -> (IConstructor) RascalValueFactory.Attribute_Assoc_Left, testServer::sendConstructor); + } + + @Override + public void testSendDateTime() { + expectSuccessful("IDateTime", () -> (IDateTime) prelude.arbDateTime(), testServer::sendDateTime); + } + + @Override + public void testSendInteger() { + expectSuccessful("IInteger", () -> (IInteger) math.arbInt(), testServer::sendInteger); + } + + @Override + public void testSendNode() { + expectUnsuccessful("INode", () -> prelude.arbNode(), testServer::sendNode); + } + + @Override + public void testSendRational() { + // expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); + } + + @Override + public void testSendReal() { + expectSuccessful("IReal", () -> (IReal) math.arbReal(), testServer::sendReal); + } + + @Override + public void testSendLocation() { + expectSuccessful("ISourceLocation", () -> prelude.arbLoc(), testServer::sendLocation); + } + + @Override + public void testSendString() { + expectSuccessful("IString", () -> prelude.arbString(vf.integer(1024)), testServer::sendString); + } + + @Override + public void testSendIntAsNumber() { + expectSuccessful("INumber", () -> (IInteger) math.arbInt(), testServer::sendNumber); + } + + @Override + public void testSendRealAsNumber() { + expectSuccessful("INumber", () -> (IReal) math.arbReal(), testServer::sendNumber); + } + + @Override + public void testSendRealAsValue() { + expectSuccessful("IValue", () -> (IReal) math.arbReal(), testServer::sendReal); + } + + @Override + public void testSendList() { + expectUnsuccessful("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList); + } + + @Override + public void testSendMap() { + IMapWriter writer = vf.mapWriter(); + writer.put(vf.integer(0), vf.string("zero")); + writer.put(vf.integer(1), vf.string("one")); + expectUnsuccessful("IMap", () -> writer.done(), testServer::sendMap); + } + + @Override + public void testSendSet() { + expectUnsuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); + } + + @Override + public void testSendTuple() { + expectUnsuccessful("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple); + } +} From 3e2261779283faf2a37588e3197062fc1f440b62 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Tue, 20 Jan 2026 20:50:10 +0100 Subject: [PATCH 13/37] Using renamed configuration method of GsonUtils --- src/org/rascalmpl/ideservices/RemoteIDEServices.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/ideservices/RemoteIDEServices.java b/src/org/rascalmpl/ideservices/RemoteIDEServices.java index cbfc44eaca..14af1ba994 100644 --- a/src/org/rascalmpl/ideservices/RemoteIDEServices.java +++ b/src/org/rascalmpl/ideservices/RemoteIDEServices.java @@ -64,7 +64,7 @@ public RemoteIDEServices(int ideServicesPort, PrintWriter stderr, IRascalMonitor .setLocalService(this) .setInput(socket.getInputStream()) .setOutput(socket.getOutputStream()) - .configureGson(GsonUtils::configureGson) + .configureGson(GsonUtils.complexAsJsonObject()) .setExecutorService(DaemonThreadPool.buildConstrainedCached("rascal-ide-services", Math.max(2, Math.min(6, Runtime.getRuntime().availableProcessors() - 2)))) .create(); From 791d420f09ef766143fbc9de8e2d0d93ab3d346d Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 08:58:16 +0100 Subject: [PATCH 14/37] Make sure that the jsonrpc tests are run during CI --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5f114aed34..784913a8a5 100644 --- a/pom.xml +++ b/pom.xml @@ -250,7 +250,7 @@ **/org/rascalmpl/test/library/LibraryLangPaths.java **/org/rascalmpl/test/value/AllTests.java **/org/rascalmpl/test/repl/*Test.java - **/org/rascalmpl/test/rpc/*Test.java + **/org/rascalmpl/test/rpc/Complex*.java **/org/rascalmpl/util/**/*Test.java **/org/rascalmpl/util/**/*Tests.java **/org/rascalmpl/uri/**/*Test.java From ba6d56a1c7dfd44827a68e494fefd53ec9b3dbe6 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 10:16:28 +0100 Subject: [PATCH 15/37] Renamed test files to conform to file name heuristics --- ...mplexAsBase64String.java => ComplexAsBase64StringTests.java} | 2 +- .../{ComplexAsJsonObject.java => ComplexAsJsonObjectTests.java} | 2 +- .../rpc/{ComplexAsString.java => ComplexAsStringTests.java} | 2 +- .../{ComplexNotSupported.java => ComplexNotSupportedTests.java} | 2 +- .../{IValueOverJsonTests.java => IValueOverJsonTestBase.java} | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename test/org/rascalmpl/test/rpc/{ComplexAsBase64String.java => ComplexAsBase64StringTests.java} (97%) rename test/org/rascalmpl/test/rpc/{ComplexAsJsonObject.java => ComplexAsJsonObjectTests.java} (97%) rename test/org/rascalmpl/test/rpc/{ComplexAsString.java => ComplexAsStringTests.java} (98%) rename test/org/rascalmpl/test/rpc/{ComplexNotSupported.java => ComplexNotSupportedTests.java} (97%) rename test/org/rascalmpl/test/rpc/{IValueOverJsonTests.java => IValueOverJsonTestBase.java} (99%) diff --git a/test/org/rascalmpl/test/rpc/ComplexAsBase64String.java b/test/org/rascalmpl/test/rpc/ComplexAsBase64StringTests.java similarity index 97% rename from test/org/rascalmpl/test/rpc/ComplexAsBase64String.java rename to test/org/rascalmpl/test/rpc/ComplexAsBase64StringTests.java index 310f32192a..3a6be769a7 100644 --- a/test/org/rascalmpl/test/rpc/ComplexAsBase64String.java +++ b/test/org/rascalmpl/test/rpc/ComplexAsBase64StringTests.java @@ -14,7 +14,7 @@ import io.usethesource.vallang.type.TypeFactory; import io.usethesource.vallang.type.TypeStore; -public class ComplexAsBase64String extends IValueOverJsonTests { +public class ComplexAsBase64StringTests extends IValueOverJsonTestBase { private static TypeFactory tf = TypeFactory.getInstance(); private static TypeStore ts = new TypeStore(); private static final Type TestAdt = tf.abstractDataType(ts, "TestAdt"); diff --git a/test/org/rascalmpl/test/rpc/ComplexAsJsonObject.java b/test/org/rascalmpl/test/rpc/ComplexAsJsonObjectTests.java similarity index 97% rename from test/org/rascalmpl/test/rpc/ComplexAsJsonObject.java rename to test/org/rascalmpl/test/rpc/ComplexAsJsonObjectTests.java index 59452fdbb5..3a11c107e1 100644 --- a/test/org/rascalmpl/test/rpc/ComplexAsJsonObject.java +++ b/test/org/rascalmpl/test/rpc/ComplexAsJsonObjectTests.java @@ -13,7 +13,7 @@ import io.usethesource.vallang.IMapWriter; import io.usethesource.vallang.IReal; -public class ComplexAsJsonObject extends IValueOverJsonTests { +public class ComplexAsJsonObjectTests extends IValueOverJsonTestBase { @BeforeClass public static void setup() throws IOException { startTestServerAndClient(GsonUtils.complexAsJsonObject()); diff --git a/test/org/rascalmpl/test/rpc/ComplexAsString.java b/test/org/rascalmpl/test/rpc/ComplexAsStringTests.java similarity index 98% rename from test/org/rascalmpl/test/rpc/ComplexAsString.java rename to test/org/rascalmpl/test/rpc/ComplexAsStringTests.java index a0c9425bc0..e115107c0a 100644 --- a/test/org/rascalmpl/test/rpc/ComplexAsString.java +++ b/test/org/rascalmpl/test/rpc/ComplexAsStringTests.java @@ -14,7 +14,7 @@ import io.usethesource.vallang.type.TypeFactory; import io.usethesource.vallang.type.TypeStore; -public class ComplexAsString extends IValueOverJsonTests { +public class ComplexAsStringTests extends IValueOverJsonTestBase { private static TypeFactory tf = TypeFactory.getInstance(); private static TypeStore ts = new TypeStore(); private static final Type TestAdt = tf.abstractDataType(ts, "TestAdt"); diff --git a/test/org/rascalmpl/test/rpc/ComplexNotSupported.java b/test/org/rascalmpl/test/rpc/ComplexNotSupportedTests.java similarity index 97% rename from test/org/rascalmpl/test/rpc/ComplexNotSupported.java rename to test/org/rascalmpl/test/rpc/ComplexNotSupportedTests.java index 0ab63a9f31..c932e401fa 100644 --- a/test/org/rascalmpl/test/rpc/ComplexNotSupported.java +++ b/test/org/rascalmpl/test/rpc/ComplexNotSupportedTests.java @@ -13,7 +13,7 @@ import io.usethesource.vallang.IMapWriter; import io.usethesource.vallang.IReal; -public class ComplexNotSupported extends IValueOverJsonTests { +public class ComplexNotSupportedTests extends IValueOverJsonTestBase { @BeforeClass public static void setup() throws IOException { startTestServerAndClient(GsonUtils.noComplexTypes()); diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java similarity index 99% rename from test/org/rascalmpl/test/rpc/IValueOverJsonTests.java rename to test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java index 0592a65bf3..92a6e506eb 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java @@ -31,7 +31,7 @@ import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; -public abstract class IValueOverJsonTests { +public abstract class IValueOverJsonTestBase { protected static final IValueFactory vf = ValueFactoryFactory.getValueFactory(); protected static final Prelude prelude = new Prelude(vf, null, null, null, null); protected static final Math math = new Math(vf); From e27f8c94555b8f9ae34d6eefb500ab975734d21b Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 10:17:23 +0100 Subject: [PATCH 16/37] Changed pom.xml to pick up the renamed test files in CI --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 784913a8a5..5da4004981 100644 --- a/pom.xml +++ b/pom.xml @@ -250,7 +250,7 @@ **/org/rascalmpl/test/library/LibraryLangPaths.java **/org/rascalmpl/test/value/AllTests.java **/org/rascalmpl/test/repl/*Test.java - **/org/rascalmpl/test/rpc/Complex*.java + **/org/rascalmpl/test/rpc/*Tests.java **/org/rascalmpl/util/**/*Test.java **/org/rascalmpl/util/**/*Tests.java **/org/rascalmpl/uri/**/*Test.java From b136188d9a970a5b8189f01c4bdac111878c7259 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 10:46:09 +0100 Subject: [PATCH 17/37] Added configuration option to JsonValueWriter to encode rationals as string. On the deserialization site, this was already available --- src/org/rascalmpl/library/lang/json/IO.java | 6 ++++-- src/org/rascalmpl/library/lang/json/IO.rsc | 5 +++-- .../lang/json/internal/JsonValueWriter.java | 18 ++++++++++++++---- .../tests/library/lang/json/JSONIOTests.rsc | 6 ++++-- src/org/rascalmpl/library/util/TermREPL.java | 2 ++ src/org/rascalmpl/library/util/Webserver.java | 2 ++ .../rascalmpl/repl/http/REPLContentServer.java | 2 ++ 7 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/org/rascalmpl/library/lang/json/IO.java b/src/org/rascalmpl/library/lang/json/IO.java index 90498e9669..55a2b12b16 100644 --- a/src/org/rascalmpl/library/lang/json/IO.java +++ b/src/org/rascalmpl/library/lang/json/IO.java @@ -111,7 +111,7 @@ public IValue parseJSON(IValue type, IString src, IString dateTimeFormat, IBool } public void writeJSON(ISourceLocation loc, IValue value, IBool unpackedLocations, IString dateTimeFormat, - IBool dateTimeAsInt, IInteger indent, IBool dropOrigins, IFunction formatter, IBool explicitConstructorNames, + IBool dateTimeAsInt, IBool rationalsAsString, IInteger indent, IBool dropOrigins, IFunction formatter, IBool explicitConstructorNames, IBool explicitDataTypes) { try (JsonWriter out = new JsonWriter(new OutputStreamWriter(URIResolverRegistry.getInstance().getOutputStream(loc, false), @@ -123,6 +123,7 @@ public void writeJSON(ISourceLocation loc, IValue value, IBool unpackedLocations new JsonValueWriter() .setCalendarFormat(dateTimeFormat.getValue()) .setDatesAsInt(dateTimeAsInt.getValue()) + .setRationalsAsString(rationalsAsString.getValue()) .setUnpackedLocations(unpackedLocations.getValue()) .setDropOrigins(dropOrigins.getValue()) .setFormatters(formatter) @@ -135,7 +136,7 @@ public void writeJSON(ISourceLocation loc, IValue value, IBool unpackedLocations } } - public IString asJSON(IValue value, IBool unpackedLocations, IString dateTimeFormat, IBool dateTimeAsInt, + public IString asJSON(IValue value, IBool unpackedLocations, IString dateTimeFormat, IBool dateTimeAsInt, IBool rationalsAsString, IInteger indent, IBool dropOrigins, IFunction formatter, IBool explicitConstructorNames, IBool explicitDataTypes) { StringWriter string = new StringWriter(); @@ -147,6 +148,7 @@ public IString asJSON(IValue value, IBool unpackedLocations, IString dateTimeFor new JsonValueWriter() .setCalendarFormat(dateTimeFormat.getValue()) .setDatesAsInt(dateTimeAsInt.getValue()) + .setRationalsAsString(rationalsAsString.getValue()) .setUnpackedLocations(unpackedLocations.getValue()) .setDropOrigins(dropOrigins.getValue()) .setFormatters(formatter) diff --git a/src/org/rascalmpl/library/lang/json/IO.rsc b/src/org/rascalmpl/library/lang/json/IO.rsc index 0a3589b120..8c71c94ed7 100644 --- a/src/org/rascalmpl/library/lang/json/IO.rsc +++ b/src/org/rascalmpl/library/lang/json/IO.rsc @@ -162,7 +162,8 @@ For `real` numbers that are larger than JVM's double you get "negative infinity" java void writeJSON(loc target, value val, bool unpackedLocations=false, str dateTimeFormat=DEFAULT_DATETIME_FORMAT, - bool dateTimeAsInt=false, + bool dateTimeAsInt=false, + bool rationalsAsString=false, int indent=0, bool dropOrigins=true, JSONFormatter[value] formatter = str (value _) { fail; }, @@ -176,7 +177,7 @@ java void writeJSON(loc target, value val, @description{ This function uses `writeJSON` and stores the result in a string. } -java str asJSON(value val, bool unpackedLocations=false, str dateTimeFormat=DEFAULT_DATETIME_FORMAT, bool dateTimeAsInt=false, int indent = 0, bool dropOrigins=true, JSONFormatter[value] formatter = str (value _) { fail; }, bool explicitConstructorNames=false, bool explicitDataTypes=false); +java str asJSON(value val, bool unpackedLocations=false, str dateTimeFormat=DEFAULT_DATETIME_FORMAT, bool dateTimeAsInt=false, bool rationalsAsString=false, int indent = 0, bool dropOrigins=true, JSONFormatter[value] formatter = str (value _) { fail; }, bool explicitConstructorNames=false, bool explicitDataTypes=false); @synopsis{((writeJSON)) and ((asJSON)) uses `Formatter` functions to flatten structured data to strings, on-demand} @description{ diff --git a/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java b/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java index 02c91578e0..ad7e3eea89 100644 --- a/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java +++ b/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java @@ -61,6 +61,7 @@ public class JsonValueWriter { private ThreadLocal format; private boolean datesAsInts = true; + private boolean rationalsAsString = false; private boolean unpackedLocations = false; private boolean dropOrigins = true; private IFunction formatters; @@ -127,6 +128,11 @@ public JsonValueWriter setDatesAsInt(boolean setting) { return this; } + public JsonValueWriter setRationalsAsString(boolean setting) { + this.rationalsAsString = setting; + return this; + } + public JsonValueWriter setUnpackedLocations(boolean setting) { this.unpackedLocations = setting; return this; @@ -175,10 +181,14 @@ public Void visitReal(IReal o) throws IOException { @Override public Void visitRational(IRational o) throws IOException { - out.beginArray(); - o.numerator().accept(this); - o.denominator().accept(this); - out.endArray(); + if (rationalsAsString) { + out.value(o.toString()); + } else { + out.beginArray(); + o.numerator().accept(this); + o.denominator().accept(this); + out.endArray(); + } return null; } diff --git a/src/org/rascalmpl/library/lang/rascal/tests/library/lang/json/JSONIOTests.rsc b/src/org/rascalmpl/library/lang/rascal/tests/library/lang/json/JSONIOTests.rsc index 1c889cb2ba..8050794add 100644 --- a/src/org/rascalmpl/library/lang/rascal/tests/library/lang/json/JSONIOTests.rsc +++ b/src/org/rascalmpl/library/lang/rascal/tests/library/lang/json/JSONIOTests.rsc @@ -14,8 +14,8 @@ import Node; loc targetFile = |memory://test-tmp/test-<"">.json|; public int maxLong = floor(pow(2,63)); -bool writeRead(type[&T] returnType, &T dt, value (value x) normalizer = value(value x) { return x; }, bool dateTimeAsInt=false, bool unpackedLocations=false, bool explicitConstructorNames=false, bool explicitDataTypes=false) { - json = asJSON(dt, dateTimeAsInt=dateTimeAsInt, unpackedLocations=unpackedLocations, explicitConstructorNames=explicitConstructorNames, explicitDataTypes=explicitDataTypes); +bool writeRead(type[&T] returnType, &T dt, value (value x) normalizer = value(value x) { return x; }, bool dateTimeAsInt=false, bool rationalsAsString=false, bool unpackedLocations=false, bool explicitConstructorNames=false, bool explicitDataTypes=false) { + json = asJSON(dt, dateTimeAsInt=dateTimeAsInt, rationalsAsString=rationalsAsString, unpackedLocations=unpackedLocations, explicitConstructorNames=explicitConstructorNames, explicitDataTypes=explicitDataTypes); readBack = normalizer(parseJSON(returnType, json, explicitConstructorNames=explicitConstructorNames, explicitDataTypes=explicitDataTypes)); if (readBack !:= normalizer(dt) /* ignores additional src fields */) { println("What is read back, a :"); @@ -67,6 +67,8 @@ test bool jsonWithSet1(set[int] dt) = writeRead(#set[int], dt); test bool jsonWithMap1(map[int, int] dt) = writeRead(#map[int,int], dt); @ignore{until #2133 is fixed} test bool jsonWithNode1(node dt) = writeRead(#node, dt, normalizer = toDefaultRec); +test bool jsonWithRational1(rat r) = writeRead(#rat, r); +test bool jsonWithRational2(rat r) = writeRead(#rat, r, rationalsAsString=true); test bool jsonWithDATA11(DATA1 dt) = writeRead(#DATA1, dt); test bool jsonWithDATA21(DATA2 dt) = writeRead(#DATA2, dt); diff --git a/src/org/rascalmpl/library/util/TermREPL.java b/src/org/rascalmpl/library/util/TermREPL.java index 5cdc2e5c3d..a98798b79e 100644 --- a/src/org/rascalmpl/library/util/TermREPL.java +++ b/src/org/rascalmpl/library/util/TermREPL.java @@ -282,6 +282,7 @@ private ICommandOutput handleJSONResponse(IConstructor response) { IValue dtf = kws.getParameter("dateTimeFormat"); IValue dai = kws.getParameter("dateTimeAsInt"); + IValue ras = kws.getParameter("rationalsAsString"); IValue formatters = kws.getParameter("formatter"); IValue ecn = kws.getParameter("explicitConstructorNames"); IValue edt = kws.getParameter("explicitDataTypes"); @@ -290,6 +291,7 @@ private ICommandOutput handleJSONResponse(IConstructor response) { .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") .setFormatters((IFunction) formatters) .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : true) + .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) ; diff --git a/src/org/rascalmpl/library/util/Webserver.java b/src/org/rascalmpl/library/util/Webserver.java index 5d6ab6fb24..5dd103a0b5 100644 --- a/src/org/rascalmpl/library/util/Webserver.java +++ b/src/org/rascalmpl/library/util/Webserver.java @@ -241,6 +241,7 @@ private Response translateJsonResponse(Method method, IConstructor cons) { IValue dtf = kws.getParameter("dateTimeFormat"); IValue dai = kws.getParameter("dateTimeAsInt"); + IValue ras = kws.getParameter("rationalsAsString"); IValue formatters = kws.getParameter("formatter"); IValue ecn = kws.getParameter("explicitConstructorNames"); IValue edt = kws.getParameter("explicitDataTypes"); @@ -249,6 +250,7 @@ private Response translateJsonResponse(Method method, IConstructor cons) { .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") .setFormatters((IFunction) formatters) .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : true) + .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) ; diff --git a/src/org/rascalmpl/repl/http/REPLContentServer.java b/src/org/rascalmpl/repl/http/REPLContentServer.java index 9fd5de65d7..498517f36b 100644 --- a/src/org/rascalmpl/repl/http/REPLContentServer.java +++ b/src/org/rascalmpl/repl/http/REPLContentServer.java @@ -163,6 +163,7 @@ private static Response translateJsonResponse(Method method, IConstructor cons) IValue dtf = kws.getParameter("dateTimeFormat"); IValue dai = kws.getParameter("dateTimeAsInt"); + IValue ras = kws.getParameter("rationalsAsString"); IValue formatters = kws.getParameter("formatter"); IValue ecn = kws.getParameter("explicitConstructorNames"); IValue edt = kws.getParameter("explicitDataTypes"); @@ -171,6 +172,7 @@ private static Response translateJsonResponse(Method method, IConstructor cons) .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") .setFormatters((IFunction) formatters) .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : true) + .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) ; From 287b7bac01928e365795bb664959c7f80034ed6a Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 10:46:24 +0100 Subject: [PATCH 18/37] Fixed error message --- .../rascalmpl/library/lang/json/internal/JsonValueReader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java b/src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java index f33a87762f..036d78c1c0 100644 --- a/src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java +++ b/src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java @@ -376,7 +376,7 @@ public IValue visitRational(Type type) throws IOException { case STRING: return vf.rational(nextString()); default: - throw parseErrorHere("Expected integer but got " + in.peek()); + throw parseErrorHere("Expected rational but got " + in.peek()); } } From de43057847bef33d83f26cbb9ecf32c6835a3681 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 10:47:01 +0100 Subject: [PATCH 19/37] GsonUtils now configures JsonValueWriter to encode rationals as strings --- src/org/rascalmpl/ideservices/GsonUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index 9e1b95e95f..6df526a251 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -79,7 +79,7 @@ public class GsonUtils { * Subtypes should be declared before their supertypes; e.g., `Number` and `Value` appear last. */ static { - writer.setDatesAsInt(true); + writer.setRationalsAsString(true); typeMappings = List.of( new TypeMapping(IBool.class, tf.boolType()), new TypeMapping(ICollection.class), // IList, IMap, ISet From 92ea92fa291834feaf0a40516468f6de5603b4ff Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 11:12:14 +0100 Subject: [PATCH 20/37] Rationals are no longer wrapped in an object in any configuration --- src/org/rascalmpl/ideservices/GsonUtils.java | 21 +------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index 6df526a251..de8e0f99d0 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -103,9 +103,6 @@ public static enum ComplexTypeMode { * All values are serialized as JSON objects. Automatic deserialization is only supported for primitive types (`bool`, * `datetime`, `int`, `rat`, `real`, `loc`, `str`, `num`); more complex types cannot be automatically deserialized as * the type is not available at deserialization time. - * - * Rationals are wrapped in an object with `rat` as key, to avoid an ambiguity between a rational's JSON representation - * (a list) and a list as a sole argument on the JSON-RPC level. */ ENCODE_AS_JSON_OBJECT, @@ -152,32 +149,16 @@ public boolean supports(Class incoming) { public TypeAdapter createAdapter(ComplexTypeMode complexTypeMode, TypeStore ts) { JsonValueReader reader = new JsonValueReader(IRascalValueFactory.getInstance(), ts, new NullRascalMonitor(), null); if (isPrimitive) { - var needsWrapping = complexTypeMode == ComplexTypeMode.ENCODE_AS_JSON_OBJECT && type.isSubtypeOf(tf.rationalType()); return new TypeAdapter() { @Override public void write(JsonWriter out, T value) throws IOException { - if (needsWrapping) { - out.beginObject(); - out.name("rat"); - } writer.write(out, (IValue) value); - if (needsWrapping) { - out.endObject(); - } } @SuppressWarnings("unchecked") @Override public T read(JsonReader in) throws IOException { - if (needsWrapping) { - in.beginObject(); - in.nextName(); - } - var ret = (T) reader.read(in, type); - if (needsWrapping) { - in.endObject(); - } - return ret; + return (T) reader.read(in, type); } }; } From 48306605a0e0b564ced78630c3b977463badfcec Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 11:12:42 +0100 Subject: [PATCH 21/37] Fixed deserialization in case of string-encoding --- src/org/rascalmpl/ideservices/GsonUtils.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index de8e0f99d0..52b20216a3 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -27,6 +27,7 @@ package org.rascalmpl.ideservices; import java.io.IOException; +import java.io.StringReader; import java.util.List; import java.util.function.Consumer; @@ -36,6 +37,7 @@ import org.rascalmpl.library.lang.json.internal.JsonValueWriter; import org.rascalmpl.util.base64.StreamingBase64; import org.rascalmpl.values.IRascalValueFactory; +import org.rascalmpl.values.ValueFactoryFactory; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -59,6 +61,8 @@ import io.usethesource.vallang.IString; import io.usethesource.vallang.ITuple; import io.usethesource.vallang.IValue; +import io.usethesource.vallang.IValueFactory; +import io.usethesource.vallang.io.StandardTextReader; import io.usethesource.vallang.io.binary.stream.IValueInputStream; import io.usethesource.vallang.io.binary.stream.IValueOutputStream; import io.usethesource.vallang.type.Type; @@ -72,6 +76,7 @@ public class GsonUtils { private static final JsonValueWriter writer = new JsonValueWriter(); private static final TypeFactory tf = TypeFactory.getInstance(); + private static final IValueFactory vf = ValueFactoryFactory.getValueFactory(); private static final List typeMappings; @@ -189,7 +194,7 @@ public T read(JsonReader in) throws IOException { case ENCODE_AS_BASE64_STRING: return base64Decode(in.nextString(), ts); case ENCODE_AS_STRING: - return (T) reader.read(in, type); + return (T) new StandardTextReader().read(vf, ts, tf.valueType(), new StringReader(in.nextString())); default: throw new IOException("Cannot handle complex type"); } From d679dff852122c90437fa294813542941ed0757c Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 11:13:17 +0100 Subject: [PATCH 22/37] Reenabled all jsonrpc tests --- .../test/rpc/ComplexAsBase64StringTests.java | 2 +- .../test/rpc/ComplexAsJsonObjectTests.java | 2 +- .../rascalmpl/test/rpc/ComplexAsStringTests.java | 14 +++++++------- .../test/rpc/ComplexNotSupportedTests.java | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/org/rascalmpl/test/rpc/ComplexAsBase64StringTests.java b/test/org/rascalmpl/test/rpc/ComplexAsBase64StringTests.java index 3a6be769a7..510f346f4e 100644 --- a/test/org/rascalmpl/test/rpc/ComplexAsBase64StringTests.java +++ b/test/org/rascalmpl/test/rpc/ComplexAsBase64StringTests.java @@ -52,7 +52,7 @@ public void testSendNode() { @Override public void testSendRational() { - // expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); + expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); } @Override diff --git a/test/org/rascalmpl/test/rpc/ComplexAsJsonObjectTests.java b/test/org/rascalmpl/test/rpc/ComplexAsJsonObjectTests.java index 3a11c107e1..da835c9b31 100644 --- a/test/org/rascalmpl/test/rpc/ComplexAsJsonObjectTests.java +++ b/test/org/rascalmpl/test/rpc/ComplexAsJsonObjectTests.java @@ -94,7 +94,7 @@ public void testSendMap() { @Override public void testSendSet() { - // expectUnsuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); + expectUnsuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); } @Override diff --git a/test/org/rascalmpl/test/rpc/ComplexAsStringTests.java b/test/org/rascalmpl/test/rpc/ComplexAsStringTests.java index e115107c0a..3e2d7de764 100644 --- a/test/org/rascalmpl/test/rpc/ComplexAsStringTests.java +++ b/test/org/rascalmpl/test/rpc/ComplexAsStringTests.java @@ -32,7 +32,7 @@ public void testSendBool() { @Override public void testSendConstructor() { - // expectSuccessful("IConstructor", () -> vf.constructor(TestAdt_testCons, vf.string("hi"), vf.integer(38)), testServer::sendConstructor); + expectSuccessful("IConstructor", () -> vf.constructor(TestAdt_testCons, vf.string("hi"), vf.integer(38)), testServer::sendConstructor); } @Override @@ -47,12 +47,12 @@ public void testSendInteger() { @Override public void testSendNode() { - // expectSuccessful("INode", () -> prelude.arbNode(), testServer::sendNode); + expectSuccessful("INode", () -> prelude.arbNode(), testServer::sendNode); } @Override public void testSendRational() { - // expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); + expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); } @Override @@ -87,7 +87,7 @@ public void testSendRealAsValue() { @Override public void testSendList() { - // expectSuccessful("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList); + expectSuccessful("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList); } @Override @@ -95,16 +95,16 @@ public void testSendMap() { IMapWriter writer = vf.mapWriter(); writer.put(vf.integer(0), vf.string("zero")); writer.put(vf.integer(1), vf.string("one")); - // expectSuccessful("IMap", () -> writer.done(), testServer::sendMap); + expectSuccessful("IMap", () -> writer.done(), testServer::sendMap); } @Override public void testSendSet() { - // expectSuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); + expectSuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); } @Override public void testSendTuple() { - // expectSuccessful("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple); + expectSuccessful("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple); } } diff --git a/test/org/rascalmpl/test/rpc/ComplexNotSupportedTests.java b/test/org/rascalmpl/test/rpc/ComplexNotSupportedTests.java index c932e401fa..9b77ce4a84 100644 --- a/test/org/rascalmpl/test/rpc/ComplexNotSupportedTests.java +++ b/test/org/rascalmpl/test/rpc/ComplexNotSupportedTests.java @@ -46,7 +46,7 @@ public void testSendNode() { @Override public void testSendRational() { - // expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); + expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); } @Override From 47e0ec7f9f04962ba16e821364cf3af84b8af3c7 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 11:31:30 +0100 Subject: [PATCH 23/37] Wrapped input/output streams in ThreadLocal in jsonrpc tests h/t @DavyLandman --- .../test/rpc/IValueOverJsonTestBase.java | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java index 92a6e506eb..df48a2d596 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java @@ -37,38 +37,35 @@ public abstract class IValueOverJsonTestBase { protected static final Math math = new Math(vf); protected static JsonRpcTestInterface testServer; - protected static final PipedInputStream is0, is1; - protected static final PipedOutputStream os0, os1; + protected static final ThreadLocal is0 = new ThreadLocal<>(), is1 = new ThreadLocal<>(); + protected static final ThreadLocal os0 = new ThreadLocal<>(), os1 = new ThreadLocal<>(); - static { + protected static void startTestServerAndClient(Consumer gsonConfig) { try { - is0 = new PipedInputStream(); - os0 = new PipedOutputStream(); - is1 = new PipedInputStream(os0); - os1 = new PipedOutputStream(is0); + is0.set(new PipedInputStream()); + os0.set(new PipedOutputStream()); + is1.set(new PipedInputStream(os0.get())); + os1.set(new PipedOutputStream(is0.get())); + new TestThread(is0.get(), os0.get(), gsonConfig).start(); + new TestClient(is1.get(), os1.get(), gsonConfig); } catch (IOException e) { throw new RuntimeException(e); } } - protected static void startTestServerAndClient(Consumer gsonConfig) { - new TestThread(is0, os0, gsonConfig).start(); - new TestClient(is1, os1, gsonConfig); - } - @AfterClass public static void teardown() throws IOException { - if (is0 != null) { - is0.close(); + if (is0.get() != null) { + is0.get().close(); } - if (is1 != null) { - is1.close(); + if (is1.get() != null) { + is1.get().close(); } - if (os0 != null) { - os0.close(); + if (os0.get() != null) { + os0.get().close(); } - if (os1 != null) { - os1.close(); + if (os1.get() != null) { + os1.get().close(); } } From 50e967ad676cc94367eb122aa67bad98bcae05a8 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 11:42:43 +0100 Subject: [PATCH 24/37] Updated comments in GsonUtil --- src/org/rascalmpl/ideservices/GsonUtils.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index 52b20216a3..11eaa0377f 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -105,25 +105,24 @@ public class GsonUtils { public static enum ComplexTypeMode { /** - * All values are serialized as JSON objects. Automatic deserialization is only supported for primitive types (`bool`, + * Complex values are serialized as JSON objects. Automatic deserialization is only supported for primitive types (`bool`, * `datetime`, `int`, `rat`, `real`, `loc`, `str`, `num`); more complex types cannot be automatically deserialized as * the type is not available at deserialization time. */ ENCODE_AS_JSON_OBJECT, /** - * Complex values are serialized as a (binary) Base64-encoded string. An appropriate {@link TypeStore} must be provided for - * deserialization with {@link ComplexTypeMode#base64Decode}. + * Complex values are serialized as a Base64-encoded string. A properly filled {@link TypeStore} is required for deserialization. */ ENCODE_AS_BASE64_STRING, /** - * Complex values are serialized as a string. + * Complex values are serialized as a string. A properly filled {@link TypeStore} is required for deserialization. */ ENCODE_AS_STRING, /** - * Only primitive types are supported; more complex types are neither serialized nor deserialized. + * Only values of primitive type are supported; more complex values are neither serialized nor deserialized. */ NOT_SUPPORTED } From b720017820e12611affcda4c34a3309e182d2d41 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 13:33:50 +0100 Subject: [PATCH 25/37] Reusing stored ValueFactory --- src/org/rascalmpl/ideservices/GsonUtils.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index 11eaa0377f..ca17a5fbdf 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -36,7 +36,6 @@ import org.rascalmpl.library.lang.json.internal.JsonValueReader; import org.rascalmpl.library.lang.json.internal.JsonValueWriter; import org.rascalmpl.util.base64.StreamingBase64; -import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.ValueFactoryFactory; import com.google.gson.Gson; @@ -151,7 +150,7 @@ public boolean supports(Class incoming) { } public TypeAdapter createAdapter(ComplexTypeMode complexTypeMode, TypeStore ts) { - JsonValueReader reader = new JsonValueReader(IRascalValueFactory.getInstance(), ts, new NullRascalMonitor(), null); + JsonValueReader reader = new JsonValueReader(vf, ts, new NullRascalMonitor(), null); if (isPrimitive) { return new TypeAdapter() { @Override @@ -288,7 +287,7 @@ public TypeAdapter create(Gson gson, TypeToken type) { public static String base64Encode(IValue value) { var builder = new StringBuilder(); try (var encoder = StreamingBase64.encode(builder); - var out = new IValueOutputStream(encoder, IRascalValueFactory.getInstance())) { + var out = new IValueOutputStream(encoder, vf)) { out.write(value); } catch (IOException e) { throw new RuntimeException(e); @@ -299,7 +298,7 @@ public static String base64Encode(IValue value) { @SuppressWarnings("unchecked") public static T base64Decode(String string, TypeStore ts) { try (var decoder = StreamingBase64.decode(string); - var in = new IValueInputStream(decoder, IRascalValueFactory.getInstance(), () -> ts)) { + var in = new IValueInputStream(decoder, vf, () -> ts)) { return (T) in.read(); } catch (IOException e) { throw new RuntimeException(e); From eb42d1101bb2814139e57f6117af7680cd9537de Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 13:34:08 +0100 Subject: [PATCH 26/37] Removed non-existent parameters from JavaDoc --- src/org/rascalmpl/ideservices/GsonUtils.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index ca17a5fbdf..848e6b2b2e 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -205,8 +205,6 @@ public T read(JsonReader in) throws IOException { * Configure Gson to encode complex (non-primitive) values as JSON objects. * * See {@link ComplexTypeMode.ENCODE_AS_JSON_OBJECT}. - * - * @param builder The {@link GsonBuilder} to be configured. */ public static Consumer complexAsJsonObject() { return builder -> configureGson(builder, ComplexTypeMode.ENCODE_AS_JSON_OBJECT, new TypeStore()); @@ -216,8 +214,6 @@ public static Consumer complexAsJsonObject() { * Configure Gson to encode complex (non-primitive) values as Base64-encoded strings. * * This configurtion should only be used for serialization; deserialization requires a {@link TypeStore). - * - * @param builder The {@link GsonBuilder} to be configured. */ public static Consumer complexAsBase64String() { return builder -> complexAsBase64String(new TypeStore()); @@ -228,7 +224,6 @@ public static Consumer complexAsBase64String() { * * This configuration can be used for both serialization and deserialization. * - * @param builder The {@link GsonBuilder} to be configured. * @param ts The {@link TypeStore} to be used during deserialization. */ public static Consumer complexAsBase64String(TypeStore ts) { @@ -239,8 +234,6 @@ public static Consumer complexAsBase64String(TypeStore ts) { * Configure Gson to encode complex (non-primitive) values as plain strings. * * This configurtion should only be used for serialization; deserialization requires a {@link TypeStore). - * - * @param builder The {@link GsonBuilder} to be configured. */ public static Consumer complexAsString() { return builder -> complexAsString(new TypeStore()); @@ -251,7 +244,6 @@ public static Consumer complexAsString() { * * This configuration can be used for both serialization and deserialization. * - * @param builder The {@link GsonBuilder} to be configured. * @param ts The {@link TypeStore} to be used during deserialization. */ public static Consumer complexAsString(TypeStore ts) { From 1f056dcdc6f91fb6a32fd216b851274f9f13fed5 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Wed, 21 Jan 2026 13:34:25 +0100 Subject: [PATCH 27/37] Added timeouts to jsonrpc tests --- .../rascalmpl/test/rpc/IValueOverJsonTestBase.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java index df48a2d596..da74d9ae9e 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java @@ -11,6 +11,9 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -118,18 +121,17 @@ public void run() { protected static void expectSuccessful(String type, Supplier supplier, Function> function) { var value = supplier.get(); try { - assertEquals(value, function.apply(value).get()); - } catch (InterruptedException | ExecutionException e) { + assertEquals(value, function.apply(value).get(10, TimeUnit.SECONDS)); + } catch (InterruptedException | ExecutionException | TimeoutException e) { fail("Error occurred while testing " + type + " over jsonrpc: " + e.getMessage()); } } protected static void expectUnsuccessful(String type, Supplier supplier, Function> function) { - var value = supplier.get(); try { - function.apply(value).get(); + function.apply(supplier.get()).get(10, TimeUnit.SECONDS); fail("Error occurred: " + type + " should not have round-tripped"); - } catch (InterruptedException | ExecutionException e) { + } catch (InterruptedException | ExecutionException | TimeoutException e) { //This is expected } } From bdad357acdf1f433b02825a0c04d2156eb61f8a9 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Thu, 22 Jan 2026 09:28:00 +0100 Subject: [PATCH 28/37] Collapsed all jsonrpc tests into one parameterized test class --- .../test/rpc/ComplexAsBase64StringTests.java | 110 --------------- .../test/rpc/ComplexAsJsonObjectTests.java | 104 -------------- .../test/rpc/ComplexAsStringTests.java | 110 --------------- .../test/rpc/ComplexNotSupportedTests.java | 104 -------------- .../test/rpc/IValueOverJsonTestBase.java | 128 +++++++++++++++--- 5 files changed, 107 insertions(+), 449 deletions(-) delete mode 100644 test/org/rascalmpl/test/rpc/ComplexAsBase64StringTests.java delete mode 100644 test/org/rascalmpl/test/rpc/ComplexAsJsonObjectTests.java delete mode 100644 test/org/rascalmpl/test/rpc/ComplexAsStringTests.java delete mode 100644 test/org/rascalmpl/test/rpc/ComplexNotSupportedTests.java diff --git a/test/org/rascalmpl/test/rpc/ComplexAsBase64StringTests.java b/test/org/rascalmpl/test/rpc/ComplexAsBase64StringTests.java deleted file mode 100644 index 510f346f4e..0000000000 --- a/test/org/rascalmpl/test/rpc/ComplexAsBase64StringTests.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.rascalmpl.test.rpc; - -import java.io.IOException; - -import org.junit.BeforeClass; -import org.rascalmpl.ideservices.GsonUtils; - -import io.usethesource.vallang.IBool; -import io.usethesource.vallang.IDateTime; -import io.usethesource.vallang.IInteger; -import io.usethesource.vallang.IMapWriter; -import io.usethesource.vallang.IReal; -import io.usethesource.vallang.type.Type; -import io.usethesource.vallang.type.TypeFactory; -import io.usethesource.vallang.type.TypeStore; - -public class ComplexAsBase64StringTests extends IValueOverJsonTestBase { - private static TypeFactory tf = TypeFactory.getInstance(); - private static TypeStore ts = new TypeStore(); - private static final Type TestAdt = tf.abstractDataType(ts, "TestAdt"); - private static final Type TestAdt_testCons = tf.constructor(ts, TestAdt, "testCons", tf.stringType(), "id", tf.integerType(), "nr"); - - @BeforeClass - public static void setup() throws IOException { - startTestServerAndClient(GsonUtils.complexAsBase64String(ts)); - } - - @Override - public void testSendBool() { - expectSuccessful("IBool", () -> (IBool) prelude.arbBool(), testServer::sendBool); - } - - @Override - public void testSendConstructor() { - expectSuccessful("IConstructor", () -> vf.constructor(TestAdt_testCons, vf.string("hi"), vf.integer(38)), testServer::sendConstructor); - } - - @Override - public void testSendDateTime() { - expectSuccessful("IDateTime", () -> (IDateTime) prelude.arbDateTime(), testServer::sendDateTime); - } - - @Override - public void testSendInteger() { - expectSuccessful("IInteger", () -> (IInteger) math.arbInt(), testServer::sendInteger); - } - - @Override - public void testSendNode() { - expectSuccessful("INode", () -> prelude.arbNode(), testServer::sendNode); - } - - @Override - public void testSendRational() { - expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); - } - - @Override - public void testSendReal() { - expectSuccessful("IReal", () -> (IReal) math.arbReal(), testServer::sendReal); - } - - @Override - public void testSendLocation() { - expectSuccessful("ISourceLocation", () -> prelude.arbLoc(), testServer::sendLocation); - } - - @Override - public void testSendString() { - expectSuccessful("IString", () -> prelude.arbString(vf.integer(1024)), testServer::sendString); - } - - @Override - public void testSendIntAsNumber() { - expectSuccessful("INumber", () -> (IInteger) math.arbInt(), testServer::sendNumber); - } - - @Override - public void testSendRealAsNumber() { - expectSuccessful("INumber", () -> (IReal) math.arbReal(), testServer::sendNumber); - } - - @Override - public void testSendRealAsValue() { - expectSuccessful("IValue", () -> (IReal) math.arbReal(), testServer::sendReal); - } - - @Override - public void testSendList() { - expectSuccessful("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList); - } - - @Override - public void testSendMap() { - IMapWriter writer = vf.mapWriter(); - writer.put(vf.integer(0), vf.string("zero")); - writer.put(vf.integer(1), vf.string("one")); - expectSuccessful("IMap", () -> writer.done(), testServer::sendMap); - } - - @Override - public void testSendSet() { - expectSuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); - } - - @Override - public void testSendTuple() { - expectSuccessful("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple); - } -} diff --git a/test/org/rascalmpl/test/rpc/ComplexAsJsonObjectTests.java b/test/org/rascalmpl/test/rpc/ComplexAsJsonObjectTests.java deleted file mode 100644 index da835c9b31..0000000000 --- a/test/org/rascalmpl/test/rpc/ComplexAsJsonObjectTests.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.rascalmpl.test.rpc; - -import java.io.IOException; - -import org.junit.BeforeClass; -import org.rascalmpl.ideservices.GsonUtils; -import org.rascalmpl.values.RascalValueFactory; - -import io.usethesource.vallang.IBool; -import io.usethesource.vallang.IConstructor; -import io.usethesource.vallang.IDateTime; -import io.usethesource.vallang.IInteger; -import io.usethesource.vallang.IMapWriter; -import io.usethesource.vallang.IReal; - -public class ComplexAsJsonObjectTests extends IValueOverJsonTestBase { - @BeforeClass - public static void setup() throws IOException { - startTestServerAndClient(GsonUtils.complexAsJsonObject()); - } - - @Override - public void testSendBool() { - expectSuccessful("IBool", () -> (IBool) prelude.arbBool(), testServer::sendBool); - } - - @Override - public void testSendConstructor() { - expectUnsuccessful("IConstructor", () -> (IConstructor) RascalValueFactory.Attribute_Assoc_Left, testServer::sendConstructor); - } - - @Override - public void testSendDateTime() { - expectSuccessful("IDateTime", () -> (IDateTime) prelude.arbDateTime(), testServer::sendDateTime); - } - - @Override - public void testSendInteger() { - expectSuccessful("IInteger", () -> (IInteger) math.arbInt(), testServer::sendInteger); - } - - @Override - public void testSendNode() { - expectUnsuccessful("INode", () -> prelude.arbNode(), testServer::sendNode); - } - - @Override - public void testSendRational() { - expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); - } - - @Override - public void testSendReal() { - expectSuccessful("IReal", () -> (IReal) math.arbReal(), testServer::sendReal); - } - - @Override - public void testSendLocation() { - expectSuccessful("ISourceLocation", () -> prelude.arbLoc(), testServer::sendLocation); - } - - @Override - public void testSendString() { - expectSuccessful("IString", () -> prelude.arbString(vf.integer(1024)), testServer::sendString); - } - - @Override - public void testSendIntAsNumber() { - expectSuccessful("INumber", () -> (IInteger) math.arbInt(), testServer::sendNumber); - } - - @Override - public void testSendRealAsNumber() { - expectSuccessful("INumber", () -> (IReal) math.arbReal(), testServer::sendNumber); - } - - @Override - public void testSendRealAsValue() { - expectSuccessful("IValue", () -> (IReal) math.arbReal(), testServer::sendReal); - } - - @Override - public void testSendList() { - expectUnsuccessful("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList); - } - - @Override - public void testSendMap() { - IMapWriter writer = vf.mapWriter(); - writer.put(vf.integer(0), vf.string("zero")); - writer.put(vf.integer(1), vf.string("one")); - expectUnsuccessful("IMap", () -> writer.done(), testServer::sendMap); - } - - @Override - public void testSendSet() { - expectUnsuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); - } - - @Override - public void testSendTuple() { - expectUnsuccessful("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple); - } -} diff --git a/test/org/rascalmpl/test/rpc/ComplexAsStringTests.java b/test/org/rascalmpl/test/rpc/ComplexAsStringTests.java deleted file mode 100644 index 3e2d7de764..0000000000 --- a/test/org/rascalmpl/test/rpc/ComplexAsStringTests.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.rascalmpl.test.rpc; - -import java.io.IOException; - -import org.junit.BeforeClass; -import org.rascalmpl.ideservices.GsonUtils; - -import io.usethesource.vallang.IBool; -import io.usethesource.vallang.IDateTime; -import io.usethesource.vallang.IInteger; -import io.usethesource.vallang.IMapWriter; -import io.usethesource.vallang.IReal; -import io.usethesource.vallang.type.Type; -import io.usethesource.vallang.type.TypeFactory; -import io.usethesource.vallang.type.TypeStore; - -public class ComplexAsStringTests extends IValueOverJsonTestBase { - private static TypeFactory tf = TypeFactory.getInstance(); - private static TypeStore ts = new TypeStore(); - private static final Type TestAdt = tf.abstractDataType(ts, "TestAdt"); - private static final Type TestAdt_testCons = tf.constructor(ts, TestAdt, "testCons", tf.stringType(), "id", tf.integerType(), "nr"); - - @BeforeClass - public static void setup() throws IOException { - startTestServerAndClient(GsonUtils.complexAsString(ts)); - } - - @Override - public void testSendBool() { - expectSuccessful("IBool", () -> (IBool) prelude.arbBool(), testServer::sendBool); - } - - @Override - public void testSendConstructor() { - expectSuccessful("IConstructor", () -> vf.constructor(TestAdt_testCons, vf.string("hi"), vf.integer(38)), testServer::sendConstructor); - } - - @Override - public void testSendDateTime() { - expectSuccessful("IDateTime", () -> (IDateTime) prelude.arbDateTime(), testServer::sendDateTime); - } - - @Override - public void testSendInteger() { - expectSuccessful("IInteger", () -> (IInteger) math.arbInt(), testServer::sendInteger); - } - - @Override - public void testSendNode() { - expectSuccessful("INode", () -> prelude.arbNode(), testServer::sendNode); - } - - @Override - public void testSendRational() { - expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); - } - - @Override - public void testSendReal() { - expectSuccessful("IReal", () -> (IReal) math.arbReal(), testServer::sendReal); - } - - @Override - public void testSendLocation() { - expectSuccessful("ISourceLocation", () -> prelude.arbLoc(), testServer::sendLocation); - } - - @Override - public void testSendString() { - expectSuccessful("IString", () -> prelude.arbString(vf.integer(1024)), testServer::sendString); - } - - @Override - public void testSendIntAsNumber() { - expectSuccessful("INumber", () -> (IInteger) math.arbInt(), testServer::sendNumber); - } - - @Override - public void testSendRealAsNumber() { - expectSuccessful("INumber", () -> (IReal) math.arbReal(), testServer::sendNumber); - } - - @Override - public void testSendRealAsValue() { - expectSuccessful("IValue", () -> (IReal) math.arbReal(), testServer::sendReal); - } - - @Override - public void testSendList() { - expectSuccessful("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList); - } - - @Override - public void testSendMap() { - IMapWriter writer = vf.mapWriter(); - writer.put(vf.integer(0), vf.string("zero")); - writer.put(vf.integer(1), vf.string("one")); - expectSuccessful("IMap", () -> writer.done(), testServer::sendMap); - } - - @Override - public void testSendSet() { - expectSuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); - } - - @Override - public void testSendTuple() { - expectSuccessful("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple); - } -} diff --git a/test/org/rascalmpl/test/rpc/ComplexNotSupportedTests.java b/test/org/rascalmpl/test/rpc/ComplexNotSupportedTests.java deleted file mode 100644 index 9b77ce4a84..0000000000 --- a/test/org/rascalmpl/test/rpc/ComplexNotSupportedTests.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.rascalmpl.test.rpc; - -import java.io.IOException; - -import org.junit.BeforeClass; -import org.rascalmpl.ideservices.GsonUtils; -import org.rascalmpl.values.RascalValueFactory; - -import io.usethesource.vallang.IBool; -import io.usethesource.vallang.IConstructor; -import io.usethesource.vallang.IDateTime; -import io.usethesource.vallang.IInteger; -import io.usethesource.vallang.IMapWriter; -import io.usethesource.vallang.IReal; - -public class ComplexNotSupportedTests extends IValueOverJsonTestBase { - @BeforeClass - public static void setup() throws IOException { - startTestServerAndClient(GsonUtils.noComplexTypes()); - } - - @Override - public void testSendBool() { - expectSuccessful("IBool", () -> (IBool) prelude.arbBool(), testServer::sendBool); - } - - @Override - public void testSendConstructor() { - expectUnsuccessful("IConstructor", () -> (IConstructor) RascalValueFactory.Attribute_Assoc_Left, testServer::sendConstructor); - } - - @Override - public void testSendDateTime() { - expectSuccessful("IDateTime", () -> (IDateTime) prelude.arbDateTime(), testServer::sendDateTime); - } - - @Override - public void testSendInteger() { - expectSuccessful("IInteger", () -> (IInteger) math.arbInt(), testServer::sendInteger); - } - - @Override - public void testSendNode() { - expectUnsuccessful("INode", () -> prelude.arbNode(), testServer::sendNode); - } - - @Override - public void testSendRational() { - expectSuccessful("IRational", () -> arbRational(), testServer::sendRational); - } - - @Override - public void testSendReal() { - expectSuccessful("IReal", () -> (IReal) math.arbReal(), testServer::sendReal); - } - - @Override - public void testSendLocation() { - expectSuccessful("ISourceLocation", () -> prelude.arbLoc(), testServer::sendLocation); - } - - @Override - public void testSendString() { - expectSuccessful("IString", () -> prelude.arbString(vf.integer(1024)), testServer::sendString); - } - - @Override - public void testSendIntAsNumber() { - expectSuccessful("INumber", () -> (IInteger) math.arbInt(), testServer::sendNumber); - } - - @Override - public void testSendRealAsNumber() { - expectSuccessful("INumber", () -> (IReal) math.arbReal(), testServer::sendNumber); - } - - @Override - public void testSendRealAsValue() { - expectSuccessful("IValue", () -> (IReal) math.arbReal(), testServer::sendReal); - } - - @Override - public void testSendList() { - expectUnsuccessful("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList); - } - - @Override - public void testSendMap() { - IMapWriter writer = vf.mapWriter(); - writer.put(vf.integer(0), vf.string("zero")); - writer.put(vf.integer(1), vf.string("one")); - expectUnsuccessful("IMap", () -> writer.done(), testServer::sendMap); - } - - @Override - public void testSendSet() { - expectUnsuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); - } - - @Override - public void testSendTuple() { - expectUnsuccessful("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple); - } -} diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java index da74d9ae9e..3ef0d54c13 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java @@ -8,12 +8,14 @@ import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -23,26 +25,61 @@ import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.junit.AfterClass; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.rascalmpl.ideservices.GsonUtils; +import org.rascalmpl.ideservices.GsonUtils.ComplexTypeMode; import org.rascalmpl.library.Prelude; import org.rascalmpl.library.util.Math; import org.rascalmpl.values.ValueFactoryFactory; import com.google.gson.GsonBuilder; +import io.usethesource.vallang.IBool; +import io.usethesource.vallang.IDateTime; import io.usethesource.vallang.IInteger; +import io.usethesource.vallang.IMapWriter; import io.usethesource.vallang.IRational; +import io.usethesource.vallang.IReal; import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; +import io.usethesource.vallang.type.Type; +import io.usethesource.vallang.type.TypeFactory; +import io.usethesource.vallang.type.TypeStore; -public abstract class IValueOverJsonTestBase { +@RunWith(Parameterized.class) +public class IValueOverJsonTestBase { protected static final IValueFactory vf = ValueFactoryFactory.getValueFactory(); protected static final Prelude prelude = new Prelude(vf, null, null, null, null); protected static final Math math = new Math(vf); + private static TypeFactory tf = TypeFactory.getInstance(); + private static TypeStore ts = new TypeStore(); + private static final Type TestAdt = tf.abstractDataType(ts, "TestAdt"); + private static final Type TestAdt_testCons = tf.constructor(ts, TestAdt, "testCons", tf.stringType(), "id", tf.integerType(), "nr"); + protected static JsonRpcTestInterface testServer; protected static final ThreadLocal is0 = new ThreadLocal<>(), is1 = new ThreadLocal<>(); protected static final ThreadLocal os0 = new ThreadLocal<>(), os1 = new ThreadLocal<>(); - + + @Parameters(name="{0}") + public static Iterable modesAndConfig() { + return Arrays.asList(new Object[][] { + { ComplexTypeMode.ENCODE_AS_JSON_OBJECT, GsonUtils.complexAsJsonObject() }, + { ComplexTypeMode.ENCODE_AS_BASE64_STRING, GsonUtils.complexAsBase64String(ts) }, + { ComplexTypeMode.ENCODE_AS_STRING, GsonUtils.complexAsString(ts) }, + { ComplexTypeMode.NOT_SUPPORTED, GsonUtils.noComplexTypes() } + }); + } + + private final ComplexTypeMode complexTypeMode; + + public IValueOverJsonTestBase(ComplexTypeMode complexTypeMode, Consumer gsonConfig) { + this.complexTypeMode = complexTypeMode; + startTestServerAndClient(gsonConfig); + } + protected static void startTestServerAndClient(Consumer gsonConfig) { try { is0.set(new PipedInputStream()); @@ -118,6 +155,20 @@ public void run() { } } + private static Set asJsonObjectOrNotSupported = new HashSet<>(Arrays.asList(ComplexTypeMode.ENCODE_AS_JSON_OBJECT, ComplexTypeMode.NOT_SUPPORTED)); + + private void runTestForPrimitiveType(String type, Supplier supplier, Function> function) { + expectSuccessful(type, supplier, function); + } + + private void runTestForComplexType(String type, Supplier supplier, Function> function) { + if (asJsonObjectOrNotSupported.contains(complexTypeMode)) { + expectUnsuccessful(type, supplier, function); + } else { + expectSuccessful(type, supplier, function); + } + } + protected static void expectSuccessful(String type, Supplier supplier, Function> function) { var value = supplier.get(); try { @@ -143,53 +194,88 @@ protected static IRational arbRational() { denominator = (IInteger) math.arbInt(); } return vf.rational(numerator, denominator); - } - + } + @Test - public abstract void testSendBool(); + public void testSendBool() { + runTestForPrimitiveType("IBool", () -> (IBool) prelude.arbBool(), testServer::sendBool); + } @Test - public abstract void testSendConstructor(); + public void testSendConstructor() { + runTestForComplexType("IConstructor", () -> vf.constructor(TestAdt_testCons, vf.string("hi"), vf.integer(38)), testServer::sendConstructor); + } @Test - public abstract void testSendDateTime(); + public void testSendDateTime() { + runTestForPrimitiveType("IDateTime", () -> (IDateTime) prelude.arbDateTime(), testServer::sendDateTime); + } @Test - public abstract void testSendInteger(); + public void testSendInteger() { + runTestForPrimitiveType("IInteger", () -> (IInteger) math.arbInt(), testServer::sendInteger); + } @Test - public abstract void testSendNode(); + public void testSendNode() { + runTestForComplexType("INode", () -> prelude.arbNode(), testServer::sendNode); + } @Test - public abstract void testSendRational(); + public void testSendRational() { + runTestForPrimitiveType("IRational", () -> arbRational(), testServer::sendRational); + } @Test - public abstract void testSendReal(); + public void testSendReal() { + runTestForPrimitiveType("IReal", () -> (IReal) math.arbReal(), testServer::sendReal); + } @Test - public abstract void testSendLocation(); + public void testSendLocation() { + runTestForPrimitiveType("ISourceLocation", () -> prelude.arbLoc(), testServer::sendLocation); + } @Test - public abstract void testSendString(); + public void testSendString() { + runTestForPrimitiveType("IString", () -> prelude.arbString(vf.integer(1024)), testServer::sendString); + } @Test - public abstract void testSendIntAsNumber(); + public void testSendIntAsNumber() { + runTestForPrimitiveType("INumber", () -> (IInteger) math.arbInt(), testServer::sendNumber); + } @Test - public abstract void testSendRealAsNumber(); + public void testSendRealAsNumber() { + runTestForPrimitiveType("INumber", () -> (IReal) math.arbReal(), testServer::sendNumber); + } @Test - public abstract void testSendRealAsValue(); + public void testSendRealAsValue() { + runTestForPrimitiveType("IValue", () -> (IReal) math.arbReal(), testServer::sendReal); + } @Test - public abstract void testSendList(); + public void testSendList() { + runTestForComplexType("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList); + } @Test - public abstract void testSendMap(); + public void testSendMap() { + IMapWriter writer = vf.mapWriter(); + writer.put(vf.integer(0), vf.string("zero")); + writer.put(vf.integer(1), vf.string("one")); + runTestForComplexType("IMap", () -> writer.done(), testServer::sendMap); + } @Test - public abstract void testSendSet(); + public void testSendSet() { + runTestForComplexType("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); + } @Test - public abstract void testSendTuple(); + public void testSendTuple() { + runTestForComplexType("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple); + } } From ad336f1f3facded78b0315e99b410fc8ea13a1a4 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Thu, 22 Jan 2026 09:28:58 +0100 Subject: [PATCH 29/37] Renamed jsonrpc test class --- .../{IValueOverJsonTestBase.java => IValueOverJsonTests.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename test/org/rascalmpl/test/rpc/{IValueOverJsonTestBase.java => IValueOverJsonTests.java} (98%) diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java similarity index 98% rename from test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java rename to test/org/rascalmpl/test/rpc/IValueOverJsonTests.java index 3ef0d54c13..7b2115aab0 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTestBase.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java @@ -49,7 +49,7 @@ import io.usethesource.vallang.type.TypeStore; @RunWith(Parameterized.class) -public class IValueOverJsonTestBase { +public class IValueOverJsonTests { protected static final IValueFactory vf = ValueFactoryFactory.getValueFactory(); protected static final Prelude prelude = new Prelude(vf, null, null, null, null); protected static final Math math = new Math(vf); @@ -75,7 +75,7 @@ public static Iterable modesAndConfig() { private final ComplexTypeMode complexTypeMode; - public IValueOverJsonTestBase(ComplexTypeMode complexTypeMode, Consumer gsonConfig) { + public IValueOverJsonTests(ComplexTypeMode complexTypeMode, Consumer gsonConfig) { this.complexTypeMode = complexTypeMode; startTestServerAndClient(gsonConfig); } From f839a69bc90a72300630bb341938433a3cf95bbe Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Thu, 22 Jan 2026 09:30:11 +0100 Subject: [PATCH 30/37] Made protected fields private; test class is no longer extended --- .../test/rpc/IValueOverJsonTests.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java index 7b2115aab0..9c132e7db7 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java @@ -50,18 +50,18 @@ @RunWith(Parameterized.class) public class IValueOverJsonTests { - protected static final IValueFactory vf = ValueFactoryFactory.getValueFactory(); - protected static final Prelude prelude = new Prelude(vf, null, null, null, null); - protected static final Math math = new Math(vf); + private static final IValueFactory vf = ValueFactoryFactory.getValueFactory(); + private static final Prelude prelude = new Prelude(vf, null, null, null, null); + private static final Math math = new Math(vf); private static TypeFactory tf = TypeFactory.getInstance(); private static TypeStore ts = new TypeStore(); private static final Type TestAdt = tf.abstractDataType(ts, "TestAdt"); private static final Type TestAdt_testCons = tf.constructor(ts, TestAdt, "testCons", tf.stringType(), "id", tf.integerType(), "nr"); - protected static JsonRpcTestInterface testServer; - protected static final ThreadLocal is0 = new ThreadLocal<>(), is1 = new ThreadLocal<>(); - protected static final ThreadLocal os0 = new ThreadLocal<>(), os1 = new ThreadLocal<>(); + private static JsonRpcTestInterface testServer; + private static final ThreadLocal is0 = new ThreadLocal<>(), is1 = new ThreadLocal<>(); + private static final ThreadLocal os0 = new ThreadLocal<>(), os1 = new ThreadLocal<>(); @Parameters(name="{0}") public static Iterable modesAndConfig() { @@ -80,7 +80,7 @@ public IValueOverJsonTests(ComplexTypeMode complexTypeMode, Consumer gsonConfig) { + private static void startTestServerAndClient(Consumer gsonConfig) { try { is0.set(new PipedInputStream()); os0.set(new PipedOutputStream()); @@ -169,7 +169,7 @@ private void runTestForComplexType(String type, Supplier s } } - protected static void expectSuccessful(String type, Supplier supplier, Function> function) { + private static void expectSuccessful(String type, Supplier supplier, Function> function) { var value = supplier.get(); try { assertEquals(value, function.apply(value).get(10, TimeUnit.SECONDS)); @@ -178,7 +178,7 @@ protected static void expectSuccessful(String type, Supplier< } } - protected static void expectUnsuccessful(String type, Supplier supplier, Function> function) { + private static void expectUnsuccessful(String type, Supplier supplier, Function> function) { try { function.apply(supplier.get()).get(10, TimeUnit.SECONDS); fail("Error occurred: " + type + " should not have round-tripped"); @@ -187,7 +187,7 @@ protected static void expectUnsuccessful(String type, Supplie } } - protected static IRational arbRational() { + private static IRational arbRational() { IInteger numerator = (IInteger) math.arbInt(); IInteger denominator = (IInteger) math.arbInt(); while (denominator.equals(vf.integer(0))) { From 336522728c707841b64d81a6bfdaad7801b1fe08 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Thu, 22 Jan 2026 09:31:53 +0100 Subject: [PATCH 31/37] Incorporated jsonrpc test interface into the test class itself --- .../test/rpc/IValueOverJsonTests.java | 87 ++++++++++++++++ .../test/rpc/JsonRpcTestInterface.java | 98 ------------------- 2 files changed, 87 insertions(+), 98 deletions(-) delete mode 100644 test/org/rascalmpl/test/rpc/JsonRpcTestInterface.java diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java index 9c132e7db7..1f96b91d81 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java @@ -23,6 +23,7 @@ import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.junit.AfterClass; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,11 +38,20 @@ import com.google.gson.GsonBuilder; import io.usethesource.vallang.IBool; +import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IDateTime; import io.usethesource.vallang.IInteger; +import io.usethesource.vallang.IList; +import io.usethesource.vallang.IMap; import io.usethesource.vallang.IMapWriter; +import io.usethesource.vallang.INode; +import io.usethesource.vallang.INumber; import io.usethesource.vallang.IRational; import io.usethesource.vallang.IReal; +import io.usethesource.vallang.ISet; +import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IString; +import io.usethesource.vallang.ITuple; import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; import io.usethesource.vallang.type.Type; @@ -196,6 +206,83 @@ private static IRational arbRational() { return vf.rational(numerator, denominator); } + private static interface JsonRpcTestInterface { + @JsonRequest + default CompletableFuture sendBool(IBool bool) { + return CompletableFuture.completedFuture(bool); + } + + @JsonRequest + default CompletableFuture sendConstructor(IConstructor constructor) { + return CompletableFuture.completedFuture(constructor); + } + + @JsonRequest + default CompletableFuture sendDateTime(IDateTime dateTime) { + return CompletableFuture.completedFuture(dateTime); + } + + @JsonRequest + default CompletableFuture sendInteger(IInteger integer) { + return CompletableFuture.completedFuture(integer); + } + + @JsonRequest + default CompletableFuture sendNode(INode node) { + return CompletableFuture.completedFuture(node); + } + + @JsonRequest + default CompletableFuture sendRational(IRational rational) { + return CompletableFuture.completedFuture(rational); + } + + @JsonRequest + default CompletableFuture sendReal(IReal real) { + return CompletableFuture.completedFuture(real); + } + + @JsonRequest + default CompletableFuture sendLocation(ISourceLocation loc) { + return CompletableFuture.completedFuture(loc); + } + + @JsonRequest + default CompletableFuture sendString(IString string) { + return CompletableFuture.completedFuture(string); + } + + @JsonRequest + default CompletableFuture sendNumber(INumber number) { + return CompletableFuture.completedFuture(number); + } + + @JsonRequest + default CompletableFuture sendValue(IValue value) { + return CompletableFuture.completedFuture(value); + } + + @JsonRequest + default CompletableFuture sendList(IList list) { + return CompletableFuture.completedFuture(list); + } + + @JsonRequest + default CompletableFuture sendMap(IMap map) { + return CompletableFuture.completedFuture(map); + } + + @JsonRequest + default CompletableFuture sendSet(ISet set) { + return CompletableFuture.completedFuture(set); + } + + @JsonRequest + default CompletableFuture sendTuple(ITuple tuple) { + return CompletableFuture.completedFuture(tuple); + } + } + @Test public void testSendBool() { runTestForPrimitiveType("IBool", () -> (IBool) prelude.arbBool(), testServer::sendBool); diff --git a/test/org/rascalmpl/test/rpc/JsonRpcTestInterface.java b/test/org/rascalmpl/test/rpc/JsonRpcTestInterface.java deleted file mode 100644 index 3ab86c75f1..0000000000 --- a/test/org/rascalmpl/test/rpc/JsonRpcTestInterface.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.rascalmpl.test.rpc; - -import java.util.concurrent.CompletableFuture; - -import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; - -import io.usethesource.vallang.IBool; -import io.usethesource.vallang.IConstructor; -import io.usethesource.vallang.IDateTime; -import io.usethesource.vallang.IInteger; -import io.usethesource.vallang.IList; -import io.usethesource.vallang.IMap; -import io.usethesource.vallang.INode; -import io.usethesource.vallang.INumber; -import io.usethesource.vallang.IRational; -import io.usethesource.vallang.IReal; -import io.usethesource.vallang.ISet; -import io.usethesource.vallang.ISourceLocation; -import io.usethesource.vallang.IString; -import io.usethesource.vallang.ITuple; -import io.usethesource.vallang.IValue; - -public interface JsonRpcTestInterface { - @JsonRequest - default CompletableFuture sendBool(IBool bool) { - return CompletableFuture.completedFuture(bool); - } - - @JsonRequest - default CompletableFuture sendConstructor(IConstructor constructor) { - return CompletableFuture.completedFuture(constructor); - } - - @JsonRequest - default CompletableFuture sendDateTime(IDateTime dateTime) { - return CompletableFuture.completedFuture(dateTime); - } - - @JsonRequest - default CompletableFuture sendInteger(IInteger integer) { - return CompletableFuture.completedFuture(integer); - } - - @JsonRequest - default CompletableFuture sendNode(INode node) { - return CompletableFuture.completedFuture(node); - } - - @JsonRequest - default CompletableFuture sendRational(IRational rational) { - return CompletableFuture.completedFuture(rational); - } - - @JsonRequest - default CompletableFuture sendReal(IReal real) { - return CompletableFuture.completedFuture(real); - } - - @JsonRequest - default CompletableFuture sendLocation(ISourceLocation loc) { - return CompletableFuture.completedFuture(loc); - } - - @JsonRequest - default CompletableFuture sendString(IString string) { - return CompletableFuture.completedFuture(string); - } - - @JsonRequest - default CompletableFuture sendNumber(INumber number) { - return CompletableFuture.completedFuture(number); - } - - @JsonRequest - default CompletableFuture sendValue(IValue value) { - return CompletableFuture.completedFuture(value); - } - - @JsonRequest - default CompletableFuture sendList(IList list) { - return CompletableFuture.completedFuture(list); - } - - @JsonRequest - default CompletableFuture sendMap(IMap map) { - return CompletableFuture.completedFuture(map); - } - - @JsonRequest - default CompletableFuture sendSet(ISet set) { - return CompletableFuture.completedFuture(set); - } - - @JsonRequest - default CompletableFuture sendTuple(ITuple tuple) { - return CompletableFuture.completedFuture(tuple); - } -} From 8258d4cd2cfadf1a5d813f05aca446f05480baf9 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Thu, 22 Jan 2026 09:41:35 +0100 Subject: [PATCH 32/37] Fixed jsonrpc test leaking input/output streams --- .../test/rpc/IValueOverJsonTests.java | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java index 1f96b91d81..115522aa92 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java @@ -24,7 +24,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; -import org.junit.AfterClass; +import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -69,9 +69,9 @@ public class IValueOverJsonTests { private static final Type TestAdt = tf.abstractDataType(ts, "TestAdt"); private static final Type TestAdt_testCons = tf.constructor(ts, TestAdt, "testCons", tf.stringType(), "id", tf.integerType(), "nr"); - private static JsonRpcTestInterface testServer; - private static final ThreadLocal is0 = new ThreadLocal<>(), is1 = new ThreadLocal<>(); - private static final ThreadLocal os0 = new ThreadLocal<>(), os1 = new ThreadLocal<>(); + private JsonRpcTestInterface testServer; + private final PipedInputStream is0, is1; + private final PipedOutputStream os0, os1; @Parameters(name="{0}") public static Iterable modesAndConfig() { @@ -87,39 +87,35 @@ public static Iterable modesAndConfig() { public IValueOverJsonTests(ComplexTypeMode complexTypeMode, Consumer gsonConfig) { this.complexTypeMode = complexTypeMode; - startTestServerAndClient(gsonConfig); - } - - private static void startTestServerAndClient(Consumer gsonConfig) { try { - is0.set(new PipedInputStream()); - os0.set(new PipedOutputStream()); - is1.set(new PipedInputStream(os0.get())); - os1.set(new PipedOutputStream(is0.get())); - new TestThread(is0.get(), os0.get(), gsonConfig).start(); - new TestClient(is1.get(), os1.get(), gsonConfig); + is0 = new PipedInputStream(); + os0 = new PipedOutputStream(); + is1 = new PipedInputStream(os0); + os1 = new PipedOutputStream(is0); + new TestThread(is0, os0, gsonConfig).start(); + new TestClient(is1, os1, gsonConfig); } catch (IOException e) { throw new RuntimeException(e); } } - @AfterClass - public static void teardown() throws IOException { - if (is0.get() != null) { - is0.get().close(); + @After + public void teardown() throws IOException { + if (is0 != null) { + is0.close(); } - if (is1.get() != null) { - is1.get().close(); + if (is1 != null) { + is1.close(); } - if (os0.get() != null) { - os0.get().close(); + if (os0 != null) { + os0.close(); } - if (os1.get() != null) { - os1.get().close(); + if (os1 != null) { + os1.close(); } } - static class TestClient { + class TestClient { public TestClient(InputStream is, OutputStream os, Consumer gsonConfig) { Launcher clientLauncher = new Launcher.Builder() .setRemoteInterface(JsonRpcTestInterface.class) From bdd87e504b7432b7dae6351c6f2f95c0a7f195e2 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Thu, 22 Jan 2026 11:19:34 +0100 Subject: [PATCH 33/37] Added dateTimeAsInt and rationalsAsString to jsonResponse in Content.rsc --- src/org/rascalmpl/library/Content.rsc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/org/rascalmpl/library/Content.rsc b/src/org/rascalmpl/library/Content.rsc index bc3c95f09f..c803689ca3 100644 --- a/src/org/rascalmpl/library/Content.rsc +++ b/src/org/rascalmpl/library/Content.rsc @@ -81,7 +81,8 @@ which involves a handy, automatic, encoding of Rascal values into json values. data Response = response(Status status, str mimeType, map[str,str] header, str content) | fileResponse(loc file, str mimeType, map[str,str] header) - | jsonResponse(Status status, map[str,str] header, value val, str dateTimeFormat = "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'", JSONFormatter[value] formatter = str (value _) { fail; }, bool explicitConstructorNames=false, bool explicitDataTypes=false) + | jsonResponse(Status status, map[str,str] header, value val, str dateTimeFormat = "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'", JSONFormatter[value] formatter = str (value _) { fail; }, + bool explicitConstructorNames=false, bool explicitDataTypes=false, bool dateTimeAsInt=false, bool rationalsAsString=false) ; @synopsis{Utility to quickly render a string as HTML content} From b861ec5488da7149ea3b8a2761a2cd7680ae3f15 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Thu, 22 Jan 2026 11:20:07 +0100 Subject: [PATCH 34/37] Changed default value for dateTimeAsInt to conform to the Rascal definition --- src/org/rascalmpl/library/util/TermREPL.java | 2 +- src/org/rascalmpl/library/util/Webserver.java | 2 +- src/org/rascalmpl/repl/http/REPLContentServer.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/org/rascalmpl/library/util/TermREPL.java b/src/org/rascalmpl/library/util/TermREPL.java index a98798b79e..b03ddfa5b7 100644 --- a/src/org/rascalmpl/library/util/TermREPL.java +++ b/src/org/rascalmpl/library/util/TermREPL.java @@ -290,7 +290,7 @@ private ICommandOutput handleJSONResponse(IConstructor response) { JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") .setFormatters((IFunction) formatters) - .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : true) + .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : false) .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) diff --git a/src/org/rascalmpl/library/util/Webserver.java b/src/org/rascalmpl/library/util/Webserver.java index 5dd103a0b5..6b2995a07b 100644 --- a/src/org/rascalmpl/library/util/Webserver.java +++ b/src/org/rascalmpl/library/util/Webserver.java @@ -249,7 +249,7 @@ private Response translateJsonResponse(Method method, IConstructor cons) { JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") .setFormatters((IFunction) formatters) - .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : true) + .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : false) .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) diff --git a/src/org/rascalmpl/repl/http/REPLContentServer.java b/src/org/rascalmpl/repl/http/REPLContentServer.java index 498517f36b..029e4904ef 100644 --- a/src/org/rascalmpl/repl/http/REPLContentServer.java +++ b/src/org/rascalmpl/repl/http/REPLContentServer.java @@ -171,7 +171,7 @@ private static Response translateJsonResponse(Method method, IConstructor cons) JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") .setFormatters((IFunction) formatters) - .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : true) + .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : false) .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) From d2816a54773fc4efdf0dd7f44a7e1baf84d04762 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Thu, 22 Jan 2026 14:47:36 +0100 Subject: [PATCH 35/37] Revert "Changed default value for dateTimeAsInt to conform to the Rascal definition" This reverts commit b861ec5488da7149ea3b8a2761a2cd7680ae3f15. --- src/org/rascalmpl/library/util/TermREPL.java | 2 +- src/org/rascalmpl/library/util/Webserver.java | 2 +- src/org/rascalmpl/repl/http/REPLContentServer.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/org/rascalmpl/library/util/TermREPL.java b/src/org/rascalmpl/library/util/TermREPL.java index b03ddfa5b7..a98798b79e 100644 --- a/src/org/rascalmpl/library/util/TermREPL.java +++ b/src/org/rascalmpl/library/util/TermREPL.java @@ -290,7 +290,7 @@ private ICommandOutput handleJSONResponse(IConstructor response) { JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") .setFormatters((IFunction) formatters) - .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : false) + .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : true) .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) diff --git a/src/org/rascalmpl/library/util/Webserver.java b/src/org/rascalmpl/library/util/Webserver.java index 6b2995a07b..5dd103a0b5 100644 --- a/src/org/rascalmpl/library/util/Webserver.java +++ b/src/org/rascalmpl/library/util/Webserver.java @@ -249,7 +249,7 @@ private Response translateJsonResponse(Method method, IConstructor cons) { JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") .setFormatters((IFunction) formatters) - .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : false) + .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : true) .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) diff --git a/src/org/rascalmpl/repl/http/REPLContentServer.java b/src/org/rascalmpl/repl/http/REPLContentServer.java index 029e4904ef..498517f36b 100644 --- a/src/org/rascalmpl/repl/http/REPLContentServer.java +++ b/src/org/rascalmpl/repl/http/REPLContentServer.java @@ -171,7 +171,7 @@ private static Response translateJsonResponse(Method method, IConstructor cons) JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") .setFormatters((IFunction) formatters) - .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : false) + .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : true) .setRationalsAsString(ras != null ? ((IBool) ras).getValue() : false) .setExplicitConstructorNames(ecn != null ? ((IBool) ecn).getValue() : false) .setExplicitDataTypes(edt != null ? ((IBool) edt).getValue() : false) From 169bb1decc93f25da5b0b568a93ab05bbe7cd7e0 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Thu, 22 Jan 2026 14:49:43 +0100 Subject: [PATCH 36/37] Changed type to var --- src/org/rascalmpl/ideservices/GsonUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index 848e6b2b2e..e9b2ee320c 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -150,7 +150,7 @@ public boolean supports(Class incoming) { } public TypeAdapter createAdapter(ComplexTypeMode complexTypeMode, TypeStore ts) { - JsonValueReader reader = new JsonValueReader(vf, ts, new NullRascalMonitor(), null); + var reader = new JsonValueReader(vf, ts, new NullRascalMonitor(), null); if (isPrimitive) { return new TypeAdapter() { @Override From f0ff59c972d2698f71c77db5aea407537b3a5c27 Mon Sep 17 00:00:00 2001 From: Rodin Aarssen Date: Thu, 22 Jan 2026 14:49:59 +0100 Subject: [PATCH 37/37] Flipped default value of new keyword argument --- src/org/rascalmpl/library/Content.rsc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/library/Content.rsc b/src/org/rascalmpl/library/Content.rsc index c803689ca3..f7615a6336 100644 --- a/src/org/rascalmpl/library/Content.rsc +++ b/src/org/rascalmpl/library/Content.rsc @@ -82,7 +82,7 @@ data Response = response(Status status, str mimeType, map[str,str] header, str content) | fileResponse(loc file, str mimeType, map[str,str] header) | jsonResponse(Status status, map[str,str] header, value val, str dateTimeFormat = "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'", JSONFormatter[value] formatter = str (value _) { fail; }, - bool explicitConstructorNames=false, bool explicitDataTypes=false, bool dateTimeAsInt=false, bool rationalsAsString=false) + bool explicitConstructorNames=false, bool explicitDataTypes=false, bool dateTimeAsInt=true, bool rationalsAsString=false) ; @synopsis{Utility to quickly render a string as HTML content}