>() {});
@@ -847,4 +956,6 @@ public static void expect(JsonToken expected, JsonParser p) throws IOException {
}
}
+ private static final String FIRST = "-first";
+ private static final String LAST = "-last";
}
diff --git a/bosk-jackson/src/main/java/works/bosk/jackson/JacksonPluginConfiguration.java b/bosk-jackson/src/main/java/works/bosk/jackson/JacksonPluginConfiguration.java
new file mode 100644
index 00000000..9810482c
--- /dev/null
+++ b/bosk-jackson/src/main/java/works/bosk/jackson/JacksonPluginConfiguration.java
@@ -0,0 +1,62 @@
+package works.bosk.jackson;
+
+public record JacksonPluginConfiguration(
+ MapShape mapShape
+) {
+ public static JacksonPluginConfiguration defaultConfiguration() {
+ return new JacksonPluginConfiguration(MapShape.ARRAY);
+ }
+
+ /**
+ * How bosk's ordered maps should translate to JSON, where the order of object
+ * fields is generally not preserved.
+ */
+ public enum MapShape {
+ /**
+ * A shape intended for brevity, human readability, and efficient serialization/deserialization.
+ *
+ * An array of single-field objects, where the field name is the map entry's key
+ * and the field value is the map entry's value.
+ */
+ ARRAY,
+
+ /**
+ * A shape intended for efficient incremental modification,
+ * especially for values stored in a database that supports JSON
+ * but does not preserve object field order (like Postgresql).
+ * Inspired by {@link java.util.LinkedHashMap LinkedHashMap}.
+ *
+ * An object containing the natural keys and values of the map being stored,
+ * with a few changes:
+ *
+ *
+ * -
+ * The object also has fields "{@code -first}" and "{@code -last}" pointing
+ * at the first and last map entries, so they can be found efficiently.
+ * (These names have leading dashes to distinguish them from valid
+ * {@link works.bosk.Identifier Identifier} values.)
+ *
+ * -
+ * The object's field values are themselves objects defined by
+ * {@link works.bosk.jackson.JacksonPlugin.LinkedMapEntry LinkedMapEntry}.
+ *
+ *
+ *
+ * The resulting structure supports the following operations in O(1) time:
+ *
+ * -
+ * Lookup an entry given its ID
+ *
+ * -
+ * Add a new entry at the end.
+ *
+ * -
+ * Delete an entry.
+ *
+ *
+ *
+ * It also supports linear time walking of the entries in both forward and reverse order.
+ */
+ LINKED_MAP,
+ }
+}
diff --git a/bosk-jackson/src/test/java/works/bosk/jackson/JacksonPluginTest.java b/bosk-jackson/src/test/java/works/bosk/jackson/JacksonPluginTest.java
index 38b95fe8..bbb8543a 100644
--- a/bosk-jackson/src/test/java/works/bosk/jackson/JacksonPluginTest.java
+++ b/bosk-jackson/src/test/java/works/bosk/jackson/JacksonPluginTest.java
@@ -626,11 +626,6 @@ void nonexistentPath_throws() {
.readValue("\"/some/nonexistent/path\""));
}
- @Test
- void catalogFromEmptyMap_throws() {
- assertJsonException("{}", Catalog.class, TestEntity.class);
- }
-
@Test
void catalogWithContentsArray_throws() {
assertJsonException("{ \"contents\": [] }", Catalog.class, TestEntity.class);
@@ -681,11 +676,6 @@ void sideTableWithTwoDomains_throws() {
assertJsonException("{ \"domain\": \"/entities\", \"domain\": \"/entities\", \"valuesById\": [] }", SideTable.class, TestEntity.class, String.class);
}
- @Test
- void sideTableWithValuesMap_throws() {
- assertJsonException("{ \"domain\": \"/entities\", \"valuesById\": {} }", SideTable.class, TestEntity.class, String.class);
- }
-
@Test
void sideTableWithTwoValuesFields_throws() {
assertJsonException("{ \"domain\": \"/entities\", \"valuesById\": [], \"valuesById\": [] }", SideTable.class, TestEntity.class, String.class);
diff --git a/bosk-jackson/src/test/java/works/bosk/jackson/JacksonRoundTripConformanceTest.java b/bosk-jackson/src/test/java/works/bosk/jackson/JacksonRoundTripConformanceTest.java
index 283ebb1f..14e16916 100644
--- a/bosk-jackson/src/test/java/works/bosk/jackson/JacksonRoundTripConformanceTest.java
+++ b/bosk-jackson/src/test/java/works/bosk/jackson/JacksonRoundTripConformanceTest.java
@@ -1,13 +1,20 @@
package works.bosk.jackson;
-import org.junit.jupiter.api.BeforeEach;
+import java.util.stream.Stream;
import works.bosk.drivers.DriverConformanceTest;
+import works.bosk.junit.ParametersByName;
import static works.bosk.AbstractRoundTripTest.jacksonRoundTripFactory;
+import static works.bosk.jackson.JacksonPluginConfiguration.MapShape.LINKED_MAP;
public class JacksonRoundTripConformanceTest extends DriverConformanceTest {
- @BeforeEach
- void setupDriverFactory() {
- driverFactory = jacksonRoundTripFactory();
+ @ParametersByName
+ JacksonRoundTripConformanceTest(JacksonPluginConfiguration config) {
+ driverFactory = jacksonRoundTripFactory(config);
+ }
+
+ static Stream config() {
+ return Stream.of(JacksonPluginConfiguration.MapShape.values())
+ .map(shape -> new JacksonPluginConfiguration(shape));
}
}
diff --git a/lib-testing/src/main/java/works/bosk/AbstractRoundTripTest.java b/lib-testing/src/main/java/works/bosk/AbstractRoundTripTest.java
index 59b12ea6..07b9b25d 100644
--- a/lib-testing/src/main/java/works/bosk/AbstractRoundTripTest.java
+++ b/lib-testing/src/main/java/works/bosk/AbstractRoundTripTest.java
@@ -31,10 +31,12 @@
import works.bosk.drivers.mongo.BsonPlugin;
import works.bosk.exceptions.InvalidTypeException;
import works.bosk.jackson.JacksonPlugin;
+import works.bosk.jackson.JacksonPluginConfiguration;
import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
import static java.lang.System.identityHashCode;
import static java.util.Collections.newSetFromMap;
+import static works.bosk.jackson.JacksonPluginConfiguration.MapShape.LINKED_MAP;
public abstract class AbstractRoundTripTest extends AbstractBoskTest {
@@ -43,7 +45,8 @@ static Stream> driverFactories() {
directFactory(),
factoryThatMakesAReference(),
- jacksonRoundTripFactory(),
+ jacksonRoundTripFactory(JacksonPluginConfiguration.defaultConfiguration()),
+ jacksonRoundTripFactory(new JacksonPluginConfiguration(LINKED_MAP)),
bsonRoundTripFactory()
);
@@ -60,13 +63,18 @@ public static DriverFactory factoryThatMakesAReference() {
};
}
- public static DriverFactory jacksonRoundTripFactory() {
- return new JacksonRoundTripDriverFactory<>();
+ public static DriverFactory jacksonRoundTripFactory(JacksonPluginConfiguration config) {
+ return new JacksonRoundTripDriverFactory<>(config);
}
- @RequiredArgsConstructor
private static class JacksonRoundTripDriverFactory implements DriverFactory {
- private final JacksonPlugin jp = new JacksonPlugin();
+ private final JacksonPluginConfiguration config;
+ private final JacksonPlugin jp;
+
+ private JacksonRoundTripDriverFactory(JacksonPluginConfiguration config) {
+ this.config = config;
+ this.jp = new JacksonPlugin(config);
+ }
@Override
public BoskDriver build(BoskInfo boskInfo, BoskDriver driver) {