diff --git a/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/ContractCompanion.java b/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/ContractCompanion.java index a201858e37..6d21726163 100644 --- a/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/ContractCompanion.java +++ b/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/ContractCompanion.java @@ -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 /** @hidden */ protected final Function fromValue; + @FunctionalInterface // Defines the function type which throws. + public static interface FromJson { + T decode(String s) throws JsonLfDecoder.Error; + } + + protected final FromJson 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 Identifier templateId, Function newContractId, Function fromValue, + FromJson fromJson, List> 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 extends ContractCompanion { @@ -96,9 +110,10 @@ public abstract class ContractCompanion Identifier templateId, Function newContractId, Function fromValue, + FromJson fromJson, NewContract newContract, List> 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 Identifier templateId, Function newContractId, Function fromValue, + FromJson fromJson, NewContract newContract, List> choices, Function keyFromValue) { - super(templateClassName, templateId, newContractId, fromValue, choices); + super(templateClassName, templateId, newContractId, fromValue, fromJson, choices); this.newContract = newContract; this.keyFromValue = keyFromValue; } diff --git a/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/json/FromJson.java b/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/json/FromJson.java deleted file mode 100644 index 5a8720b5b1..0000000000 --- a/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/json/FromJson.java +++ /dev/null @@ -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 = JsonLfReader.list(JsonLfReader.text).read(reader); -public interface FromJson { - public T read(JsonLfReader r) throws Error; - - public static class Error extends IOException { - public Error(String msg) { - super(msg); - } - } -} diff --git a/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/json/JsonLfDecoder.java b/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/json/JsonLfDecoder.java new file mode 100644 index 0000000000..5442bea0eb --- /dev/null +++ b/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/json/JsonLfDecoder.java @@ -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 { + 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); + } + } +} diff --git a/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/json/JsonLfReader.java b/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/json/JsonLfReader.java index 548313d58c..623ea52fd4 100644 --- a/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/json/JsonLfReader.java +++ b/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/codegen/json/JsonLfReader.java @@ -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 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 = - r -> { + public static final JsonLfDecoder unit = + r -> { + r.readStartObject(); + r.readEndObject(); + return Unit.getInstance(); + }; + + public static final JsonLfDecoder 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 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 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 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 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 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 party = text; + + public static > JsonLfDecoder contractId( + Function 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 JsonLfDecoder> list(JsonLfDecoder decodeItem) { + return r -> { + List 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 JsonLfDecoder> textMap(JsonLfDecoder decodeValue) { + return r -> { + Map 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 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 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 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 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 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 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 party = text; - - public static > FromJson contractId(Function 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 FromJson> list(FromJson readItem) { - return r -> { - List 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 FromJson> textMap(FromJson readValue) { - return r -> { - Map 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 FromJson> genMap(FromJson readKey, FromJson readValue) { - return r -> { - Map 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 JsonLfDecoder> genMap( + JsonLfDecoder decodeKey, JsonLfDecoder decodeVal) { + return r -> { + Map 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 FromJson> optional(FromJson 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 FromJson>> optionalNested( - FromJson> readValue) { - return r -> { - if (r.parser.currentToken() == JsonToken.VALUE_NULL) { - r.moveNext(); - return Optional.empty(); - } else { - r.readStartArray(); - Optional val = r.notEndArray() ? readValue.read(r) : Optional.empty(); r.readEndArray(); - return Optional.of(val); - } - }; - } + return map; + }; + } - public static > FromJson enumeration(Class 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 JsonLfDecoder> optional(JsonLfDecoder 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 JsonLfDecoder>> optionalNested( + JsonLfDecoder> decodeVal) { + return r -> { + if (r.parser.currentToken() == JsonToken.VALUE_NULL) { + r.moveNext(); + return Optional.empty(); + } else { + r.readStartArray(); + Optional val = r.notEndArray() ? decodeVal.decode(r) : Optional.empty(); + r.readEndArray(); + return Optional.of(val); + } + }; + } + + public static > JsonLfDecoder enumeration(Map 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 JsonLfDecoder variant( + List tagNames, Function> 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) args[0], (Boolean) args[1])) + // ) + public static JsonLfDecoder record( + List fieldNames, + Function> fieldsByName, + Function constr) { + return r -> { + List missingFields = new ArrayList<>(); + List 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 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 { + final int argIndex; + final JsonLfDecoder decode; + final T defaultVal; // If non-null, used to populate value of missing fields. + + private Field(int argIndex, JsonLfDecoder decode, T defaultVal) { + this.argIndex = argIndex; + this.decode = decode; + this.defaultVal = defaultVal; } - return null; - }; + + public static Field at(int argIndex, JsonLfDecoder decode, T defaultVal) { + return new Field(argIndex, decode, defaultVal); + } + + public static Field at(int argIndex, JsonLfDecoder decode) { + return new Field(argIndex, decode, null); + } + } + + @SuppressWarnings("unchecked") + // Can be used within the `constr` arg to `record`, to allow casting without producing warnings. + public static 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 decodeWith(FromJson decoder) throws FromJson.Error { + public T decodeWith(JsonLfDecoder 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 FromJson variant(List tagNames, TagReader 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 { - FromJson 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 FromJson record( - Function constr, - List fieldNames, - Function> fieldsByName) { - return r -> { - List missingFields = new ArrayList<>(); - List 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 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 { - final int argIndex; - final FromJson fromJson; - final T defaultVal; // If non-null, used to populate value of missing fields. - - private Field(int argIndex, FromJson fromJson, T defaultVal) { - this.argIndex = argIndex; - this.fromJson = fromJson; - this.defaultVal = defaultVal; - } - - public static Field optional(int argIndex, FromJson fromJson, T defaultVal) { - return new Field(argIndex, fromJson, defaultVal); - } - - public static Field required(int argIndex, FromJson fromJson) { - return new Field(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); } } } diff --git a/language-support/java/bindings/src/test/java/com/daml/ledger/javaapi/data/codegen/json/JsonLfReaderTest.java b/language-support/java/bindings/src/test/java/com/daml/ledger/javaapi/data/codegen/json/JsonLfReaderTest.java index 48dc1826af..25d4a6f072 100644 --- a/language-support/java/bindings/src/test/java/com/daml/ledger/javaapi/data/codegen/json/JsonLfReaderTest.java +++ b/language-support/java/bindings/src/test/java/com/daml/ledger/javaapi/data/codegen/json/JsonLfReaderTest.java @@ -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) 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 i; + private final Boolean b; - public SomeRecord(long i, boolean b) { + public SomeRecord(List 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 damlNames = + new HashMap<>() { + { + put("Hearts", HEARTS); + put("Diamonds", DIAMONDS); + put("Clubs", CLUBS); + put("Spades", SPADES); + } + }; } - private void checkReadAll(FromJson readT, TestCase... testCases) throws IOException { + private void checkReadAll(JsonLfDecoder decoder, TestCase... 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); } } diff --git a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/EnumClass.scala b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/EnumClass.scala index 27b7cca3f3..d61c2d3281 100644 --- a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/EnumClass.scala +++ b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/EnumClass.scala @@ -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() } diff --git a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/FromJsonGenerator.scala b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/FromJsonGenerator.scala new file mode 100644 index 0000000000..dd6177b649 --- /dev/null +++ b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/FromJsonGenerator.scala @@ -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 + 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") + } + } +} diff --git a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/RecordMethods.scala b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/RecordMethods.scala index ebee3c1a75..1678cbe2e1 100644 --- a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/RecordMethods.scala +++ b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/RecordMethods.scala @@ -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)) } } diff --git a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateClass.scala b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateClass.scala index e7b5cc8063..26f2cc3a38 100644 --- a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateClass.scala +++ b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateClass.scala @@ -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 diff --git a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateMethods.scala b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateMethods.scala index 5fbaff5df7..cf471e8267 100644 --- a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateMethods.scala +++ b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateMethods.scala @@ -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)) } } diff --git a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/VariantClass.scala b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/VariantClass.scala index fdf5713643..1be863a706 100644 --- a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/VariantClass.scala +++ b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/VariantClass.scala @@ -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, diff --git a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/VariantValueDecodersMethods.scala b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/VariantValueDecodersMethods.scala index 5b19eb9460..963b3d1aaf 100644 --- a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/VariantValueDecodersMethods.scala +++ b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/VariantValueDecodersMethods.scala @@ -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( diff --git a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/package.scala b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/package.scala index 01057411c7..0593e9f46f 100644 --- a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/package.scala +++ b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/package.scala @@ -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]) diff --git a/language-support/java/codegen/src/test/java/com/digitalasset/testing/DecimalTestForAll.java b/language-support/java/codegen/src/test/java/com/digitalasset/testing/DecimalTestForAll.java index d5a52bbe53..023c110d19 100644 --- a/language-support/java/codegen/src/test/java/com/digitalasset/testing/DecimalTestForAll.java +++ b/language-support/java/codegen/src/test/java/com/digitalasset/testing/DecimalTestForAll.java @@ -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); + } + } } diff --git a/language-support/java/codegen/src/test/java/com/digitalasset/testing/EnumTestForForAll.java b/language-support/java/codegen/src/test/java/com/digitalasset/testing/EnumTestForForAll.java index fc746674ae..1c1b3c4fd2 100644 --- a/language-support/java/codegen/src/test/java/com/digitalasset/testing/EnumTestForForAll.java +++ b/language-support/java/codegen/src/test/java/com/digitalasset/testing/EnumTestForForAll.java @@ -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"); diff --git a/language-support/java/codegen/src/test/java/com/digitalasset/testing/GenMapTestFor1_11AndFor1_12ndFor1_13AndFor1_14AndFor1_15AndFor1_devAndFor2_dev.java b/language-support/java/codegen/src/test/java/com/digitalasset/testing/GenMapTestFor1_11AndFor1_12ndFor1_13AndFor1_14AndFor1_15AndFor1_devAndFor2_dev.java index 9b91ad5860..80b5d84dbf 100644 --- a/language-support/java/codegen/src/test/java/com/digitalasset/testing/GenMapTestFor1_11AndFor1_12ndFor1_13AndFor1_14AndFor1_15AndFor1_devAndFor2_dev.java +++ b/language-support/java/codegen/src/test/java/com/digitalasset/testing/GenMapTestFor1_11AndFor1_12ndFor1_13AndFor1_14AndFor1_15AndFor1_devAndFor2_dev.java @@ -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))); diff --git a/language-support/java/codegen/src/test/java/com/digitalasset/testing/NumericTestFor1_7AndFor1_8AndFor1_11AndFor1_12ndFor1_13AndFor1_14AndFor1_15AndFor1_devAndFor2_dev.java b/language-support/java/codegen/src/test/java/com/digitalasset/testing/NumericTestFor1_7AndFor1_8AndFor1_11AndFor1_12ndFor1_13AndFor1_14AndFor1_15AndFor1_devAndFor2_dev.java index 9ba2735720..ab53e77bee 100644 --- a/language-support/java/codegen/src/test/java/com/digitalasset/testing/NumericTestFor1_7AndFor1_8AndFor1_11AndFor1_12ndFor1_13AndFor1_14AndFor1_15AndFor1_devAndFor2_dev.java +++ b/language-support/java/codegen/src/test/java/com/digitalasset/testing/NumericTestFor1_7AndFor1_8AndFor1_11AndFor1_12ndFor1_13AndFor1_14AndFor1_15AndFor1_devAndFor2_dev.java @@ -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\"}")); + } }