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.DamlRecord;
|
||||||
import com.daml.ledger.javaapi.data.Identifier;
|
import com.daml.ledger.javaapi.data.Identifier;
|
||||||
import com.daml.ledger.javaapi.data.Value;
|
import com.daml.ledger.javaapi.data.Value;
|
||||||
|
import com.daml.ledger.javaapi.data.codegen.json.JsonLfDecoder;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -34,6 +35,13 @@ public abstract class ContractCompanion<Ct, Id, Data>
|
|||||||
/** @hidden */
|
/** @hidden */
|
||||||
protected final Function<DamlRecord, Data> fromValue;
|
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
|
* Static method to generate an implementation of {@code ValueDecoder} of type {@code Data} with
|
||||||
* metadata from the provided {@code ContractCompanion}.
|
* metadata from the provided {@code ContractCompanion}.
|
||||||
@ -74,9 +82,15 @@ public abstract class ContractCompanion<Ct, Id, Data>
|
|||||||
Identifier templateId,
|
Identifier templateId,
|
||||||
Function<String, Id> newContractId,
|
Function<String, Id> newContractId,
|
||||||
Function<DamlRecord, Data> fromValue,
|
Function<DamlRecord, Data> fromValue,
|
||||||
|
FromJson<Data> fromJson,
|
||||||
List<Choice<Data, ?, ?>> choices) {
|
List<Choice<Data, ?, ?>> choices) {
|
||||||
super(templateId, templateClassName, newContractId, choices);
|
super(templateId, templateClassName, newContractId, choices);
|
||||||
this.fromValue = fromValue;
|
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> {
|
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,
|
Identifier templateId,
|
||||||
Function<String, Id> newContractId,
|
Function<String, Id> newContractId,
|
||||||
Function<DamlRecord, Data> fromValue,
|
Function<DamlRecord, Data> fromValue,
|
||||||
|
FromJson<Data> fromJson,
|
||||||
NewContract<Ct, Id, Data> newContract,
|
NewContract<Ct, Id, Data> newContract,
|
||||||
List<Choice<Data, ?, ?>> choices) {
|
List<Choice<Data, ?, ?>> choices) {
|
||||||
super(templateClassName, templateId, newContractId, fromValue, choices);
|
super(templateClassName, templateId, newContractId, fromValue, fromJson, choices);
|
||||||
this.newContract = newContract;
|
this.newContract = newContract;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,10 +168,11 @@ public abstract class ContractCompanion<Ct, Id, Data>
|
|||||||
Identifier templateId,
|
Identifier templateId,
|
||||||
Function<String, Id> newContractId,
|
Function<String, Id> newContractId,
|
||||||
Function<DamlRecord, Data> fromValue,
|
Function<DamlRecord, Data> fromValue,
|
||||||
|
FromJson<Data> fromJson,
|
||||||
NewContract<Ct, Id, Data, Key> newContract,
|
NewContract<Ct, Id, Data, Key> newContract,
|
||||||
List<Choice<Data, ?, ?>> choices,
|
List<Choice<Data, ?, ?>> choices,
|
||||||
Function<Value, Key> keyFromValue) {
|
Function<Value, Key> keyFromValue) {
|
||||||
super(templateClassName, templateId, newContractId, fromValue, choices);
|
super(templateClassName, templateId, newContractId, fromValue, fromJson, choices);
|
||||||
this.newContract = newContract;
|
this.newContract = newContract;
|
||||||
this.keyFromValue = keyFromValue;
|
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.LocalDate;
|
||||||
import java.time.format.DateTimeParseException;
|
import java.time.format.DateTimeParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.TreeMap;
|
|
||||||
import java.util.function.Function;
|
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 {
|
public class JsonLfReader {
|
||||||
private final String json; // Used to reference unknown values until they can be decoded.
|
private final String json; // Used to reference unknown values until they can be decoded.
|
||||||
private static final JsonFactory jsonFactory = new JsonFactory();
|
private static final JsonFactory jsonFactory = new JsonFactory();
|
||||||
private final JsonParser parser;
|
private final JsonParser parser;
|
||||||
|
|
||||||
public JsonLfReader(String json) throws IOException {
|
public JsonLfReader(String json) throws JsonLfDecoder.Error {
|
||||||
this.json = json;
|
this.json = json;
|
||||||
parser = jsonFactory.createParser(json);
|
try {
|
||||||
parser.nextToken();
|
parser = jsonFactory.createParser(json);
|
||||||
}
|
parser.nextToken();
|
||||||
|
} catch (IOException e) {
|
||||||
// Can override these two for different handling of these cases.
|
throw new JsonLfDecoder.Error("initialization error", e);
|
||||||
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()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Readers for built-in LF types. ///
|
/// Readers for built-in LF types. ///
|
||||||
|
public static class Decoders {
|
||||||
|
|
||||||
public static final FromJson<Unit> unit =
|
public static final JsonLfDecoder<Unit> unit =
|
||||||
r -> {
|
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();
|
r.readStartObject();
|
||||||
|
while (r.notEndObject()) {
|
||||||
|
String key = r.readFieldName();
|
||||||
|
V val = decodeValue.decode(r);
|
||||||
|
map.put(key, val);
|
||||||
|
}
|
||||||
r.readEndObject();
|
r.readEndObject();
|
||||||
return Unit.getInstance();
|
return map;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static final FromJson<Boolean> bool =
|
// Read a map with unknown number of items of the same types.
|
||||||
r -> {
|
public static <K, V> JsonLfDecoder<Map<K, V>> genMap(
|
||||||
r.expectIsAt("boolean", JsonToken.VALUE_TRUE, JsonToken.VALUE_FALSE);
|
JsonLfDecoder<K> decodeKey, JsonLfDecoder<V> decodeVal) {
|
||||||
Boolean value = null;
|
return r -> {
|
||||||
try {
|
Map<K, V> map = new LinkedHashMap<>();
|
||||||
value = r.parser.getBooleanValue();
|
// Maps are represented as an array of 2-element arrays.
|
||||||
} 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()) {
|
|
||||||
r.readStartArray();
|
r.readStartArray();
|
||||||
K key = readKey.read(r);
|
while (r.notEndArray()) {
|
||||||
V val = readValue.read(r);
|
r.readStartArray();
|
||||||
r.readEndArray();
|
K key = decodeKey.decode(r);
|
||||||
map.put(key, val);
|
V val = decodeVal.decode(r);
|
||||||
}
|
r.readEndArray();
|
||||||
r.readEndArray();
|
map.put(key, val);
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
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();
|
r.readEndArray();
|
||||||
return Optional.of(val);
|
return map;
|
||||||
}
|
};
|
||||||
};
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static <E extends Enum<E>> FromJson<E> enumeration(Class<E> enumClass) {
|
// The T type should not itself be Optional<?>. In that case use OptionalNested below.
|
||||||
return r -> {
|
public static <T> JsonLfDecoder<Optional<T>> optional(JsonLfDecoder<T> decodeVal) {
|
||||||
String value = text.read(r);
|
return r -> {
|
||||||
try {
|
if (r.parser.currentToken() == JsonToken.VALUE_NULL) {
|
||||||
return Enum.valueOf(enumClass, value);
|
r.moveNext();
|
||||||
} catch (IllegalArgumentException e) {
|
return Optional.empty();
|
||||||
r.parseExpected(String.format("constant of %s", enumClass.getName()));
|
} 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.
|
// 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;
|
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();
|
JsonLocation from = r.parser.currentTokenLocation();
|
||||||
try {
|
try {
|
||||||
r.parser.skipChildren();
|
r.parser.skipChildren();
|
||||||
@ -273,140 +384,31 @@ public class JsonLfReader {
|
|||||||
if (repr.endsWith(",")) repr = repr.substring(0, repr.length() - 1); // drop trailing comma
|
if (repr.endsWith(",")) repr = repr.substring(0, repr.length() - 1); // drop trailing comma
|
||||||
return new UnknownValue(repr, from);
|
return new UnknownValue(repr, from);
|
||||||
} catch (IOException e) {
|
} 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 {
|
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.
|
// 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) {
|
} catch (IOException e) {
|
||||||
throw new FromJson.Error(
|
throw new JsonLfDecoder.Error(
|
||||||
String.format("cannot decode unknown value '%s': %s", this.jsonRepr, e));
|
String.format("cannot decode unknown value '%s'", this.jsonRepr), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provides a generic way to read a variant type, by specifying each tag.
|
// Can override these two for different handling of these cases.
|
||||||
public static <T> FromJson<T> variant(List<String> tagNames, TagReader<T> readTag) {
|
protected void missingField(String fieldName) throws JsonLfDecoder.Error {
|
||||||
return r -> {
|
throw new JsonLfDecoder.Error(String.format("Missing %s at %s", fieldName, location()));
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static interface TagReader<T> {
|
protected void unknownField(String fieldName) throws JsonLfDecoder.Error {
|
||||||
FromJson<T> get(String tagName) throws FromJson.Error;
|
UnknownValue.read(this); // Consume the value from the reader.
|
||||||
}
|
throw new JsonLfDecoder.Error(String.format("Unknown %s at %s", fieldName, location()));
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used for branching and looping on objects and arrays. ///
|
/// 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. ///
|
/// 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);
|
expectIsAt("{", JsonToken.START_OBJECT);
|
||||||
moveNext();
|
moveNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readEndObject() throws FromJson.Error {
|
private void readEndObject() throws JsonLfDecoder.Error {
|
||||||
expectIsAt("}", JsonToken.END_OBJECT);
|
expectIsAt("}", JsonToken.END_OBJECT);
|
||||||
moveNext();
|
moveNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readStartArray() throws FromJson.Error {
|
private void readStartArray() throws JsonLfDecoder.Error {
|
||||||
expectIsAt("[", JsonToken.START_ARRAY);
|
expectIsAt("[", JsonToken.START_ARRAY);
|
||||||
moveNext();
|
moveNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readEndArray() throws FromJson.Error {
|
private void readEndArray() throws JsonLfDecoder.Error {
|
||||||
expectIsAt("]", JsonToken.END_ARRAY);
|
expectIsAt("]", JsonToken.END_ARRAY);
|
||||||
moveNext();
|
moveNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String readFieldName() throws FromJson.Error {
|
private String readFieldName() throws JsonLfDecoder.Error {
|
||||||
expectIsAt("field name", JsonToken.FIELD_NAME);
|
expectIsAt("field name", JsonToken.FIELD_NAME);
|
||||||
String fieldName = null;
|
String fieldName = null;
|
||||||
try {
|
try {
|
||||||
fieldName = parser.getText();
|
fieldName = parser.getText();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
parseExpected("textual field name");
|
parseExpected("textual field name", e);
|
||||||
}
|
}
|
||||||
moveNext();
|
moveNext();
|
||||||
return fieldName;
|
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++) {
|
for (int i = 0; i < expected.length; i++) {
|
||||||
if (parser.currentToken() == expected[i]) return;
|
if (parser.currentToken() == expected[i]) return;
|
||||||
}
|
}
|
||||||
parseExpected(description);
|
parseExpected(description);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void moveNext() throws FromJson.Error {
|
private void moveNext() throws JsonLfDecoder.Error {
|
||||||
try {
|
try {
|
||||||
parser.nextToken();
|
parser.nextToken();
|
||||||
} catch (IOException e) {
|
} 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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
import com.daml.ledger.javaapi.data.Unit;
|
import com.daml.ledger.javaapi.data.Unit;
|
||||||
|
import com.daml.ledger.javaapi.data.codegen.json.JsonLfReader.Decoders;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@ -16,7 +17,9 @@ import java.time.LocalDate;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.Month;
|
import java.time.Month;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@ -29,19 +32,18 @@ public class JsonLfReaderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testUnit() throws IOException {
|
void testUnit() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(Decoders.unit, eq("{}", Unit.getInstance()), eq("\t{\n} ", Unit.getInstance()));
|
||||||
JsonLfReader.unit, eq("{}", Unit.getInstance()), eq("\t{\n} ", Unit.getInstance()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testBool() throws IOException {
|
void testBool() throws IOException {
|
||||||
checkReadAll(JsonLfReader.bool, eq("false", false), eq("true", true));
|
checkReadAll(Decoders.bool, eq("false", false), eq("true", true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInt64() throws IOException {
|
void testInt64() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.int64,
|
Decoders.int64,
|
||||||
eq("42", 42L),
|
eq("42", 42L),
|
||||||
eq("\"+42\"", 42L),
|
eq("\"+42\"", 42L),
|
||||||
eq("-42", -42L),
|
eq("-42", -42L),
|
||||||
@ -56,7 +58,7 @@ public class JsonLfReaderTest {
|
|||||||
@Test
|
@Test
|
||||||
void testDecimal() throws IOException {
|
void testDecimal() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.decimal,
|
Decoders.decimal,
|
||||||
cmpEq("42", dec("42")),
|
cmpEq("42", dec("42")),
|
||||||
cmpEq("42.0", dec("42")),
|
cmpEq("42.0", dec("42")),
|
||||||
cmpEq("\"42\"", dec("42")),
|
cmpEq("\"42\"", dec("42")),
|
||||||
@ -75,7 +77,7 @@ public class JsonLfReaderTest {
|
|||||||
@Test
|
@Test
|
||||||
void testTimestamp() throws IOException {
|
void testTimestamp() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.timestamp,
|
Decoders.timestamp,
|
||||||
eq(
|
eq(
|
||||||
"\"1990-11-09T04:30:23.123456Z\"",
|
"\"1990-11-09T04:30:23.123456Z\"",
|
||||||
timestampUTC(1990, Month.NOVEMBER, 9, 4, 30, 23, 123456)),
|
timestampUTC(1990, Month.NOVEMBER, 9, 4, 30, 23, 123456)),
|
||||||
@ -94,7 +96,7 @@ public class JsonLfReaderTest {
|
|||||||
@Test
|
@Test
|
||||||
void testDate() throws IOException {
|
void testDate() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.date,
|
Decoders.date,
|
||||||
eq("\"2019-06-18\"", date(2019, Month.JUNE, 18)),
|
eq("\"2019-06-18\"", date(2019, Month.JUNE, 18)),
|
||||||
eq("\"9999-12-31\"", date(9999, Month.DECEMBER, 31)),
|
eq("\"9999-12-31\"", date(9999, Month.DECEMBER, 31)),
|
||||||
eq("\"0001-01-01\"", date(1, Month.JANUARY, 1)));
|
eq("\"0001-01-01\"", date(1, Month.JANUARY, 1)));
|
||||||
@ -102,40 +104,38 @@ public class JsonLfReaderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testParty() throws IOException {
|
void testParty() throws IOException {
|
||||||
checkReadAll(JsonLfReader.party, eq("\"Alice\"", "Alice"));
|
checkReadAll(Decoders.party, eq("\"Alice\"", "Alice"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testText() throws IOException {
|
void testText() throws IOException {
|
||||||
checkReadAll(JsonLfReader.text, eq("\"\"", ""), eq("\" \"", " "), eq("\"hello\"", "hello"));
|
checkReadAll(Decoders.text, eq("\"\"", ""), eq("\" \"", " "), eq("\"hello\"", "hello"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testContractId() throws IOException {
|
void testContractId() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(Decoders.contractId(Tmpl.Cid::new), eq("\"deadbeef\"", new Tmpl.Cid("deadbeef")));
|
||||||
JsonLfReader.contractId(Tmpl.Cid::new), eq("\"deadbeef\"", new Tmpl.Cid("deadbeef")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testEnum() throws IOException {
|
void testEnum() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.enumeration(Suit.class),
|
Decoders.enumeration(Suit.damlNames),
|
||||||
eq("\"Hearts\"", Suit.Hearts),
|
eq("\"Hearts\"", Suit.HEARTS),
|
||||||
eq("\"Diamonds\"", Suit.Diamonds),
|
eq("\"Diamonds\"", Suit.DIAMONDS),
|
||||||
eq("\"Clubs\"", Suit.Clubs),
|
eq("\"Clubs\"", Suit.CLUBS),
|
||||||
eq("\"Spades\"", Suit.Spades));
|
eq("\"Spades\"", Suit.SPADES));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testList() throws IOException {
|
void testList() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(Decoders.list(Decoders.int64), eq("[]", emptyList()), eq("[1,2]", asList(1L, 2L)));
|
||||||
JsonLfReader.list(JsonLfReader.int64), eq("[]", emptyList()), eq("[1,2]", asList(1L, 2L)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testTextMap() throws IOException {
|
void testTextMap() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.textMap(JsonLfReader.int64),
|
Decoders.textMap(Decoders.int64),
|
||||||
eq("{}", emptyMap()),
|
eq("{}", emptyMap()),
|
||||||
eq("{\"foo\":1, \"bar\": 2}", java.util.Map.of("foo", 1L, "bar", 2L)));
|
eq("{\"foo\":1, \"bar\": 2}", java.util.Map.of("foo", 1L, "bar", 2L)));
|
||||||
}
|
}
|
||||||
@ -143,7 +143,7 @@ public class JsonLfReaderTest {
|
|||||||
@Test
|
@Test
|
||||||
void testGenMap() throws IOException {
|
void testGenMap() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.genMap(JsonLfReader.text, JsonLfReader.int64),
|
Decoders.genMap(Decoders.text, Decoders.int64),
|
||||||
eq("[]", emptyMap()),
|
eq("[]", emptyMap()),
|
||||||
eq("[[\"foo\", 1], [\"bar\", 2]]", java.util.Map.of("foo", 1L, "bar", 2L)));
|
eq("[[\"foo\", 1], [\"bar\", 2]]", java.util.Map.of("foo", 1L, "bar", 2L)));
|
||||||
}
|
}
|
||||||
@ -151,7 +151,7 @@ public class JsonLfReaderTest {
|
|||||||
@Test
|
@Test
|
||||||
void testOptionalNonNested() throws IOException {
|
void testOptionalNonNested() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.optional(JsonLfReader.int64),
|
Decoders.optional(Decoders.int64),
|
||||||
eq("null", Optional.empty()),
|
eq("null", Optional.empty()),
|
||||||
eq("42", Optional.of(42L)));
|
eq("42", Optional.of(42L)));
|
||||||
}
|
}
|
||||||
@ -159,7 +159,7 @@ public class JsonLfReaderTest {
|
|||||||
@Test
|
@Test
|
||||||
void testOptionalNested() throws IOException {
|
void testOptionalNested() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.optionalNested(JsonLfReader.optional(JsonLfReader.int64)),
|
Decoders.optionalNested(Decoders.optional(Decoders.int64)),
|
||||||
eq("null", Optional.empty()),
|
eq("null", Optional.empty()),
|
||||||
eq("[]", Optional.of(Optional.empty())),
|
eq("[]", Optional.of(Optional.empty())),
|
||||||
eq("[42]", Optional.of(Optional.of(42L))));
|
eq("[42]", Optional.of(Optional.of(42L))));
|
||||||
@ -168,8 +168,7 @@ public class JsonLfReaderTest {
|
|||||||
@Test
|
@Test
|
||||||
void testOptionalNestedDeeper() throws IOException {
|
void testOptionalNestedDeeper() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.optionalNested(
|
Decoders.optionalNested(Decoders.optionalNested(Decoders.optional(Decoders.int64))),
|
||||||
JsonLfReader.optionalNested(JsonLfReader.optional(JsonLfReader.int64))),
|
|
||||||
eq("null", Optional.empty()),
|
eq("null", Optional.empty()),
|
||||||
eq("[]", Optional.of(Optional.empty())),
|
eq("[]", Optional.of(Optional.empty())),
|
||||||
eq("[[]]", Optional.of(Optional.of(Optional.empty()))),
|
eq("[[]]", Optional.of(Optional.of(Optional.empty()))),
|
||||||
@ -179,7 +178,7 @@ public class JsonLfReaderTest {
|
|||||||
@Test
|
@Test
|
||||||
void testOptionalLists() throws IOException {
|
void testOptionalLists() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.optional(JsonLfReader.list(JsonLfReader.list(JsonLfReader.int64))),
|
Decoders.optional(Decoders.list(Decoders.list(Decoders.int64))),
|
||||||
eq("null", Optional.empty()),
|
eq("null", Optional.empty()),
|
||||||
eq("[]", Optional.of(emptyList())),
|
eq("[]", Optional.of(emptyList())),
|
||||||
eq("[[]]", Optional.of(asList(emptyList()))),
|
eq("[[]]", Optional.of(asList(emptyList()))),
|
||||||
@ -187,21 +186,20 @@ public class JsonLfReaderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testVariant() throws IOException, FromJson.Error {
|
void testVariant() throws IOException, JsonLfDecoder.Error {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.variant(
|
Decoders.variant(
|
||||||
asList("Bar", "Baz", "Quux"),
|
asList("Bar", "Baz", "Quux", "Flarp"),
|
||||||
tagName -> {
|
tagName -> {
|
||||||
switch (tagName) {
|
switch (tagName) {
|
||||||
case "Bar":
|
case "Bar":
|
||||||
return r -> new SomeVariant.Bar(JsonLfReader.int64.read(r));
|
return r -> new SomeVariant.Bar(Decoders.int64.decode(r));
|
||||||
case "Baz":
|
case "Baz":
|
||||||
return r -> new SomeVariant.Baz(JsonLfReader.unit.read(r));
|
return r -> new SomeVariant.Baz(Decoders.unit.decode(r));
|
||||||
case "Quux":
|
case "Quux":
|
||||||
return r ->
|
return r -> new SomeVariant.Quux(Decoders.optional(Decoders.int64).decode(r));
|
||||||
new SomeVariant.Quux(JsonLfReader.optional(JsonLfReader.int64).read(r));
|
|
||||||
case "Flarp":
|
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:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -217,23 +215,24 @@ public class JsonLfReaderTest {
|
|||||||
@Test
|
@Test
|
||||||
void testRecord() throws IOException {
|
void testRecord() throws IOException {
|
||||||
checkReadAll(
|
checkReadAll(
|
||||||
JsonLfReader.record(
|
Decoders.record(
|
||||||
args -> new SomeRecord((Long) args[0], (Boolean) args[1]),
|
|
||||||
asList("i", "b"),
|
asList("i", "b"),
|
||||||
fieldName -> {
|
name -> {
|
||||||
switch (fieldName) {
|
switch (name) {
|
||||||
case "i":
|
case "i":
|
||||||
return JsonLfReader.Field.required(0, JsonLfReader.int64);
|
return Decoders.Field.at(0, Decoders.list(Decoders.int64));
|
||||||
case "b":
|
case "b":
|
||||||
return JsonLfReader.Field.optional(1, JsonLfReader.bool, false);
|
return Decoders.Field.at(1, Decoders.bool, false);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
eq("[1,true]", new SomeRecord(1L, true)),
|
args -> new SomeRecord((List<Long>) args[0], (Boolean) args[1])),
|
||||||
eq("{\"i\":1,\"b\":true}", new SomeRecord(1L, true)),
|
eq("[[1],true]", new SomeRecord(asList(1L), true)),
|
||||||
eq("{\"b\":true,\"i\":1}", new SomeRecord(1L, true)),
|
eq("{\"i\":[],\"b\":true}", new SomeRecord(asList(), true)),
|
||||||
eq("{\"i\":1}", new SomeRecord(1L, false)));
|
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) {
|
private BigDecimal dec(String s) {
|
||||||
@ -251,10 +250,10 @@ public class JsonLfReaderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SomeRecord {
|
class SomeRecord {
|
||||||
private final long i;
|
private final List<Long> i;
|
||||||
private final boolean b;
|
private final Boolean b;
|
||||||
|
|
||||||
public SomeRecord(long i, boolean b) {
|
public SomeRecord(List<Long> i, Boolean b) {
|
||||||
this.i = i;
|
this.i = i;
|
||||||
this.b = b;
|
this.b = b;
|
||||||
}
|
}
|
||||||
@ -267,8 +266,8 @@ public class JsonLfReaderTest {
|
|||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
return o != null
|
return o != null
|
||||||
&& (o instanceof SomeRecord)
|
&& (o instanceof SomeRecord)
|
||||||
&& ((SomeRecord) o).i == i
|
&& ((SomeRecord) o).i.equals(i)
|
||||||
&& (((SomeRecord) o).b == b);
|
&& (((SomeRecord) o).b.equals(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -380,15 +379,28 @@ public class JsonLfReaderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Suit {
|
enum Suit {
|
||||||
Hearts,
|
HEARTS,
|
||||||
Diamonds,
|
DIAMONDS,
|
||||||
Clubs,
|
CLUBS,
|
||||||
Spades
|
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) {
|
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(generateDeprecatedFromValue(className, enumeration))
|
||||||
.addMethod(generateValueDecoder(className, enumeration))
|
.addMethod(generateValueDecoder(className, enumeration))
|
||||||
.addMethod(generateToValue(className))
|
.addMethod(generateToValue(className))
|
||||||
|
.addMethods(FromJsonGenerator.forEnum(className, "__enums$").asJava)
|
||||||
logger.debug("End")
|
logger.debug("End")
|
||||||
enumType.build()
|
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)
|
List(deprecatedFromValue, valueDecoder, toValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector(constructor) ++ conversionMethods ++
|
val jsonConversionMethods = FromJsonGenerator.forRecordLike(
|
||||||
|
fields,
|
||||||
|
className,
|
||||||
|
typeParameters,
|
||||||
|
)
|
||||||
|
|
||||||
|
Vector(constructor) ++ conversionMethods ++ jsonConversionMethods ++
|
||||||
ObjectMethods(className, typeParameters, fields.map(_.javaName))
|
ObjectMethods(className, typeParameters, fields.map(_.javaName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -589,7 +589,7 @@ private[inner] object TemplateClass extends StrictLogging {
|
|||||||
Modifier.PUBLIC,
|
Modifier.PUBLIC,
|
||||||
)
|
)
|
||||||
.initializer(
|
.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(
|
Seq(
|
||||||
fieldClass,
|
fieldClass,
|
||||||
templateClassName,
|
templateClassName,
|
||||||
@ -598,6 +598,7 @@ private[inner] object TemplateClass extends StrictLogging {
|
|||||||
valueDecoderLambdaArgName,
|
valueDecoderLambdaArgName,
|
||||||
templateClassName,
|
templateClassName,
|
||||||
valueDecoderLambdaArgName,
|
valueDecoderLambdaArgName,
|
||||||
|
templateClassName,
|
||||||
contractName,
|
contractName,
|
||||||
classOf[java.util.List[_]],
|
classOf[java.util.List[_]],
|
||||||
CodeBlock
|
CodeBlock
|
||||||
|
@ -61,7 +61,15 @@ private[inner] object TemplateMethods {
|
|||||||
.addStatement("return $T.of(COMPANION)", classOf[ContractFilter[_]])
|
.addStatement("return $T.of(COMPANION)", classOf[ContractFilter[_]])
|
||||||
.build()
|
.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))
|
ObjectMethods(className, IndexedSeq.empty[String], fields.map(_.javaName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,9 @@ private[inner] object VariantClass extends StrictLogging {
|
|||||||
generateDeprecatedFromValue(typeArguments, variantClassName)
|
generateDeprecatedFromValue(typeArguments, variantClassName)
|
||||||
)
|
)
|
||||||
.addMethod(generateValueDecoder(typeArguments, constructorInfo, variantClassName))
|
.addMethod(generateValueDecoder(typeArguments, constructorInfo, variantClassName))
|
||||||
|
.addMethods(
|
||||||
|
FromJsonGenerator.forVariant(variantClassName, typeArguments, constructorInfo).asJava
|
||||||
|
)
|
||||||
.addMethods(
|
.addMethods(
|
||||||
VariantValueDecodersMethods(
|
VariantValueDecodersMethods(
|
||||||
typeArguments,
|
typeArguments,
|
||||||
|
@ -21,22 +21,26 @@ object VariantValueDecodersMethods {
|
|||||||
typeWithContext: TypeWithContext,
|
typeWithContext: TypeWithContext,
|
||||||
subPackage: String,
|
subPackage: String,
|
||||||
)(implicit packagePrefixes: PackagePrefixes): Vector[MethodSpec] = {
|
)(implicit packagePrefixes: PackagePrefixes): Vector[MethodSpec] = {
|
||||||
val (variantRecords, methodSpecs) =
|
val (variantRecords, variantSimples) =
|
||||||
getFieldsWithTypes(variant.fields).partitionMap { fieldInfo =>
|
getFieldsWithTypes(variant.fields).partitionMap { fieldInfo =>
|
||||||
val FieldInfo(damlName, damlType, javaName, _) = fieldInfo
|
val FieldInfo(damlName, damlType, _, _) = fieldInfo
|
||||||
damlType match {
|
damlType match {
|
||||||
case TypeCon(TypeConName(id), _) if isVariantRecord(typeWithContext, damlName, id) =>
|
case TypeCon(TypeConName(id), _) if isVariantRecord(typeWithContext, damlName, id) =>
|
||||||
// Variant records will be dealt with in a subsequent phase
|
// Variant records will be dealt with in a subsequent phase
|
||||||
Left(damlName)
|
Left(damlName)
|
||||||
case _ =>
|
case _ =>
|
||||||
val className =
|
Right(fieldInfo)
|
||||||
ClassName.bestGuess(s"$subPackage.$javaName").parameterized(typeArgs)
|
|
||||||
Right(
|
|
||||||
variantConDecoderMethod(damlName, typeArgs, className, damlType)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
val recordAddons = for {
|
||||||
child <- typeWithContext.typesLineages
|
child <- typeWithContext.typesLineages
|
||||||
if variantRecords.contains(child.name)
|
if variantRecords.contains(child.name)
|
||||||
@ -48,14 +52,22 @@ object VariantValueDecodersMethods {
|
|||||||
case Some(Normal(DefDataType(typeVars, record: Record.FWT))) =>
|
case Some(Normal(DefDataType(typeVars, record: Record.FWT))) =>
|
||||||
val typeParameters = typeVars.map(JavaEscaper.escapeString)
|
val typeParameters = typeVars.map(JavaEscaper.escapeString)
|
||||||
val className =
|
val className =
|
||||||
ClassName.bestGuess(s"$subPackage.${child.name}").parameterized(typeParameters)
|
ClassName.bestGuess(s"$subPackage.${child.name}")
|
||||||
|
|
||||||
FromValueGenerator.generateValueDecoderForRecordLike(
|
Seq(
|
||||||
getFieldsWithTypes(record.fields),
|
FromValueGenerator.generateValueDecoderForRecordLike(
|
||||||
className,
|
getFieldsWithTypes(record.fields),
|
||||||
typeArgs,
|
className.parameterized(typeParameters),
|
||||||
s"valueDecoder${child.name}",
|
typeArgs,
|
||||||
FromValueGenerator.variantCheck(child.name, _, _),
|
s"valueDecoder${child.name}",
|
||||||
|
FromValueGenerator.variantCheck(child.name, _, _),
|
||||||
|
),
|
||||||
|
FromJsonGenerator.forVariantRecord(
|
||||||
|
child.name,
|
||||||
|
getFieldsWithTypes(record.fields),
|
||||||
|
className,
|
||||||
|
typeArgs,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
case t =>
|
case t =>
|
||||||
val c = s"${typeWithContext.name}.${child.name}"
|
val c = s"${typeWithContext.name}.${child.name}"
|
||||||
@ -64,7 +76,7 @@ object VariantValueDecodersMethods {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(methodSpecs ++ recordAddons).toVector
|
(methodSpecs ++ recordAddons.flatten).toVector
|
||||||
}
|
}
|
||||||
|
|
||||||
private def variantConDecoderMethod(
|
private def variantConDecoderMethod(
|
||||||
|
@ -47,15 +47,17 @@ package object inner {
|
|||||||
toJavaTypeName(fwt._2),
|
toJavaTypeName(fwt._2),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private[inner] def guessClass(ident: Identifier)(implicit packagePrefixes: PackagePrefixes) =
|
||||||
|
ClassName.bestGuess(fullyQualifiedName(ident))
|
||||||
|
|
||||||
private[inner] def toJavaTypeName(
|
private[inner] def toJavaTypeName(
|
||||||
damlType: Type
|
damlType: Type
|
||||||
)(implicit packagePrefixes: PackagePrefixes): TypeName =
|
)(implicit packagePrefixes: PackagePrefixes): TypeName =
|
||||||
damlType match {
|
damlType match {
|
||||||
case TypeCon(TypeConName(ident), Seq()) =>
|
case TypeCon(TypeConName(ident), Seq()) => guessClass(ident).box()
|
||||||
ClassName.bestGuess(fullyQualifiedName(ident)).box()
|
|
||||||
case TypeCon(TypeConName(ident), typeParameters) =>
|
case TypeCon(TypeConName(ident), typeParameters) =>
|
||||||
ParameterizedTypeName.get(
|
ParameterizedTypeName.get(
|
||||||
ClassName.bestGuess(fullyQualifiedName(ident)),
|
guessClass(ident),
|
||||||
typeParameters.map(toJavaTypeName(_)): _*
|
typeParameters.map(toJavaTypeName(_)): _*
|
||||||
)
|
)
|
||||||
case TypePrim(PrimTypeBool, _) => ClassName.get(classOf[java.lang.Boolean])
|
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.DamlRecord;
|
||||||
import com.daml.ledger.javaapi.data.Numeric;
|
import com.daml.ledger.javaapi.data.Numeric;
|
||||||
import com.daml.ledger.javaapi.data.Party;
|
import com.daml.ledger.javaapi.data.Party;
|
||||||
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.platform.runner.JUnitPlatform;
|
import org.junit.platform.runner.JUnitPlatform;
|
||||||
@ -44,4 +45,12 @@ public class DecimalTestForAll {
|
|||||||
assertEquals(Box.fromValue(record).toValue(), record);
|
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.Party;
|
||||||
import com.daml.ledger.javaapi.data.Unit;
|
import com.daml.ledger.javaapi.data.Unit;
|
||||||
import com.daml.ledger.javaapi.data.Variant;
|
import com.daml.ledger.javaapi.data.Variant;
|
||||||
|
import java.io.IOException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.platform.runner.JUnitPlatform;
|
import org.junit.platform.runner.JUnitPlatform;
|
||||||
import org.junit.runner.RunWith;
|
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
|
@Test
|
||||||
void badValue2Enum() {
|
void badValue2Enum() {
|
||||||
DamlEnum value = new DamlEnum("Yellow");
|
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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
import com.daml.ledger.javaapi.data.*;
|
import com.daml.ledger.javaapi.data.*;
|
||||||
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -69,6 +70,21 @@ public class GenMapTestFor1_11AndFor1_12ndFor1_13AndFor1_14AndFor1_15AndFor1_dev
|
|||||||
assertEquals(keys[2], pair3());
|
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) {
|
private DamlRecord pair(Long fst, BigDecimal snd) {
|
||||||
return new DamlRecord(
|
return new DamlRecord(
|
||||||
new DamlRecord.Field("fst", new Int64(fst)), new DamlRecord.Field("snd", new Numeric(snd)));
|
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")));
|
new DamlRecord.Field("party", new Party("alice")));
|
||||||
assertEquals(Box.fromValue(record).toValue(), record);
|
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