Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added optional storage of constructor names and datatype names while serializing to JSON. Wired this through the different webservers as well #1862

Merged
merged 12 commits into from
Nov 25, 2024
Merged
2 changes: 1 addition & 1 deletion src/org/rascalmpl/library/Content.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ 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\'")
| jsonResponse(Status status, map[str,str] header, value val, str dateTimeFormat = "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'", bool explicitConstructorNames=false, bool explicitDataTypes=false)
;


Expand Down
19 changes: 12 additions & 7 deletions src/org/rascalmpl/library/lang/json/IO.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,16 @@
}


public IValue readJSON(IValue type, ISourceLocation loc, IString dateTimeFormat, IBool lenient, IBool trackOrigins) {
public IValue readJSON(IValue type, ISourceLocation loc, IString dateTimeFormat, IBool lenient, IBool trackOrigins, IBool explicitConstructorNames, IBool explicitDataTypes) {
TypeStore store = new TypeStore();
Type start = new TypeReifier(values).valueToType((IConstructor) type, store);

try (JsonReader in = new JsonReader(URIResolverRegistry.getInstance().getCharacterReader(loc))) {
in.setLenient(lenient.getValue());
return new JsonValueReader(values, store, monitor, trackOrigins.getValue() ? loc : null)
.setCalendarFormat(dateTimeFormat.getValue())
.setExplicitConstructorNames(explicitConstructorNames.getValue())
.setExplicitDataTypes(explicitDataTypes.getValue())
.read(in, start);
}
catch (IOException e) {
Expand All @@ -119,14 +121,16 @@
}
}

public IValue parseJSON(IValue type, IString src, IString dateTimeFormat, IBool lenient, IBool trackOrigins) {
public IValue parseJSON(IValue type, IString src, IString dateTimeFormat, IBool lenient, IBool trackOrigins, IBool explicitConstructorNames, IBool explicitDataTypes) {
TypeStore store = new TypeStore();
Type start = new TypeReifier(values).valueToType((IConstructor) type, store);

try (JsonReader in = new JsonReader(new StringReader(src.getValue()))) {
in.setLenient(lenient.getValue());
return new JsonValueReader(values, store, monitor, trackOrigins.getValue() ? URIUtil.rootLocation("unknown") : null)
.setCalendarFormat(dateTimeFormat.getValue())
.setExplicitConstructorNames(explicitConstructorNames.getValue())
.setExplicitDataTypes(explicitDataTypes.getValue())
.read(in, start);
}
catch (IOException e) {
Expand All @@ -137,7 +141,7 @@
}
}

public void writeJSON(ISourceLocation loc, IValue value, IBool unpackedLocations, IString dateTimeFormat, IBool dateTimeAsInt, IInteger indent, IBool dropOrigins) {
public void writeJSON(ISourceLocation loc, IValue value, IBool unpackedLocations, IString dateTimeFormat, IBool dateTimeAsInt, IInteger indent, IBool dropOrigins, IBool explicitConstructorNames, IBool explicitDataTypes) {
try (JsonWriter out = new JsonWriter(new OutputStreamWriter(URIResolverRegistry.getInstance().getOutputStream(loc, false), Charset.forName("UTF8")))) {
if (indent.intValue() > 0) {
out.setIndent(" ".substring(0, indent.intValue() % 9));
Expand All @@ -148,13 +152,15 @@
.setDatesAsInt(dateTimeAsInt.getValue())
.setUnpackedLocations(unpackedLocations.getValue())
.setDropOrigins(dropOrigins.getValue())
.setExplicitConstructorNames(explicitConstructorNames.getValue())
.setExplicitDataTypes(explicitDataTypes.getValue())

Check warning on line 156 in src/org/rascalmpl/library/lang/json/IO.java

View check run for this annotation

Codecov / codecov/patch

src/org/rascalmpl/library/lang/json/IO.java#L155-L156

Added lines #L155 - L156 were not covered by tests
.write(out, value);
} catch (IOException e) {
throw RuntimeExceptionFactory.io(values.string(e.getMessage()), null, null);
}
}

public IString asJSON(IValue value, IBool unpackedLocations, IString dateTimeFormat, IBool dateTimeAsInt, IInteger indent, IBool dropOrigins) {
public IString asJSON(IValue value, IBool unpackedLocations, IString dateTimeFormat, IBool dateTimeAsInt, IInteger indent, IBool dropOrigins, IBool explicitConstructorNames, IBool explicitDataTypes) {
StringWriter string = new StringWriter();

try (JsonWriter out = new JsonWriter(string)) {
Expand All @@ -166,14 +172,13 @@
.setDatesAsInt(dateTimeAsInt.getValue())
.setUnpackedLocations(unpackedLocations.getValue())
.setDropOrigins(dropOrigins.getValue())
.setExplicitConstructorNames(explicitConstructorNames.getValue())
.setExplicitDataTypes(explicitDataTypes.getValue())
.write(out, value);

return values.string(string.toString());
} catch (IOException e) {
throw RuntimeExceptionFactory.io(values.string(e.getMessage()), null, null);
}
}



}
52 changes: 35 additions & 17 deletions src/org/rascalmpl/library/lang/json/IO.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,23 @@
module lang::json::IO

@javaClass{org.rascalmpl.library.lang.json.IO}
@deprecated{
Use writeJSON
}
@synopsis{Maps any Rascal value to a JSON string}
@deprecated{use ((writeJSON))}
public java str toJSON(value v);

@javaClass{org.rascalmpl.library.lang.json.IO}
@deprecated{
Use asJSON
}
@synopsis{Maps any Rascal value to a JSON string, optionally in compact form.}
@deprecated{use ((asJSON))}
public java str toJSON(value v, bool compact);

@javaClass{org.rascalmpl.library.lang.json.IO}
@deprecated{
Use readJSON
}
@deprecated{use ((readJSON))}
@synopsis{Parses a JSON string and maps it to the requested type of Rascal value.}
public java &T fromJSON(type[&T] typ, str src);

@javaClass{org.rascalmpl.library.lang.json.IO}
@synopsis{reads JSON values from a stream
@synopsis{reads JSON values from a stream}
@description{
In general the translation behaves as follows:
* Objects translate to map[str,value] by default, unless a node is expected (properties are then translated to keyword fields)
* Arrays translate to lists by default, or to a set if that is expected or a tuple if that is expected. Arrays may also be interpreted as constructors or nodes (see below)
Expand All @@ -44,20 +42,40 @@ In general the translation behaves as follows:
* If num, int, real or rat are expected both strings and number values are mapped
* If loc is expected than strings which look like URI are parsed (containing :/) or a file:/// URI is build, or if an object is found each separate field of
a location object is read from the respective properties: { scheme : str, authority: str?, path: str?, fragment: str?, query: str?, offset: int, length: int, begin: [bl, bc], end: [el, ec]}}
java &T readJSON(type[&T] expected, loc src, str dateTimeFormat = "yyyy-MM-dd\'T\'HH:mm:ssZZZZZ", bool lenient=false, bool trackOrigins=false);
java &T readJSON(type[&T] expected, loc src, str dateTimeFormat = "yyyy-MM-dd\'T\'HH:mm:ssZZZZZ", bool lenient=false, bool trackOrigins=false, bool explicitConstructorNames=false, bool explicitDataTypes=false);

@javaClass{org.rascalmpl.library.lang.json.IO}
@synopsis{parses JSON values from a string
In general the translation behaves as the same as for ((readJSON)).}
java &T parseJSON(type[&T] expected, str src, str dateTimeFormat = "yyyy-MM-dd\'T\'HH:mm:ssZZZZZ", bool lenient=false, bool trackOrigins=false);
java &T parseJSON(type[&T] expected, str src, str dateTimeFormat = "yyyy-MM-dd\'T\'HH:mm:ssZZZZZ", bool lenient=false, bool trackOrigins=false, bool explicitConstructorNames=false, bool explicitDataTypes=false);

@javaClass{org.rascalmpl.library.lang.json.IO}
@synopsis{writes `val` to the location `target`}
@synopsis{Serializes a value as a JSON string and stream it}
@description{
If `dateTimeAsInt` is set to `true`, the dateTime values are converted to an int that represents the number of milliseconds from 1970-01-01T00:00Z.
If `indent` is set to a number greater than 0, the JSON file will be formatted with `indent` number of spaces as indentation.
This function tries to map Rascal values to JSON values in a natural way.
In particular it tries to create a value that has the same number of recursive levels,
such that one constructor maps to one object. The serialization is typically _lossy_ since
JSON values by default do not explicitly encode the class or constructor while Rascal data types do.

If you need the names of constructors or data-types in your result, then use the parameters:
* `explicitConstructorNames=true` will store the name of every constructor in a field `_constructor`
* `explicitDataTypes=true` will store the name of the ADT in a field called `_type`

The `dateTimeFormat` parameter dictates how `datetime` values will be printed.

The `unpackedLocations` parameter will produce an object with many fields for every property of a `loc` value, but
if set to false a `loc` will be printed as a string.
}
@pitfalls{
* It is understood that Rascal's number types have arbitrary precision, but this is not supported by the JSON writer.
As such when an `int` is printed that does not fit into a JVM `long`, there will be truncation to the lower 64 bits.
For `real` numbers that are larger than JVM's double you get "negative infinity" or "positive infinity" as a result.
}
java void writeJSON(loc target, value val, bool unpackedLocations=false, str dateTimeFormat="yyyy-MM-dd\'T\'HH:mm:ssZZZZZ", bool dateTimeAsInt=false, int indent=0, bool dropOrigins=true);
java void writeJSON(loc target, value val, bool unpackedLocations=false, str dateTimeFormat="yyyy-MM-dd\'T\'HH:mm:ssZZZZZ", bool dateTimeAsInt=false, int indent=0, bool dropOrigins=true, bool explicitConstructorNames=false, bool explicitDataTypes=false);

@javaClass{org.rascalmpl.library.lang.json.IO}
java str asJSON(value val, bool unpackedLocations=false, str dateTimeFormat="yyyy-MM-dd\'T\'HH:mm:ssZZZZZ", bool dateTimeAsInt=false, int indent = 0, bool dropOrigins=true);
@synopsis{Serializes a value as a JSON string and stores it as a string}
@description{
This function uses `writeJSON` and stores the result in a string.
}
java str asJSON(value val, bool unpackedLocations=false, str dateTimeFormat="yyyy-MM-dd\'T\'HH:mm:ssZZZZZ", bool dateTimeAsInt=false, int indent = 0, bool dropOrigins=true, bool explicitConstructorNames=false, bool explicitDataTypes=false);
102 changes: 93 additions & 9 deletions src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.rascalmpl.debug.IRascalMonitor;
import org.rascalmpl.uri.URIUtil;
import io.usethesource.vallang.IInteger;
Expand Down Expand Up @@ -59,6 +58,8 @@
private VarHandle posHandler;
private VarHandle lineHandler;
private VarHandle lineStartHandler;
private boolean explicitConstructorNames;
private boolean explicitDataTypes;

/**
* @param vf factory which will be used to construct values
Expand Down Expand Up @@ -105,6 +106,20 @@
return this;
}

public JsonValueReader setExplicitConstructorNames(boolean value) {
this.explicitConstructorNames = value;
return this;
}

public JsonValueReader setExplicitDataTypes(boolean value) {
this.explicitDataTypes = value;
if (value) {
this.explicitConstructorNames = true;
}
return this;
}


/**
* Read and validate a Json stream as an IValue
* @param in json stream
Expand Down Expand Up @@ -477,7 +492,6 @@
}
}


@Override
public IValue visitAbstractData(Type type) throws IOException {
if (in.peek() == JsonToken.STRING) {
Expand All @@ -493,18 +507,75 @@
throw new IOException("no nullary constructor found for " + type);
}

assert in.peek() == JsonToken.BEGIN_OBJECT;
Set<Type> alternatives = null;

in.beginObject();
int startPos = getPos();
int startLine = getLine();
int startCol = getCol();

// use explicit information in the JSON to select and filter constructors from the TypeStore
// we expect always to have the field _constructor before _type.
if (explicitConstructorNames || explicitDataTypes) {
String consName = null;
String typeName = null; // this one is optional, and the order with cons is not defined.

String consLabel = in.nextName();

// first we read either a cons name or a type name
if (explicitConstructorNames && "_constructor".equals(consLabel)) {
consName = in.nextString();
}
else if (explicitDataTypes && "_type".equals(consLabel)) {
typeName = in.nextString();
}

// optionally read the second field
if (explicitDataTypes && typeName == null) {
// we've read a constructor name, but we still need a type name
consLabel = in.nextName();
if (explicitDataTypes && "_type".equals(consLabel)) {
typeName = in.nextString();
}
}
else if (explicitDataTypes && consName == null) {
// we've read type name, but we still need a constructor name
consLabel = in.nextName();
if (explicitDataTypes && "_constructor".equals(consLabel)) {
consName = in.nextString();
}
}

if (explicitDataTypes && typeName == null) {
throw new IOException("Missing a _type field: " + in.getPath());

Check warning on line 550 in src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java

View check run for this annotation

Codecov / codecov/patch

src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java#L550

Added line #L550 was not covered by tests
}
else if (explicitConstructorNames && consName == null) {
throw new IOException("Missing a _constructor field: " + in.getPath());

Check warning on line 553 in src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java

View check run for this annotation

Codecov / codecov/patch

src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java#L553

Added line #L553 was not covered by tests
}

if (typeName != null && consName != null) {
// first focus on the given type name
var dataType = TF.abstractDataType(store, typeName);
alternatives = store.lookupConstructor(dataType, consName);
}
else {
// we only have a constructor name
// lookup over all data types by constructor name
alternatives = store.lookupConstructors(consName);
}
}
else {
alternatives = store.lookupAlternatives(type);
}

Set<Type> alternatives = store.lookupAlternatives(type);
if (alternatives.size() > 1) {
monitor.warning("selecting arbitrary constructor for " + type, vf.sourceLocation(in.getPath()));
}
else if (alternatives.size() == 0) {
throw new IOException("No fitting constructor found for " + in.getPath());

Check warning on line 575 in src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java

View check run for this annotation

Codecov / codecov/patch

src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java#L575

Added line #L575 was not covered by tests
}
Type cons = alternatives.iterator().next();

in.beginObject();
int startPos = getPos();
int startLine = getLine();
int startCol = getCol();

IValue[] args = new IValue[cons.getArity()];
Map<String,IValue> kwParams = new HashMap<>();
Expand Down Expand Up @@ -532,7 +603,20 @@
}
}
else { // its a normal arg, pass its label to the child
throw new IOException("Unknown field " + label + ":" + in.getPath());
if (!explicitConstructorNames && "_constructor".equals(label)) {
// ignore additional _constructor fields.
in.nextString(); // skip the constructor value
continue;
}
else if (!explicitDataTypes && "_type".equals(label)) {
// ignore additional _type fields.
in.nextString(); // skip the type value
continue;
}
else {
// field label does not match data type definition
throw new IOException("Unknown field " + label + ":" + in.getPath());

Check warning on line 618 in src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java

View check run for this annotation

Codecov / codecov/patch

src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java#L618

Added line #L618 was not covered by tests
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public class JsonValueWriter {
private boolean datesAsInts = true;
private boolean unpackedLocations = false;
private boolean dropOrigins = true;
private boolean explicitConstructorNames = false;
private boolean explicitDataTypes;

public JsonValueWriter() {
setCalendarFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
Expand Down Expand Up @@ -78,6 +80,16 @@ public JsonValueWriter setDropOrigins(boolean setting) {
return this;
}

public JsonValueWriter setExplicitConstructorNames(boolean setting) {
this.explicitConstructorNames = setting;
return this;
}

public JsonValueWriter setExplicitDataTypes(boolean setting) {
this.explicitDataTypes = setting;
return this;
}

public void write(JsonWriter out, IValue value) throws IOException {
value.accept(new IValueVisitor<Void, IOException>() {

Expand Down Expand Up @@ -222,13 +234,24 @@ public Void visitNode(INode o) throws IOException {

@Override
public Void visitConstructor(IConstructor o) throws IOException {
if (o.getConstructorType().getArity() == 0 && !o.asWithKeywordParameters().hasParameters()) {
if (!explicitConstructorNames && !explicitDataTypes && o.getConstructorType().getArity() == 0 && !o.asWithKeywordParameters().hasParameters()) {
// enums!
out.value(o.getName());
return null;
}

out.beginObject();

if (explicitConstructorNames || explicitDataTypes) {
out.name("_constructor");
out.value(o.getName());
}

if (explicitDataTypes) {
out.name("_type");
out.value(o.getType().getName());
}

int i = 0;
for (IValue arg : o) {
out.name(o.getConstructorType().getFieldName(i));
Expand Down
Loading
Loading