Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
10fbde8
No longer wrapping complex types in an auxiliary JSON object. Fixes u…
rodinaarssen Jan 16, 2026
42bf046
Extracted jsonrpc test interface to a separate class
rodinaarssen Jan 20, 2026
28b6173
Made test class abstract to allow for code reuse later on
rodinaarssen Jan 20, 2026
e876cee
Moved function around a bit
rodinaarssen Jan 20, 2026
e29a164
Prepared abstract test class for instantiation
rodinaarssen Jan 20, 2026
713f19e
Improved configuration methods in GsonUtils
rodinaarssen Jan 20, 2026
805ca60
Added deserialization functionality in GsonUtils given a TypeStore in…
rodinaarssen Jan 20, 2026
c3a0b31
Improved comments describing what happens with complex values during …
rodinaarssen Jan 20, 2026
34dd61f
Added jsonrpc test class: complex values as JSON objects
rodinaarssen Jan 20, 2026
5eea68a
Added jsonrpc test class: complex values as Base64-enoded string
rodinaarssen Jan 20, 2026
19c4f0b
Added jsonrpc test class: complex values as string
rodinaarssen Jan 20, 2026
f1ad106
Added jsonrpc test class: complex values unsupported
rodinaarssen Jan 20, 2026
3e22617
Using renamed configuration method of GsonUtils
rodinaarssen Jan 20, 2026
791d420
Make sure that the jsonrpc tests are run during CI
rodinaarssen Jan 21, 2026
ba6d56a
Renamed test files to conform to file name heuristics
rodinaarssen Jan 21, 2026
e27f8c9
Changed pom.xml to pick up the renamed test files in CI
rodinaarssen Jan 21, 2026
b136188
Added configuration option to JsonValueWriter to encode rationals as …
rodinaarssen Jan 21, 2026
287b7ba
Fixed error message
rodinaarssen Jan 21, 2026
de43057
GsonUtils now configures JsonValueWriter to encode rationals as strings
rodinaarssen Jan 21, 2026
92ea92f
Rationals are no longer wrapped in an object in any configuration
rodinaarssen Jan 21, 2026
4830660
Fixed deserialization in case of string-encoding
rodinaarssen Jan 21, 2026
d679dff
Reenabled all jsonrpc tests
rodinaarssen Jan 21, 2026
47e0ec7
Wrapped input/output streams in ThreadLocal in jsonrpc tests
rodinaarssen Jan 21, 2026
cd57210
Merge branch 'main' into fix-jsonrpc-wrapping
rodinaarssen Jan 21, 2026
50e967a
Updated comments in GsonUtil
rodinaarssen Jan 21, 2026
b720017
Reusing stored ValueFactory
rodinaarssen Jan 21, 2026
eb42d11
Removed non-existent parameters from JavaDoc
rodinaarssen Jan 21, 2026
1f056dc
Added timeouts to jsonrpc tests
rodinaarssen Jan 21, 2026
bdad357
Collapsed all jsonrpc tests into one parameterized test class
rodinaarssen Jan 22, 2026
ad336f1
Renamed jsonrpc test class
rodinaarssen Jan 22, 2026
f839a69
Made protected fields private; test class is no longer extended
rodinaarssen Jan 22, 2026
3365227
Incorporated jsonrpc test interface into the test class itself
rodinaarssen Jan 22, 2026
8258d4c
Fixed jsonrpc test leaking input/output streams
rodinaarssen Jan 22, 2026
bdd87e5
Added dateTimeAsInt and rationalsAsString to jsonResponse in Content.rsc
rodinaarssen Jan 22, 2026
b861ec5
Changed default value for dateTimeAsInt to conform to the Rascal defi…
rodinaarssen Jan 22, 2026
d2816a5
Revert "Changed default value for dateTimeAsInt to conform to the Ras…
rodinaarssen Jan 22, 2026
169bb1d
Changed type to var
rodinaarssen Jan 22, 2026
f0ff59c
Flipped default value of new keyword argument
rodinaarssen Jan 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@
<include>**/org/rascalmpl/test/library/LibraryLangPaths.java</include>
<include>**/org/rascalmpl/test/value/AllTests.java</include>
<include>**/org/rascalmpl/test/repl/*Test.java</include>
<include>**/org/rascalmpl/test/rpc/*Test.java</include>
<include>**/org/rascalmpl/test/rpc/*Tests.java</include>
<include>**/org/rascalmpl/util/**/*Test.java</include>
<include>**/org/rascalmpl/util/**/*Tests.java</include>
<include>**/org/rascalmpl/uri/**/*Test.java</include>
Expand Down
138 changes: 93 additions & 45 deletions src/org/rascalmpl/ideservices/GsonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -71,16 +74,16 @@
*/
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<TypeMapping> typeMappings;

/* Mappings from Java types to `vallang` types are declared here.
* 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
Expand All @@ -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
}

Expand All @@ -129,35 +149,19 @@ public boolean supports(Class<?> incoming) {
return clazz.isAssignableFrom(incoming);
}

public <T> TypeAdapter<T> createAdapter(ComplexTypeMode complexTypeMode) {
public <T> TypeAdapter<T> createAdapter(ComplexTypeMode complexTypeMode, TypeStore ts) {
var reader = new JsonValueReader(vf, ts, new NullRascalMonitor(), null);
if (isPrimitive) {
return new TypeAdapter<T>() {
@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);
}
};
}
Expand All @@ -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));
Expand All @@ -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<GsonBuilder> 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<GsonBuilder> 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<GsonBuilder> 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<GsonBuilder> 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<GsonBuilder> 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<GsonBuilder> 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 <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Expand All @@ -222,7 +270,7 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
return typeMappings.stream()
.filter(m -> m.supports(rawType))
.findFirst()
.map(m -> m.<T>createAdapter(complexTypeMode))
.map(m -> m.<T>createAdapter(complexTypeMode, ts))
.orElse(null);
}
});
Expand All @@ -231,7 +279,7 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> 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);
Expand All @@ -242,7 +290,7 @@ public static String base64Encode(IValue value) {
@SuppressWarnings("unchecked")
public static <T extends IValue> 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);
Expand Down
2 changes: 1 addition & 1 deletion src/org/rascalmpl/ideservices/RemoteIDEServices.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
3 changes: 2 additions & 1 deletion src/org/rascalmpl/library/Content.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
6 changes: 4 additions & 2 deletions src/org/rascalmpl/library/lang/json/IO.java
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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)
Expand All @@ -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();
Expand All @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions src/org/rascalmpl/library/lang/json/IO.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -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; },
Expand All @@ -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{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}

Expand Down
18 changes: 14 additions & 4 deletions src/org/rascalmpl/library/lang/json/internal/JsonValueWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
public class JsonValueWriter {
private ThreadLocal<SimpleDateFormat> format;
private boolean datesAsInts = true;
private boolean rationalsAsString = false;
private boolean unpackedLocations = false;
private boolean dropOrigins = true;
private IFunction formatters;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Loading
Loading