LT-30: Add initial codegen for reading from JSON-LF format into Java Codegen objects (#17367)

This is not complete, but gets it far enough that once can start playing around with the feature.

done in this PR
- pass the actual `JsonLfReader` in as the final arg to `decode`, and it can then be threaded through all the sub-decoders
- renamed `FromJson` to `JsonLfDecoder` to more better match the existing `ValueDecoder`
- make the non-generic decoders simple fields rather than nullary methods
- support variants with simple type args, as well as with their own records
- add a `T fromJson(String)` to all relevant types, as the main user-facing method, rather than just `JsonLfDecoder<T> jsonDecoder()`.

to be done:
- complete testing of different combinations of types, including nested optionals
- `JsonLfEncoder`, and round-trip testing
- alternative handling of missing and unknown fields
- capability to decode from an in-memory JSON object, for frameworks where the original JSON has already been decoded and the object embedded by the time you get access to it.
This commit is contained in:
Raphael Speyer 2023-09-18 09:27:20 +10:00 committed by GitHub
parent 78487e18a8
commit 49b2a472da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 841 additions and 446 deletions

View File

@ -7,6 +7,7 @@ import com.daml.ledger.javaapi.data.CreatedEvent;
import com.daml.ledger.javaapi.data.DamlRecord;
import com.daml.ledger.javaapi.data.Identifier;
import com.daml.ledger.javaapi.data.Value;
import com.daml.ledger.javaapi.data.codegen.json.JsonLfDecoder;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@ -34,6 +35,13 @@ public abstract class ContractCompanion<Ct, Id, Data>
/** @hidden */
protected final Function<DamlRecord, Data> fromValue;
@FunctionalInterface // Defines the function type which throws.
public static interface FromJson<T> {
T decode(String s) throws JsonLfDecoder.Error;
}
protected final FromJson<Data> fromJson;
/**
* Static method to generate an implementation of {@code ValueDecoder} of type {@code Data} with
* metadata from the provided {@code ContractCompanion}.
@ -74,9 +82,15 @@ public abstract class ContractCompanion<Ct, Id, Data>
Identifier templateId,
Function<String, Id> newContractId,
Function<DamlRecord, Data> fromValue,
FromJson<Data> fromJson,
List<Choice<Data, ?, ?>> choices) {
super(templateId, templateClassName, newContractId, choices);
this.fromValue = fromValue;
this.fromJson = fromJson;
}
public Data fromJson(String json) throws JsonLfDecoder.Error {
return this.fromJson.decode(json);
}
public static final class WithoutKey<Ct, Id, Data> extends ContractCompanion<Ct, Id, Data> {
@ -96,9 +110,10 @@ public abstract class ContractCompanion<Ct, Id, Data>
Identifier templateId,
Function<String, Id> newContractId,
Function<DamlRecord, Data> fromValue,
FromJson<Data> fromJson,
NewContract<Ct, Id, Data> newContract,
List<Choice<Data, ?, ?>> choices) {
super(templateClassName, templateId, newContractId, fromValue, choices);
super(templateClassName, templateId, newContractId, fromValue, fromJson, choices);
this.newContract = newContract;
}
@ -153,10 +168,11 @@ public abstract class ContractCompanion<Ct, Id, Data>
Identifier templateId,
Function<String, Id> newContractId,
Function<DamlRecord, Data> fromValue,
FromJson<Data> fromJson,
NewContract<Ct, Id, Data, Key> newContract,
List<Choice<Data, ?, ?>> choices,
Function<Value, Key> keyFromValue) {
super(templateClassName, templateId, newContractId, fromValue, choices);
super(templateClassName, templateId, newContractId, fromValue, fromJson, choices);
this.newContract = newContract;
this.keyFromValue = keyFromValue;
}

View File

@ -1,24 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.ledger.javaapi.data.codegen.json;
import java.io.IOException;
// Functional interface, that can be used to either read a value of the given type directly
// (using .read(r)), or can be used to build a FromJson for types with generic arguments,
// to tell them how to read that argument type.
//
// e.g.
// String str = JsonLfReader.text.read(reader);
// or
// List<String> = JsonLfReader.list(JsonLfReader.text).read(reader);
public interface FromJson<T> {
public T read(JsonLfReader r) throws Error;
public static class Error extends IOException {
public Error(String msg) {
super(msg);
}
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.ledger.javaapi.data.codegen.json;
import java.io.IOException;
@FunctionalInterface
public interface JsonLfDecoder<T> {
public T decode(JsonLfReader r) throws Error;
public static class Error extends IOException {
public Error(String message) {
super(message);
}
public Error(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@ -15,242 +15,353 @@ import java.time.Instant;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.function.Function;
// Utility to read LF-JSON data in a streaming fashion. Can be used by code-gen.
// Reads LF-JSON data in a streaming fashion.
// Usage for these is simply to construct them, and then pass them into a decoder
// which can attempt decode the appropriate type.
// Can be used by code-gen.
public class JsonLfReader {
private final String json; // Used to reference unknown values until they can be decoded.
private static final JsonFactory jsonFactory = new JsonFactory();
private final JsonParser parser;
public JsonLfReader(String json) throws IOException {
public JsonLfReader(String json) throws JsonLfDecoder.Error {
this.json = json;
parser = jsonFactory.createParser(json);
parser.nextToken();
}
// Can override these two for different handling of these cases.
protected void missingField(Object obj, String fieldName) throws FromJson.Error {
throw new FromJson.Error(
String.format(
"Missing field %s.%s at %s", obj.getClass().getCanonicalName(), fieldName, location()));
}
protected void unknownFields(Object obj, List<String> fieldNames) throws FromJson.Error {
throw new FromJson.Error(
String.format(
"Unknown fields %s.%s at %s",
obj.getClass().getCanonicalName(), fieldNames.toString(), location()));
}
private void parseExpected(String expected) throws FromJson.Error {
throw new FromJson.Error(
String.format("Expected %s but was %s at %s", expected, currentText(), location()));
try {
parser = jsonFactory.createParser(json);
parser.nextToken();
} catch (IOException e) {
throw new JsonLfDecoder.Error("initialization error", e);
}
}
/// Readers for built-in LF types. ///
public static class Decoders {
public static final FromJson<Unit> unit =
r -> {
public static final JsonLfDecoder<Unit> unit =
r -> {
r.readStartObject();
r.readEndObject();
return Unit.getInstance();
};
public static final JsonLfDecoder<Boolean> bool =
r -> {
r.expectIsAt("boolean", JsonToken.VALUE_TRUE, JsonToken.VALUE_FALSE);
Boolean value = null;
try {
value = r.parser.getBooleanValue();
} catch (IOException e) {
r.parseExpected("true or false", e);
}
r.moveNext();
return value;
};
public static final JsonLfDecoder<Long> int64 =
r -> {
r.expectIsAt("int64", JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_STRING);
Long value = null;
try {
value = Long.parseLong(r.parser.getText());
} catch (IOException e) {
r.parseExpected("int64", e);
} catch (NumberFormatException e) {
r.parseExpected("int64", e);
}
r.moveNext();
return value;
};
public static final JsonLfDecoder<BigDecimal> decimal =
r -> {
r.expectIsAt(
"decimal",
JsonToken.VALUE_NUMBER_INT,
JsonToken.VALUE_NUMBER_FLOAT,
JsonToken.VALUE_STRING);
BigDecimal value = null;
try {
value = new BigDecimal(r.parser.getText());
} catch (NumberFormatException e) {
r.parseExpected("decimal", e);
} catch (IOException e) {
r.parseExpected("decimal", e);
}
r.moveNext();
return value;
};
public static final JsonLfDecoder<Instant> timestamp =
r -> {
r.expectIsAt("timestamp", JsonToken.VALUE_STRING);
Instant value = null;
try {
value = Instant.parse(r.parser.getText());
} catch (DateTimeParseException e) {
r.parseExpected("timestamp", e);
} catch (IOException e) {
r.parseExpected("timestamp", e);
}
r.moveNext();
return value;
};
public static final JsonLfDecoder<LocalDate> date =
r -> {
r.expectIsAt("date", JsonToken.VALUE_STRING);
LocalDate value = null;
try {
value = LocalDate.parse(r.parser.getText());
} catch (DateTimeParseException e) {
r.parseExpected("date", e);
} catch (IOException e) {
r.parseExpected("date", e);
}
r.moveNext();
return value;
};
public static final JsonLfDecoder<String> text =
r -> {
r.expectIsAt("text", JsonToken.VALUE_STRING);
String value = null;
try {
value = r.parser.getText();
} catch (IOException e) {
r.parseExpected("valid textual value", e);
}
r.moveNext();
return value;
};
public static final JsonLfDecoder<String> party = text;
public static <C extends ContractId<?>> JsonLfDecoder<C> contractId(
Function<String, C> constr) {
return r -> {
String id = text.decode(r);
return constr.apply(id);
};
}
// Read an list with an unknown number of items of the same type.
public static <T> JsonLfDecoder<List<T>> list(JsonLfDecoder<T> decodeItem) {
return r -> {
List<T> list = new ArrayList<>();
r.readStartArray();
while (r.notEndArray()) {
T item = decodeItem.decode(r);
list.add(item);
}
r.readEndArray();
return list;
};
}
// Read a map with textual keys, and unknown number of items of the same type.
public static <V> JsonLfDecoder<Map<String, V>> textMap(JsonLfDecoder<V> decodeValue) {
return r -> {
Map<String, V> map = new LinkedHashMap<>();
r.readStartObject();
while (r.notEndObject()) {
String key = r.readFieldName();
V val = decodeValue.decode(r);
map.put(key, val);
}
r.readEndObject();
return Unit.getInstance();
return map;
};
}
public static final FromJson<Boolean> bool =
r -> {
r.expectIsAt("boolean", JsonToken.VALUE_TRUE, JsonToken.VALUE_FALSE);
Boolean value = null;
try {
value = r.parser.getBooleanValue();
} catch (IOException e) {
r.parseExpected("true or false");
}
r.moveNext();
return value;
};
public static final FromJson<Long> int64 =
r -> {
r.expectIsAt("int64", JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_STRING);
Long value = null;
try {
value = Long.parseLong(r.parser.getText());
} catch (IOException e) {
r.parseExpected("int64");
} catch (NumberFormatException e) {
r.parseExpected("int64");
}
r.moveNext();
return value;
};
public static final FromJson<BigDecimal> decimal =
r -> {
r.expectIsAt(
"decimal",
JsonToken.VALUE_NUMBER_INT,
JsonToken.VALUE_NUMBER_FLOAT,
JsonToken.VALUE_STRING);
BigDecimal value = null;
try {
value = new BigDecimal(r.parser.getText());
} catch (NumberFormatException e) {
r.parseExpected("decimal");
} catch (IOException e) {
r.parseExpected("decimal");
}
r.moveNext();
return value;
};
public static final FromJson<Instant> timestamp =
r -> {
r.expectIsAt("timestamp", JsonToken.VALUE_STRING);
Instant value = null;
try {
value = Instant.parse(r.parser.getText());
} catch (DateTimeParseException e) {
r.parseExpected("timestamp");
} catch (IOException e) {
r.parseExpected("timestamp");
}
r.moveNext();
return value;
};
public static final FromJson<LocalDate> date =
r -> {
r.expectIsAt("date", JsonToken.VALUE_STRING);
LocalDate value = null;
try {
value = LocalDate.parse(r.parser.getText());
} catch (DateTimeParseException e) {
r.parseExpected("date");
} catch (IOException e) {
r.parseExpected("date");
}
r.moveNext();
return value;
};
public static final FromJson<String> text =
r -> {
r.expectIsAt("text", JsonToken.VALUE_STRING);
String value = null;
try {
value = r.parser.getText();
} catch (IOException e) {
r.parseExpected("valid textual value");
}
r.moveNext();
return value;
};
public static final FromJson<String> party = text;
public static <C extends ContractId<?>> FromJson<C> contractId(Function<String, C> constr) {
return r -> {
String id = text.read(r);
return constr.apply(id);
};
}
// Read an list with an unknown number of items of the same type.
public static <T> FromJson<List<T>> list(FromJson<T> readItem) {
return r -> {
List<T> list = new ArrayList<>();
r.readStartArray();
while (r.notEndArray()) {
T item = readItem.read(r);
list.add(item);
}
r.readEndArray();
return list;
};
}
// Read a map with textual keys, and unknown number of items of the same type.
public static <V> FromJson<Map<String, V>> textMap(FromJson<V> readValue) {
return r -> {
Map<String, V> map = new TreeMap<>();
r.readStartObject();
while (r.notEndObject()) {
String key = r.readFieldName();
V val = readValue.read(r);
map.put(key, val);
}
r.readEndObject();
return map;
};
}
// Read a map with unknown number of items of the same types.
public static <K, V> FromJson<Map<K, V>> genMap(FromJson<K> readKey, FromJson<V> readValue) {
return r -> {
Map<K, V> map = new TreeMap<>();
// Maps are represented as an array of 2-element arrays.
r.readStartArray();
while (r.notEndArray()) {
// Read a map with unknown number of items of the same types.
public static <K, V> JsonLfDecoder<Map<K, V>> genMap(
JsonLfDecoder<K> decodeKey, JsonLfDecoder<V> decodeVal) {
return r -> {
Map<K, V> map = new LinkedHashMap<>();
// Maps are represented as an array of 2-element arrays.
r.readStartArray();
K key = readKey.read(r);
V val = readValue.read(r);
r.readEndArray();
map.put(key, val);
}
r.readEndArray();
return map;
};
}
// The T type should not itself be Optional<?>. In that case use OptionalNested below.
public static <T> FromJson<Optional<T>> optional(FromJson<T> readValue) {
return r -> {
if (r.parser.currentToken() == JsonToken.VALUE_NULL) {
r.moveNext();
return Optional.empty();
} else {
T some = readValue.read(r);
if (some instanceof Optional) {
throw new IllegalArgumentException(
"Used `optional` to decode a "
+ some.getClass()
+ " but `optionalNested` must be used for the outer decoders of nested Optional");
while (r.notEndArray()) {
r.readStartArray();
K key = decodeKey.decode(r);
V val = decodeVal.decode(r);
r.readEndArray();
map.put(key, val);
}
return Optional.of(some);
}
};
}
public static <T> FromJson<Optional<Optional<T>>> optionalNested(
FromJson<Optional<T>> readValue) {
return r -> {
if (r.parser.currentToken() == JsonToken.VALUE_NULL) {
r.moveNext();
return Optional.empty();
} else {
r.readStartArray();
Optional<T> val = r.notEndArray() ? readValue.read(r) : Optional.empty();
r.readEndArray();
return Optional.of(val);
}
};
}
return map;
};
}
public static <E extends Enum<E>> FromJson<E> enumeration(Class<E> enumClass) {
return r -> {
String value = text.read(r);
try {
return Enum.valueOf(enumClass, value);
} catch (IllegalArgumentException e) {
r.parseExpected(String.format("constant of %s", enumClass.getName()));
// The T type should not itself be Optional<?>. In that case use OptionalNested below.
public static <T> JsonLfDecoder<Optional<T>> optional(JsonLfDecoder<T> decodeVal) {
return r -> {
if (r.parser.currentToken() == JsonToken.VALUE_NULL) {
r.moveNext();
return Optional.empty();
} else {
T some = decodeVal.decode(r);
if (some instanceof Optional) {
throw new IllegalArgumentException(
"Used `optional` to decode a "
+ some.getClass()
+ " but `optionalNested` must be used for the outer decoders of nested"
+ " Optional");
}
return Optional.of(some);
}
};
}
public static <T> JsonLfDecoder<Optional<Optional<T>>> optionalNested(
JsonLfDecoder<Optional<T>> decodeVal) {
return r -> {
if (r.parser.currentToken() == JsonToken.VALUE_NULL) {
r.moveNext();
return Optional.empty();
} else {
r.readStartArray();
Optional<T> val = r.notEndArray() ? decodeVal.decode(r) : Optional.empty();
r.readEndArray();
return Optional.of(val);
}
};
}
public static <E extends Enum<E>> JsonLfDecoder<E> enumeration(Map<String, E> damlNameToEnum) {
return r -> {
String name = text.decode(r);
E value = damlNameToEnum.get(name);
if (value == null) r.parseExpected(String.format("one of %s", damlNameToEnum.keySet()));
return value;
};
}
// Provides a generic way to read a variant type, by specifying each tag.
public static <T> JsonLfDecoder<T> variant(
List<String> tagNames, Function<String, JsonLfDecoder<? extends T>> decoderByName) {
return r -> {
r.readStartObject();
T result = null;
switch (r.readFieldName()) {
case "tag":
{
String tagName = text.decode(r);
if (!r.readFieldName().equals("value")) r.parseExpected("value field");
result = decoderByName.apply(tagName).decode(r);
break;
}
case "value":
{
UnknownValue unknown = UnknownValue.read(r);
if (!r.readFieldName().equals("tag")) r.parseExpected("tag field");
String tagName = text.decode(r);
result = unknown.decodeWith(decoderByName.apply(tagName));
break;
}
default:
r.parseExpected("tag or value");
break;
}
r.readEndObject();
if (result == null) r.parseExpected(String.format("tag %s", String.join(" or ", tagNames)));
return result;
};
}
// Provides a generic way to read a record type, with a constructor arg for each field.
// This is a little fragile, so is better used by code-gen. Specifically:
// - The constructor must cast the elements and pass them to the T's constructor appropriately.
// - The elements of fieldNames should all evaluate to non non-null when applied to
// fieldsByName.
// - The argIndex field values should correspond to the args passed to the constructor.
//
// e.g.
// r.record(
// asList("i", "b"),
// name -> {
// switch (name) {
// case "i":
// return JsonLfReader.Field.at(0, r.list(r.int64()));
// case "b":
// return JsonLfReader.Field.at(1, r.bool(), false);
// default:
// return null;
// }
// },
// args -> new Foo((List<Long>) args[0], (Boolean) args[1]))
// )
public static <T> JsonLfDecoder<T> record(
List<String> fieldNames,
Function<String, Field<? extends Object>> fieldsByName,
Function<Object[], T> constr) {
return r -> {
List<String> missingFields = new ArrayList<>();
List<String> unknownFields = new ArrayList<>();
Object[] args = new Object[fieldNames.size()];
if (r.isStartObject()) {
r.readStartObject();
while (r.notEndObject()) {
String fieldName = r.readFieldName();
var field = fieldsByName.apply(fieldName);
if (field == null) r.unknownField(fieldName);
else args[field.argIndex] = field.decode.decode(r);
}
r.readEndObject();
} else if (r.isStartArray()) {
r.readStartArray();
for (String fieldName : fieldNames) {
var field = fieldsByName.apply(fieldName);
args[field.argIndex] = field.decode.decode(r);
}
r.readEndArray();
} else {
r.parseExpected("object or array");
}
// Handle missing fields.
for (String fieldName : fieldNames) {
Field<? extends Object> field = fieldsByName.apply(fieldName);
if (args[field.argIndex] != null) continue;
if (field.defaultVal == null) r.missingField(fieldName);
args[field.argIndex] = field.defaultVal;
}
return constr.apply(args);
};
}
public static class Field<T> {
final int argIndex;
final JsonLfDecoder<T> decode;
final T defaultVal; // If non-null, used to populate value of missing fields.
private Field(int argIndex, JsonLfDecoder<T> decode, T defaultVal) {
this.argIndex = argIndex;
this.decode = decode;
this.defaultVal = defaultVal;
}
return null;
};
public static <T> Field<T> at(int argIndex, JsonLfDecoder<T> decode, T defaultVal) {
return new Field<T>(argIndex, decode, defaultVal);
}
public static <T> Field<T> at(int argIndex, JsonLfDecoder<T> decode) {
return new Field<T>(argIndex, decode, null);
}
}
@SuppressWarnings("unchecked")
// Can be used within the `constr` arg to `record`, to allow casting without producing warnings.
public static <T> T cast(Object o) {
return (T) o;
}
}
// Represents a value whose type is not yet known, but should be preserved for later decoding.
@ -263,7 +374,7 @@ public class JsonLfReader {
this.start = start;
}
public static UnknownValue read(JsonLfReader r) throws FromJson.Error {
public static UnknownValue read(JsonLfReader r) throws JsonLfDecoder.Error {
JsonLocation from = r.parser.currentTokenLocation();
try {
r.parser.skipChildren();
@ -273,140 +384,31 @@ public class JsonLfReader {
if (repr.endsWith(",")) repr = repr.substring(0, repr.length() - 1); // drop trailing comma
return new UnknownValue(repr, from);
} catch (IOException e) {
throw new FromJson.Error("cannot read unknown value: " + e);
throw new JsonLfDecoder.Error("cannot read unknown value", e);
}
}
public <T> T decodeWith(FromJson<T> decoder) throws FromJson.Error {
public <T> T decodeWith(JsonLfDecoder<T> decoder) throws JsonLfDecoder.Error {
try {
return decoder.read(new JsonLfReader(this.jsonRepr));
return decoder.decode(new JsonLfReader(this.jsonRepr));
// TODO(raphael-speyer-da): fix the location on parse errors by adding start offset, e.g.
// catch (FromJson.Error e) { throw new FromJson.Error(e.message, add(start, e.location)); }
// catch (JsonLfDecoder.Error e) { throw new JsonLfDecoder.Error(e.message, add(start,
// e.location)); }
} catch (IOException e) {
throw new FromJson.Error(
String.format("cannot decode unknown value '%s': %s", this.jsonRepr, e));
throw new JsonLfDecoder.Error(
String.format("cannot decode unknown value '%s'", this.jsonRepr), e);
}
}
}
// Provides a generic way to read a variant type, by specifying each tag.
public static <T> FromJson<T> variant(List<String> tagNames, TagReader<T> readTag) {
return r -> {
r.readStartObject();
T result = null;
switch (r.readFieldName()) {
case "tag":
{
String tagName = text.read(r);
if (!r.readFieldName().equals("value")) r.parseExpected("value field");
result = readTag.get(tagName).read(r);
break;
}
case "value":
{
UnknownValue unknown = UnknownValue.read(r);
if (!r.readFieldName().equals("tag")) r.parseExpected("tag field");
String tagName = text.read(r);
result = unknown.decodeWith(readTag.get(tagName));
break;
}
default:
r.parseExpected("tag or value");
break;
}
r.readEndObject();
if (result == null) r.parseExpected(String.format("tag %s", String.join(" or ", tagNames)));
return result;
};
// Can override these two for different handling of these cases.
protected void missingField(String fieldName) throws JsonLfDecoder.Error {
throw new JsonLfDecoder.Error(String.format("Missing %s at %s", fieldName, location()));
}
public static interface TagReader<T> {
FromJson<T> get(String tagName) throws FromJson.Error;
}
// Provides a generic way to read a record type, with a constructor arg for each field.
// This is a little fragile, so is better used by code-gen. Specifically:
// - The constructor must cast the elements and pass them to the T's constructor appropriately.
// - The elements of fieldNames should all evaluate to non non-null when applied to fieldsByName.
// - The argIndex field values should correspond to the args passed to the constructor.
//
// e.g.
// r.record(
// args -> new Foo((Long) args[0], (Boolean) args[1]),
// asList("i", "b"),
// fieldName -> {
// switch (fieldName) {
// case "i":
// return JsonLfReader.Field.required(0, r.int64());
// case "b":
// return JsonLfReader.Field.optional(1, r.bool(), false);
// default:
// return null;
// }
// }
// )
public static <T> FromJson<T> record(
Function<Object[], T> constr,
List<String> fieldNames,
Function<String, Field<? extends Object>> fieldsByName) {
return r -> {
List<String> missingFields = new ArrayList<>();
List<String> unknownFields = new ArrayList<>();
Object[] args = new Object[fieldNames.size()];
if (r.isStartObject()) {
r.readStartObject();
while (r.notEndObject()) {
String fieldName = r.readFieldName();
var field = fieldsByName.apply(fieldName);
if (field == null) unknownFields.add(fieldName);
else args[field.argIndex] = field.fromJson.read(r);
}
r.readEndObject();
} else if (r.isStartArray()) {
r.readStartArray();
for (String fieldName : fieldNames) {
var field = fieldsByName.apply(fieldName);
args[field.argIndex] = field.fromJson.read(r);
}
r.readEndArray();
} else {
r.parseExpected("object or array");
}
// Handle missing and unknown fields.
for (String fieldName : fieldNames) {
Field<? extends Object> field = fieldsByName.apply(fieldName);
if (args[field.argIndex] != null) continue;
if (field.defaultVal == null) missingFields.add(fieldName);
args[field.argIndex] = field.defaultVal;
}
T result = constr.apply(args);
for (String f : missingFields) r.missingField(result, f);
if (!unknownFields.isEmpty()) r.unknownFields(result, unknownFields);
return result;
};
}
public static class Field<T> {
final int argIndex;
final FromJson<T> fromJson;
final T defaultVal; // If non-null, used to populate value of missing fields.
private Field(int argIndex, FromJson<T> fromJson, T defaultVal) {
this.argIndex = argIndex;
this.fromJson = fromJson;
this.defaultVal = defaultVal;
}
public static <T> Field<T> optional(int argIndex, FromJson<T> fromJson, T defaultVal) {
return new Field<T>(argIndex, fromJson, defaultVal);
}
public static <T> Field<T> required(int argIndex, FromJson<T> fromJson) {
return new Field<T>(argIndex, fromJson, null);
}
protected void unknownField(String fieldName) throws JsonLfDecoder.Error {
UnknownValue.read(this); // Consume the value from the reader.
throw new JsonLfDecoder.Error(String.format("Unknown %s at %s", fieldName, location()));
}
/// Used for branching and looping on objects and arrays. ///
@ -429,33 +431,33 @@ public class JsonLfReader {
/// Used for consuming the structural components of objects and arrays. ///
private void readStartObject() throws FromJson.Error {
private void readStartObject() throws JsonLfDecoder.Error {
expectIsAt("{", JsonToken.START_OBJECT);
moveNext();
}
private void readEndObject() throws FromJson.Error {
private void readEndObject() throws JsonLfDecoder.Error {
expectIsAt("}", JsonToken.END_OBJECT);
moveNext();
}
private void readStartArray() throws FromJson.Error {
private void readStartArray() throws JsonLfDecoder.Error {
expectIsAt("[", JsonToken.START_ARRAY);
moveNext();
}
private void readEndArray() throws FromJson.Error {
private void readEndArray() throws JsonLfDecoder.Error {
expectIsAt("]", JsonToken.END_ARRAY);
moveNext();
}
private String readFieldName() throws FromJson.Error {
private String readFieldName() throws JsonLfDecoder.Error {
expectIsAt("field name", JsonToken.FIELD_NAME);
String fieldName = null;
try {
fieldName = parser.getText();
} catch (IOException e) {
parseExpected("textual field name");
parseExpected("textual field name", e);
}
moveNext();
return fieldName;
@ -473,18 +475,30 @@ public class JsonLfReader {
}
}
private void expectIsAt(String description, JsonToken... expected) throws FromJson.Error {
private void parseExpected(String expected) throws JsonLfDecoder.Error {
parseExpected(expected, null);
}
private void parseExpected(String expected, Throwable cause) throws JsonLfDecoder.Error {
String message =
String.format("Expected %s but was %s at %s", expected, currentText(), location());
throw (cause == null
? new JsonLfDecoder.Error(message)
: new JsonLfDecoder.Error(message, cause));
}
private void expectIsAt(String description, JsonToken... expected) throws JsonLfDecoder.Error {
for (int i = 0; i < expected.length; i++) {
if (parser.currentToken() == expected[i]) return;
}
parseExpected(description);
}
private void moveNext() throws FromJson.Error {
private void moveNext() throws JsonLfDecoder.Error {
try {
parser.nextToken();
} catch (IOException e) {
parseExpected("more input");
parseExpected("more input", e);
}
}
}

View File

@ -9,6 +9,7 @@ import static java.util.Collections.emptyMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.daml.ledger.javaapi.data.Unit;
import com.daml.ledger.javaapi.data.codegen.json.JsonLfReader.Decoders;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.Instant;
@ -16,7 +17,9 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
@ -29,19 +32,18 @@ public class JsonLfReaderTest {
@Test
void testUnit() throws IOException {
checkReadAll(
JsonLfReader.unit, eq("{}", Unit.getInstance()), eq("\t{\n} ", Unit.getInstance()));
checkReadAll(Decoders.unit, eq("{}", Unit.getInstance()), eq("\t{\n} ", Unit.getInstance()));
}
@Test
void testBool() throws IOException {
checkReadAll(JsonLfReader.bool, eq("false", false), eq("true", true));
checkReadAll(Decoders.bool, eq("false", false), eq("true", true));
}
@Test
void testInt64() throws IOException {
checkReadAll(
JsonLfReader.int64,
Decoders.int64,
eq("42", 42L),
eq("\"+42\"", 42L),
eq("-42", -42L),
@ -56,7 +58,7 @@ public class JsonLfReaderTest {
@Test
void testDecimal() throws IOException {
checkReadAll(
JsonLfReader.decimal,
Decoders.decimal,
cmpEq("42", dec("42")),
cmpEq("42.0", dec("42")),
cmpEq("\"42\"", dec("42")),
@ -75,7 +77,7 @@ public class JsonLfReaderTest {
@Test
void testTimestamp() throws IOException {
checkReadAll(
JsonLfReader.timestamp,
Decoders.timestamp,
eq(
"\"1990-11-09T04:30:23.123456Z\"",
timestampUTC(1990, Month.NOVEMBER, 9, 4, 30, 23, 123456)),
@ -94,7 +96,7 @@ public class JsonLfReaderTest {
@Test
void testDate() throws IOException {
checkReadAll(
JsonLfReader.date,
Decoders.date,
eq("\"2019-06-18\"", date(2019, Month.JUNE, 18)),
eq("\"9999-12-31\"", date(9999, Month.DECEMBER, 31)),
eq("\"0001-01-01\"", date(1, Month.JANUARY, 1)));
@ -102,40 +104,38 @@ public class JsonLfReaderTest {
@Test
void testParty() throws IOException {
checkReadAll(JsonLfReader.party, eq("\"Alice\"", "Alice"));
checkReadAll(Decoders.party, eq("\"Alice\"", "Alice"));
}
@Test
void testText() throws IOException {
checkReadAll(JsonLfReader.text, eq("\"\"", ""), eq("\" \"", " "), eq("\"hello\"", "hello"));
checkReadAll(Decoders.text, eq("\"\"", ""), eq("\" \"", " "), eq("\"hello\"", "hello"));
}
@Test
void testContractId() throws IOException {
checkReadAll(
JsonLfReader.contractId(Tmpl.Cid::new), eq("\"deadbeef\"", new Tmpl.Cid("deadbeef")));
checkReadAll(Decoders.contractId(Tmpl.Cid::new), eq("\"deadbeef\"", new Tmpl.Cid("deadbeef")));
}
@Test
void testEnum() throws IOException {
checkReadAll(
JsonLfReader.enumeration(Suit.class),
eq("\"Hearts\"", Suit.Hearts),
eq("\"Diamonds\"", Suit.Diamonds),
eq("\"Clubs\"", Suit.Clubs),
eq("\"Spades\"", Suit.Spades));
Decoders.enumeration(Suit.damlNames),
eq("\"Hearts\"", Suit.HEARTS),
eq("\"Diamonds\"", Suit.DIAMONDS),
eq("\"Clubs\"", Suit.CLUBS),
eq("\"Spades\"", Suit.SPADES));
}
@Test
void testList() throws IOException {
checkReadAll(
JsonLfReader.list(JsonLfReader.int64), eq("[]", emptyList()), eq("[1,2]", asList(1L, 2L)));
checkReadAll(Decoders.list(Decoders.int64), eq("[]", emptyList()), eq("[1,2]", asList(1L, 2L)));
}
@Test
void testTextMap() throws IOException {
checkReadAll(
JsonLfReader.textMap(JsonLfReader.int64),
Decoders.textMap(Decoders.int64),
eq("{}", emptyMap()),
eq("{\"foo\":1, \"bar\": 2}", java.util.Map.of("foo", 1L, "bar", 2L)));
}
@ -143,7 +143,7 @@ public class JsonLfReaderTest {
@Test
void testGenMap() throws IOException {
checkReadAll(
JsonLfReader.genMap(JsonLfReader.text, JsonLfReader.int64),
Decoders.genMap(Decoders.text, Decoders.int64),
eq("[]", emptyMap()),
eq("[[\"foo\", 1], [\"bar\", 2]]", java.util.Map.of("foo", 1L, "bar", 2L)));
}
@ -151,7 +151,7 @@ public class JsonLfReaderTest {
@Test
void testOptionalNonNested() throws IOException {
checkReadAll(
JsonLfReader.optional(JsonLfReader.int64),
Decoders.optional(Decoders.int64),
eq("null", Optional.empty()),
eq("42", Optional.of(42L)));
}
@ -159,7 +159,7 @@ public class JsonLfReaderTest {
@Test
void testOptionalNested() throws IOException {
checkReadAll(
JsonLfReader.optionalNested(JsonLfReader.optional(JsonLfReader.int64)),
Decoders.optionalNested(Decoders.optional(Decoders.int64)),
eq("null", Optional.empty()),
eq("[]", Optional.of(Optional.empty())),
eq("[42]", Optional.of(Optional.of(42L))));
@ -168,8 +168,7 @@ public class JsonLfReaderTest {
@Test
void testOptionalNestedDeeper() throws IOException {
checkReadAll(
JsonLfReader.optionalNested(
JsonLfReader.optionalNested(JsonLfReader.optional(JsonLfReader.int64))),
Decoders.optionalNested(Decoders.optionalNested(Decoders.optional(Decoders.int64))),
eq("null", Optional.empty()),
eq("[]", Optional.of(Optional.empty())),
eq("[[]]", Optional.of(Optional.of(Optional.empty()))),
@ -179,7 +178,7 @@ public class JsonLfReaderTest {
@Test
void testOptionalLists() throws IOException {
checkReadAll(
JsonLfReader.optional(JsonLfReader.list(JsonLfReader.list(JsonLfReader.int64))),
Decoders.optional(Decoders.list(Decoders.list(Decoders.int64))),
eq("null", Optional.empty()),
eq("[]", Optional.of(emptyList())),
eq("[[]]", Optional.of(asList(emptyList()))),
@ -187,21 +186,20 @@ public class JsonLfReaderTest {
}
@Test
void testVariant() throws IOException, FromJson.Error {
void testVariant() throws IOException, JsonLfDecoder.Error {
checkReadAll(
JsonLfReader.variant(
asList("Bar", "Baz", "Quux"),
Decoders.variant(
asList("Bar", "Baz", "Quux", "Flarp"),
tagName -> {
switch (tagName) {
case "Bar":
return r -> new SomeVariant.Bar(JsonLfReader.int64.read(r));
return r -> new SomeVariant.Bar(Decoders.int64.decode(r));
case "Baz":
return r -> new SomeVariant.Baz(JsonLfReader.unit.read(r));
return r -> new SomeVariant.Baz(Decoders.unit.decode(r));
case "Quux":
return r ->
new SomeVariant.Quux(JsonLfReader.optional(JsonLfReader.int64).read(r));
return r -> new SomeVariant.Quux(Decoders.optional(Decoders.int64).decode(r));
case "Flarp":
return r -> new SomeVariant.Flarp(JsonLfReader.list(JsonLfReader.int64).read(r));
return r -> new SomeVariant.Flarp(Decoders.list(Decoders.int64).decode(r));
default:
return null;
}
@ -217,23 +215,24 @@ public class JsonLfReaderTest {
@Test
void testRecord() throws IOException {
checkReadAll(
JsonLfReader.record(
args -> new SomeRecord((Long) args[0], (Boolean) args[1]),
Decoders.record(
asList("i", "b"),
fieldName -> {
switch (fieldName) {
name -> {
switch (name) {
case "i":
return JsonLfReader.Field.required(0, JsonLfReader.int64);
return Decoders.Field.at(0, Decoders.list(Decoders.int64));
case "b":
return JsonLfReader.Field.optional(1, JsonLfReader.bool, false);
return Decoders.Field.at(1, Decoders.bool, false);
default:
return null;
}
}),
eq("[1,true]", new SomeRecord(1L, true)),
eq("{\"i\":1,\"b\":true}", new SomeRecord(1L, true)),
eq("{\"b\":true,\"i\":1}", new SomeRecord(1L, true)),
eq("{\"i\":1}", new SomeRecord(1L, false)));
},
args -> new SomeRecord((List<Long>) args[0], (Boolean) args[1])),
eq("[[1],true]", new SomeRecord(asList(1L), true)),
eq("{\"i\":[],\"b\":true}", new SomeRecord(asList(), true)),
eq("{\"i\":[1,2],\"b\":true}", new SomeRecord(asList(1L, 2L), true)),
eq("{\"b\":true,\"i\":[1]}", new SomeRecord(asList(1L), true)),
eq("{\"i\":[1]}", new SomeRecord(asList(1L), false)));
}
private BigDecimal dec(String s) {
@ -251,10 +250,10 @@ public class JsonLfReaderTest {
}
class SomeRecord {
private final long i;
private final boolean b;
private final List<Long> i;
private final Boolean b;
public SomeRecord(long i, boolean b) {
public SomeRecord(List<Long> i, Boolean b) {
this.i = i;
this.b = b;
}
@ -267,8 +266,8 @@ public class JsonLfReaderTest {
public boolean equals(Object o) {
return o != null
&& (o instanceof SomeRecord)
&& ((SomeRecord) o).i == i
&& (((SomeRecord) o).b == b);
&& ((SomeRecord) o).i.equals(i)
&& (((SomeRecord) o).b.equals(b));
}
@Override
@ -380,15 +379,28 @@ public class JsonLfReaderTest {
}
enum Suit {
Hearts,
Diamonds,
Clubs,
Spades
HEARTS,
DIAMONDS,
CLUBS,
SPADES;
static final Map<String, Suit> damlNames =
new HashMap<>() {
{
put("Hearts", HEARTS);
put("Diamonds", DIAMONDS);
put("Clubs", CLUBS);
put("Spades", SPADES);
}
};
}
private <T> void checkReadAll(FromJson<T> readT, TestCase<T>... testCases) throws IOException {
private <T> void checkReadAll(JsonLfDecoder<T> decoder, TestCase<T>... testCases)
throws IOException {
for (var tc : testCases) {
tc.check.accept(readT.read(new JsonLfReader(tc.input)));
JsonLfReader r = new JsonLfReader(tc.input);
T actual = decoder.decode(r);
tc.check.accept(actual);
}
}

View File

@ -35,6 +35,7 @@ private[inner] object EnumClass extends StrictLogging {
.addMethod(generateDeprecatedFromValue(className, enumeration))
.addMethod(generateValueDecoder(className, enumeration))
.addMethod(generateToValue(className))
.addMethods(FromJsonGenerator.forEnum(className, "__enums$").asJava)
logger.debug("End")
enumType.build()
}

View File

@ -0,0 +1,260 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.codegen.backend.java.inner
import com.daml.ledger.javaapi.data.codegen.json.{JsonLfReader, JsonLfDecoder}
import com.typesafe.scalalogging.StrictLogging
import javax.lang.model.element.Modifier
import com.squareup.javapoet.{
CodeBlock,
ClassName,
MethodSpec,
ParameterSpec,
ParameterizedTypeName,
TypeName,
TypeVariableName,
}
import scala.jdk.CollectionConverters._
private[inner] object FromJsonGenerator extends StrictLogging {
private def decodeClass = ClassName.get(classOf[JsonLfReader.Decoders])
// JsonLfDecoder<T>
private def decoderTypeName(t: TypeName) =
ParameterizedTypeName.get(ClassName.get(classOf[JsonLfDecoder[_]]), t)
private def decodeTypeParamName(t: String): String = s"decode$t"
private def decoderForTagName(t: String): String = s"jsonDecoder$t"
private def jsonDecoderParamsForTypeParams(
typeParams: IndexedSeq[String]
): java.util.List[ParameterSpec] =
typeParams.map { t =>
ParameterSpec
.builder(decoderTypeName(TypeVariableName.get(t)), decodeTypeParamName(t))
.build()
}.asJava
def forRecordLike(fields: Fields, className: ClassName, typeParams: IndexedSeq[String])(implicit
packagePrefixes: PackagePrefixes
): Seq[MethodSpec] = {
Seq(
forRecordLike(
"jsonDecoder",
Seq(Modifier.PUBLIC, Modifier.STATIC),
fields,
className,
typeParams,
),
fromJsonString(className, typeParams),
)
}
private def forRecordLike(
methodName: String,
modifiers: Seq[Modifier],
fields: Fields,
className: ClassName,
typeParams: IndexedSeq[String],
)(implicit packagePrefixes: PackagePrefixes): MethodSpec = {
val typeName = className.parameterized(typeParams)
val fieldNames = {
val names = fields.map(f => CodeBlock.of("$S", f.javaName))
CodeBlock.of("$T.asList($L)", classOf[java.util.Arrays], CodeBlock.join(names.asJava, ", "))
}
val fieldsByName = {
val block = CodeBlock
.builder()
.beginControlFlow("name ->")
.beginControlFlow("switch (name)")
fields.zipWithIndex.foreach { case (f, i) =>
// We generate `JsonLfReader.Field` as a literal as $T seems to always use fully qualified name.
block.addStatement(
"case $S: return JsonLfReader.Decoders.Field.at($L, $L)",
f.javaName,
i,
jsonDecoderForType(f.damlType),
)
}
block
.addStatement("default: return null")
.endControlFlow() // end switch
.endControlFlow() // end lambda
.build()
}
val constr = {
val args =
(0 until fields.size).map(CodeBlock.of("$T.cast(args[$L])", decodeClass, _))
CodeBlock.of("(Object[] args) -> new $T($L)", typeName, CodeBlock.join(args.asJava, ", "))
}
MethodSpec
.methodBuilder(methodName)
.addModifiers(modifiers: _*)
.addTypeVariables(typeParams.map(TypeVariableName.get).asJava)
.addParameters(jsonDecoderParamsForTypeParams(typeParams))
.returns(decoderTypeName(typeName))
.addStatement(
"return $T.record($L, $L, $L)",
decodeClass,
fieldNames,
fieldsByName.toString(),
constr,
)
.build()
}
private def fromJsonString(
className: ClassName,
typeParams: IndexedSeq[String],
): MethodSpec =
MethodSpec
.methodBuilder("fromJson")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addTypeVariables(typeParams.map(TypeVariableName.get).asJava)
.addParameter(classOf[String], "json")
.addParameters(jsonDecoderParamsForTypeParams(typeParams))
.returns(className.parameterized(typeParams))
.addException(classOf[JsonLfDecoder.Error])
.addStatement(
"return jsonDecoder($L).decode(new $T(json))",
CodeBlock.join(typeParams.map(t => CodeBlock.of(decodeTypeParamName(t))).asJava, ", "),
classOf[JsonLfReader],
)
.build()
def forVariant(
className: ClassName,
typeParams: IndexedSeq[String],
fields: Fields,
): Seq[MethodSpec] = {
val typeName = className.parameterized(typeParams)
val tagNames = CodeBlock.of(
"$T.asList($L)",
classOf[java.util.Arrays],
CodeBlock.join(fields.map(f => CodeBlock.of("$S", f.javaName)).asJava, ", "),
)
val variantsByTag = {
val block = CodeBlock
.builder()
.beginControlFlow("name ->")
.beginControlFlow("switch (name)")
fields.foreach { f =>
block.addStatement(
"case $S: return $L($L)",
f.damlName,
decoderForTagName(f.damlName),
CodeBlock.join(typeParams.map(t => CodeBlock.of(decodeTypeParamName(t))).asJava, ", "),
)
}
block
.addStatement("default: return null")
.endControlFlow() // end switch
.endControlFlow() // end lambda
.build()
}
val jsonDecoder = MethodSpec
.methodBuilder("jsonDecoder")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addTypeVariables(typeParams.map(TypeVariableName.get).asJava)
.addParameters(jsonDecoderParamsForTypeParams(typeParams))
.returns(decoderTypeName(typeName))
.addStatement("return $T.variant($L, $L)", decodeClass, tagNames, variantsByTag.toString())
.build()
Seq(jsonDecoder, fromJsonString(className, typeParams))
}
def forVariantRecord(
tag: String,
fields: Fields,
className: ClassName,
typeParams: IndexedSeq[String],
)(implicit
packagePrefixes: PackagePrefixes
) =
forRecordLike(
decoderForTagName(tag),
Seq(Modifier.PRIVATE, Modifier.STATIC),
fields,
className,
typeParams,
)
def forVariantSimple(typeName: TypeName, typeParams: IndexedSeq[String], field: FieldInfo)(
implicit packagePrefixes: PackagePrefixes
) =
MethodSpec
.methodBuilder(decoderForTagName(field.damlName))
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addTypeVariables(typeParams.map(TypeVariableName.get).asJava)
.addParameters(jsonDecoderParamsForTypeParams(typeParams))
.returns(decoderTypeName(typeName))
.addStatement(
"return r -> new $T($L.decode(r))",
typeName,
jsonDecoderForType(field.damlType),
)
.build()
def forEnum(className: ClassName, damlNameToEnumMap: String): Seq[MethodSpec] = {
val jsonDecoder = MethodSpec
.methodBuilder("jsonDecoder")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(decoderTypeName(className))
.addStatement("return $T.enumeration($L)", decodeClass, damlNameToEnumMap)
.build()
Seq(jsonDecoder, fromJsonString(className, IndexedSeq.empty[String]))
}
import com.daml.lf.typesig.Type
private def jsonDecoderForType(
damlType: Type
)(implicit packagePrefixes: PackagePrefixes): CodeBlock = {
import com.daml.lf.typesig._
import com.daml.lf.data.ImmArray.ImmArraySeq
import com.daml.ledger.javaapi.data.codegen.ContractId
def typeReaders(types: Iterable[Type]): CodeBlock =
CodeBlock.join(types.map(jsonDecoderForType).asJava, ", ")
damlType match {
case TypeCon(TypeConName(ident), typeParams) =>
CodeBlock.of("$T.jsonDecoder($L)", guessClass(ident), typeReaders(typeParams))
case TypePrim(PrimTypeBool, _) => CodeBlock.of("$T.bool", decodeClass)
case TypePrim(PrimTypeInt64, _) => CodeBlock.of("$T.int64", decodeClass)
case TypeNumeric(_) => CodeBlock.of("$T.decimal", decodeClass)
case TypePrim(PrimTypeText, _) => CodeBlock.of("$T.text", decodeClass)
case TypePrim(PrimTypeDate, _) => CodeBlock.of("$T.date", decodeClass)
case TypePrim(PrimTypeTimestamp, _) => CodeBlock.of("$T.timestamp", decodeClass)
case TypePrim(PrimTypeParty, _) => CodeBlock.of("$T.party", decodeClass)
case TypePrim(PrimTypeContractId, ImmArraySeq(templateType)) =>
val contractIdType = toJavaTypeName(templateType) match {
case templateClass: ClassName => templateClass.nestedClass("ContractId")
case typeVariableName: TypeVariableName =>
ParameterizedTypeName.get(ClassName.get(classOf[ContractId[_]]), typeVariableName)
case unexpected => sys.error(s"Unexpected type [$unexpected] for Daml type [$damlType]")
}
CodeBlock.of("$T.contractId($T::new)", decodeClass, contractIdType)
case TypePrim(PrimTypeList, typeParams) =>
CodeBlock.of("$T.list($L)", decodeClass, typeReaders(typeParams))
case TypePrim(PrimTypeOptional, typeParams) =>
// TODO(raphael-speyer-da): Handle nested optionals
CodeBlock.of("$T.optional($L)", decodeClass, typeReaders(typeParams))
case TypePrim(PrimTypeTextMap, typeParams) =>
CodeBlock.of("$T.textMap($L)", decodeClass, typeReaders(typeParams))
case TypePrim(PrimTypeGenMap, typeParams) =>
CodeBlock.of("$T.genMap($L)", decodeClass, typeReaders(typeParams))
case TypePrim(PrimTypeUnit, _) => CodeBlock.of("$T.unit", decodeClass)
case TypeVar(name) => CodeBlock.of(decodeTypeParamName(name))
case _ => throw new IllegalArgumentException(s"Invalid Daml datatype: $damlType")
}
}
}

View File

@ -44,7 +44,13 @@ private[inner] object RecordMethods {
List(deprecatedFromValue, valueDecoder, toValue)
}
Vector(constructor) ++ conversionMethods ++
val jsonConversionMethods = FromJsonGenerator.forRecordLike(
fields,
className,
typeParameters,
)
Vector(constructor) ++ conversionMethods ++ jsonConversionMethods ++
ObjectMethods(className, typeParameters, fields.map(_.javaName))
}
}

View File

@ -589,7 +589,7 @@ private[inner] object TemplateClass extends StrictLogging {
Modifier.PUBLIC,
)
.initializer(
"$Znew $T<>($>$Z$S,$W$N,$W$T::new,$W$N -> $T.templateValueDecoder().decode($N),$W$T::new,$W$T.of($L)" + keyParams + "$<)",
"$Znew $T<>($>$Z$S,$W$N,$W$T::new,$W$N -> $T.templateValueDecoder().decode($N),$W$T::fromJson,$W$T::new,$W$T.of($L)" + keyParams + "$<)",
Seq(
fieldClass,
templateClassName,
@ -598,6 +598,7 @@ private[inner] object TemplateClass extends StrictLogging {
valueDecoderLambdaArgName,
templateClassName,
valueDecoderLambdaArgName,
templateClassName,
contractName,
classOf[java.util.List[_]],
CodeBlock

View File

@ -61,7 +61,15 @@ private[inner] object TemplateMethods {
.addStatement("return $T.of(COMPANION)", classOf[ContractFilter[_]])
.build()
Vector(constructor) ++ conversionMethods ++ Vector(contractFilterMethod) ++
val jsonConversionMethods = FromJsonGenerator.forRecordLike(
fields,
className,
IndexedSeq(),
)
Vector(constructor) ++ conversionMethods ++ jsonConversionMethods ++ Vector(
contractFilterMethod
) ++
ObjectMethods(className, IndexedSeq.empty[String], fields.map(_.javaName))
}
}

View File

@ -46,6 +46,9 @@ private[inner] object VariantClass extends StrictLogging {
generateDeprecatedFromValue(typeArguments, variantClassName)
)
.addMethod(generateValueDecoder(typeArguments, constructorInfo, variantClassName))
.addMethods(
FromJsonGenerator.forVariant(variantClassName, typeArguments, constructorInfo).asJava
)
.addMethods(
VariantValueDecodersMethods(
typeArguments,

View File

@ -21,22 +21,26 @@ object VariantValueDecodersMethods {
typeWithContext: TypeWithContext,
subPackage: String,
)(implicit packagePrefixes: PackagePrefixes): Vector[MethodSpec] = {
val (variantRecords, methodSpecs) =
val (variantRecords, variantSimples) =
getFieldsWithTypes(variant.fields).partitionMap { fieldInfo =>
val FieldInfo(damlName, damlType, javaName, _) = fieldInfo
val FieldInfo(damlName, damlType, _, _) = fieldInfo
damlType match {
case TypeCon(TypeConName(id), _) if isVariantRecord(typeWithContext, damlName, id) =>
// Variant records will be dealt with in a subsequent phase
Left(damlName)
case _ =>
val className =
ClassName.bestGuess(s"$subPackage.$javaName").parameterized(typeArgs)
Right(
variantConDecoderMethod(damlName, typeArgs, className, damlType)
)
Right(fieldInfo)
}
}
val methodSpecs = variantSimples.flatMap { fi =>
val className = ClassName.bestGuess(s"$subPackage.${fi.javaName}").parameterized(typeArgs)
Seq(
variantConDecoderMethod(fi.damlName, typeArgs, className, fi.damlType),
FromJsonGenerator.forVariantSimple(className, typeArgs, fi),
)
}
val recordAddons = for {
child <- typeWithContext.typesLineages
if variantRecords.contains(child.name)
@ -48,14 +52,22 @@ object VariantValueDecodersMethods {
case Some(Normal(DefDataType(typeVars, record: Record.FWT))) =>
val typeParameters = typeVars.map(JavaEscaper.escapeString)
val className =
ClassName.bestGuess(s"$subPackage.${child.name}").parameterized(typeParameters)
ClassName.bestGuess(s"$subPackage.${child.name}")
FromValueGenerator.generateValueDecoderForRecordLike(
getFieldsWithTypes(record.fields),
className,
typeArgs,
s"valueDecoder${child.name}",
FromValueGenerator.variantCheck(child.name, _, _),
Seq(
FromValueGenerator.generateValueDecoderForRecordLike(
getFieldsWithTypes(record.fields),
className.parameterized(typeParameters),
typeArgs,
s"valueDecoder${child.name}",
FromValueGenerator.variantCheck(child.name, _, _),
),
FromJsonGenerator.forVariantRecord(
child.name,
getFieldsWithTypes(record.fields),
className,
typeArgs,
),
)
case t =>
val c = s"${typeWithContext.name}.${child.name}"
@ -64,7 +76,7 @@ object VariantValueDecodersMethods {
)
}
}
(methodSpecs ++ recordAddons).toVector
(methodSpecs ++ recordAddons.flatten).toVector
}
private def variantConDecoderMethod(

View File

@ -47,15 +47,17 @@ package object inner {
toJavaTypeName(fwt._2),
)
private[inner] def guessClass(ident: Identifier)(implicit packagePrefixes: PackagePrefixes) =
ClassName.bestGuess(fullyQualifiedName(ident))
private[inner] def toJavaTypeName(
damlType: Type
)(implicit packagePrefixes: PackagePrefixes): TypeName =
damlType match {
case TypeCon(TypeConName(ident), Seq()) =>
ClassName.bestGuess(fullyQualifiedName(ident)).box()
case TypeCon(TypeConName(ident), Seq()) => guessClass(ident).box()
case TypeCon(TypeConName(ident), typeParameters) =>
ParameterizedTypeName.get(
ClassName.bestGuess(fullyQualifiedName(ident)),
guessClass(ident),
typeParameters.map(toJavaTypeName(_)): _*
)
case TypePrim(PrimTypeBool, _) => ClassName.get(classOf[java.lang.Boolean])

View File

@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import com.daml.ledger.javaapi.data.DamlRecord;
import com.daml.ledger.javaapi.data.Numeric;
import com.daml.ledger.javaapi.data.Party;
import java.io.IOException;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
import org.junit.platform.runner.JUnitPlatform;
@ -44,4 +45,12 @@ public class DecimalTestForAll {
assertEquals(Box.fromValue(record).toValue(), record);
}
}
@Test
void testFromJson() throws IOException {
for (String s : goodValues) {
Box b = new Box(new BigDecimal(s), "alice");
assertEquals(Box.fromJson(String.format("{\"x\": \"%s\", \"party\": \"alice\"}", s)), b);
}
}
}

View File

@ -10,6 +10,7 @@ import com.daml.ledger.javaapi.data.DamlRecord;
import com.daml.ledger.javaapi.data.Party;
import com.daml.ledger.javaapi.data.Unit;
import com.daml.ledger.javaapi.data.Variant;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;
@ -57,6 +58,29 @@ public class EnumTestForForAll {
}
}
@Test
void fromJson() throws IOException {
for (String s : new String[] {"Red", "Green", "Blue"}) {
String damlEnum = String.format("\"%s\"", s);
String record = String.format("{\"x\": %s, \"party\":\"party\"}", damlEnum);
String variant = String.format("{\"tag\": \"SomeColor\", \"value\": %s}", damlEnum);
String leaf = "{\"tag\": \"Leaf\", \"value\": {}}";
String node =
String.format("{\"color\": %s, \"left\": %s, \"right\": %s}", damlEnum, leaf, leaf);
String tree = String.format("{\"tag\": \"Node\", \"value\": %s}", node);
assertEquals(Color.valueOf(s.toUpperCase()), Color.fromJson(damlEnum));
assertEquals(new Box(Color.valueOf(s.toUpperCase()), "party"), Box.fromJson(record));
assertEquals(new SomeColor(Color.valueOf(s.toUpperCase())), OptionalColor.fromJson(variant));
assertEquals(
new Node(
Color.valueOf(s.toUpperCase()),
new Leaf(Unit.getInstance()),
new Leaf(Unit.getInstance())),
ColoredTree.fromJson(tree));
}
}
@Test
void badValue2Enum() {
DamlEnum value = new DamlEnum("Yellow");

View File

@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.daml.ledger.javaapi.data.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;
@ -69,6 +70,21 @@ public class GenMapTestFor1_11AndFor1_12ndFor1_13AndFor1_14AndFor1_15AndFor1_dev
assertEquals(keys[2], pair3());
}
@Test
void fromJson() throws IOException {
Box b =
Box.fromJson(
"{"
+ "\"party\": \"alice\", "
+ "\"x\": [ "
+ " [ [1, \"1.0000000000\"], {\"tag\": \"Right\", \"value\": \"1.0000000000\"} ], "
+ " [ [2, \"-2.2222222222\"], {\"tag\": \"Left\", \"value\": 2} ], "
+ " [ [3, \"3.3333333333\"], {\"tag\": \"Right\", \"value\": \"3.3333333333\"} ] "
+ "]"
+ "}");
assertEquals(box(), b);
}
private DamlRecord pair(Long fst, BigDecimal snd) {
return new DamlRecord(
new DamlRecord.Field("fst", new Int64(fst)), new DamlRecord.Field("snd", new Numeric(snd)));

View File

@ -42,4 +42,18 @@ class NumericTestFor1_7AndFor1_8AndFor1_11AndFor1_12ndFor1_13AndFor1_14AndFor1_1
new DamlRecord.Field("party", new Party("alice")));
assertEquals(Box.fromValue(record).toValue(), record);
}
@Test
void testFromJson() throws java.io.IOException {
Box expected =
new Box(
new BigDecimal(0),
new BigDecimal(10),
new BigDecimal(17),
new BigDecimal("0.37"),
"alice");
assertEquals(
expected,
Box.fromJson("{\"x0\":0, \"x10\":\"10\", \"x17\":17, \"x37\":0.37, \"party\":\"alice\"}"));
}
}