From 4cecec1d1325ae89fd37a1984e57749e45a3b414 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 14:42:22 -0400 Subject: [PATCH 01/15] Upgrade jackson --- bosk-jackson/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bosk-jackson/build.gradle b/bosk-jackson/build.gradle index beb5305f..1fbd9126 100644 --- a/bosk-jackson/build.gradle +++ b/bosk-jackson/build.gradle @@ -7,7 +7,7 @@ plugins { } dependencies { - api 'com.fasterxml.jackson.core:jackson-databind:2.14.2' + api 'com.fasterxml.jackson.core:jackson-databind:2.15.1' api project(":bosk-core") testImplementation project(":lib-testing") From b53714d8dfe9441617e2d28bb1c18baf7ee1f74b Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 12:56:53 -0400 Subject: [PATCH 02/15] Refactor: modernize test method names --- .../vena/bosk/jackson/JacksonPluginTest.java | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java b/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java index 3ad4615a..ed7629af 100644 --- a/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java +++ b/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java @@ -94,7 +94,7 @@ void setUpJackson() throws Exception { @ParameterizedTest @MethodSource("catalogArguments") - void testToJson_catalog(List ids) { + void catalog_works(List ids) { // Build entities and put them in a Catalog List entities = new ArrayList<>(); for (String id : ids) { @@ -122,7 +122,7 @@ private static Arguments catalogCase(String ...ids) { @ParameterizedTest @MethodSource("listingArguments") - void testToJson_listing(List strings, List ids) { + void listing_works(List strings, List ids) { Listing listing = Listing.of(entitiesRef, ids); Map expected = new LinkedHashMap<>(); @@ -144,14 +144,14 @@ private static Arguments listingCase(String ...strings) { } @Test - void testListingEntry() throws JsonProcessingException { + void listingEntry_works() throws JsonProcessingException { assertEquals("true", boskMapper.writeValueAsString(LISTING_ENTRY)); Assertions.assertEquals(LISTING_ENTRY, boskMapper.readValue("true", ListingEntry.class)); } @ParameterizedTest @MethodSource("sideTableArguments") - void testToJson_sideTable(List keys, Map valuesByString, Map valuesById) { + void sideTable_works(List keys, Map valuesByString, Map valuesById) { SideTable sideTable = SideTable.fromOrderedMap(entitiesRef, valuesById); List> expectedList = new ArrayList<>(); @@ -193,21 +193,21 @@ static Arguments sideTableCase(Consumer> initializer) { } @Test - void testPhantomIsOmitted() throws InvalidTypeException, JsonProcessingException { + void phantom_isOmitted() throws InvalidTypeException, JsonProcessingException { TestEntity entity = makeEntityWithOptionalString(Optional.empty()); String json = boskMapper.writeValueAsString(entity); assertThat(json, not(containsString(Phantoms.Fields.phantomString))); } @Test - void testOptionalIsOmitted() throws InvalidTypeException, JsonProcessingException { + void optional_isOmitted() throws InvalidTypeException, JsonProcessingException { TestEntity entity = makeEntityWithOptionalString(Optional.empty()); String json = boskMapper.writeValueAsString(entity); assertThat(json, not(containsString(Optionals.Fields.optionalString))); } @Test - void testOptionalIsIncluded() throws InvalidTypeException, JsonProcessingException { + void optional_isIncluded() throws InvalidTypeException, JsonProcessingException { String contents = "OPTIONAL STRING CONTENTS"; TestEntity entity = makeEntityWithOptionalString(Optional.of(contents)); String json = boskMapper.writeValueAsString(entity); @@ -216,14 +216,14 @@ void testOptionalIsIncluded() throws InvalidTypeException, JsonProcessingExcepti } @Test - void testRootReference() throws JsonProcessingException { + void rootReference_works() throws JsonProcessingException { String json = boskMapper.writeValueAsString(bosk.rootReference()); assertEquals("\"/\"", json); } @ParameterizedTest @MethodSource("listValueArguments") - void testToJson_listValue(List list, JavaType type) throws JsonProcessingException { + void listValue_serializationWorks(List list, JavaType type) throws JsonProcessingException { ListValue listValue = ListValue.from(list); String expected = plainMapper.writeValueAsString(list); assertEquals(expected, boskMapper.writerFor(type).writeValueAsString(listValue)); @@ -231,7 +231,7 @@ void testToJson_listValue(List list, JavaType type) throws JsonProcessingExce @ParameterizedTest @MethodSource("listValueArguments") - void testFromJson_listValue(List list, JavaType type) throws JsonProcessingException { + void listValue_deserializationWorks(List list, JavaType type) throws JsonProcessingException { ListValue expected = ListValue.from(list); String json = plainMapper.writeValueAsString(list); Object actual = boskMapper.readerFor(type).readValue(json); @@ -271,7 +271,7 @@ private static class NodeWithGenerics implements StateTreeNode { } @Test - void testImplicitsAreOmitted() throws InvalidTypeException, JsonProcessingException { + void implicitRefs_omitted() throws InvalidTypeException, JsonProcessingException { TestEntity entity = makeEntityWithOptionalString(Optional.empty()); String json = boskMapper.writeValueAsString(entity); assertThat(json, not(containsString(ImplicitRefs.Fields.reference))); @@ -279,7 +279,7 @@ void testImplicitsAreOmitted() throws InvalidTypeException, JsonProcessingExcept } @Test - void testBasicDerivedRecord() throws InvalidTypeException, JsonProcessingException { + void derivedRecord_basic_works() throws InvalidTypeException, JsonProcessingException { Reference iref = parentRef.then(ImplicitRefs.class, TestEntity.Fields.implicitRefs); ImplicitRefs reflectiveEntity; try (ReadContext context = bosk.readContext()) { @@ -334,7 +334,7 @@ public static class ActualBasic { } @Test - void testDerivedRecordList() throws InvalidTypeException, JsonProcessingException { + void derivedRecord_list_works() throws InvalidTypeException, JsonProcessingException { Reference iref = parentRef.then(ImplicitRefs.class, TestEntity.Fields.implicitRefs); ImplicitRefs reflectiveEntity; try (ReadContext context = bosk.readContext()) { @@ -364,7 +364,7 @@ protected ActualList(ReflectiveEntity... entries) { } @Test - void testDeserializationPath() throws InvalidTypeException { + void deserializationPath_works() throws InvalidTypeException { Reference anyImplicitRefs = bosk.reference(ImplicitRefs.class, Path.of(TestRoot.Fields.entities, "-entity-", TestEntity.Fields.implicitRefs)); Reference ref1 = anyImplicitRefs.boundTo(Identifier.from("123")); ImplicitRefs firstObject = new ImplicitRefs( @@ -493,7 +493,7 @@ private Object boskListFor(List plainList, JavaType boskListType, Path path) // Sad paths @Test - void testBadJson_badReference() { + void nonexistentPath_throws() { assertThrows(UnexpectedPathException.class, () -> boskMapper .readerFor(TypeFactory.defaultInstance().constructParametricType(Reference.class, String.class)) @@ -501,67 +501,67 @@ void testBadJson_badReference() { } @Test - void testBadJson_catalogFromEmptyMap() { + void catalogFromEmptyMap_throws() { assertJsonException("{}", Catalog.class, TestEntity.class); } @Test - void testBadJson_catalogWithContentsArray() { + void catalogWithContentsArray_throws() { assertJsonException("{ \"contents\": [] }", Catalog.class, TestEntity.class); } @Test - void testBadJson_listingWithNoCatalog() { + void listingWithoutDomain_throws() { assertJsonException("{ \"ids\": [] }", Listing.class, TestEntity.class); } @Test - void testBadJson_listingWithNoIds() { + void listingWithoutIDs_throws() { assertJsonException("{ \"domain\": \"/entities\" }", Listing.class, TestEntity.class); } @Test - void testBadJson_listingWithExtraneousField() { + void listingWithExtraneousField_throws() { assertJsonException("{ \"domain\": \"/entities\", \"extraneous\": 0, \"ids\": [] }", Listing.class, TestEntity.class); } @Test - void testBadJson_listingWithTwoDomains() { + void listingWithTwoDomains_throws() { assertJsonException("{ \"domain\": \"/entities\", \"domain\": \"/entities\", \"ids\": [] }", Listing.class, TestEntity.class); } @Test - void testBadJson_listingWithTwoIdsFields() { + void listingWithTwoIDsFields_throws() { assertJsonException("{ \"domain\": \"/entities\", \"ids\": [], \"ids\": [] }", Listing.class, TestEntity.class); } @Test - void testBadJson_sideTableWithNoDomain() { + void sideTableWithNoDomain_throws() { assertJsonException("{ \"valuesById\": [] }", SideTable.class, TestEntity.class, String.class); } @Test - void testBadJson_sideTableWithNoValues() { + void sideTableWithNoValues_throws() { assertJsonException("{ \"domain\": \"/entities\" }", SideTable.class, TestEntity.class, String.class); } @Test - void testBadJson_sideTableWithExtraneousField() { + void sideTableWithExtraneousField_throws() { assertJsonException("{ \"domain\": \"/entities\", \"valuesById\": [], \"extraneous\": 0 }", SideTable.class, TestEntity.class, String.class); } @Test - void testBadJson_sideTableWithTwoDomains() { + void sideTableWithTwoDomains_throws() { assertJsonException("{ \"domain\": \"/entities\", \"domain\": \"/entities\", \"valuesById\": [] }", SideTable.class, TestEntity.class, String.class); } @Test - void testBadJson_sideTableWithValuesMap() { + void sideTableWithValuesMap_throws() { assertJsonException("{ \"domain\": \"/entities\", \"valuesById\": {} }", SideTable.class, TestEntity.class, String.class); } @Test - void testBadJson_sideTableWithTwoValuesFields() { + void sideTableWithTwoValuesFields_throws() { assertJsonException("{ \"domain\": \"/entities\", \"valuesById\": [], \"valuesById\": [] }", SideTable.class, TestEntity.class, String.class); } @@ -575,7 +575,7 @@ private void assertJsonException(String json, Class rawClass, Type... paramet } @Test - void testBadDeserializationPath_wrongType() { + void deserializationPath_wrongType_throws() { assertThrows(UnexpectedPathException.class, () -> { boskMapper.readerFor(WrongType.class).readValue("{ \"notAString\": { \"id\": \"123\" } }"); }); @@ -588,7 +588,7 @@ public static class WrongType implements StateTreeNode { } @Test - void testBadDeserializationPath_parameterUnbound() { + void deserializationPath_parameterUnbound_throws() { assertThrows(ParameterUnboundException.class, () -> { boskMapper.readerFor(EntityParameter.class).readValue("{ \"field\": { \"id\": \"123\" } }"); }); @@ -601,7 +601,7 @@ public static class EntityParameter implements StateTreeNode { } @Test - void testBadDeserializationPath_malformedPath() { + void deserializationPath_malformedPath() { assertThrows(MalformedPathException.class, () -> { boskMapper.readerFor(MalformedPath.class).readValue("{ \"field\": { \"id\": \"123\" } }"); }); @@ -614,7 +614,7 @@ public static class MalformedPath implements StateTreeNode { } @Test - void testBadDeserializationPath_nonexistentPath() { + void deserializationPath_nonexistentPath_throws() { assertThrows(UnexpectedPathException.class, () -> { boskMapper.readerFor(NonexistentPath.class).readValue("{ \"field\": { \"id\": \"123\" } }"); }); From 2dc0d7ff9d669e01753fe5bd6b0b8330a8ba657b Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 13:34:14 -0400 Subject: [PATCH 03/15] Add JacksonPluginTest for MapValue --- .../vena/bosk/jackson/JacksonPluginTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java b/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java index ed7629af..1a429571 100644 --- a/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java +++ b/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java @@ -16,6 +16,7 @@ import io.vena.bosk.ListValue; import io.vena.bosk.Listing; import io.vena.bosk.ListingEntry; +import io.vena.bosk.MapValue; import io.vena.bosk.Path; import io.vena.bosk.Reference; import io.vena.bosk.ReflectiveEntity; @@ -42,6 +43,7 @@ import lombok.RequiredArgsConstructor; import lombok.Value; import lombok.experimental.FieldNameConstants; +import lombok.var; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -261,6 +263,46 @@ private static Arguments listValueCase(Type entryType, Object...entries) { return Arguments.of(asList(entries), TypeFactory.defaultInstance().constructParametricType(ListValue.class, entryJavaType)); } + @ParameterizedTest + @MethodSource("mapValueArguments") + void mapValue_serializationWorks(Map map, JavaType type) throws JsonProcessingException { + MapValue mapValue = MapValue.fromOrderedMap(map); + String expected = plainMapper.writeValueAsString(map); + assertEquals(expected, boskMapper.writerFor(type).writeValueAsString(mapValue)); + } + + @ParameterizedTest + @MethodSource("mapValueArguments") + void mapValue_deserializationWorks(Map map, JavaType type) throws JsonProcessingException { + MapValue expected = MapValue.fromOrderedMap(map); + String json = plainMapper.writeValueAsString(map); + Object actual = boskMapper.readerFor(type).readValue(json); + assertEquals(expected, actual); + assertTrue(actual instanceof MapValue); + } + + private static Stream mapValueArguments() { + return Stream.of( + mapValueCase(String.class), + mapValueCase(String.class, kv("key1", "Hello")), + mapValueCase(String.class, kv("first", "firstValue"), kv("second", "secondValue")) + ); + } + + @SafeVarargs + private static Arguments mapValueCase(Type entryType, Map...entries) { + JavaType entryJavaType = TypeFactory.defaultInstance().constructType(entryType); + Map map = new LinkedHashMap<>(); + for (var entry: entries) { + map.putAll(entry); + } + return Arguments.of(map, TypeFactory.defaultInstance().constructParametricType(MapValue.class, entryJavaType)); + } + + private static Map kv(String key, Object value) { + return singletonMap(key, value); + } + /** * Exercise the type-parameter handling a bit */ From cdb92d848dd506622ac6f8d2f586b86d5d29fa2d Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 13:06:17 -0400 Subject: [PATCH 04/15] Refactor: separate serializer and deserializer methods --- .../io/vena/bosk/jackson/JacksonPlugin.java | 120 +++++++++++++++--- 1 file changed, 100 insertions(+), 20 deletions(-) diff --git a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java index 94d22d18..bf0d7884 100644 --- a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java +++ b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java @@ -106,35 +106,75 @@ public BoskSerializers(Bosk bosk) { public JsonSerializer findSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { Class theClass = type.getRawClass(); if (theClass.isAnnotationPresent(DerivedRecord.class)) { - return derivedRecordSerDes(type, beanDesc, bosk).serializer(config); + return derivedRecordSerializer(config, type, beanDesc); } else if (Catalog.class.isAssignableFrom(theClass)) { - return catalogSerDes(type, beanDesc, bosk).serializer(config); + return catalogSerializer(config, type, beanDesc); } else if (Listing.class.isAssignableFrom(theClass)) { - return listingSerDes(type, beanDesc, bosk).serializer(config); + return listingSerializer(config, type, beanDesc); } else if (Reference.class.isAssignableFrom(theClass)) { - return referenceSerDes(type, beanDesc, bosk).serializer(config); + return referenceSerializer(config, type, beanDesc); } else if (Identifier.class.isAssignableFrom(theClass)) { - return identifierSerDes(type, beanDesc, bosk).serializer(config); + return identifierSerializer(config, type, beanDesc); } else if (ListingEntry.class.isAssignableFrom(theClass)) { - return listingEntrySerDes(type, beanDesc, bosk).serializer(config); + return listingEntrySerializer(config, type, beanDesc); } else if (SideTable.class.isAssignableFrom(theClass)) { - return sideTableSerDes(type, beanDesc, bosk).serializer(config); + return sideTableSerializer(config, type, beanDesc); } else if (StateTreeNode.class.isAssignableFrom(theClass)) { - return stateTreeNodeSerDes(type, beanDesc, bosk).serializer(config); + return stateTreeNodeSerializer(config, type, beanDesc); } else if (Optional.class.isAssignableFrom(theClass)) { // Optional.empty() can't be serialized on its own because the field name itself must also be omitted throw new IllegalArgumentException("Cannot serialize an Optional on its own; only as a field of another object"); } else if (Phantom.class.isAssignableFrom(theClass)) { throw new IllegalArgumentException("Cannot serialize a Phantom on its own; only as a field of another object"); } else if (ListValue.class.isAssignableFrom(theClass)) { - return listValueSerDes(type, beanDesc, bosk).serializer(config); + return listValueSerializer(config, type, beanDesc); } else if (MapValue.class.isAssignableFrom(theClass)) { - return mapValueSerDes(type, beanDesc, bosk).serializer(config); + return mapValueSerializer(config, type, beanDesc); } else { return null; } } + private JsonSerializer derivedRecordSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + return derivedRecordSerDes(type, beanDesc, bosk).serializer(config); + } + + private JsonSerializer> catalogSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + return catalogSerDes(type, beanDesc, bosk).serializer(config); + } + + private JsonSerializer> listingSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + return listingSerDes(type, beanDesc, bosk).serializer(config); + } + + private JsonSerializer> referenceSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + return referenceSerDes(type, beanDesc, bosk).serializer(config); + } + + private JsonSerializer identifierSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + return identifierSerDes(type, beanDesc, bosk).serializer(config); + } + + private JsonSerializer listingEntrySerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + return listingEntrySerDes(type, beanDesc, bosk).serializer(config); + } + + private JsonSerializer> sideTableSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + return sideTableSerDes(type, beanDesc, bosk).serializer(config); + } + + private JsonSerializer stateTreeNodeSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + return stateTreeNodeSerDes(type, beanDesc, bosk).serializer(config); + } + + private JsonSerializer> listValueSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + return listValueSerDes(type, beanDesc, bosk).serializer(config); + } + + private JsonSerializer> mapValueSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + return mapValueSerDes(type, beanDesc, bosk).serializer(config); + } + // Thanks but no thanks, Jackson. We don't need your help. @Override @@ -160,35 +200,75 @@ public BoskDeserializers(Bosk bosk) { public JsonDeserializer findBeanDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { Class theClass = type.getRawClass(); if (theClass.isAnnotationPresent(DerivedRecord.class)) { - return derivedRecordSerDes(type, beanDesc, bosk).deserializer(config); + return derivedRecordDeserializer(type, config, beanDesc); } else if (Catalog.class.isAssignableFrom(theClass)) { - return catalogSerDes(type, beanDesc, bosk).deserializer(config); + return catalogDeserializer(type, config, beanDesc); } else if (Listing.class.isAssignableFrom(theClass)) { - return listingSerDes(type, beanDesc, bosk).deserializer(config); + return listingDeserializer(type, config, beanDesc); } else if (Reference.class.isAssignableFrom(theClass)) { - return referenceSerDes(type, beanDesc, bosk).deserializer(config); + return referenceDeserializer(type, config, beanDesc); } else if (Identifier.class.isAssignableFrom(theClass)) { - return identifierSerDes(type, beanDesc, bosk).deserializer(config); + return identifierDeserialier(type, config, beanDesc); } else if (ListingEntry.class.isAssignableFrom(theClass)) { - return listingEntrySerDes(type, beanDesc, bosk).deserializer(config); + return listingEntryDeserializer(type, config, beanDesc); } else if (SideTable.class.isAssignableFrom(theClass)) { - return sideTableSerDes(type, beanDesc, bosk).deserializer(config); + return sideTableDeserializer(type, config, beanDesc); } else if (StateTreeNode.class.isAssignableFrom(theClass)) { - return stateTreeNodeSerDes(type, beanDesc, bosk).deserializer(config); + return stateTreeNodeDeserializer(type, config, beanDesc); } else if (Optional.class.isAssignableFrom(theClass)) { // Optional.empty() can't be serialized on its own because the field name itself must also be omitted throw new IllegalArgumentException("Cannot serialize an Optional on its own; only as a field of another object"); } else if (Phantom.class.isAssignableFrom(theClass)) { throw new IllegalArgumentException("Cannot serialize a Phantom on its own; only as a field of another object"); } else if (ListValue.class.isAssignableFrom(theClass)) { - return listValueSerDes(type, beanDesc, bosk).deserializer(config); + return listValueDeserializer(type, config, beanDesc); } else if (MapValue.class.isAssignableFrom(theClass)) { - return mapValueSerDes(type, beanDesc, bosk).deserializer(config); + return mapValueDeserializer(type, config, beanDesc); } else { return null; } } + private JsonDeserializer derivedRecordDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { + return derivedRecordSerDes(type, beanDesc, bosk).deserializer(config); + } + + private JsonDeserializer> catalogDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { + return catalogSerDes(type, beanDesc, bosk).deserializer(config); + } + + private JsonDeserializer> listingDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { + return listingSerDes(type, beanDesc, bosk).deserializer(config); + } + + private JsonDeserializer> referenceDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { + return referenceSerDes(type, beanDesc, bosk).deserializer(config); + } + + private JsonDeserializer identifierDeserialier(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { + return identifierSerDes(type, beanDesc, bosk).deserializer(config); + } + + private JsonDeserializer listingEntryDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { + return listingEntrySerDes(type, beanDesc, bosk).deserializer(config); + } + + private JsonDeserializer> sideTableDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { + return sideTableSerDes(type, beanDesc, bosk).deserializer(config); + } + + private JsonDeserializer stateTreeNodeDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { + return stateTreeNodeSerDes(type, beanDesc, bosk).deserializer(config); + } + + private JsonDeserializer> listValueDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { + return listValueSerDes(type, beanDesc, bosk).deserializer(config); + } + + private JsonDeserializer> mapValueDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { + return mapValueSerDes(type, beanDesc, bosk).deserializer(config); + } + // Thanks but no thanks, Jackson. We don't need your help. @Override From 7831e222ab66851145d1275c78e6b37852aaaa48 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 13:19:50 -0400 Subject: [PATCH 05/15] Refactor: Inline and eliminate many uses of SerDes --- .../io/vena/bosk/jackson/JacksonPlugin.java | 629 +++++++----------- 1 file changed, 258 insertions(+), 371 deletions(-) diff --git a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java index bf0d7884..384421fb 100644 --- a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java +++ b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java @@ -140,39 +140,127 @@ private JsonSerializer derivedRecordSerializer(SerializationConfig confi } private JsonSerializer> catalogSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - return catalogSerDes(type, beanDesc, bosk).serializer(config); + JavaType entryType = catalogEntryType(type); + + return new JsonSerializer>() { + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public void serialize(Catalog value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + JsonSerializer valueSerializer = serializers.findContentValueSerializer(entryType, null); + writeMapEntries(gen, value.asMap().entrySet(), valueSerializer, serializers); + } + }; } private JsonSerializer> listingSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - return listingSerDes(type, beanDesc, bosk).serializer(config); + return new JsonSerializer>() { + @Override + public void serialize(Listing value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + + gen.writeFieldName("ids"); + serializers + .findContentValueSerializer(ID_LIST_TYPE, null) + .serialize(new ArrayList<>(value.ids()), gen, serializers); + + gen.writeFieldName("domain"); + serializers + .findContentValueSerializer(Reference.class, null) + .serialize(value.domain(), gen, serializers); + + gen.writeEndObject(); + } + }; } private JsonSerializer> referenceSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - return referenceSerDes(type, beanDesc, bosk).serializer(config); + return new JsonSerializer>() { + @Override + public void serialize(Reference value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(value.path().urlEncoded()); + } + }; } private JsonSerializer identifierSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - return identifierSerDes(type, beanDesc, bosk).serializer(config); + return new JsonSerializer() { + @Override + public void serialize(Identifier value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(value.toString()); + } + }; } private JsonSerializer listingEntrySerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - return listingEntrySerDes(type, beanDesc, bosk).serializer(config); + // We serialize ListingEntry as a boolean `true` with the following rationale: + // - The only "unit type" in JSON is null + // - `null` is not suitable because many systems treat that as being equivalent to an absent field + // - Of the other types, boolean seems the most likely to be efficiently processed in every system + // - `false` gives the wrong impression + // Hence, by a process of elimination, `true` it is + + return new JsonSerializer() { + @Override + public void serialize(ListingEntry value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeBoolean(true); + } + }; } private JsonSerializer> sideTableSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - return sideTableSerDes(type, beanDesc, bosk).serializer(config); + JavaType valueType = sideTableValueType(type); + return new JsonSerializer>() { + @Override + public void serialize(SideTable value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + + gen.writeFieldName("valuesById"); + @SuppressWarnings("unchecked") + JsonSerializer contentValueSerializer = (JsonSerializer) serializers.findContentValueSerializer(valueType, null); + writeMapEntries(gen, value.idEntrySet(), contentValueSerializer, serializers); + + gen.writeFieldName("domain"); + serializers + .findContentValueSerializer(Reference.class, null) + .serialize(value.domain(), gen, serializers); + + gen.writeEndObject(); + } + }; } private JsonSerializer stateTreeNodeSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - return stateTreeNodeSerDes(type, beanDesc, bosk).serializer(config); + StateTreeNodeFieldModerator moderator = new StateTreeNodeFieldModerator(type); + return compiler.compiled(type, bosk, moderator).serializer(config); } private JsonSerializer> listValueSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - return listValueSerDes(type, beanDesc, bosk).serializer(config); + JavaType listType = listValueEquivalentListType(type); + return new JsonSerializer>() { + @Override + public void serialize(ListValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + // Note that a ListValue can actually contain an Object[], + // which Jackson won't serialize as a String[], so we can't use arrayType. + serializers.findValueSerializer(listType, null) + .serialize(value, gen, serializers); + } + }; } private JsonSerializer> mapValueSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - return mapValueSerDes(type, beanDesc, bosk).serializer(config); + JavaType valueType = mapValueValueType(type); + return new JsonSerializer>() { + @Override + public void serialize(MapValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + JsonSerializer valueSerializer = serializers.findValueSerializer(valueType); + gen.writeStartObject(); + for (Entry element : value.entrySet()) { + gen.writeFieldName(requireNonNull(element.getKey())); + valueSerializer.serialize(requireNonNull(element.getValue()), gen, serializers); + } + gen.writeEndObject(); + } + }; } // Thanks but no thanks, Jackson. We don't need your help. @@ -234,39 +322,191 @@ private JsonDeserializer derivedRecordDeserializer(JavaType type, Deseri } private JsonDeserializer> catalogDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { - return catalogSerDes(type, beanDesc, bosk).deserializer(config); + JavaType entryType = catalogEntryType(type); + + return new BoskDeserializer>() { + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public Catalog deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonDeserializer valueDeserializer = ctxt.findContextualValueDeserializer(entryType, null); + LinkedHashMap entries = readMapEntries(p, valueDeserializer, ctxt); + return Catalog.of(entries.values()); + } + }; } private JsonDeserializer> listingDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { - return listingSerDes(type, beanDesc, bosk).deserializer(config); + return new BoskDeserializer>() { + @Override + @SuppressWarnings("unchecked") + public Listing deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + Reference> domain = null; + List ids = null; + + expect(START_OBJECT, p); + while (p.nextToken() != END_OBJECT) { + p.nextValue(); + switch (p.currentName()) { + case "ids": + if (ids != null) { + throw new JsonParseException(p, "'ids' field appears twice"); + } + ids = (List) ctxt + .findContextualValueDeserializer(ID_LIST_TYPE, null) + .deserialize(p, ctxt); + break; + case "domain": + if (domain != null) { + throw new JsonParseException(p, "'domain' field appears twice"); + } + domain = (Reference>) ctxt + .findContextualValueDeserializer(CATALOG_REF_TYPE, null) + .deserialize(p, ctxt); + break; + default: + throw new JsonParseException(p, "Unrecognized field in Listing: " + p.currentName()); + } + } + + if (domain == null) { + throw new JsonParseException(p, "Missing 'domain' field"); + } else if (ids == null) { + throw new JsonParseException(p, "Missing 'ids' field"); + } else { + return Listing.of(domain, ids); + } + } + }; } private JsonDeserializer> referenceDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { - return referenceSerDes(type, beanDesc, bosk).deserializer(config); + return new BoskDeserializer>() { + @Override + public Reference deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + try { + return bosk.reference(Object.class, Path.parse(p.getText())); + } catch (InvalidTypeException e) { + throw new UnexpectedPathException(e); + } + } + }; } private JsonDeserializer identifierDeserialier(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { - return identifierSerDes(type, beanDesc, bosk).deserializer(config); + return new BoskDeserializer() { + @Override + public Identifier deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return Identifier.from(p.getText()); + } + }; } private JsonDeserializer listingEntryDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { - return listingEntrySerDes(type, beanDesc, bosk).deserializer(config); + return new BoskDeserializer() { + @Override + public ListingEntry deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.getBooleanValue()) { + return LISTING_ENTRY; + } else { + throw new JsonParseException(p, "Unexpected Listing entry value: " + p.getBooleanValue()); + } + } + }; } private JsonDeserializer> sideTableDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { - return sideTableSerDes(type, beanDesc, bosk).deserializer(config); + JavaType valueType = sideTableValueType(type); + return new BoskDeserializer>() { + @Override + public SideTable deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + Reference> domain = null; + LinkedHashMap valuesById = null; + + JsonDeserializer valueDeserializer = (JsonDeserializer) ctxt.findContextualValueDeserializer(valueType, null); + + expect(START_OBJECT, p); + while (p.nextToken() != END_OBJECT) { + p.nextValue(); + switch (p.currentName()) { + case "valuesById": + if (valuesById == null) { + valuesById = readMapEntries(p, valueDeserializer, ctxt); + } else { + throw new JsonParseException(p, "'valuesById' field appears twice"); + } + break; + case "domain": + if (domain == null) { + domain = (Reference>) ctxt + .findContextualValueDeserializer(CATALOG_REF_TYPE, null) + .deserialize(p, ctxt); + } else { + throw new JsonParseException(p, "'domain' field appears twice"); + } + break; + default: + throw new JsonParseException(p, "Unrecognized field in SideTable: " + p.currentName()); + } + } + expect(END_OBJECT, p); + + if (domain == null) { + throw new JsonParseException(p, "Missing 'domain' field"); + } else if (valuesById == null) { + throw new JsonParseException(p, "Missing 'valuesById' field"); + } else { + return SideTable.fromOrderedMap(domain, valuesById); + } + } + }; } private JsonDeserializer stateTreeNodeDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { - return stateTreeNodeSerDes(type, beanDesc, bosk).deserializer(config); + StateTreeNodeFieldModerator moderator = new StateTreeNodeFieldModerator(type); + return compiler.compiled(type, bosk, moderator).deserializer(config); } private JsonDeserializer> listValueDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { - return listValueSerDes(type, beanDesc, bosk).deserializer(config); + Constructor ctor = theOnlyConstructorFor(type.getRawClass()); + JavaType arrayType = listValueEquivalentArrayType(type); + return new BoskDeserializer>() { + @Override + @SuppressWarnings({"unchecked"}) + public ListValue deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + Object elementArray = ctxt + .findContextualValueDeserializer(arrayType, null) + .deserialize(p, ctxt); + try { + return (ListValue) ctor.newInstance(elementArray); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new IOException("Failed to instantiate " + type.getRawClass().getSimpleName() + ": " + e.getMessage(), e); + } + } + }; } private JsonDeserializer> mapValueDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) { - return mapValueSerDes(type, beanDesc, bosk).deserializer(config); + JavaType valueType = mapValueValueType(type); + return new BoskDeserializer>() { + @Override + public MapValue deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + LinkedHashMap result1 = new LinkedHashMap<>(); + expect(START_OBJECT, p); + while (p.nextToken() != END_OBJECT) { + p.nextValue(); + String key = p.currentName(); + @SuppressWarnings("unchecked") + Object value = (Object) ctxt.findContextualValueDeserializer(valueType, null) + .deserialize(p, ctxt); + Object old = result1.put(key, value); + if (old != null) { + throw new JsonParseException(p, "MapValue key appears twice: \"" + key + "\""); + } + } + expect(END_OBJECT, p); + return MapValue.fromOrderedMap(result1); + } + }; } // Thanks but no thanks, Jackson. We don't need your help. @@ -294,264 +534,6 @@ private abstract static class BoskDeserializer extends JsonDeserializer { @Override public boolean isCachable() { return true; } } - private SerDes> listValueSerDes(JavaType type, BeanDescription beanDesc, Bosk bosk) { - Constructor ctor = theOnlyConstructorFor(type.getRawClass()); - JavaType arrayType = listValueEquivalentArrayType(type); - JavaType listType = listValueEquivalentListType(type); - return new SerDes>() { - @Override - public JsonSerializer> serializer(SerializationConfig serializationConfig) { - return new JsonSerializer>() { - @Override - public void serialize(ListValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - // Note that a ListValue can actually contain an Object[], - // which Jackson won't serialize as a String[], so we can't use arrayType. - serializers.findValueSerializer(listType, null) - .serialize(value, gen, serializers); - } - }; - } - - @Override - public JsonDeserializer> deserializer(DeserializationConfig deserializationConfig) { - return new BoskDeserializer>() { - @Override - @SuppressWarnings({"unchecked"}) - public ListValue deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - Object elementArray = ctxt - .findContextualValueDeserializer(arrayType, null) - .deserialize(p, ctxt); - try { - return (ListValue) ctor.newInstance(elementArray); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new IOException("Failed to instantiate " + type.getRawClass().getSimpleName() + ": " + e.getMessage(), e); - } - } - }; - } - }; - } - - private SerDes> mapValueSerDes(JavaType type, BeanDescription beanDesc, Bosk bosk) { - JavaType valueType = mapValueValueType(type); - return new SerDes>() { - @Override - public JsonSerializer> serializer(SerializationConfig serializationConfig) { - return new JsonSerializer>() { - @Override - public void serialize(MapValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - JsonSerializer valueSerializer = serializers.findValueSerializer(valueType); - gen.writeStartObject(); - for (Entry element : value.entrySet()) { - gen.writeFieldName(requireNonNull(element.getKey())); - valueSerializer.serialize(requireNonNull(element.getValue()), gen, serializers); - } - gen.writeEndObject(); - } - }; - } - - @Override - public JsonDeserializer> deserializer(DeserializationConfig deserializationConfig) { - return new BoskDeserializer>() { - @Override - public MapValue deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - LinkedHashMap result = new LinkedHashMap<>(); - expect(START_OBJECT, p); - while (p.nextToken() != END_OBJECT) { - p.nextValue(); - String key = p.currentName(); - @SuppressWarnings("unchecked") - V value = (V) ctxt.findContextualValueDeserializer(valueType, null) - .deserialize(p, ctxt); - V old = result.put(key, value); - if (old != null) { - throw new JsonParseException(p, "MapValue key appears twice: \"" + key + "\""); - } - } - expect(END_OBJECT, p); - return MapValue.fromOrderedMap(result); - } - }; - } - }; - } - - private SerDes> referenceSerDes(JavaType type, BeanDescription beanDesc, Bosk bosk) { - return new SerDes>() { - @Override - public JsonSerializer> serializer(SerializationConfig config) { - return new JsonSerializer>() { - @Override - public void serialize(Reference value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeString(value.path().urlEncoded()); - } - }; - } - - @Override - public JsonDeserializer> deserializer(DeserializationConfig config) { - return new BoskDeserializer>() { - @Override - public Reference deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - try { - return bosk.reference(Object.class, Path.parse(p.getText())); - } catch (InvalidTypeException e) { - throw new UnexpectedPathException(e); - } - } - }; - } - }; - } - - private SerDes> listingSerDes(JavaType type, BeanDescription beanDesc, Bosk bosk) { - return new SerDes>() { - @Override - public JsonSerializer> serializer(SerializationConfig config) { - return new JsonSerializer>() { - @Override - public void serialize(Listing value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeStartObject(); - - gen.writeFieldName("ids"); - serializers - .findContentValueSerializer(ID_LIST_TYPE, null) - .serialize(new ArrayList<>(value.ids()), gen, serializers); - - gen.writeFieldName("domain"); - serializers - .findContentValueSerializer(Reference.class, null) - .serialize(value.domain(), gen, serializers); - - gen.writeEndObject(); - } - }; - } - - @Override - public JsonDeserializer> deserializer(DeserializationConfig config) { - return new BoskDeserializer>() { - @Override - @SuppressWarnings("unchecked") - public Listing deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - Reference> domain = null; - List ids = null; - - expect(START_OBJECT, p); - while (p.nextToken() != END_OBJECT) { - p.nextValue(); - switch (p.currentName()) { - case "ids": - if (ids != null) { - throw new JsonParseException(p, "'ids' field appears twice"); - } - ids = (List) ctxt - .findContextualValueDeserializer(ID_LIST_TYPE, null) - .deserialize(p, ctxt); - break; - case "domain": - if (domain != null) { - throw new JsonParseException(p, "'domain' field appears twice"); - } - domain = (Reference>) ctxt - .findContextualValueDeserializer(CATALOG_REF_TYPE, null) - .deserialize(p, ctxt); - break; - default: - throw new JsonParseException(p, "Unrecognized field in Listing: " + p.currentName()); - } - } - - if (domain == null) { - throw new JsonParseException(p, "Missing 'domain' field"); - } else if (ids == null) { - throw new JsonParseException(p, "Missing 'ids' field"); - } else { - return Listing.of(domain, ids); - } - } - }; - } - }; - } - - private SerDes> sideTableSerDes(JavaType type, BeanDescription beanDesc, Bosk bosk) { - JavaType valueType = sideTableValueType(type); - return new SerDes>() { - @Override - public JsonSerializer> serializer(SerializationConfig config) { - return new JsonSerializer>() { - @Override - public void serialize(SideTable value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeStartObject(); - - gen.writeFieldName("valuesById"); - @SuppressWarnings("unchecked") - JsonSerializer contentValueSerializer = (JsonSerializer) serializers.findContentValueSerializer(valueType, null); - writeMapEntries(gen, value.idEntrySet(), contentValueSerializer, serializers); - - gen.writeFieldName("domain"); - serializers - .findContentValueSerializer(Reference.class, null) - .serialize(value.domain(), gen, serializers); - - gen.writeEndObject(); - } - }; - } - - @Override - @SuppressWarnings("unchecked") - public JsonDeserializer> deserializer(DeserializationConfig config) { - return new BoskDeserializer>() { - @Override - public SideTable deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - Reference> domain = null; - LinkedHashMap valuesById = null; - - JsonDeserializer valueDeserializer = (JsonDeserializer) ctxt.findContextualValueDeserializer(valueType, null); - - expect(START_OBJECT, p); - while (p.nextToken() != END_OBJECT) { - p.nextValue(); - switch (p.currentName()) { - case "valuesById": - if (valuesById == null) { - valuesById = readMapEntries(p, valueDeserializer, ctxt); - } else { - throw new JsonParseException(p, "'valuesById' field appears twice"); - } - break; - case "domain": - if (domain == null) { - domain = (Reference>) ctxt - .findContextualValueDeserializer(CATALOG_REF_TYPE, null) - .deserialize(p, ctxt); - } else { - throw new JsonParseException(p, "'domain' field appears twice"); - } - break; - default: - throw new JsonParseException(p, "Unrecognized field in SideTable: " + p.currentName()); - } - } - expect(END_OBJECT, p); - - if (domain == null) { - throw new JsonParseException(p, "Missing 'domain' field"); - } else if (valuesById == null) { - throw new JsonParseException(p, "Missing 'valuesById' field"); - } else { - return SideTable.fromOrderedMap(domain, valuesById); - } - } - }; - } - - }; - } - private void writeMapEntries(JsonGenerator gen, Set> entries, JsonSerializer valueSerializer, SerializerProvider serializers) throws IOException { gen.writeStartArray(); for (Entry entry: entries) { @@ -588,107 +570,12 @@ private LinkedHashMap readMapEntries(JsonParser p, JsonDeseri return result; } - private SerDes> catalogSerDes(JavaType type, BeanDescription beanDesc, Bosk bosk) { - JavaType entryType = catalogEntryType(type); - - return new SerDes>() { - @Override - public JsonSerializer> serializer(SerializationConfig config) { - return new JsonSerializer>() { - @Override - @SuppressWarnings({"rawtypes", "unchecked"}) - public void serialize(Catalog value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - JsonSerializer valueSerializer = serializers.findContentValueSerializer(entryType, null); - writeMapEntries(gen, value.asMap().entrySet(), valueSerializer, serializers); - } - }; - } - - @Override - public JsonDeserializer> deserializer(DeserializationConfig config) { - return new BoskDeserializer>() { - @Override - @SuppressWarnings({"rawtypes", "unchecked"}) - public Catalog deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - JsonDeserializer valueDeserializer = ctxt.findContextualValueDeserializer(entryType, null); - LinkedHashMap entries = readMapEntries(p, valueDeserializer, ctxt); - return Catalog.of(entries.values()); - } - }; - } - }; - } - private static final JavaType ID_LIST_TYPE = TypeFactory.defaultInstance().constructType(new TypeReference< List>() {}); private static final JavaType CATALOG_REF_TYPE = TypeFactory.defaultInstance().constructType(new TypeReference< Reference>>() {}); - private SerDes identifierSerDes(JavaType type, BeanDescription beanDesc, Bosk bosk) { - return new SerDes() { - @Override - public JsonSerializer serializer(SerializationConfig config) { - return new JsonSerializer() { - @Override - public void serialize(Identifier value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeString(value.toString()); - } - }; - } - - @Override - public JsonDeserializer deserializer(DeserializationConfig config) { - return new BoskDeserializer() { - @Override - public Identifier deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return Identifier.from(p.getText()); - } - }; - } - }; - } - - private SerDes listingEntrySerDes(JavaType type, BeanDescription beanDesc, Bosk bosk) { - // We serialize ListingEntry as a boolean `true` with the following rationale: - // - The only "unit type" in JSON is null - // - `null` is not suitable because many systems treat that as being equivalent to an absent field - // - Of the other types, boolean seems the most likely to be efficiently processed in every system - // - `false` gives the wrong impression - // Hence, by a process of elimination, `true` it is - - return new SerDes() { - @Override - public JsonSerializer serializer(SerializationConfig config) { - return new JsonSerializer() { - @Override - public void serialize(ListingEntry value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeBoolean(true); - } - }; - } - - @Override - public JsonDeserializer deserializer(DeserializationConfig config) { - return new BoskDeserializer() { - @Override - public ListingEntry deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - if (p.getBooleanValue()) { - return LISTING_ENTRY; - } else { - throw new JsonParseException(p, "Unexpected Listing entry value: " + p.getBooleanValue()); - } - } - }; - } - }; - } - - private SerDes stateTreeNodeSerDes(JavaType type, BeanDescription beanDesc, Bosk bosk) { - StateTreeNodeFieldModerator moderator = new StateTreeNodeFieldModerator(type); - return compiler.compiled(type, bosk, moderator); - } - private SerDes derivedRecordSerDes(JavaType objType, BeanDescription beanDesc, Bosk bosk) { // Check for special cases Class objClass = objType.getRawClass(); From a224d423d26fa198f05cccf543c35d9ea70a69b9 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 13:52:57 -0400 Subject: [PATCH 06/15] Refactor: delete redundant SuppressWarnings --- .../src/main/java/io/vena/bosk/jackson/JacksonPlugin.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java index 384421fb..44aa72b3 100644 --- a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java +++ b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java @@ -144,7 +144,6 @@ private JsonSerializer> catalogSerializer(SerializationConfig co return new JsonSerializer>() { @Override - @SuppressWarnings({"rawtypes", "unchecked"}) public void serialize(Catalog value, JsonGenerator gen, SerializerProvider serializers) throws IOException { JsonSerializer valueSerializer = serializers.findContentValueSerializer(entryType, null); writeMapEntries(gen, value.asMap().entrySet(), valueSerializer, serializers); @@ -495,8 +494,7 @@ public MapValue deserialize(JsonParser p, DeserializationContext ctxt) t while (p.nextToken() != END_OBJECT) { p.nextValue(); String key = p.currentName(); - @SuppressWarnings("unchecked") - Object value = (Object) ctxt.findContextualValueDeserializer(valueType, null) + Object value = ctxt.findContextualValueDeserializer(valueType, null) .deserialize(p, ctxt); Object old = result1.put(key, value); if (old != null) { From 5d1e9c5030ce601ad91f82aa30c63ca3f9284df9 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 13:39:10 -0400 Subject: [PATCH 07/15] Refactor: determine valueSerializer inside writeMapEntries --- .../main/java/io/vena/bosk/jackson/JacksonPlugin.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java index 44aa72b3..3f30548b 100644 --- a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java +++ b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java @@ -145,8 +145,7 @@ private JsonSerializer> catalogSerializer(SerializationConfig co return new JsonSerializer>() { @Override public void serialize(Catalog value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - JsonSerializer valueSerializer = serializers.findContentValueSerializer(entryType, null); - writeMapEntries(gen, value.asMap().entrySet(), valueSerializer, serializers); + writeMapEntries(gen, value.asMap().entrySet(), entryType, serializers); } }; } @@ -214,9 +213,7 @@ public void serialize(SideTable value, JsonGenerator gen, Serial gen.writeStartObject(); gen.writeFieldName("valuesById"); - @SuppressWarnings("unchecked") - JsonSerializer contentValueSerializer = (JsonSerializer) serializers.findContentValueSerializer(valueType, null); - writeMapEntries(gen, value.idEntrySet(), contentValueSerializer, serializers); + writeMapEntries(gen, value.idEntrySet(), valueType, serializers); gen.writeFieldName("domain"); serializers @@ -532,7 +529,8 @@ private abstract static class BoskDeserializer extends JsonDeserializer { @Override public boolean isCachable() { return true; } } - private void writeMapEntries(JsonGenerator gen, Set> entries, JsonSerializer valueSerializer, SerializerProvider serializers) throws IOException { + private void writeMapEntries(JsonGenerator gen, Set> entries, JavaType entryType, SerializerProvider serializers) throws IOException { + JsonSerializer valueSerializer = serializers.findContentValueSerializer(entryType, null); gen.writeStartArray(); for (Entry entry: entries) { gen.writeStartObject(); From 04462a973ee1a7dfc1c9b6c2b88bf075cec99445 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 13:42:08 -0400 Subject: [PATCH 08/15] Refactor: compute valueSerializer for each map entry --- .../src/main/java/io/vena/bosk/jackson/JacksonPlugin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java index 3f30548b..a7ac2db5 100644 --- a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java +++ b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java @@ -248,10 +248,10 @@ private JsonSerializer> mapValueSerializer(SerializationConfig return new JsonSerializer>() { @Override public void serialize(MapValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - JsonSerializer valueSerializer = serializers.findValueSerializer(valueType); gen.writeStartObject(); for (Entry element : value.entrySet()) { gen.writeFieldName(requireNonNull(element.getKey())); + JsonSerializer valueSerializer = serializers.findValueSerializer(valueType); valueSerializer.serialize(requireNonNull(element.getValue()), gen, serializers); } gen.writeEndObject(); @@ -530,11 +530,11 @@ private abstract static class BoskDeserializer extends JsonDeserializer { } private void writeMapEntries(JsonGenerator gen, Set> entries, JavaType entryType, SerializerProvider serializers) throws IOException { - JsonSerializer valueSerializer = serializers.findContentValueSerializer(entryType, null); gen.writeStartArray(); for (Entry entry: entries) { gen.writeStartObject(); gen.writeFieldName(entry.getKey().toString()); + JsonSerializer valueSerializer = serializers.findContentValueSerializer(entryType, null); valueSerializer.serialize(entry.getValue(), gen, serializers); gen.writeEndObject(); } From cfdd41d4608fc97767857adb508b47e36c4d5721 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 13:48:14 -0400 Subject: [PATCH 09/15] Delete unnecessary ListValueSerializer. ListValue implements List, so Jackson already handles it just fine. --- .../io/vena/bosk/jackson/JacksonPlugin.java | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java index a7ac2db5..9a125b91 100644 --- a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java +++ b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java @@ -126,8 +126,6 @@ public JsonSerializer findSerializer(SerializationConfig config, JavaType typ throw new IllegalArgumentException("Cannot serialize an Optional on its own; only as a field of another object"); } else if (Phantom.class.isAssignableFrom(theClass)) { throw new IllegalArgumentException("Cannot serialize a Phantom on its own; only as a field of another object"); - } else if (ListValue.class.isAssignableFrom(theClass)) { - return listValueSerializer(config, type, beanDesc); } else if (MapValue.class.isAssignableFrom(theClass)) { return mapValueSerializer(config, type, beanDesc); } else { @@ -230,19 +228,6 @@ private JsonSerializer stateTreeNodeSerializer(SerializationConfi return compiler.compiled(type, bosk, moderator).serializer(config); } - private JsonSerializer> listValueSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - JavaType listType = listValueEquivalentListType(type); - return new JsonSerializer>() { - @Override - public void serialize(ListValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - // Note that a ListValue can actually contain an Object[], - // which Jackson won't serialize as a String[], so we can't use arrayType. - serializers.findValueSerializer(listType, null) - .serialize(value, gen, serializers); - } - }; - } - private JsonSerializer> mapValueSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { JavaType valueType = mapValueValueType(type); return new JsonSerializer>() { @@ -791,10 +776,6 @@ private static JavaType sideTableValueType(JavaType sideTableType) { return javaParameterType(sideTableType, SideTable.class, 1); } - private static JavaType listValueEquivalentListType(JavaType listValueType) { - return TypeFactory.defaultInstance().constructCollectionType(List.class, javaParameterType(listValueType, ListValue.class, 0)); - } - private static JavaType listValueEquivalentArrayType(JavaType listValueType) { return TypeFactory.defaultInstance().constructArrayType(javaParameterType(listValueType, ListValue.class, 0)); } From aa0cd93aeb302544e9e1b51dc99369e574443ec7 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 12:57:10 -0400 Subject: [PATCH 10/15] WIP - stop calling ObjectMapper.writerFor. This is relying on rich type info that Jackson won't always have available. We should be able to serialize without this. With this change, some tests fail. --- .../java/io/vena/bosk/jackson/JacksonPluginTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java b/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java index 1a429571..6caaa497 100644 --- a/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java +++ b/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java @@ -228,7 +228,7 @@ void rootReference_works() throws JsonProcessingException { void listValue_serializationWorks(List list, JavaType type) throws JsonProcessingException { ListValue listValue = ListValue.from(list); String expected = plainMapper.writeValueAsString(list); - assertEquals(expected, boskMapper.writerFor(type).writeValueAsString(listValue)); + assertEquals(expected, boskMapper.writeValueAsString(listValue)); } @ParameterizedTest @@ -268,7 +268,7 @@ private static Arguments listValueCase(Type entryType, Object...entries) { void mapValue_serializationWorks(Map map, JavaType type) throws JsonProcessingException { MapValue mapValue = MapValue.fromOrderedMap(map); String expected = plainMapper.writeValueAsString(map); - assertEquals(expected, boskMapper.writerFor(type).writeValueAsString(mapValue)); + assertEquals(expected, boskMapper.writeValueAsString(mapValue)); } @ParameterizedTest @@ -486,9 +486,8 @@ private void assertJacksonWorks(List plainList, Object boskObject, TypeRefere private Map plainObjectFor(Object boskObject, JavaType boskObjectType) { try { - JavaType boskJavaType = TypeFactory.defaultInstance().constructType(boskObjectType); JavaType mapJavaType = TypeFactory.defaultInstance().constructParametricType(Map.class, String.class, Object.class); - String json = boskMapper.writerFor(boskJavaType).writeValueAsString(boskObject); + String json = boskMapper.writeValueAsString(boskObject); return plainMapper.readerFor(mapJavaType).readValue(json); } catch (JsonProcessingException e) { throw new AssertionError(e); @@ -497,9 +496,8 @@ private Map plainObjectFor(Object boskObject, JavaType boskObjec private List plainListFor(Object boskObject, JavaType boskObjectType) { try { - JavaType boskJavaType = TypeFactory.defaultInstance().constructType(boskObjectType); JavaType listJavaType = TypeFactory.defaultInstance().constructParametricType(List.class, Object.class); - String json = boskMapper.writerFor(boskJavaType).writeValueAsString(boskObject); + String json = boskMapper.writeValueAsString(boskObject); return plainMapper.readerFor(listJavaType).readValue(json); } catch (JsonProcessingException e) { throw new AssertionError(e); From b3cdd9177ddb24940dd2fdc88401a6fb9dfd2f1d Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 13:57:50 -0400 Subject: [PATCH 11/15] Fix: serialize map values on dynamic type info. Static declared type info is not always available in Jackson. Note that "map values" is not to be confused with "MapValues", though the former does include the latter. (Phew, good thing I prevented confusion there.) --- .../java/io/vena/bosk/jackson/JacksonPlugin.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java index 9a125b91..e2e15adb 100644 --- a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java +++ b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java @@ -138,12 +138,10 @@ private JsonSerializer derivedRecordSerializer(SerializationConfig confi } private JsonSerializer> catalogSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - JavaType entryType = catalogEntryType(type); - return new JsonSerializer>() { @Override public void serialize(Catalog value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - writeMapEntries(gen, value.asMap().entrySet(), entryType, serializers); + writeMapEntries(gen, value.asMap().entrySet(), serializers); } }; } @@ -204,14 +202,13 @@ public void serialize(ListingEntry value, JsonGenerator gen, SerializerProvider } private JsonSerializer> sideTableSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - JavaType valueType = sideTableValueType(type); return new JsonSerializer>() { @Override public void serialize(SideTable value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeStartObject(); gen.writeFieldName("valuesById"); - writeMapEntries(gen, value.idEntrySet(), valueType, serializers); + writeMapEntries(gen, value.idEntrySet(), serializers); gen.writeFieldName("domain"); serializers @@ -236,8 +233,9 @@ public void serialize(MapValue value, JsonGenerator gen, SerializerProvi gen.writeStartObject(); for (Entry element : value.entrySet()) { gen.writeFieldName(requireNonNull(element.getKey())); - JsonSerializer valueSerializer = serializers.findValueSerializer(valueType); - valueSerializer.serialize(requireNonNull(element.getValue()), gen, serializers); + Object val = requireNonNull(element.getValue()); + JsonSerializer valueSerializer = serializers.findValueSerializer(val.getClass()); + valueSerializer.serialize(val, gen, serializers); } gen.writeEndObject(); } @@ -514,12 +512,12 @@ private abstract static class BoskDeserializer extends JsonDeserializer { @Override public boolean isCachable() { return true; } } - private void writeMapEntries(JsonGenerator gen, Set> entries, JavaType entryType, SerializerProvider serializers) throws IOException { + private void writeMapEntries(JsonGenerator gen, Set> entries, SerializerProvider serializers) throws IOException { gen.writeStartArray(); for (Entry entry: entries) { gen.writeStartObject(); gen.writeFieldName(entry.getKey().toString()); - JsonSerializer valueSerializer = serializers.findContentValueSerializer(entryType, null); + JsonSerializer valueSerializer = serializers.findContentValueSerializer(entry.getValue().getClass(), null); valueSerializer.serialize(entry.getValue(), gen, serializers); gen.writeEndObject(); } From 50c681801196f009690873901ed9243f10d08907 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 14:02:23 -0400 Subject: [PATCH 12/15] Refactor: stop accepting type info when getting serializers --- .../io/vena/bosk/jackson/JacksonPlugin.java | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java index e2e15adb..ef60532a 100644 --- a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java +++ b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java @@ -108,17 +108,17 @@ public JsonSerializer findSerializer(SerializationConfig config, JavaType typ if (theClass.isAnnotationPresent(DerivedRecord.class)) { return derivedRecordSerializer(config, type, beanDesc); } else if (Catalog.class.isAssignableFrom(theClass)) { - return catalogSerializer(config, type, beanDesc); + return catalogSerializer(config, beanDesc); } else if (Listing.class.isAssignableFrom(theClass)) { - return listingSerializer(config, type, beanDesc); + return listingSerializer(config, beanDesc); } else if (Reference.class.isAssignableFrom(theClass)) { - return referenceSerializer(config, type, beanDesc); + return referenceSerializer(config, beanDesc); } else if (Identifier.class.isAssignableFrom(theClass)) { - return identifierSerializer(config, type, beanDesc); + return identifierSerializer(config, beanDesc); } else if (ListingEntry.class.isAssignableFrom(theClass)) { - return listingEntrySerializer(config, type, beanDesc); + return listingEntrySerializer(config, beanDesc); } else if (SideTable.class.isAssignableFrom(theClass)) { - return sideTableSerializer(config, type, beanDesc); + return sideTableSerializer(config, beanDesc); } else if (StateTreeNode.class.isAssignableFrom(theClass)) { return stateTreeNodeSerializer(config, type, beanDesc); } else if (Optional.class.isAssignableFrom(theClass)) { @@ -127,7 +127,7 @@ public JsonSerializer findSerializer(SerializationConfig config, JavaType typ } else if (Phantom.class.isAssignableFrom(theClass)) { throw new IllegalArgumentException("Cannot serialize a Phantom on its own; only as a field of another object"); } else if (MapValue.class.isAssignableFrom(theClass)) { - return mapValueSerializer(config, type, beanDesc); + return mapValueSerializer(config, beanDesc); } else { return null; } @@ -137,7 +137,7 @@ private JsonSerializer derivedRecordSerializer(SerializationConfig confi return derivedRecordSerDes(type, beanDesc, bosk).serializer(config); } - private JsonSerializer> catalogSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + private JsonSerializer> catalogSerializer(SerializationConfig config, BeanDescription beanDesc) { return new JsonSerializer>() { @Override public void serialize(Catalog value, JsonGenerator gen, SerializerProvider serializers) throws IOException { @@ -146,7 +146,7 @@ public void serialize(Catalog value, JsonGenerator gen, SerializerProvid }; } - private JsonSerializer> listingSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + private JsonSerializer> listingSerializer(SerializationConfig config, BeanDescription beanDesc) { return new JsonSerializer>() { @Override public void serialize(Listing value, JsonGenerator gen, SerializerProvider serializers) throws IOException { @@ -167,7 +167,7 @@ public void serialize(Listing value, JsonGenerator gen, SerializerProvid }; } - private JsonSerializer> referenceSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + private JsonSerializer> referenceSerializer(SerializationConfig config, BeanDescription beanDesc) { return new JsonSerializer>() { @Override public void serialize(Reference value, JsonGenerator gen, SerializerProvider serializers) throws IOException { @@ -176,7 +176,7 @@ public void serialize(Reference value, JsonGenerator gen, SerializerProvider }; } - private JsonSerializer identifierSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + private JsonSerializer identifierSerializer(SerializationConfig config, BeanDescription beanDesc) { return new JsonSerializer() { @Override public void serialize(Identifier value, JsonGenerator gen, SerializerProvider serializers) throws IOException { @@ -185,7 +185,7 @@ public void serialize(Identifier value, JsonGenerator gen, SerializerProvider se }; } - private JsonSerializer listingEntrySerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + private JsonSerializer listingEntrySerializer(SerializationConfig config, BeanDescription beanDesc) { // We serialize ListingEntry as a boolean `true` with the following rationale: // - The only "unit type" in JSON is null // - `null` is not suitable because many systems treat that as being equivalent to an absent field @@ -201,7 +201,7 @@ public void serialize(ListingEntry value, JsonGenerator gen, SerializerProvider }; } - private JsonSerializer> sideTableSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + private JsonSerializer> sideTableSerializer(SerializationConfig config, BeanDescription beanDesc) { return new JsonSerializer>() { @Override public void serialize(SideTable value, JsonGenerator gen, SerializerProvider serializers) throws IOException { @@ -225,8 +225,7 @@ private JsonSerializer stateTreeNodeSerializer(SerializationConfi return compiler.compiled(type, bosk, moderator).serializer(config); } - private JsonSerializer> mapValueSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { - JavaType valueType = mapValueValueType(type); + private JsonSerializer> mapValueSerializer(SerializationConfig config, BeanDescription beanDesc) { return new JsonSerializer>() { @Override public void serialize(MapValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException { From 7a82f61fd3fc083fd27f976f4df96cecbbf02f27 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 14:59:39 -0400 Subject: [PATCH 13/15] Refactor: delete unused type parameter --- .../io/vena/bosk/jackson/JacksonPluginTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java b/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java index 6caaa497..b30f5fc1 100644 --- a/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java +++ b/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java @@ -106,7 +106,7 @@ void catalog_works(List ids) { // Build the expected JSON structure List> expected = new ArrayList<>(); - entities.forEach(e1 -> expected.add(singletonMap(e1.id().toString(), plainObjectFor(e1, TypeFactory.defaultInstance().constructType(e1.getClass()))))); + entities.forEach(e1 -> expected.add(singletonMap(e1.id().toString(), plainObjectFor(e1)))); assertJacksonWorks(expected, catalog, new TypeReference>(){}, Path.just(TestRoot.Fields.entities)); } @@ -460,31 +460,31 @@ private TestEntity makeEntityWithOptionalString(Optional optionalString) private void assertJacksonWorks(Map plainObject, Object boskObject, TypeReference boskObjectTypeRef, Path path) { JavaType boskObjectType = TypeFactory.defaultInstance().constructType(boskObjectTypeRef); - Map actualPlainObject = plainObjectFor(boskObject, boskObjectType); + Map actualPlainObject = plainObjectFor(boskObject); assertEquals(plainObject, actualPlainObject, "Serialized object should match expected"); Object deserializedBoskObject = boskObjectFor(plainObject, boskObjectType, path); assertEquals(boskObject, deserializedBoskObject, "Deserialized object should match expected"); - Map roundTripPlainObject = plainObjectFor(deserializedBoskObject, boskObjectType); + Map roundTripPlainObject = plainObjectFor(deserializedBoskObject); assertEquals(plainObject, roundTripPlainObject, "Round-trip serialized object should match expected"); } private void assertJacksonWorks(List plainList, Object boskObject, TypeReference boskObjectTypeRef, Path path) { JavaType boskObjectType = TypeFactory.defaultInstance().constructType(boskObjectTypeRef); - List actualPlainList = plainListFor(boskObject, boskObjectType); + List actualPlainList = plainListFor(boskObject); assertEquals(plainList, actualPlainList, "Serialized object should match expected"); Object deserializedBoskObject = boskListFor(plainList, boskObjectType, path); assertEquals(boskObject, deserializedBoskObject, "Deserialized object should match expected"); - List roundTripPlainObject = plainListFor(deserializedBoskObject, boskObjectType); + List roundTripPlainObject = plainListFor(deserializedBoskObject); assertEquals(plainList, roundTripPlainObject, "Round-trip serialized object should match expected"); } - private Map plainObjectFor(Object boskObject, JavaType boskObjectType) { + private Map plainObjectFor(Object boskObject) { try { JavaType mapJavaType = TypeFactory.defaultInstance().constructParametricType(Map.class, String.class, Object.class); String json = boskMapper.writeValueAsString(boskObject); @@ -494,7 +494,7 @@ private Map plainObjectFor(Object boskObject, JavaType boskObjec } } - private List plainListFor(Object boskObject, JavaType boskObjectType) { + private List plainListFor(Object boskObject) { try { JavaType listJavaType = TypeFactory.defaultInstance().constructParametricType(List.class, Object.class); String json = boskMapper.writeValueAsString(boskObject); From f0700ddb41f95b822f641c579f4cb5987d79e064 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 14:58:02 -0400 Subject: [PATCH 14/15] Various fixes for JacksonPluginTest --- .../vena/bosk/jackson/JacksonPluginTest.java | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java b/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java index b30f5fc1..8bb6b374 100644 --- a/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java +++ b/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java @@ -246,15 +246,6 @@ private static Stream listValueArguments() { listValueCase(String.class), listValueCase(String.class, "Hello"), listValueCase(String.class, "first", "second") - /* - TODO: We can't yet handle parameterized node types! - Can't tell that inside NodeWithGenerics the field listOfA has type ListValue. - We currently don't do parameter substitution on type variables. - - listValueCase( - parameterizedType(NodeWithGenerics.class, Double.class, Integer.class), - new NodeWithGenerics<>(ListValue.of(1.0, 2.0), ListValue.of(3, 4))) - */ ); } @@ -263,6 +254,37 @@ private static Arguments listValueCase(Type entryType, Object...entries) { return Arguments.of(asList(entries), TypeFactory.defaultInstance().constructParametricType(ListValue.class, entryJavaType)); } + @Test + void listValue_parameterizedElement_works() { + var actual = new NodeWithGenerics<>( + ListValue.of(1, 2), + ListValue.of( + new NodeWithGenerics<>( + ListValue.of(3.0, 4.0), + ListValue.of("string1, string2") + ) + ) + ); + LinkedHashMap expectedB = new LinkedHashMap<>(); + expectedB.put("listOfA", asList(3.0, 4.0)); + expectedB.put("listOfB", asList("string1, string2")); + Map expected = new LinkedHashMap<>(); + expected.put("listOfA", asList(1, 2)); + expected.put("listOfB", singletonList(expectedB)); + + assertJacksonWorks( + expected, + actual, + new TypeReference>>() {}, + Path.empty()); + } + + @Value + public static class NodeWithGenerics implements StateTreeNode { + ListValue listOfA; + ListValue listOfB; + } + @ParameterizedTest @MethodSource("mapValueArguments") void mapValue_serializationWorks(Map map, JavaType type) throws JsonProcessingException { @@ -303,15 +325,6 @@ private static Map kv(String key, Object value) { return singletonMap(key, value); } - /** - * Exercise the type-parameter handling a bit - */ - @Value - private static class NodeWithGenerics implements StateTreeNode { - ListValue listOfA; - ListValue listOfB; - } - @Test void implicitRefs_omitted() throws InvalidTypeException, JsonProcessingException { TestEntity entity = makeEntityWithOptionalString(Optional.empty()); From a896f1a6b0b31b5c0d5c92b8546d70cde5755ee8 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Mon, 3 Jul 2023 16:25:55 -0400 Subject: [PATCH 15/15] Fix: use resolveMemberType in JacksonPlugin --- .../io/vena/bosk/jackson/JacksonCompiler.java | 16 +++++++++------- .../java/io/vena/bosk/jackson/JacksonPlugin.java | 5 +++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonCompiler.java b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonCompiler.java index 93f0f99c..9cb41364 100644 --- a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonCompiler.java +++ b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonCompiler.java @@ -82,7 +82,7 @@ public SerDes compiled(JavaType nodeType, Bosk bosk, FieldModerator mo ClassBuilder cb = new ClassBuilder<>("BOSK_JACKSON_" + nodeClass.getSimpleName(), JacksonCodecRuntime.class, nodeClass.getClassLoader(), here()); cb.beginClass(); - generate_writeFields(nodeClass, parameters, cb); + generate_writeFields(nodeType, parameters, cb); generate_instantiateFrom(constructor, parameters, cb); Codec codec = cb.buildInstance(); @@ -90,7 +90,7 @@ public SerDes compiled(JavaType nodeType, Bosk bosk, FieldModerator mo // Return a CodecWrapper for the codec LinkedHashMap parametersByName = new LinkedHashMap<>(); parameters.forEach(p -> parametersByName.put(p.getName(), p)); - return new CodecWrapper<>(codec, bosk, nodeClass, parametersByName, moderator); + return new CodecWrapper<>(codec, bosk, nodeType, parametersByName, moderator); } finally { Type removed = compilationsInProgress.get().removeLast(); assert removed.equals(nodeType); @@ -119,7 +119,9 @@ interface Codec { /** * Generates the body of the {@link Codec#writeFields} method. */ - private void generate_writeFields(Class nodeClass, List parameters, ClassBuilder cb) { + private void generate_writeFields(Type nodeType, List parameters, ClassBuilder cb) { + JavaType nodeJavaType = TypeFactory.defaultInstance().constructType(nodeType); + Class nodeClass = nodeJavaType.getRawClass(); cb.beginMethod(CODEC_WRITE_FIELDS); // Incoming arguments final LocalVariable node = cb.parameter(1); @@ -141,7 +143,7 @@ private void generate_writeFields(Class nodeClass, List parameters // building the plan. The plan should be straightforward and "obviously // correct". The execution of the plan should contain the sophistication. FieldWritePlan plan; - JavaType parameterType = TypeFactory.defaultInstance().constructType(parameter.getParameterizedType()); + JavaType parameterType = TypeFactory.defaultInstance().resolveMemberType(parameter.getParameterizedType(), nodeJavaType.getBindings()); // TODO: Is the static optimization possible?? // if (compilationsInProgress.get().contains(parameterType)) { // // Avoid infinite recursion - look up this field's adapter dynamically @@ -378,7 +380,7 @@ public void generateFieldWrite(String name, ClassBuilder cb, LocalVariabl private class CodecWrapper implements SerDes { Codec codec; Bosk bosk; - Class nodeClass; + JavaType nodeJavaType; LinkedHashMap parametersByName; FieldModerator moderator; @@ -402,9 +404,9 @@ public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOExcepti // Performance-critical. Pre-compute as much as possible outside this method. // Note: the reading side can't be as efficient as the writing side // because we need to tolerate the fields arriving in arbitrary order. - Map valueMap = jacksonPlugin.gatherParameterValuesByName(nodeClass, parametersByName, moderator, p, ctxt); + Map valueMap = jacksonPlugin.gatherParameterValuesByName(nodeJavaType, parametersByName, moderator, p, ctxt); - List parameterValues = jacksonPlugin.parameterValueList(nodeClass, valueMap, parametersByName, bosk); + List parameterValues = jacksonPlugin.parameterValueList(nodeJavaType.getRawClass(), valueMap, parametersByName, bosk); @SuppressWarnings("unchecked") T result = (T)codec.instantiateFrom(parameterValues); diff --git a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java index ef60532a..66ce7f07 100644 --- a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java +++ b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java @@ -723,7 +723,8 @@ private boolean reflectiveEntity(JavaType parameterType) { * Returns the fields present in the JSON, with value objects deserialized * using type information from parametersByName. */ - public Map gatherParameterValuesByName(Class nodeClass, Map parametersByName, FieldModerator moderator, JsonParser p, DeserializationContext ctxt) throws IOException { + public Map gatherParameterValuesByName(JavaType nodeJavaType, Map parametersByName, FieldModerator moderator, JsonParser p, DeserializationContext ctxt) throws IOException { + Class nodeClass = nodeJavaType.getRawClass(); Map parameterValuesByName = new HashMap<>(); expect(START_OBJECT, p); while (p.nextToken() != END_OBJECT) { @@ -733,7 +734,7 @@ public Map gatherParameterValuesByName(Class nodeClass, Map