diff --git a/pom.xml b/pom.xml index 5f114aed34f..5da40049817 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/*Tests.java **/org/rascalmpl/util/**/*Test.java **/org/rascalmpl/util/**/*Tests.java **/org/rascalmpl/uri/**/*Test.java diff --git a/src/org/rascalmpl/ideservices/GsonUtils.java b/src/org/rascalmpl/ideservices/GsonUtils.java index fdf0d273f51..e9b2ee320c9 100644 --- a/src/org/rascalmpl/ideservices/GsonUtils.java +++ b/src/org/rascalmpl/ideservices/GsonUtils.java @@ -27,15 +27,16 @@ package org.rascalmpl.ideservices; import java.io.IOException; -import java.io.StringWriter; +import java.io.StringReader; import java.util.List; +import java.util.function.Consumer; import org.checkerframework.checker.nullness.qual.Nullable; import org.rascalmpl.interpreter.NullRascalMonitor; 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; import com.google.gson.GsonBuilder; @@ -59,6 +60,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; @@ -71,8 +74,8 @@ */ 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 IValueFactory vf = ValueFactoryFactory.getValueFactory(); private static final List typeMappings; @@ -80,7 +83,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 @@ -100,9 +103,26 @@ public class GsonUtils { } public static enum ComplexTypeMode { + /** + * 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 Base64-encoded string. A properly filled {@link TypeStore} is required for deserialization. + */ ENCODE_AS_BASE64_STRING, + + /** + * Complex values are serialized as a string. A properly filled {@link TypeStore} is required for deserialization. + */ ENCODE_AS_STRING, + + /** + * Only values of primitive type are supported; more complex values are neither serialized nor deserialized. + */ NOT_SUPPORTED } @@ -129,35 +149,19 @@ public boolean supports(Class incoming) { return clazz.isAssignableFrom(incoming); } - public TypeAdapter createAdapter(ComplexTypeMode complexTypeMode) { + public TypeAdapter createAdapter(ComplexTypeMode complexTypeMode, TypeStore ts) { + var reader = new JsonValueReader(vf, ts, new NullRascalMonitor(), null); if (isPrimitive) { 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"); - } writer.write(out, (IValue) value); - if (needsWrapping) { - out.endObject(); - } } @SuppressWarnings("unchecked") @Override public T read(JsonReader in) throws IOException { - var needsWrapping = needsWrapping(type, complexTypeMode); - 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); } }; } @@ -166,15 +170,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)); @@ -189,29 +185,81 @@ 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) new StandardTextReader().read(vf, ts, tf.valueType(), new StringReader(in.nextString())); + default: + throw new IOException("Cannot handle complex type"); + } } }; } } - + + /** + * Configure Gson to encode complex (non-primitive) values as JSON objects. + * + * See {@link ComplexTypeMode.ENCODE_AS_JSON_OBJECT}. + */ + public static Consumer complexAsJsonObject() { + return builder -> configureGson(builder, ComplexTypeMode.ENCODE_AS_JSON_OBJECT, new TypeStore()); + } + /** - * 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 + * 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). */ - private static boolean needsWrapping(Type type, ComplexTypeMode complexTypeMode) { - return complexTypeMode == ComplexTypeMode.ENCODE_AS_JSON_OBJECT && type == null || type.isSubtypeOf(tf.rationalType()); + public static Consumer complexAsBase64String() { + return builder -> complexAsBase64String(new TypeStore()); } - public static void configureGson(GsonBuilder builder) { - configureGson(builder, ComplexTypeMode.ENCODE_AS_JSON_OBJECT); + /** + * Configure Gson to encode complex (non-primitive) values as Base64-encoded strings. + * + * This configuration can be used for both serialization and deserialization. + * + * @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). + */ + 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 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) { @@ -222,7 +270,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); } }); @@ -231,7 +279,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); @@ -242,7 +290,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); diff --git a/src/org/rascalmpl/ideservices/RemoteIDEServices.java b/src/org/rascalmpl/ideservices/RemoteIDEServices.java index cbfc44eacae..14af1ba9941 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(); diff --git a/src/org/rascalmpl/library/Content.rsc b/src/org/rascalmpl/library/Content.rsc index bc3c95f09f7..f7615a63368 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=true, bool rationalsAsString=false) ; @synopsis{Utility to quickly render a string as HTML content} diff --git a/src/org/rascalmpl/library/lang/json/IO.java b/src/org/rascalmpl/library/lang/json/IO.java index 90498e96690..55a2b12b160 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 0a3589b1201..8c71c94ed72 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/JsonValueReader.java b/src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java index f33a87762f3..036d78c1c0c 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()); } } diff --git a/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java b/src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java index 02c91578e0d..ad7e3eea891 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 1c889cb2baf..8050794adda 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 5cdc2e5c3d9..a98798b79ea 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 5d6ab6fb24e..5dd103a0b5e 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 9fd5de65d73..498517f36ba 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) ; diff --git a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java index dbbdfaff5d9..115522aa92a 100644 --- a/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java +++ b/test/org/rascalmpl/test/rpc/IValueOverJsonTests.java @@ -8,23 +8,35 @@ 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.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.After; 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.RascalValueFactory; import org.rascalmpl.values.ValueFactoryFactory; +import com.google.gson.GsonBuilder; + import io.usethesource.vallang.IBool; import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IDateTime; @@ -37,34 +49,58 @@ 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; +import io.usethesource.vallang.type.Type; +import io.usethesource.vallang.type.TypeFactory; +import io.usethesource.vallang.type.TypeStore; +@RunWith(Parameterized.class) public 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); - private static TestInterface server; - private static PipedInputStream is0 = null, is1 = null; - private static PipedOutputStream os0 = null, os1 = null; - - @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); + 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"); + + private JsonRpcTestInterface testServer; + private final PipedInputStream is0, is1; + private final PipedOutputStream os0, os1; + + @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 IValueOverJsonTests(ComplexTypeMode complexTypeMode, Consumer gsonConfig) { + this.complexTypeMode = complexTypeMode; + try { + 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 { + @After + public void teardown() throws IOException { if (is0 != null) { is0.close(); } @@ -79,306 +115,233 @@ 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); + class TestClient { + public TestClient(InputStream is, OutputStream os, Consumer gsonConfig) { + Launcher clientLauncher = new Launcher.Builder() + .setRemoteInterface(JsonRpcTestInterface.class) + .setLocalService(this) + .setInput(is) + .setOutput(os) + .configureGson(gsonConfig) + .setExecutorService(Executors.newCachedThreadPool()) + .create(); - @JsonRequest - CompletableFuture sendValue(IValue value); + clientLauncher.startListening(); + testServer = clientLauncher.getRemoteProxy(); + } + } - @JsonRequest - CompletableFuture sendList(IList list); + static class TestThread extends Thread { + private final InputStream is; + private final OutputStream os; + private final Consumer gsonConfig; - @JsonRequest - CompletableFuture sendMap(IMap map); + public TestThread(InputStream is, OutputStream os, Consumer gsonConfig) { + this.is = is; + this.os = os; + this.gsonConfig = gsonConfig; + this.setDaemon(true); + } - @JsonRequest - CompletableFuture sendSet(ISet set); + @Override + public void run() { + Launcher serverLauncher = new Launcher.Builder() + .setLocalService(new JsonRpcTestInterface() {}) // `setLocalService` explicitly requires an interface, not a class + .setRemoteInterface(JsonRpcTestInterface.class) + .setInput(is) + .setOutput(os) + .configureGson(gsonConfig) + .setExceptionHandler(e -> { + System.err.println(e); + return new ResponseError(ResponseErrorCode.InternalError, e.getMessage(), e); + }) + .create(); - @JsonRequest - CompletableFuture sendTuple(ITuple tuple); + serverLauncher.startListening(); + } } - static class TestServer implements TestInterface { + private static Set asJsonObjectOrNotSupported = new HashSet<>(Arrays.asList(ComplexTypeMode.ENCODE_AS_JSON_OBJECT, ComplexTypeMode.NOT_SUPPORTED)); - @Override - public CompletableFuture sendBool(IBool value) { - return CompletableFuture.completedFuture(value); + 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); } + } - @Override - public CompletableFuture sendConstructor(IConstructor value) { - return CompletableFuture.failedFuture(new IllegalStateException("IConstructor should not have been decoded")); + private static void expectSuccessful(String type, Supplier supplier, Function> function) { + var value = supplier.get(); + try { + assertEquals(value, function.apply(value).get(10, TimeUnit.SECONDS)); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + fail("Error occurred while testing " + type + " over jsonrpc: " + e.getMessage()); } + } - @Override - public CompletableFuture sendDateTime(IDateTime value) { - return CompletableFuture.completedFuture(value); + 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"); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + //This is expected } + } - @Override - public CompletableFuture sendInteger(IInteger value) { - return CompletableFuture.completedFuture(value); + 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); + } - @Override - public CompletableFuture sendNode(INode value) { - return CompletableFuture.failedFuture(new IllegalStateException("INode should not have been decoded")); + private static interface JsonRpcTestInterface { + @JsonRequest + default CompletableFuture sendBool(IBool bool) { + return CompletableFuture.completedFuture(bool); } - - @Override - public CompletableFuture sendRational(IRational value) { - return CompletableFuture.completedFuture(value); + + @JsonRequest + default CompletableFuture sendConstructor(IConstructor constructor) { + return CompletableFuture.completedFuture(constructor); } - @Override - public CompletableFuture sendReal(IReal value) { - return CompletableFuture.completedFuture(value); + @JsonRequest + default CompletableFuture sendDateTime(IDateTime dateTime) { + return CompletableFuture.completedFuture(dateTime); } - @Override - public CompletableFuture sendLocation(ISourceLocation value) { - return CompletableFuture.completedFuture(value); + @JsonRequest + default CompletableFuture sendInteger(IInteger integer) { + return CompletableFuture.completedFuture(integer); } - @Override - public CompletableFuture sendString(IString value) { - return CompletableFuture.completedFuture(value); + @JsonRequest + default CompletableFuture sendNode(INode node) { + return CompletableFuture.completedFuture(node); } - @Override - public CompletableFuture sendNumber(INumber value) { - return CompletableFuture.completedFuture(value); + @JsonRequest + default CompletableFuture sendRational(IRational rational) { + return CompletableFuture.completedFuture(rational); } - @Override - public CompletableFuture sendValue(IValue value) { - return CompletableFuture.completedFuture(value); + @JsonRequest + default CompletableFuture sendReal(IReal real) { + return CompletableFuture.completedFuture(real); } - @Override - public CompletableFuture sendList(IList list) { - return CompletableFuture.failedFuture(new IllegalStateException("IList should not have been decoded")); + @JsonRequest + default CompletableFuture sendLocation(ISourceLocation loc) { + return CompletableFuture.completedFuture(loc); } - @Override - public CompletableFuture sendMap(IMap map) { - return CompletableFuture.failedFuture(new IllegalStateException("IMap should not have been decoded")); + @JsonRequest + default CompletableFuture sendString(IString string) { + return CompletableFuture.completedFuture(string); } - @Override - public CompletableFuture sendSet(ISet set) { - return CompletableFuture.failedFuture(new IllegalStateException("ISet should not have been decoded")); + @JsonRequest + default CompletableFuture sendNumber(INumber number) { + return CompletableFuture.completedFuture(number); } - @Override - public CompletableFuture sendTuple(ITuple tuple) { - return CompletableFuture.failedFuture(new IllegalStateException("ITuple should not have been decoded")); + @JsonRequest + default CompletableFuture sendValue(IValue value) { + return CompletableFuture.completedFuture(value); } - } - - static class TestClient { - public TestClient(InputStream is, OutputStream os) { - Launcher clientLauncher = new Launcher.Builder() - .setRemoteInterface(TestInterface.class) - .setLocalService(this) - .setInput(is) - .setOutput(os) - .configureGson(GsonUtils::configureGson) - .setExecutorService(Executors.newCachedThreadPool()) - .create(); - clientLauncher.startListening(); - server = clientLauncher.getRemoteProxy(); + @JsonRequest + default CompletableFuture sendList(IList list) { + return CompletableFuture.completedFuture(list); } - } - - static class TestThread extends Thread { - private final InputStream is; - private final OutputStream os; - public TestThread(InputStream is, OutputStream os) { - this.is = is; - this.os = os; - this.setDaemon(true); + @JsonRequest + default CompletableFuture sendMap(IMap map) { + return CompletableFuture.completedFuture(map); } - @Override - public void run() { - Launcher serverLauncher = new Launcher.Builder() - .setLocalService(new TestServer()) - .setRemoteInterface(TestInterface.class) - .setInput(is) - .setOutput(os) - .configureGson(GsonUtils::configureGson) - .setExceptionHandler(e -> { - System.err.println(e); - return new ResponseError(ResponseErrorCode.InternalError, e.getMessage(), e); - }) - .create(); + @JsonRequest + default CompletableFuture sendSet(ISet set) { + return CompletableFuture.completedFuture(set); + } - serverLauncher.startListening(); + @JsonRequest + default CompletableFuture sendTuple(ITuple tuple) { + return CompletableFuture.completedFuture(tuple); } } @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); - } + runTestForPrimitiveType("IBool", () -> (IBool) prelude.arbBool(), testServer::sendBool); } - + @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 - } + runTestForComplexType("IConstructor", () -> vf.constructor(TestAdt_testCons, vf.string("hi"), vf.integer(38)), testServer::sendConstructor); } @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); - } + runTestForPrimitiveType("IDateTime", () -> (IDateTime) prelude.arbDateTime(), testServer::sendDateTime); } @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); - } + runTestForPrimitiveType("IInteger", () -> (IInteger) math.arbInt(), testServer::sendInteger); } @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 - } + runTestForComplexType("INode", () -> prelude.arbNode(), testServer::sendNode); } @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); - } + runTestForPrimitiveType("IRational", () -> arbRational(), testServer::sendRational); } @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); - } + runTestForPrimitiveType("IReal", () -> (IReal) math.arbReal(), testServer::sendReal); } @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); - } + runTestForPrimitiveType("ISourceLocation", () -> prelude.arbLoc(), testServer::sendLocation); } @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); - } + runTestForPrimitiveType("IString", () -> prelude.arbString(vf.integer(1024)), testServer::sendString); } - + @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); - } + runTestForPrimitiveType("INumber", () -> (IInteger) math.arbInt(), testServer::sendNumber); } @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); - } + runTestForPrimitiveType("INumber", () -> (IReal) math.arbReal(), testServer::sendNumber); } - + @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); - } + runTestForPrimitiveType("IValue", () -> (IReal) math.arbReal(), testServer::sendReal); } @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 - } + runTestForComplexType("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList); } @Test @@ -386,45 +349,16 @@ 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 - } + runTestForComplexType("IMap", () -> writer.done(), testServer::sendMap); } @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 - } + runTestForComplexType("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet); } @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 - } - } - - 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); + runTestForComplexType("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple); } }