mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
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:
parent
78487e18a8
commit
49b2a472da
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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])
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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)));
|
||||
|
@ -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\"}"));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user